diff --git a/.circleci/config.yml b/.circleci/config.yml index 2703ce10c..717a6260f 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -17,12 +17,12 @@ commands: gomod: steps: - restore_cache: - keys: [deps-] + keys: [deps-v2-] - run: name: Download go module dependencies command: go mod download - save_cache: - key: deps-{{ checksum "go.sum" }}-{{ checksum "go.sum" }} + key: deps-v2-{{ checksum "go.sum" }}-{{ checksum "go.sum" }} paths: [/home/circleci/go/pkg/mod] jobs: diff --git a/internal/contracts/contracts.go b/internal/contracts/contracts.go new file mode 100644 index 000000000..d922985ff --- /dev/null +++ b/internal/contracts/contracts.go @@ -0,0 +1,103 @@ +package contracts + +import ( + "encoding/json" + "errors" + "fmt" + "os" + "path/filepath" + "testing" + + "github.com/nspcc-dev/neo-go/pkg/core/state" + "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/util" + "github.com/stretchr/testify/require" +) + +var ( + helper1ContractNEFPath = filepath.Join("management_helper", "management_helper1.nef") + helper1ContractManifestPath = filepath.Join("management_helper", "management_helper1.manifest.json") + helper2ContractNEFPath = filepath.Join("management_helper", "management_helper2.nef") + helper2ContractManifestPath = filepath.Join("management_helper", "management_helper2.manifest.json") + + oracleContractNEFPath = filepath.Join("oracle_contract", "oracle.nef") + oracleContractManifestPath = filepath.Join("oracle_contract", "oracle.manifest.json") +) + +// GetTestContractState reads 2 pre-compiled contracts generated by +// TestGenerateHelperContracts second of which is allowed to call the first. +func GetTestContractState(t *testing.T, pathToInternalContracts string, id1, id2 int32, sender2 util.Uint160) (*state.Contract, *state.Contract) { + errNotFound := errors.New("auto-generated oracle contract is not found, use TestGenerateHelperContracts to regenerate") + neBytes, err := os.ReadFile(filepath.Join(pathToInternalContracts, helper1ContractNEFPath)) + require.NoError(t, err, fmt.Errorf("nef1: %w", errNotFound)) + ne, err := nef.FileFromBytes(neBytes) + require.NoError(t, err) + + mBytes, err := os.ReadFile(filepath.Join(pathToInternalContracts, helper1ContractManifestPath)) + require.NoError(t, err, fmt.Errorf("manifest1: %w", errNotFound)) + m := &manifest.Manifest{} + err = json.Unmarshal(mBytes, m) + require.NoError(t, err) + + cs1 := &state.Contract{ + ContractBase: state.ContractBase{ + NEF: ne, + Manifest: *m, + ID: id1, + }, + } + + neBytes, err = os.ReadFile(filepath.Join(pathToInternalContracts, helper2ContractNEFPath)) + require.NoError(t, err, fmt.Errorf("nef2: %w", errNotFound)) + ne, err = nef.FileFromBytes(neBytes) + require.NoError(t, err) + + mBytes, err = os.ReadFile(filepath.Join(pathToInternalContracts, helper2ContractManifestPath)) + require.NoError(t, err, fmt.Errorf("manifest2: %w", errNotFound)) + m = &manifest.Manifest{} + err = json.Unmarshal(mBytes, m) + require.NoError(t, err) + + // Retrieve hash of the first contract from the permissions of the second contract. + require.Equal(t, 1, len(m.Permissions)) + require.Equal(t, manifest.PermissionHash, m.Permissions[0].Contract.Type) + cs1.Hash = m.Permissions[0].Contract.Hash() + + cs2 := &state.Contract{ + ContractBase: state.ContractBase{ + NEF: ne, + Manifest: *m, + ID: id2, + Hash: state.CreateContractHash(sender2, ne.Checksum, m.Name), + }, + } + + return cs1, cs2 +} + +// GetOracleContractState reads pre-compiled oracle contract generated by +// TestGenerateHelperContracts and returns its state. +func GetOracleContractState(t *testing.T, pathToInternalContracts string, sender util.Uint160, id int32) *state.Contract { + errNotFound := errors.New("auto-generated oracle contract is not found, use TestGenerateHelperContracts to regenerate") + + neBytes, err := os.ReadFile(filepath.Join(pathToInternalContracts, oracleContractNEFPath)) + require.NoError(t, err, fmt.Errorf("nef: %w", errNotFound)) + ne, err := nef.FileFromBytes(neBytes) + require.NoError(t, err) + + mBytes, err := os.ReadFile(filepath.Join(pathToInternalContracts, oracleContractManifestPath)) + require.NoError(t, err, fmt.Errorf("manifest: %w", errNotFound)) + m := &manifest.Manifest{} + err = json.Unmarshal(mBytes, m) + require.NoError(t, err) + + return &state.Contract{ + ContractBase: state.ContractBase{ + NEF: ne, + Hash: state.CreateContractHash(sender, ne.Checksum, m.Name), + Manifest: *m, + ID: id, + }, + } +} diff --git a/internal/contracts/contracts_test.go b/internal/contracts/contracts_test.go new file mode 100644 index 000000000..9b6fea22d --- /dev/null +++ b/internal/contracts/contracts_test.go @@ -0,0 +1,509 @@ +package contracts + +import ( + "encoding/json" + "os" + "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/native/nativenames" + "github.com/nspcc-dev/neo-go/pkg/core/state" + "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/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/util" + "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" +) + +// TestGenerateHelperContracts generates contract states that are used in tests. +// See generateOracleContract and generateManagementHelperContracts comments for +// details. +func TestGenerateHelperContracts(t *testing.T) { + const saveState = false + + generateOracleContract(t, saveState) + generateManagementHelperContracts(t, saveState) + + require.False(t, saveState) +} + +// generateOracleContract generates helper contract that is able to call +// native Oracle contract and has callback method. It uses test chain to define +// Oracle and StdLib native hashes and saves generated NEF and manifest to `oracle_contract` folder. +// Set `saveState` flag to true and run the test to rewrite NEF and manifest files. +func generateOracleContract(t *testing.T, saveState bool) { + bc, validator, committee := chain.NewMultiWithCustomConfig(t, func(c *config.ProtocolConfiguration) { + c.P2PSigExtensions = true + }) + e := neotest.NewExecutor(t, bc, validator, committee) + + oracleHash := e.NativeHash(t, nativenames.Oracle) + stdHash := e.NativeHash(t, nativenames.StdLib) + + w := io.NewBufBinWriter() + emit.Int(w.BinWriter, 5) + emit.Opcodes(w.BinWriter, opcode.PACK) + emit.Int(w.BinWriter, int64(callflag.All)) + emit.String(w.BinWriter, "request") + emit.Bytes(w.BinWriter, oracleHash.BytesBE()) + emit.Syscall(w.BinWriter, interopnames.SystemContractCall) + emit.Opcodes(w.BinWriter, opcode.DROP) + emit.Opcodes(w.BinWriter, opcode.RET) + + // `handle` method aborts if len(userData) == 2 and does NOT perform witness checks + // for the sake of contract code simplicity (the contract is used in multiple testchains). + offset := w.Len() + + emit.Opcodes(w.BinWriter, opcode.OVER) + emit.Opcodes(w.BinWriter, opcode.SIZE) + emit.Int(w.BinWriter, 2) + emit.Instruction(w.BinWriter, opcode.JMPNE, []byte{3}) + emit.Opcodes(w.BinWriter, opcode.ABORT) + emit.Int(w.BinWriter, 4) // url, userData, code, result + emit.Opcodes(w.BinWriter, opcode.PACK) + emit.Int(w.BinWriter, 1) // 1 byte (args count for `serialize`) + emit.Opcodes(w.BinWriter, opcode.PACK) // 1 byte (pack args into array for `serialize`) + emit.AppCallNoArgs(w.BinWriter, stdHash, "serialize", callflag.All) // 39 bytes + emit.String(w.BinWriter, "lastOracleResponse") + emit.Syscall(w.BinWriter, interopnames.SystemStorageGetContext) + emit.Syscall(w.BinWriter, interopnames.SystemStoragePut) + emit.Opcodes(w.BinWriter, opcode.RET) + + m := manifest.NewManifest("TestOracle") + m.ABI.Methods = []manifest.Method{ + { + Name: "requestURL", + Offset: 0, + Parameters: []manifest.Parameter{ + manifest.NewParameter("url", smartcontract.StringType), + manifest.NewParameter("filter", smartcontract.StringType), + manifest.NewParameter("callback", smartcontract.StringType), + manifest.NewParameter("userData", smartcontract.AnyType), + manifest.NewParameter("gasForResponse", smartcontract.IntegerType), + }, + ReturnType: smartcontract.VoidType, + }, + { + Name: "handle", + Offset: offset, + Parameters: []manifest.Parameter{ + manifest.NewParameter("url", smartcontract.StringType), + manifest.NewParameter("userData", smartcontract.AnyType), + manifest.NewParameter("code", smartcontract.IntegerType), + manifest.NewParameter("result", smartcontract.ByteArrayType), + }, + ReturnType: smartcontract.VoidType, + }, + } + + perm := manifest.NewPermission(manifest.PermissionHash, oracleHash) + perm.Methods.Add("request") + m.Permissions = append(m.Permissions, *perm) + + // Generate NEF file. + script := w.Bytes() + ne, err := nef.NewFile(script) + require.NoError(t, err) + + // Write NEF file. + bytes, err := ne.Bytes() + require.NoError(t, err) + if saveState { + err = os.WriteFile(oracleContractNEFPath, bytes, os.ModePerm) + require.NoError(t, err) + } + + // Write manifest file. + mData, err := json.Marshal(m) + require.NoError(t, err) + if saveState { + err = os.WriteFile(oracleContractManifestPath, mData, os.ModePerm) + require.NoError(t, err) + } +} + +// generateManagementHelperContracts generates 2 helper contracts second of which is +// allowed to call the first. It uses test chain to define Management and StdLib +// native hashes and saves generated NEF and manifest to `management_contract` folder. +// Set `saveState` flag to true and run the test to rewrite NEF and manifest files. +func generateManagementHelperContracts(t *testing.T, saveState bool) { + bc, validator, committee := chain.NewMultiWithCustomConfig(t, func(c *config.ProtocolConfiguration) { + c.P2PSigExtensions = true + }) + e := neotest.NewExecutor(t, bc, validator, committee) + + mgmtHash := e.NativeHash(t, nativenames.Management) + stdHash := e.NativeHash(t, nativenames.StdLib) + neoHash := e.NativeHash(t, nativenames.Neo) + singleChainValidatorAcc := e.Validator.(neotest.MultiSigner).Single(2).Account() // priv0 + require.NoError(t, singleChainValidatorAcc.ConvertMultisig(1, keys.PublicKeys{singleChainValidatorAcc.PrivateKey().PublicKey()})) + singleChainValidatorHash := singleChainValidatorAcc.Contract.ScriptHash() + + w := io.NewBufBinWriter() + emit.Opcodes(w.BinWriter, opcode.ABORT) + addOff := w.Len() + emit.Opcodes(w.BinWriter, opcode.ADD, opcode.RET) + addMultiOff := w.Len() + emit.Opcodes(w.BinWriter, opcode.ADD, opcode.ADD, opcode.RET) + ret7Off := w.Len() + emit.Opcodes(w.BinWriter, opcode.PUSH7, opcode.RET) + dropOff := w.Len() + emit.Opcodes(w.BinWriter, opcode.DROP, opcode.RET) + initOff := w.Len() + emit.Opcodes(w.BinWriter, opcode.INITSSLOT, 1, opcode.PUSH3, opcode.STSFLD0, opcode.RET) + add3Off := w.Len() + emit.Opcodes(w.BinWriter, opcode.LDSFLD0, opcode.ADD, opcode.RET) + invalidRetOff := w.Len() + emit.Opcodes(w.BinWriter, opcode.PUSH1, opcode.PUSH2, opcode.RET) + justRetOff := w.Len() + emit.Opcodes(w.BinWriter, opcode.RET) + verifyOff := w.Len() + emit.Opcodes(w.BinWriter, opcode.LDSFLD0, opcode.SUB, + opcode.CONVERT, opcode.Opcode(stackitem.BooleanT), opcode.RET) + deployOff := w.Len() + emit.Opcodes(w.BinWriter, opcode.SWAP, opcode.JMPIF, 2+8+1+1+1+1+39+3) + emit.String(w.BinWriter, "create") // 8 bytes + emit.Int(w.BinWriter, 2) // 1 byte + emit.Opcodes(w.BinWriter, opcode.PACK) // 1 byte + emit.Int(w.BinWriter, 1) // 1 byte (args count for `serialize`) + emit.Opcodes(w.BinWriter, opcode.PACK) // 1 byte (pack args into array for `serialize`) + emit.AppCallNoArgs(w.BinWriter, stdHash, "serialize", callflag.All) // 39 bytes + emit.Opcodes(w.BinWriter, opcode.CALL, 3+8+1+1+1+1+39+3, opcode.RET) + emit.String(w.BinWriter, "update") // 8 bytes + emit.Int(w.BinWriter, 2) // 1 byte + emit.Opcodes(w.BinWriter, opcode.PACK) // 1 byte + emit.Int(w.BinWriter, 1) // 1 byte (args count for `serialize`) + emit.Opcodes(w.BinWriter, opcode.PACK) // 1 byte (pack args into array for `serialize`) + emit.AppCallNoArgs(w.BinWriter, stdHash, "serialize", callflag.All) // 39 bytes + emit.Opcodes(w.BinWriter, opcode.CALL, 3, opcode.RET) + putValOff := w.Len() + emit.String(w.BinWriter, "initial") + emit.Syscall(w.BinWriter, interopnames.SystemStorageGetContext) + emit.Syscall(w.BinWriter, interopnames.SystemStoragePut) + emit.Opcodes(w.BinWriter, opcode.RET) + getValOff := w.Len() + emit.String(w.BinWriter, "initial") + emit.Syscall(w.BinWriter, interopnames.SystemStorageGetContext) + emit.Syscall(w.BinWriter, interopnames.SystemStorageGet) + emit.Opcodes(w.BinWriter, opcode.RET) + delValOff := w.Len() + emit.Syscall(w.BinWriter, interopnames.SystemStorageGetContext) + emit.Syscall(w.BinWriter, interopnames.SystemStorageDelete) + emit.Opcodes(w.BinWriter, opcode.RET) + onNEP17PaymentOff := w.Len() + emit.Syscall(w.BinWriter, interopnames.SystemRuntimeGetCallingScriptHash) + emit.Int(w.BinWriter, 4) + emit.Opcodes(w.BinWriter, opcode.PACK) + emit.String(w.BinWriter, "LastPayment") + emit.Syscall(w.BinWriter, interopnames.SystemRuntimeNotify) + emit.Opcodes(w.BinWriter, opcode.RET) + onNEP11PaymentOff := w.Len() + emit.Syscall(w.BinWriter, interopnames.SystemRuntimeGetCallingScriptHash) + emit.Int(w.BinWriter, 5) + emit.Opcodes(w.BinWriter, opcode.PACK) + emit.String(w.BinWriter, "LostPayment") + emit.Syscall(w.BinWriter, interopnames.SystemRuntimeNotify) + emit.Opcodes(w.BinWriter, opcode.RET) + update3Off := w.Len() + emit.Int(w.BinWriter, 3) + emit.Opcodes(w.BinWriter, opcode.JMP, 2+1) + updateOff := w.Len() + emit.Int(w.BinWriter, 2) + emit.Opcodes(w.BinWriter, opcode.PACK) + emit.AppCallNoArgs(w.BinWriter, mgmtHash, "update", callflag.All) + emit.Opcodes(w.BinWriter, opcode.DROP) + emit.Opcodes(w.BinWriter, opcode.RET) + destroyOff := w.Len() + emit.AppCall(w.BinWriter, mgmtHash, "destroy", callflag.All) + emit.Opcodes(w.BinWriter, opcode.DROP) + emit.Opcodes(w.BinWriter, opcode.RET) + invalidStack1Off := w.Len() + emit.Opcodes(w.BinWriter, opcode.NEWARRAY0, opcode.DUP, opcode.DUP, opcode.APPEND) // recursive array + emit.Opcodes(w.BinWriter, opcode.RET) + invalidStack2Off := w.Len() + emit.Syscall(w.BinWriter, interopnames.SystemStorageGetReadOnlyContext) // interop item + emit.Opcodes(w.BinWriter, opcode.RET) + callT0Off := w.Len() + emit.Opcodes(w.BinWriter, opcode.CALLT, 0, 0, opcode.PUSH1, opcode.ADD, opcode.RET) + callT1Off := w.Len() + emit.Opcodes(w.BinWriter, opcode.CALLT, 1, 0, opcode.RET) + callT2Off := w.Len() + emit.Opcodes(w.BinWriter, opcode.CALLT, 0, 0, opcode.RET) + burnGasOff := w.Len() + emit.Syscall(w.BinWriter, interopnames.SystemRuntimeBurnGas) + emit.Opcodes(w.BinWriter, opcode.RET) + invocCounterOff := w.Len() + emit.Syscall(w.BinWriter, interopnames.SystemRuntimeGetInvocationCounter) + emit.Opcodes(w.BinWriter, opcode.RET) + + script := w.Bytes() + m := manifest.NewManifest("TestMain") + m.ABI.Methods = []manifest.Method{ + { + Name: "add", + Offset: addOff, + Parameters: []manifest.Parameter{ + manifest.NewParameter("addend1", smartcontract.IntegerType), + manifest.NewParameter("addend2", smartcontract.IntegerType), + }, + ReturnType: smartcontract.IntegerType, + }, + { + Name: "add", + Offset: addMultiOff, + Parameters: []manifest.Parameter{ + manifest.NewParameter("addend1", smartcontract.IntegerType), + manifest.NewParameter("addend2", smartcontract.IntegerType), + manifest.NewParameter("addend3", smartcontract.IntegerType), + }, + ReturnType: smartcontract.IntegerType, + }, + { + Name: "ret7", + Offset: ret7Off, + Parameters: []manifest.Parameter{}, + ReturnType: smartcontract.IntegerType, + }, + { + Name: "drop", + Offset: dropOff, + ReturnType: smartcontract.VoidType, + }, + { + Name: manifest.MethodInit, + Offset: initOff, + ReturnType: smartcontract.VoidType, + }, + { + Name: "add3", + Offset: add3Off, + Parameters: []manifest.Parameter{ + manifest.NewParameter("addend", smartcontract.IntegerType), + }, + ReturnType: smartcontract.IntegerType, + }, + { + Name: "invalidReturn", + Offset: invalidRetOff, + ReturnType: smartcontract.IntegerType, + }, + { + Name: "justReturn", + Offset: justRetOff, + ReturnType: smartcontract.VoidType, + }, + { + Name: manifest.MethodVerify, + Offset: verifyOff, + ReturnType: smartcontract.BoolType, + }, + { + Name: manifest.MethodDeploy, + Offset: deployOff, + Parameters: []manifest.Parameter{ + manifest.NewParameter("data", smartcontract.AnyType), + manifest.NewParameter("isUpdate", smartcontract.BoolType), + }, + ReturnType: smartcontract.VoidType, + }, + { + Name: "getValue", + Offset: getValOff, + ReturnType: smartcontract.StringType, + }, + { + Name: "putValue", + Offset: putValOff, + Parameters: []manifest.Parameter{ + manifest.NewParameter("value", smartcontract.StringType), + }, + ReturnType: smartcontract.VoidType, + }, + { + Name: "delValue", + Offset: delValOff, + Parameters: []manifest.Parameter{ + manifest.NewParameter("key", smartcontract.StringType), + }, + ReturnType: smartcontract.VoidType, + }, + { + Name: manifest.MethodOnNEP11Payment, + Offset: onNEP11PaymentOff, + Parameters: []manifest.Parameter{ + manifest.NewParameter("from", smartcontract.Hash160Type), + manifest.NewParameter("amount", smartcontract.IntegerType), + manifest.NewParameter("tokenid", smartcontract.ByteArrayType), + manifest.NewParameter("data", smartcontract.AnyType), + }, + ReturnType: smartcontract.VoidType, + }, + { + Name: manifest.MethodOnNEP17Payment, + Offset: onNEP17PaymentOff, + Parameters: []manifest.Parameter{ + manifest.NewParameter("from", smartcontract.Hash160Type), + manifest.NewParameter("amount", smartcontract.IntegerType), + manifest.NewParameter("data", smartcontract.AnyType), + }, + ReturnType: smartcontract.VoidType, + }, + { + Name: "update", + Offset: updateOff, + Parameters: []manifest.Parameter{ + manifest.NewParameter("nef", smartcontract.ByteArrayType), + manifest.NewParameter("manifest", smartcontract.ByteArrayType), + }, + ReturnType: smartcontract.VoidType, + }, + { + Name: "update", + Offset: update3Off, + Parameters: []manifest.Parameter{ + manifest.NewParameter("nef", smartcontract.ByteArrayType), + manifest.NewParameter("manifest", smartcontract.ByteArrayType), + manifest.NewParameter("data", smartcontract.AnyType), + }, + ReturnType: smartcontract.VoidType, + }, + { + Name: "destroy", + Offset: destroyOff, + ReturnType: smartcontract.VoidType, + }, + { + Name: "invalidStack1", + Offset: invalidStack1Off, + ReturnType: smartcontract.AnyType, + }, + { + Name: "invalidStack2", + Offset: invalidStack2Off, + ReturnType: smartcontract.AnyType, + }, + { + Name: "callT0", + Offset: callT0Off, + Parameters: []manifest.Parameter{ + manifest.NewParameter("address", smartcontract.Hash160Type), + }, + ReturnType: smartcontract.IntegerType, + }, + { + Name: "callT1", + Offset: callT1Off, + ReturnType: smartcontract.IntegerType, + }, + { + Name: "callT2", + Offset: callT2Off, + ReturnType: smartcontract.IntegerType, + }, + { + Name: "burnGas", + Offset: burnGasOff, + Parameters: []manifest.Parameter{ + manifest.NewParameter("amount", smartcontract.IntegerType), + }, + ReturnType: smartcontract.VoidType, + }, + { + Name: "invocCounter", + Offset: invocCounterOff, + ReturnType: smartcontract.IntegerType, + }, + } + m.Permissions = make([]manifest.Permission, 2) + m.Permissions[0].Contract.Type = manifest.PermissionHash + m.Permissions[0].Contract.Value = neoHash + m.Permissions[0].Methods.Add("balanceOf") + + m.Permissions[1].Contract.Type = manifest.PermissionHash + m.Permissions[1].Contract.Value = util.Uint160{} + m.Permissions[1].Methods.Add("method") + + // Generate NEF file. + ne, err := nef.NewFile(script) + require.NoError(t, err) + ne.Tokens = []nef.MethodToken{ + { + Hash: neoHash, + Method: "balanceOf", + ParamCount: 1, + HasReturn: true, + CallFlag: callflag.ReadStates, + }, + { + Hash: util.Uint160{}, + Method: "method", + HasReturn: true, + CallFlag: callflag.ReadStates, + }, + } + ne.Checksum = ne.CalculateChecksum() + + // Write first NEF file. + bytes, err := ne.Bytes() + require.NoError(t, err) + if saveState { + err = os.WriteFile(helper1ContractNEFPath, bytes, os.ModePerm) + require.NoError(t, err) + } + + // Write first manifest file. + mData, err := json.Marshal(m) + require.NoError(t, err) + if saveState { + err = os.WriteFile(helper1ContractManifestPath, mData, os.ModePerm) + require.NoError(t, err) + } + + // Create hash of the first contract assuming that sender is single-chain validator. + h := state.CreateContractHash(singleChainValidatorHash, ne.Checksum, m.Name) + + currScript := []byte{byte(opcode.RET)} + m = manifest.NewManifest("TestAux") + m.ABI.Methods = []manifest.Method{ + { + Name: "simpleMethod", + Offset: 0, + ReturnType: smartcontract.VoidType, + }, + } + perm := manifest.NewPermission(manifest.PermissionHash, h) + perm.Methods.Add("add") + perm.Methods.Add("drop") + perm.Methods.Add("add3") + perm.Methods.Add("invalidReturn") + perm.Methods.Add("justReturn") + perm.Methods.Add("getValue") + m.Permissions = append(m.Permissions, *perm) + ne, err = nef.NewFile(currScript) + require.NoError(t, err) + + // Write second NEF file. + bytes, err = ne.Bytes() + require.NoError(t, err) + if saveState { + err = os.WriteFile(helper2ContractNEFPath, bytes, os.ModePerm) + require.NoError(t, err) + } + + // Write second manifest file. + mData, err = json.Marshal(m) + require.NoError(t, err) + if saveState { + err = os.WriteFile(helper2ContractManifestPath, mData, os.ModePerm) + require.NoError(t, err) + } +} diff --git a/internal/contracts/management_helper/README.md b/internal/contracts/management_helper/README.md new file mode 100644 index 000000000..888580153 --- /dev/null +++ b/internal/contracts/management_helper/README.md @@ -0,0 +1,9 @@ +## Management helper contracts + +Management helper contracts NEF and manifest files are generated automatically by +`TestGenerateHelperContracts` and are used in tests. Do not modify these files manually. +To regenerate these files: + +1. Open `TestGenerateHelperContracts` and set `saveState` flag to `true`. +2. Run `TestGenerateHelperContracts`. +3. Set `saveState` back to `false`. \ No newline at end of file diff --git a/pkg/core/test_data/management_helper/management_helper1.manifest.json b/internal/contracts/management_helper/management_helper1.manifest.json old mode 100755 new mode 100644 similarity index 69% rename from pkg/core/test_data/management_helper/management_helper1.manifest.json rename to internal/contracts/management_helper/management_helper1.manifest.json index baea0b094..181b16b7c --- a/pkg/core/test_data/management_helper/management_helper1.manifest.json +++ b/internal/contracts/management_helper/management_helper1.manifest.json @@ -1 +1 @@ -{"name":"TestMain","abi":{"methods":[{"name":"add","offset":1,"parameters":[{"name":"addend1","type":"Integer"},{"name":"addend2","type":"Integer"}],"returntype":"Integer","safe":false},{"name":"add","offset":3,"parameters":[{"name":"addend1","type":"Integer"},{"name":"addend2","type":"Integer"},{"name":"addend3","type":"Integer"}],"returntype":"Integer","safe":false},{"name":"ret7","offset":6,"parameters":[],"returntype":"Integer","safe":false},{"name":"drop","offset":8,"parameters":null,"returntype":"Void","safe":false},{"name":"_initialize","offset":10,"parameters":null,"returntype":"Void","safe":false},{"name":"add3","offset":15,"parameters":[{"name":"addend","type":"Integer"}],"returntype":"Integer","safe":false},{"name":"invalidReturn","offset":18,"parameters":null,"returntype":"Integer","safe":false},{"name":"justReturn","offset":21,"parameters":null,"returntype":"Void","safe":false},{"name":"verify","offset":22,"parameters":null,"returntype":"Boolean","safe":false},{"name":"_deploy","offset":27,"parameters":[{"name":"data","type":"Any"},{"name":"isUpdate","type":"Boolean"}],"returntype":"Void","safe":false},{"name":"getValue","offset":158,"parameters":null,"returntype":"String","safe":false},{"name":"putValue","offset":138,"parameters":[{"name":"value","type":"String"}],"returntype":"Void","safe":false},{"name":"delValue","offset":178,"parameters":[{"name":"key","type":"String"}],"returntype":"Void","safe":false},{"name":"onNEP11Payment","offset":215,"parameters":[{"name":"from","type":"Hash160"},{"name":"amount","type":"Integer"},{"name":"tokenid","type":"ByteArray"},{"name":"data","type":"Any"}],"returntype":"Void","safe":false},{"name":"onNEP17Payment","offset":189,"parameters":[{"name":"from","type":"Hash160"},{"name":"amount","type":"Integer"},{"name":"data","type":"Any"}],"returntype":"Void","safe":false},{"name":"update","offset":244,"parameters":[{"name":"nef","type":"ByteArray"},{"name":"manifest","type":"ByteArray"}],"returntype":"Void","safe":false},{"name":"update","offset":241,"parameters":[{"name":"nef","type":"ByteArray"},{"name":"manifest","type":"ByteArray"},{"name":"data","type":"Any"}],"returntype":"Void","safe":false},{"name":"destroy","offset":284,"parameters":null,"returntype":"Void","safe":false},{"name":"invalidStack","offset":325,"parameters":null,"returntype":"Void","safe":false},{"name":"callT0","offset":335,"parameters":[{"name":"address","type":"Hash160"}],"returntype":"Integer","safe":false},{"name":"callT1","offset":341,"parameters":null,"returntype":"Integer","safe":false},{"name":"callT2","offset":345,"parameters":null,"returntype":"Integer","safe":false},{"name":"burnGas","offset":349,"parameters":[{"name":"amount","type":"Integer"}],"returntype":"Void","safe":false},{"name":"invocCounter","offset":355,"parameters":null,"returntype":"Integer","safe":false}],"events":[]},"features":{},"groups":[],"permissions":[{"contract":"0xef4073a0f2b305a38ec4050e4d3d28bc40ea63f5","methods":["balanceOf"]},{"contract":"0x0000000000000000000000000000000000000000","methods":["method"]}],"supportedstandards":[],"trusts":[],"extra":null} \ No newline at end of file +{"name":"TestMain","abi":{"methods":[{"name":"add","offset":1,"parameters":[{"name":"addend1","type":"Integer"},{"name":"addend2","type":"Integer"}],"returntype":"Integer","safe":false},{"name":"add","offset":3,"parameters":[{"name":"addend1","type":"Integer"},{"name":"addend2","type":"Integer"},{"name":"addend3","type":"Integer"}],"returntype":"Integer","safe":false},{"name":"ret7","offset":6,"parameters":[],"returntype":"Integer","safe":false},{"name":"drop","offset":8,"parameters":null,"returntype":"Void","safe":false},{"name":"_initialize","offset":10,"parameters":null,"returntype":"Void","safe":false},{"name":"add3","offset":15,"parameters":[{"name":"addend","type":"Integer"}],"returntype":"Integer","safe":false},{"name":"invalidReturn","offset":18,"parameters":null,"returntype":"Integer","safe":false},{"name":"justReturn","offset":21,"parameters":null,"returntype":"Void","safe":false},{"name":"verify","offset":22,"parameters":null,"returntype":"Boolean","safe":false},{"name":"_deploy","offset":27,"parameters":[{"name":"data","type":"Any"},{"name":"isUpdate","type":"Boolean"}],"returntype":"Void","safe":false},{"name":"getValue","offset":158,"parameters":null,"returntype":"String","safe":false},{"name":"putValue","offset":138,"parameters":[{"name":"value","type":"String"}],"returntype":"Void","safe":false},{"name":"delValue","offset":178,"parameters":[{"name":"key","type":"String"}],"returntype":"Void","safe":false},{"name":"onNEP11Payment","offset":215,"parameters":[{"name":"from","type":"Hash160"},{"name":"amount","type":"Integer"},{"name":"tokenid","type":"ByteArray"},{"name":"data","type":"Any"}],"returntype":"Void","safe":false},{"name":"onNEP17Payment","offset":189,"parameters":[{"name":"from","type":"Hash160"},{"name":"amount","type":"Integer"},{"name":"data","type":"Any"}],"returntype":"Void","safe":false},{"name":"update","offset":244,"parameters":[{"name":"nef","type":"ByteArray"},{"name":"manifest","type":"ByteArray"}],"returntype":"Void","safe":false},{"name":"update","offset":241,"parameters":[{"name":"nef","type":"ByteArray"},{"name":"manifest","type":"ByteArray"},{"name":"data","type":"Any"}],"returntype":"Void","safe":false},{"name":"destroy","offset":284,"parameters":null,"returntype":"Void","safe":false},{"name":"invalidStack1","offset":324,"parameters":null,"returntype":"Any","safe":false},{"name":"invalidStack2","offset":329,"parameters":null,"returntype":"Any","safe":false},{"name":"callT0","offset":335,"parameters":[{"name":"address","type":"Hash160"}],"returntype":"Integer","safe":false},{"name":"callT1","offset":341,"parameters":null,"returntype":"Integer","safe":false},{"name":"callT2","offset":345,"parameters":null,"returntype":"Integer","safe":false},{"name":"burnGas","offset":349,"parameters":[{"name":"amount","type":"Integer"}],"returntype":"Void","safe":false},{"name":"invocCounter","offset":355,"parameters":null,"returntype":"Integer","safe":false}],"events":[]},"features":{},"groups":[],"permissions":[{"contract":"0xef4073a0f2b305a38ec4050e4d3d28bc40ea63f5","methods":["balanceOf"]},{"contract":"0x0000000000000000000000000000000000000000","methods":["method"]}],"supportedstandards":[],"trusts":[],"extra":null} \ No newline at end of file diff --git a/pkg/core/test_data/management_helper/management_helper1.nef b/internal/contracts/management_helper/management_helper1.nef old mode 100755 new mode 100644 similarity index 75% rename from pkg/core/test_data/management_helper/management_helper1.nef rename to internal/contracts/management_helper/management_helper1.nef index bccf14cdc..85288654f Binary files a/pkg/core/test_data/management_helper/management_helper1.nef and b/internal/contracts/management_helper/management_helper1.nef differ diff --git a/internal/contracts/management_helper/management_helper2.manifest.json b/internal/contracts/management_helper/management_helper2.manifest.json new file mode 100644 index 000000000..1f4efc56c --- /dev/null +++ b/internal/contracts/management_helper/management_helper2.manifest.json @@ -0,0 +1 @@ +{"name":"TestAux","abi":{"methods":[{"name":"simpleMethod","offset":0,"parameters":null,"returntype":"Void","safe":false}],"events":[]},"features":{},"groups":[],"permissions":[{"contract":"0x5a6a3b6c716e31465ed9be0b234d6223498c9f4e","methods":["add","drop","add3","invalidReturn","justReturn","getValue"]}],"supportedstandards":[],"trusts":[],"extra":null} \ No newline at end of file diff --git a/pkg/core/test_data/management_helper/management_helper2.nef b/internal/contracts/management_helper/management_helper2.nef old mode 100755 new mode 100644 similarity index 100% rename from pkg/core/test_data/management_helper/management_helper2.nef rename to internal/contracts/management_helper/management_helper2.nef diff --git a/internal/contracts/oracle_contract/README.md b/internal/contracts/oracle_contract/README.md new file mode 100644 index 000000000..d57d41cc4 --- /dev/null +++ b/internal/contracts/oracle_contract/README.md @@ -0,0 +1,9 @@ +## Oracle helper contract + +Oracle helper contract NEF and manifest files are generated automatically by +`TestGenerateHelperContracts` and are used in tests. Do not modify these files manually. +To regenerate these files: + +1. Open `TestGenerateHelperContracts` and set `saveState` flag to `true`. +2. Run `TestGenerateHelperContracts`. +3. Set `saveState` back to `false`. \ No newline at end of file diff --git a/pkg/core/test_data/oracle_contract/oracle.manifest.json b/internal/contracts/oracle_contract/oracle.manifest.json old mode 100755 new mode 100644 similarity index 100% rename from pkg/core/test_data/oracle_contract/oracle.manifest.json rename to internal/contracts/oracle_contract/oracle.manifest.json diff --git a/pkg/core/test_data/oracle_contract/oracle.nef b/internal/contracts/oracle_contract/oracle.nef old mode 100755 new mode 100644 similarity index 100% rename from pkg/core/test_data/oracle_contract/oracle.nef rename to internal/contracts/oracle_contract/oracle.nef diff --git a/pkg/core/basic_chain_test.go b/pkg/core/basic_chain_test.go new file mode 100644 index 000000000..480db1eb8 --- /dev/null +++ b/pkg/core/basic_chain_test.go @@ -0,0 +1,282 @@ +package core_test + +import ( + "encoding/base64" + "encoding/hex" + "math/big" + "os" + "path" + "path/filepath" + "testing" + "time" + + "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/encoding/fixedn" + "github.com/nspcc-dev/neo-go/pkg/interop/native/roles" + "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. + basicChainPrefix = "../rpc/server/testdata/" + // bcPersistInterval is the time period Blockchain persists changes to the + // underlying storage. + bcPersistInterval = time.Second +) + +var ( + notaryModulePath = filepath.Join("..", "services", "notary") + pathToInternalContracts = filepath.Join("..", "..", "internal", "contracts") +) + +// TestCreateBasicChain generates "../rpc/testdata/testblocks.acc" file which +// contains data for RPC unit tests. It also is a nice integration test. +// To generate new "../rpc/testdata/testblocks.acc", follow the steps: +// 1. Set saveChain down below to true +// 2. Run tests with `$ make test` +func TestCreateBasicChain(t *testing.T) { + const saveChain = false + + bc, validators, committee := chain.NewMultiWithCustomConfig(t, func(cfg *config.ProtocolConfiguration) { + cfg.P2PSigExtensions = true + }) + e := neotest.NewExecutor(t, bc, validators, committee) + + initBasicChain(t, e) + + if saveChain { + outStream, err := os.Create(basicChainPrefix + "testblocks.acc") + require.NoError(t, err) + t.Cleanup(func() { + outStream.Close() + }) + + writer := io.NewBinWriterFromIO(outStream) + writer.WriteU32LE(bc.BlockHeight()) + err = chaindump.Dump(bc, writer, 1, bc.BlockHeight()) + require.NoError(t, err) + } + + require.False(t, saveChain) +} + +func initBasicChain(t *testing.T, e *neotest.Executor) { + if !e.Chain.GetConfig().P2PSigExtensions { + t.Fatal("P2PSitExtensions 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(roles.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()) +} + +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 8bbb6b772..d747a6401 100644 --- a/pkg/core/bench_test.go +++ b/pkg/core/bench_test.go @@ -1,34 +1,33 @@ -package core +package core_test import ( "fmt" "testing" "github.com/nspcc-dev/neo-go/internal/random" - "github.com/nspcc-dev/neo-go/internal/testchain" "github.com/nspcc-dev/neo-go/pkg/config/netmode" + "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/storage" "github.com/nspcc-dev/neo-go/pkg/core/transaction" "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" ) -func BenchmarkVerifyWitness(t *testing.B) { - bc := newTestChain(t) - acc, err := wallet.NewAccount() - require.NoError(t, err) - - tx := bc.newTestTx(acc.Contract.ScriptHash(), []byte{byte(opcode.PUSH1)}) - require.NoError(t, acc.SignTx(netmode.UnitTestNet, tx)) +func BenchmarkBlockchain_VerifyWitness(t *testing.B) { + bc, acc := chain.NewSingle(t) + e := neotest.NewExecutor(t, bc, acc, acc) + tx := e.NewTx(t, []neotest.Signer{acc}, e.NativeHash(t, nativenames.Gas), "transfer", acc.ScriptHash(), acc.Script(), 1, nil) t.ResetTimer() for n := 0; n < t.N; n++ { - _, _ = bc.VerifyWitness(tx.Signers[0].Account, tx, &tx.Scripts[0], 100000000) + _, err := bc.VerifyWitness(tx.Signers[0].Account, tx, &tx.Scripts[0], 100000000) + require.NoError(t, err) } } @@ -57,38 +56,35 @@ func BenchmarkBlockchain_ForEachNEP17Transfer(t *testing.B) { func benchmarkForEachNEP17Transfer(t *testing.B, ps storage.Store, startFromBlock, nBlocksToTake int) { var ( - nonce uint32 = 1 - chainHeight = 2_100 // constant chain height to be able to compare paging results - transfersPerBlock = state.TokenTransferBatchSize/4 + // 4 blocks per batch + chainHeight = 2_100 // constant chain height to be able to compare paging results + transfersPerBlock = state.TokenTransferBatchSize/4 + // 4 blocks per batch state.TokenTransferBatchSize/32 // shift ) - bc := newTestChainWithCustomCfgAndStore(t, ps, nil) - gasHash := bc.contracts.GAS.Hash + bc, validators, committee := chain.NewMultiWithCustomConfigAndStore(t, nil, ps, true) + + e := neotest.NewExecutor(t, bc, validators, committee) + gasHash := e.NativeHash(t, nativenames.Gas) + acc := random.Uint160() + from := e.Validator.ScriptHash() for j := 0; j < chainHeight; j++ { w := io.NewBufBinWriter() for i := 0; i < transfersPerBlock; i++ { - emit.AppCall(w.BinWriter, gasHash, "transfer", callflag.All, testchain.MultisigScriptHash(), acc, 1, nil) + emit.AppCall(w.BinWriter, gasHash, "transfer", callflag.All, from, acc, 1, nil) emit.Opcodes(w.BinWriter, opcode.ASSERT) - require.NoError(t, w.Err) } + require.NoError(t, w.Err) script := w.Bytes() tx := transaction.New(script, int64(1100_0000*transfersPerBlock)) + tx.NetworkFee = 1_0000_000 tx.ValidUntilBlock = bc.BlockHeight() + 1 - tx.Nonce = nonce - nonce++ - tx.Signers = []transaction.Signer{{ - Account: testchain.MultisigScriptHash(), - Scopes: transaction.CalledByEntry, - AllowedContracts: nil, - AllowedGroups: nil, - }} - require.NoError(t, testchain.SignTx(bc, tx)) - b := bc.newBlock(tx) - require.NoError(t, bc.AddBlock(b)) - checkTxHalt(t, bc, tx.Hash()) + tx.Nonce = neotest.Nonce() + tx.Signers = []transaction.Signer{{Account: from, Scopes: transaction.CalledByEntry}} + require.NoError(t, validators.SignTx(netmode.UnitTestNet, tx)) + e.AddNewBlock(t, tx) + e.CheckHalt(t, tx.Hash()) } newestB, err := bc.GetBlock(bc.GetHeaderHash(int(bc.BlockHeight()) - startFromBlock + 1)) diff --git a/pkg/core/blockchain.go b/pkg/core/blockchain.go index 3694d0459..a7bc1f4c7 100644 --- a/pkg/core/blockchain.go +++ b/pkg/core/blockchain.go @@ -2342,6 +2342,14 @@ func (bc *Blockchain) GetMaxVerificationGAS() int64 { return bc.contracts.Policy.GetMaxVerificationGas(bc.dao) } +// GetMaxNotValidBeforeDelta returns maximum NotValidBeforeDelta Notary limit. +func (bc *Blockchain) GetMaxNotValidBeforeDelta() uint32 { + if !bc.config.P2PSigExtensions { + panic("disallowed call to Notary") + } + return bc.contracts.Notary.GetMaxNotValidBeforeDelta(bc.dao) +} + // GetStoragePrice returns current storage price. func (bc *Blockchain) GetStoragePrice() int64 { if bc.BlockHeight() == 0 { diff --git a/pkg/core/blockchain_core_test.go b/pkg/core/blockchain_core_test.go new file mode 100644 index 000000000..4a68fb16c --- /dev/null +++ b/pkg/core/blockchain_core_test.go @@ -0,0 +1,322 @@ +package core + +import ( + "encoding/binary" + "errors" + "fmt" + "strings" + "testing" + "time" + + "github.com/nspcc-dev/neo-go/internal/testchain" + "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/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/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/opcode" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.uber.org/zap/zaptest" +) + +func TestVerifyHeader(t *testing.T) { + bc := newTestChain(t) + prev := bc.topBlock.Load().(*block.Block).Header + t.Run("Invalid", func(t *testing.T) { + t.Run("Hash", func(t *testing.T) { + h := prev.Hash() + h[0] = ^h[0] + hdr := newBlock(bc.config, 1, h).Header + require.True(t, errors.Is(bc.verifyHeader(&hdr, &prev), ErrHdrHashMismatch)) + }) + t.Run("Index", func(t *testing.T) { + hdr := newBlock(bc.config, 3, prev.Hash()).Header + require.True(t, errors.Is(bc.verifyHeader(&hdr, &prev), ErrHdrIndexMismatch)) + }) + t.Run("Timestamp", func(t *testing.T) { + hdr := newBlock(bc.config, 1, prev.Hash()).Header + hdr.Timestamp = 0 + require.True(t, errors.Is(bc.verifyHeader(&hdr, &prev), ErrHdrInvalidTimestamp)) + }) + }) + t.Run("Valid", func(t *testing.T) { + hdr := newBlock(bc.config, 1, prev.Hash()).Header + require.NoError(t, bc.verifyHeader(&hdr, &prev)) + }) +} + +func TestAddBlock(t *testing.T) { + const size = 3 + bc := newTestChain(t) + blocks, err := bc.genBlocks(size) + require.NoError(t, err) + + lastBlock := blocks[len(blocks)-1] + assert.Equal(t, lastBlock.Index, bc.HeaderHeight()) + assert.Equal(t, lastBlock.Hash(), bc.CurrentHeaderHash()) + + // This one tests persisting blocks, so it does need to persist() + _, err = bc.persist(false) + require.NoError(t, err) + + key := make([]byte, 1+util.Uint256Size) + key[0] = byte(storage.DataExecutable) + for _, block := range blocks { + copy(key[1:], block.Hash().BytesBE()) + _, err := bc.dao.Store.Get(key) + require.NoErrorf(t, err, "block %s not persisted", block.Hash()) + } + + assert.Equal(t, lastBlock.Index, bc.BlockHeight()) + assert.Equal(t, lastBlock.Hash(), bc.CurrentHeaderHash()) +} + +func TestRemoveOldTransfers(t *testing.T) { + // Creating proper number of transfers/blocks takes unneccessary time, so emulate + // some DB with stale entries. + bc := newTestChain(t) + h, err := bc.GetHeader(bc.GetHeaderHash(0)) + require.NoError(t, err) + older := h.Timestamp - 1000 + newer := h.Timestamp + 1000 + acc1 := util.Uint160{1} + acc2 := util.Uint160{2} + acc3 := util.Uint160{3} + ttl := state.TokenTransferLog{Raw: []byte{1}} // It's incorrect, but who cares. + + for i := uint32(0); i < 3; i++ { + bc.dao.PutTokenTransferLog(acc1, older, i, false, &ttl) + } + for i := uint32(0); i < 3; i++ { + bc.dao.PutTokenTransferLog(acc2, newer, i, false, &ttl) + } + for i := uint32(0); i < 2; i++ { + bc.dao.PutTokenTransferLog(acc3, older, i, true, &ttl) + } + for i := uint32(0); i < 2; i++ { + bc.dao.PutTokenTransferLog(acc3, newer, i, true, &ttl) + } + + _, err = bc.dao.Persist() + require.NoError(t, err) + _ = bc.removeOldTransfers(0) + + for i := uint32(0); i < 2; i++ { + log, err := bc.dao.GetTokenTransferLog(acc1, older, i, false) + require.NoError(t, err) + require.Equal(t, 0, len(log.Raw)) + } + + log, err := bc.dao.GetTokenTransferLog(acc1, older, 2, false) + require.NoError(t, err) + require.NotEqual(t, 0, len(log.Raw)) + + for i := uint32(0); i < 3; i++ { + log, err = bc.dao.GetTokenTransferLog(acc2, newer, i, false) + require.NoError(t, err) + require.NotEqual(t, 0, len(log.Raw)) + } + + log, err = bc.dao.GetTokenTransferLog(acc3, older, 0, true) + require.NoError(t, err) + require.Equal(t, 0, len(log.Raw)) + + log, err = bc.dao.GetTokenTransferLog(acc3, older, 1, true) + require.NoError(t, err) + require.NotEqual(t, 0, len(log.Raw)) + + for i := uint32(0); i < 2; i++ { + log, err = bc.dao.GetTokenTransferLog(acc3, newer, i, true) + require.NoError(t, err) + require.NotEqual(t, 0, len(log.Raw)) + } +} + +func TestBlockchain_InitWithIncompleteStateJump(t *testing.T) { + var ( + stateSyncInterval = 4 + maxTraceable uint32 = 6 + ) + spountCfg := func(c *config.Config) { + c.ProtocolConfiguration.RemoveUntraceableBlocks = true + c.ProtocolConfiguration.StateRootInHeader = true + c.ProtocolConfiguration.P2PStateExchangeExtensions = true + c.ProtocolConfiguration.StateSyncInterval = stateSyncInterval + c.ProtocolConfiguration.MaxTraceableBlocks = maxTraceable + c.ProtocolConfiguration.KeepOnlyLatestState = true + } + bcSpout := newTestChainWithCustomCfg(t, spountCfg) + + // Generate some content. + for i := 0; i < len(bcSpout.GetConfig().StandbyCommittee); i++ { + require.NoError(t, bcSpout.AddBlock(bcSpout.newBlock())) + } + + // reach next to the latest state sync point and pretend that we've just restored + stateSyncPoint := (int(bcSpout.BlockHeight())/stateSyncInterval + 1) * stateSyncInterval + for i := bcSpout.BlockHeight() + 1; i <= uint32(stateSyncPoint); i++ { + require.NoError(t, bcSpout.AddBlock(bcSpout.newBlock())) + } + require.Equal(t, uint32(stateSyncPoint), bcSpout.BlockHeight()) + b := bcSpout.newBlock() + require.NoError(t, bcSpout.AddHeaders(&b.Header)) + + // put storage items with STTemp prefix + batch := storage.NewMemCachedStore(bcSpout.dao.Store) + tempPrefix := storage.STTempStorage + if bcSpout.dao.Version.StoragePrefix == tempPrefix { + tempPrefix = storage.STStorage + } + bPrefix := make([]byte, 1) + bPrefix[0] = byte(bcSpout.dao.Version.StoragePrefix) + bcSpout.dao.Store.Seek(storage.SeekRange{Prefix: bPrefix}, func(k, v []byte) bool { + key := slice.Copy(k) + key[0] = byte(tempPrefix) + value := slice.Copy(v) + batch.Put(key, value) + return true + }) + _, err := batch.Persist() + require.NoError(t, err) + + checkNewBlockchainErr := func(t *testing.T, cfg func(c *config.Config), store storage.Store, errText string) { + unitTestNetCfg, err := config.Load("../../config", testchain.Network()) + require.NoError(t, err) + cfg(&unitTestNetCfg) + log := zaptest.NewLogger(t) + _, err = NewBlockchain(store, unitTestNetCfg.ProtocolConfiguration, log) + if len(errText) != 0 { + require.Error(t, err) + require.True(t, strings.Contains(err.Error(), errText)) + } else { + require.NoError(t, err) + } + } + boltCfg := func(c *config.Config) { + spountCfg(c) + c.ProtocolConfiguration.KeepOnlyLatestState = true + } + // manually store statejump stage to check statejump recover process + bPrefix[0] = byte(storage.SYSStateJumpStage) + t.Run("invalid RemoveUntraceableBlocks setting", func(t *testing.T) { + bcSpout.dao.Store.Put(bPrefix, []byte{byte(stateJumpStarted)}) + checkNewBlockchainErr(t, func(c *config.Config) { + boltCfg(c) + c.ProtocolConfiguration.RemoveUntraceableBlocks = false + }, bcSpout.dao.Store, "state jump was not completed, but P2PStateExchangeExtensions are disabled or archival node capability is on") + }) + t.Run("invalid state jump stage format", func(t *testing.T) { + bcSpout.dao.Store.Put(bPrefix, []byte{0x01, 0x02}) + checkNewBlockchainErr(t, boltCfg, bcSpout.dao.Store, "invalid state jump stage format") + }) + t.Run("missing state sync point", func(t *testing.T) { + bcSpout.dao.Store.Put(bPrefix, []byte{byte(stateJumpStarted)}) + checkNewBlockchainErr(t, boltCfg, bcSpout.dao.Store, "failed to get state sync point from the storage") + }) + t.Run("invalid state sync point", func(t *testing.T) { + bcSpout.dao.Store.Put(bPrefix, []byte{byte(stateJumpStarted)}) + point := make([]byte, 4) + binary.LittleEndian.PutUint32(point, uint32(len(bcSpout.headerHashes))) + bcSpout.dao.Store.Put([]byte{byte(storage.SYSStateSyncPoint)}, point) + checkNewBlockchainErr(t, boltCfg, bcSpout.dao.Store, "invalid state sync point") + }) + for _, stage := range []stateJumpStage{stateJumpStarted, newStorageItemsAdded, genesisStateRemoved, 0x03} { + t.Run(fmt.Sprintf("state jump stage %d", stage), func(t *testing.T) { + bcSpout.dao.Store.Put(bPrefix, []byte{byte(stage)}) + point := make([]byte, 4) + binary.LittleEndian.PutUint32(point, uint32(stateSyncPoint)) + bcSpout.dao.Store.Put([]byte{byte(storage.SYSStateSyncPoint)}, point) + var errText string + if stage == 0x03 { + errText = "unknown state jump stage" + } + checkNewBlockchainErr(t, spountCfg, bcSpout.dao.Store, errText) + }) + } +} + +func TestChainWithVolatileNumOfValidators(t *testing.T) { + bc := newTestChainWithCustomCfg(t, func(c *config.Config) { + c.ProtocolConfiguration.ValidatorsCount = 0 + c.ProtocolConfiguration.CommitteeHistory = map[uint32]int{ + 0: 1, + 4: 4, + 24: 6, + } + c.ProtocolConfiguration.ValidatorsHistory = map[uint32]int{ + 0: 1, + 4: 4, + } + require.NoError(t, c.ProtocolConfiguration.Validate()) + }) + require.Equal(t, uint32(0), bc.BlockHeight()) + + priv0 := testchain.PrivateKeyByID(0) + + vals, err := bc.GetValidators() + require.NoError(t, err) + script, err := smartcontract.CreateDefaultMultiSigRedeemScript(vals) + require.NoError(t, err) + curWit := transaction.Witness{ + VerificationScript: script, + } + for i := 1; i < 26; i++ { + comm, err := bc.GetCommittee() + require.NoError(t, err) + if i < 5 { + require.Equal(t, 1, len(comm)) + } else if i < 25 { + require.Equal(t, 4, len(comm)) + } else { + require.Equal(t, 6, len(comm)) + } + // Mimic consensus. + if bc.config.ShouldUpdateCommitteeAt(uint32(i)) { + vals, err = bc.GetValidators() + } else { + vals, err = bc.GetNextBlockValidators() + } + require.NoError(t, err) + if i < 4 { + require.Equalf(t, 1, len(vals), "at %d", i) + } else { + require.Equalf(t, 4, len(vals), "at %d", i) + } + require.NoError(t, err) + script, err := smartcontract.CreateDefaultMultiSigRedeemScript(vals) + require.NoError(t, err) + nextWit := transaction.Witness{ + VerificationScript: script, + } + b := &block.Block{ + Header: block.Header{ + NextConsensus: nextWit.ScriptHash(), + Script: curWit, + }, + } + curWit = nextWit + b.PrevHash = bc.GetHeaderHash(i - 1) + b.Timestamp = uint64(time.Now().UTC().Unix())*1000 + uint64(i) + b.Index = uint32(i) + b.RebuildMerkleRoot() + if i < 5 { + signa := priv0.SignHashable(uint32(bc.config.Magic), b) + b.Script.InvocationScript = append([]byte{byte(opcode.PUSHDATA1), byte(len(signa))}, signa...) + } else { + b.Script.InvocationScript = testchain.Sign(b) + } + err = bc.AddBlock(b) + require.NoErrorf(t, err, "at %d", i) + } +} + +func setSigner(tx *transaction.Transaction, h util.Uint160) { + tx.Signers = []transaction.Signer{{ + Account: h, + Scopes: transaction.Global, + }} +} diff --git a/pkg/core/blockchain_test.go b/pkg/core/blockchain_neotest_test.go similarity index 51% rename from pkg/core/blockchain_test.go rename to pkg/core/blockchain_neotest_test.go index 7e827461e..e09be9a04 100644 --- a/pkg/core/blockchain_test.go +++ b/pkg/core/blockchain_neotest_test.go @@ -1,20 +1,21 @@ -package core +package core_test import ( "encoding/binary" "errors" "fmt" "math/big" - "math/rand" "path/filepath" "strings" "testing" "time" + "github.com/nspcc-dev/neo-go/internal/contracts" "github.com/nspcc-dev/neo-go/internal/random" - "github.com/nspcc-dev/neo-go/internal/testchain" + "github.com/nspcc-dev/neo-go/pkg/compiler" "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/chaindump" "github.com/nspcc-dev/neo-go/pkg/core/dao" @@ -24,20 +25,20 @@ import ( "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/nativeprices" - "github.com/nspcc-dev/neo-go/pkg/core/native/noderoles" "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/encoding/address" + "github.com/nspcc-dev/neo-go/pkg/interop/native/roles" "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/response/result/subscriptions" "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/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" "github.com/nspcc-dev/neo-go/pkg/vm/emit" "github.com/nspcc-dev/neo-go/pkg/vm/opcode" @@ -45,138 +46,418 @@ import ( "github.com/nspcc-dev/neo-go/pkg/wallet" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "go.uber.org/zap/zaptest" ) -func TestVerifyHeader(t *testing.T) { - bc := newTestChain(t) - prev := bc.topBlock.Load().(*block.Block).Header - t.Run("Invalid", func(t *testing.T) { - t.Run("Hash", func(t *testing.T) { - h := prev.Hash() - h[0] = ^h[0] - hdr := newBlock(bc.config, 1, h).Header - require.True(t, errors.Is(bc.verifyHeader(&hdr, &prev), ErrHdrHashMismatch)) - }) - t.Run("Index", func(t *testing.T) { - hdr := newBlock(bc.config, 3, prev.Hash()).Header - require.True(t, errors.Is(bc.verifyHeader(&hdr, &prev), ErrHdrIndexMismatch)) - }) - t.Run("Timestamp", func(t *testing.T) { - hdr := newBlock(bc.config, 1, prev.Hash()).Header - hdr.Timestamp = 0 - require.True(t, errors.Is(bc.verifyHeader(&hdr, &prev), ErrHdrInvalidTimestamp)) - }) +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("Valid", func(t *testing.T) { - hdr := newBlock(bc.config, 1, prev.Hash()).Header - require.NoError(t, bc.verifyHeader(&hdr, &prev)) + 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 TestAddHeaders(t *testing.T) { - bc := newTestChain(t) - lastBlock := bc.topBlock.Load().(*block.Block) - h1 := newBlock(bc.config, 1, lastBlock.Hash()).Header - h2 := newBlock(bc.config, 2, h1.Hash()).Header - h3 := newBlock(bc.config, 3, h2.Hash()).Header +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() + } + dbOptions := storage.LevelDBOptions{ + DataDirectoryPath: dbPath, + } + newLevelStore, err := storage.NewLevelDBStore(dbOptions) + require.Nil(t, err, "NewLevelDBStore error") + return newLevelStore, dbPath +} + +func TestBlockchain_StartFromExistingDB(t *testing.T) { + ps, path := newLevelDBForTestingWithPath(t, "") + customConfig := func(c *config.ProtocolConfiguration) { + c.StateRootInHeader = true // Need for P2PStateExchangeExtensions check. + c.P2PSigExtensions = true // Need for basic chain initializer. + } + bc, validators, committee, err := chain.NewMultiWithCustomConfigAndStoreNoCheck(t, customConfig, ps) + require.NoError(t, err) + go bc.Run() + e := neotest.NewExecutor(t, bc, validators, committee) + initBasicChain(t, e) + require.True(t, bc.BlockHeight() > 5, "ensure that basic chain is correctly initialised") + + // Information for further tests. + h := bc.BlockHeight() + cryptoLibHash, err := bc.GetNativeContractScriptHash(nativenames.CryptoLib) + require.NoError(t, err) + cryptoLibState := bc.GetContractState(cryptoLibHash) + require.NotNil(t, cryptoLibState) + var ( + managementID = -1 + managementContractPrefix = 8 + ) + + bc.Close() // Ensure persist is done and persistent store is properly closed. + + newPS := func(t *testing.T) storage.Store { + ps, _ = newLevelDBForTestingWithPath(t, path) + t.Cleanup(func() { require.NoError(t, ps.Close()) }) + return ps + } + t.Run("mismatch storage version", func(t *testing.T) { + ps = newPS(t) + cache := storage.NewMemCachedStore(ps) // Extra wrapper to avoid good DB corruption. + d := dao.NewSimple(cache, bc.GetConfig().StateRootInHeader, bc.GetConfig().P2PStateExchangeExtensions) + d.PutVersion(dao.Version{ + Value: "0.0.0", + }) + _, err := d.Persist() // Persist to `cache` wrapper. + require.NoError(t, err) + _, _, _, err = chain.NewMultiWithCustomConfigAndStoreNoCheck(t, customConfig, cache) + require.Error(t, err) + require.True(t, strings.Contains(err.Error(), "storage version mismatch"), err) + }) + t.Run("mismatch StateRootInHeader", func(t *testing.T) { + ps = newPS(t) + _, _, _, err := chain.NewMultiWithCustomConfigAndStoreNoCheck(t, func(c *config.ProtocolConfiguration) { + customConfig(c) + c.StateRootInHeader = false + }, ps) + require.Error(t, err) + require.True(t, strings.Contains(err.Error(), "StateRootInHeader setting mismatch"), err) + }) + t.Run("mismatch P2PSigExtensions", func(t *testing.T) { + ps = newPS(t) + _, _, _, err := chain.NewMultiWithCustomConfigAndStoreNoCheck(t, func(c *config.ProtocolConfiguration) { + customConfig(c) + c.P2PSigExtensions = false + }, ps) + require.Error(t, err) + require.True(t, strings.Contains(err.Error(), "P2PSigExtensions setting mismatch"), err) + }) + t.Run("mismatch P2PStateExchangeExtensions", func(t *testing.T) { + ps = newPS(t) + _, _, _, err := chain.NewMultiWithCustomConfigAndStoreNoCheck(t, func(c *config.ProtocolConfiguration) { + customConfig(c) + c.StateRootInHeader = true + c.P2PStateExchangeExtensions = true + }, ps) + require.Error(t, err) + require.True(t, strings.Contains(err.Error(), "P2PStateExchangeExtensions setting mismatch"), err) + }) + t.Run("mismatch KeepOnlyLatestState", func(t *testing.T) { + ps = newPS(t) + _, _, _, err := chain.NewMultiWithCustomConfigAndStoreNoCheck(t, func(c *config.ProtocolConfiguration) { + customConfig(c) + c.KeepOnlyLatestState = true + }, ps) + require.Error(t, err) + require.True(t, strings.Contains(err.Error(), "KeepOnlyLatestState setting mismatch"), err) + }) + t.Run("corrupted headers", func(t *testing.T) { + ps = newPS(t) + + // Corrupt headers hashes batch. + cache := storage.NewMemCachedStore(ps) // Extra wrapper to avoid good DB corruption. + key := make([]byte, 5) + key[0] = byte(storage.IXHeaderHashList) + binary.BigEndian.PutUint32(key[1:], 1) + cache.Put(key, []byte{1, 2, 3}) + + _, _, _, err := chain.NewMultiWithCustomConfigAndStoreNoCheck(t, customConfig, cache) + require.Error(t, err) + require.True(t, strings.Contains(err.Error(), "failed to read batch of 2000"), err) + }) + t.Run("corrupted current header height", func(t *testing.T) { + ps = newPS(t) + + // Remove current header. + cache := storage.NewMemCachedStore(ps) // Extra wrapper to avoid good DB corruption. + cache.Delete([]byte{byte(storage.SYSCurrentHeader)}) + + _, _, _, err := chain.NewMultiWithCustomConfigAndStoreNoCheck(t, customConfig, cache) + require.Error(t, err) + require.True(t, strings.Contains(err.Error(), "failed to retrieve current header"), err) + }) + t.Run("missing last batch of 2000 headers and missing last header", func(t *testing.T) { + ps = newPS(t) + + // Remove latest headers hashes batch and current header. + cache := storage.NewMemCachedStore(ps) // Extra wrapper to avoid good DB corruption. + cache.Delete([]byte{byte(storage.IXHeaderHashList)}) + currHeaderInfo, err := cache.Get([]byte{byte(storage.SYSCurrentHeader)}) + require.NoError(t, err) + currHeaderHash, err := util.Uint256DecodeBytesLE(currHeaderInfo[:32]) + require.NoError(t, err) + cache.Delete(append([]byte{byte(storage.DataExecutable)}, currHeaderHash.BytesBE()...)) + + _, _, _, err = chain.NewMultiWithCustomConfigAndStoreNoCheck(t, customConfig, cache) + require.Error(t, err) + require.True(t, strings.Contains(err.Error(), "could not get header"), err) + }) + t.Run("missing last block", func(t *testing.T) { + ps = newPS(t) + + // Remove current block from storage. + cache := storage.NewMemCachedStore(ps) // Extra wrapper to avoid good DB corruption. + cache.Delete([]byte{byte(storage.SYSCurrentBlock)}) + + _, _, _, err := chain.NewMultiWithCustomConfigAndStoreNoCheck(t, customConfig, cache) + require.Error(t, err) + require.True(t, strings.Contains(err.Error(), "failed to retrieve current block height"), err) + }) + t.Run("missing last stateroot", func(t *testing.T) { + ps = newPS(t) + + // Remove latest stateroot from storage. + cache := storage.NewMemCachedStore(ps) // Extra wrapper to avoid good DB corruption. + key := make([]byte, 5) + key[0] = byte(storage.DataMPTAux) + binary.BigEndian.PutUint32(key, h) + cache.Delete(key) + + _, _, _, err := chain.NewMultiWithCustomConfigAndStoreNoCheck(t, customConfig, cache) + require.Error(t, err) + require.True(t, strings.Contains(err.Error(), "can't init MPT at height"), err) + }) + t.Run("failed native Management initialisation", func(t *testing.T) { + ps = newPS(t) + + // Corrupt serialised CryptoLib state. + cache := storage.NewMemCachedStore(ps) // Extra wrapper to avoid good DB corruption. + key := make([]byte, 1+4+1+20) + key[0] = byte(storage.STStorage) + binary.LittleEndian.PutUint32(key[1:], uint32(managementID)) + key[5] = byte(managementContractPrefix) + copy(key[6:], cryptoLibHash.BytesBE()) + cache.Put(key, []byte{1, 2, 3}) + + _, _, _, err := chain.NewMultiWithCustomConfigAndStoreNoCheck(t, customConfig, cache) + require.Error(t, err) + require.True(t, strings.Contains(err.Error(), "can't init cache for Management native contract"), err) + }) + t.Run("invalid native contract deactivation", func(t *testing.T) { + ps = newPS(t) + _, _, _, err := chain.NewMultiWithCustomConfigAndStoreNoCheck(t, func(c *config.ProtocolConfiguration) { + customConfig(c) + c.NativeUpdateHistories = map[string][]uint32{ + nativenames.Policy: {0}, + nativenames.Neo: {0}, + nativenames.Gas: {0}, + nativenames.Designation: {0}, + nativenames.StdLib: {0}, + nativenames.Management: {0}, + nativenames.Oracle: {0}, + nativenames.Ledger: {0}, + nativenames.Notary: {0}, + nativenames.CryptoLib: {h + 10}, + } + }, ps) + require.Error(t, err) + require.True(t, strings.Contains(err.Error(), fmt.Sprintf("native contract %s is already stored, but marked as inactive for height %d in config", nativenames.CryptoLib, h)), err) + }) + t.Run("invalid native contract activation", func(t *testing.T) { + ps = newPS(t) + + // Remove CryptoLib from storage. + cache := storage.NewMemCachedStore(ps) // Extra wrapper to avoid good DB corruption. + key := make([]byte, 1+4+1+20) + key[0] = byte(storage.STStorage) + binary.LittleEndian.PutUint32(key[1:], uint32(managementID)) + key[5] = byte(managementContractPrefix) + copy(key[6:], cryptoLibHash.BytesBE()) + cache.Delete(key) + + _, _, _, err := chain.NewMultiWithCustomConfigAndStoreNoCheck(t, customConfig, cache) + require.Error(t, err) + require.True(t, strings.Contains(err.Error(), fmt.Sprintf("native contract %s is not stored, but should be active at height %d according to config", nativenames.CryptoLib, h)), err) + }) + t.Run("stored and autogenerated native contract's states mismatch", func(t *testing.T) { + ps = newPS(t) + + // Change stored CryptoLib state. + cache := storage.NewMemCachedStore(ps) // Extra wrapper to avoid good DB corruption. + key := make([]byte, 1+4+1+20) + key[0] = byte(storage.STStorage) + binary.LittleEndian.PutUint32(key[1:], uint32(managementID)) + key[5] = byte(managementContractPrefix) + copy(key[6:], cryptoLibHash.BytesBE()) + cs := *cryptoLibState + cs.ID = -123 + csBytes, err := stackitem.SerializeConvertible(&cs) + require.NoError(t, err) + cache.Put(key, csBytes) + + _, _, _, err = chain.NewMultiWithCustomConfigAndStoreNoCheck(t, customConfig, cache) + require.Error(t, err) + require.True(t, strings.Contains(err.Error(), fmt.Sprintf("native %s: version mismatch (stored contract state differs from autogenerated one)", nativenames.CryptoLib)), err) + }) + + t.Run("good", func(t *testing.T) { + ps = newPS(t) + _, _, _, err := chain.NewMultiWithCustomConfigAndStoreNoCheck(t, customConfig, ps) + require.NoError(t, err) + }) +} + +func TestBlockchain_AddHeaders(t *testing.T) { + bc, acc := chain.NewSingleWithCustomConfig(t, func(c *config.ProtocolConfiguration) { + c.StateRootInHeader = true + }) + e := neotest.NewExecutor(t, bc, acc, acc) + + newHeader := func(t *testing.T, index uint32, prevHash util.Uint256, timestamp uint64) *block.Header { + b := e.NewUnsignedBlock(t) + b.Index = index + b.PrevHash = prevHash + b.PrevStateRoot = util.Uint256{} + b.Timestamp = timestamp + e.SignBlock(b) + return &b.Header + } + b1 := e.NewUnsignedBlock(t) + h1 := &e.SignBlock(b1).Header + h2 := newHeader(t, h1.Index+1, h1.Hash(), h1.Timestamp+1) + h3 := newHeader(t, h2.Index+1, h2.Hash(), h2.Timestamp+1) require.NoError(t, bc.AddHeaders()) - require.NoError(t, bc.AddHeaders(&h1, &h2)) - require.NoError(t, bc.AddHeaders(&h2, &h3)) + require.NoError(t, bc.AddHeaders(h1, h2)) + require.NoError(t, bc.AddHeaders(h2, h3)) assert.Equal(t, h3.Index, bc.HeaderHeight()) assert.Equal(t, uint32(0), bc.BlockHeight()) assert.Equal(t, h3.Hash(), bc.CurrentHeaderHash()) // Add them again, they should not be added. - require.NoError(t, bc.AddHeaders(&h3, &h2, &h1)) + require.NoError(t, bc.AddHeaders(h3, h2, h1)) assert.Equal(t, h3.Index, bc.HeaderHeight()) assert.Equal(t, uint32(0), bc.BlockHeight()) assert.Equal(t, h3.Hash(), bc.CurrentHeaderHash()) - h4 := newBlock(bc.config, 4, h3.Hash().Reverse()).Header - h5 := newBlock(bc.config, 5, h4.Hash()).Header + h4Bad := newHeader(t, h3.Index+1, h3.Hash().Reverse(), h3.Timestamp+1) + h5Bad := newHeader(t, h4Bad.Index+1, h4Bad.Hash(), h4Bad.Timestamp+1) - assert.Error(t, bc.AddHeaders(&h4, &h5)) + assert.Error(t, bc.AddHeaders(h4Bad, h5Bad)) assert.Equal(t, h3.Index, bc.HeaderHeight()) assert.Equal(t, uint32(0), bc.BlockHeight()) assert.Equal(t, h3.Hash(), bc.CurrentHeaderHash()) - h6 := newBlock(bc.config, 4, h3.Hash()).Header - h6.Script.InvocationScript = nil - assert.Error(t, bc.AddHeaders(&h6)) + h4Bad2 := newHeader(t, h3.Index+1, h3.Hash().Reverse(), h3.Timestamp+1) + h4Bad2.Script.InvocationScript = []byte{} + assert.Error(t, bc.AddHeaders(h4Bad2)) assert.Equal(t, h3.Index, bc.HeaderHeight()) assert.Equal(t, uint32(0), bc.BlockHeight()) assert.Equal(t, h3.Hash(), bc.CurrentHeaderHash()) } -func TestAddBlock(t *testing.T) { - const size = 3 - bc := newTestChain(t) - blocks, err := bc.genBlocks(size) - require.NoError(t, err) - - lastBlock := blocks[len(blocks)-1] - assert.Equal(t, lastBlock.Index, bc.HeaderHeight()) - assert.Equal(t, lastBlock.Hash(), bc.CurrentHeaderHash()) - - // This one tests persisting blocks, so it does need to persist() - _, err = bc.persist(false) - require.NoError(t, err) - - key := make([]byte, 1+util.Uint256Size) - key[0] = byte(storage.DataExecutable) - for _, block := range blocks { - copy(key[1:], block.Hash().BytesBE()) - _, err := bc.dao.Store.Get(key) - require.NoErrorf(t, err, "block %s not persisted", block.Hash()) - } - - assert.Equal(t, lastBlock.Index, bc.BlockHeight()) - assert.Equal(t, lastBlock.Hash(), bc.CurrentHeaderHash()) -} - -func TestAddBlockStateRoot(t *testing.T) { - bc := newTestChainWithCustomCfg(t, func(c *config.Config) { - c.ProtocolConfiguration.StateRootInHeader = true +func TestBlockchain_AddBlockStateRoot(t *testing.T) { + bc, acc := chain.NewSingleWithCustomConfig(t, func(c *config.ProtocolConfiguration) { + c.StateRootInHeader = true }) + e := neotest.NewExecutor(t, bc, acc, acc) sr, err := bc.GetStateModule().GetStateRoot(bc.BlockHeight()) require.NoError(t, err) - tx := newNEP17Transfer(bc.contracts.NEO.Hash, neoOwner, util.Uint160{}, 1) - tx.ValidUntilBlock = bc.BlockHeight() + 1 - addSigners(neoOwner, tx) - require.NoError(t, testchain.SignTx(bc, tx)) - - lastBlock := bc.topBlock.Load().(*block.Block) - b := newBlock(bc.config, lastBlock.Index+1, lastBlock.Hash(), tx) + b := e.NewUnsignedBlock(t) + b.StateRootEnabled = false + b.PrevStateRoot = util.Uint256{} + e.SignBlock(b) err = bc.AddBlock(b) - require.True(t, errors.Is(err, ErrHdrStateRootSetting), "got: %v", err) + require.True(t, errors.Is(err, core.ErrHdrStateRootSetting), "got: %v", err) u := sr.Root u[0] ^= 0xFF - b = newBlockWithState(bc.config, lastBlock.Index+1, lastBlock.Hash(), &u, tx) + b = e.NewUnsignedBlock(t) + b.PrevStateRoot = u + e.SignBlock(b) err = bc.AddBlock(b) - require.True(t, errors.Is(err, ErrHdrInvalidStateRoot), "got: %v", err) + require.True(t, errors.Is(err, core.ErrHdrInvalidStateRoot), "got: %v", err) - b = bc.newBlock(tx) + b = e.NewUnsignedBlock(t) + e.SignBlock(b) require.NoError(t, bc.AddBlock(b)) } -func TestAddHeadersStateRoot(t *testing.T) { - bc := newTestChainWithCustomCfg(t, func(c *config.Config) { - c.ProtocolConfiguration.StateRootInHeader = true +func TestBlockchain_AddHeadersStateRoot(t *testing.T) { + bc, acc := chain.NewSingleWithCustomConfig(t, func(c *config.ProtocolConfiguration) { + c.StateRootInHeader = true }) + e := neotest.NewExecutor(t, bc, acc, acc) - r := bc.stateRoot.CurrentLocalStateRoot() - h1 := bc.newBlock().Header + b := e.NewUnsignedBlock(t) + e.SignBlock(b) + h1 := b.Header + r := bc.GetStateModule().CurrentLocalStateRoot() // invalid stateroot h1.PrevStateRoot[0] ^= 0xFF - require.True(t, errors.Is(bc.AddHeaders(&h1), ErrHdrInvalidStateRoot)) + require.True(t, errors.Is(bc.AddHeaders(&h1), core.ErrHdrInvalidStateRoot)) // valid stateroot h1.PrevStateRoot = r @@ -184,97 +465,93 @@ func TestAddHeadersStateRoot(t *testing.T) { // unable to verify stateroot (stateroot is computed for block #0 only => can // verify stateroot of header #1 only) => just store the header - h2 := newBlockWithState(bc.config, 2, h1.Hash(), nil).Header - require.NoError(t, bc.AddHeaders(&h2)) + b = e.NewUnsignedBlock(t) + b.PrevHash = h1.Hash() + b.Timestamp = h1.Timestamp + 1 + b.PrevStateRoot = util.Uint256{} + b.Index = h1.Index + 1 + e.SignBlock(b) + require.NoError(t, bc.AddHeaders(&b.Header)) } -func TestAddBadBlock(t *testing.T) { - bc := newTestChain(t) - // It has ValidUntilBlock == 0, which is wrong - tx := transaction.New([]byte{byte(opcode.PUSH1)}, 0) - tx.Signers = []transaction.Signer{{ - Account: testchain.MultisigScriptHash(), - Scopes: transaction.None, - }} - require.NoError(t, testchain.SignTx(bc, tx)) - b1 := bc.newBlock(tx) - - require.Error(t, bc.AddBlock(b1)) - bc.config.VerifyTransactions = false - require.NoError(t, bc.AddBlock(b1)) - - b2 := bc.newBlock() - b2.PrevHash = util.Uint256{} - - require.Error(t, bc.AddBlock(b2)) - bc.config.VerifyBlocks = false - require.NoError(t, bc.AddBlock(b2)) - - tx = transaction.New([]byte{byte(opcode.PUSH1)}, 0) - tx.ValidUntilBlock = 128 - tx.Signers = []transaction.Signer{{ - Account: testchain.MultisigScriptHash(), - Scopes: transaction.None, - }} - require.NoError(t, testchain.SignTx(bc, tx)) - require.NoError(t, bc.PoolTx(tx)) - bc.config.VerifyTransactions = true - bc.config.VerifyBlocks = true - b3 := bc.newBlock(tx) - require.NoError(t, bc.AddBlock(b3)) -} - -func TestGetHeader(t *testing.T) { - bc := newTestChain(t) - tx := transaction.New([]byte{byte(opcode.PUSH1)}, 0) - tx.ValidUntilBlock = bc.BlockHeight() + 1 - addSigners(neoOwner, tx) - assert.Nil(t, testchain.SignTx(bc, tx)) - block := bc.newBlock(tx) - err := bc.AddBlock(block) - assert.Nil(t, err) - - // Test unpersisted and persisted access - for i := 0; i < 2; i++ { - hash := block.Hash() - header, err := bc.GetHeader(hash) - require.NoError(t, err) - assert.Equal(t, &block.Header, header) - - b2 := bc.newBlock() - _, err = bc.GetHeader(b2.Hash()) - assert.Error(t, err) - _, err = bc.persist(false) - assert.NoError(t, err) - } -} - -func TestGetBlock(t *testing.T) { - bc := newTestChain(t) - blocks, err := bc.genBlocks(100) - require.NoError(t, err) - - // Test unpersisted and persisted access - for j := 0; j < 2; j++ { - for i := 0; i < len(blocks); i++ { - block, err := bc.GetBlock(blocks[i].Hash()) - require.NoErrorf(t, err, "can't get block %d: %s, attempt %d", i, err, j) - assert.Equal(t, blocks[i].Index, block.Index) - assert.Equal(t, blocks[i].Hash(), block.Hash()) +func TestBlockchain_AddBadBlock(t *testing.T) { + check := func(t *testing.T, b *block.Block, cfg func(c *config.ProtocolConfiguration)) { + bc, _ := chain.NewSingleWithCustomConfig(t, cfg) + err := bc.AddBlock(b) + if cfg == nil { + require.Error(t, err) + } else { + require.NoError(t, err) } - _, err = bc.persist(false) - assert.NoError(t, err) + } + bc, acc := chain.NewSingle(t) + e := neotest.NewExecutor(t, bc, acc, acc) + neoHash := e.NativeHash(t, nativenames.Neo) + + tx := e.NewUnsignedTx(t, neoHash, "transfer", acc.ScriptHash(), util.Uint160{1, 2, 3}, 1, nil) + tx.ValidUntilBlock = 0 // Intentionally make the transaction invalid. + e.SignTx(t, tx, -1, acc) + b := e.NewUnsignedBlock(t, tx) + e.SignBlock(b) + check(t, b, nil) + check(t, b, func(c *config.ProtocolConfiguration) { + c.VerifyBlocks = false + }) + + b = e.NewUnsignedBlock(t) + b.PrevHash = util.Uint256{} // Intentionally make block invalid. + e.SignBlock(b) + check(t, b, nil) + check(t, b, func(c *config.ProtocolConfiguration) { + c.VerifyBlocks = false + }) + + tx = e.NewUnsignedTx(t, neoHash, "transfer", acc.ScriptHash(), util.Uint160{1, 2, 3}, 1, nil) // Check the good tx. + e.SignTx(t, tx, -1, acc) + b = e.NewUnsignedBlock(t, tx) + e.SignBlock(b) + check(t, b, func(c *config.ProtocolConfiguration) { + c.VerifyTransactions = true + c.VerifyBlocks = true + }) +} + +func TestBlockchain_GetHeader(t *testing.T) { + bc, acc := chain.NewSingle(t) + e := neotest.NewExecutor(t, bc, acc, acc) + + block := e.AddNewBlock(t) + hash := block.Hash() + header, err := bc.GetHeader(hash) + require.NoError(t, err) + assert.Equal(t, &block.Header, header) + + b2 := e.NewUnsignedBlock(t) + _, err = bc.GetHeader(b2.Hash()) + assert.Error(t, err) +} + +func TestBlockchain_GetBlock(t *testing.T) { + bc, acc := chain.NewSingle(t) + e := neotest.NewExecutor(t, bc, acc, acc) + blocks := e.GenerateNewBlocks(t, 10) + neoValidatorInvoker := e.ValidatorInvoker(e.NativeHash(t, nativenames.Neo)) + + for i := 0; i < len(blocks); i++ { + block, err := bc.GetBlock(blocks[i].Hash()) + require.NoErrorf(t, err, "can't get block %d: %s", i, err) + assert.Equal(t, blocks[i].Index, block.Index) + assert.Equal(t, blocks[i].Hash(), block.Hash()) } t.Run("store only header", func(t *testing.T) { t.Run("non-empty block", func(t *testing.T) { - tx, err := testchain.NewTransferFromOwner(bc, bc.contracts.NEO.Hash, - random.Uint160(), 1, 1, 1000) - require.NoError(t, err) - b := bc.newBlock(tx) + tx := neoValidatorInvoker.PrepareInvoke(t, "transfer", acc.ScriptHash(), util.Uint160{1, 2, 3}, 1, nil) + b := e.NewUnsignedBlock(t, tx) + e.SignBlock(b) require.NoError(t, bc.AddHeaders(&b.Header)) - _, err = bc.GetBlock(b.Hash()) + _, err := bc.GetBlock(b.Hash()) require.Error(t, err) _, err = bc.GetHeader(b.Hash()) @@ -286,869 +563,58 @@ func TestGetBlock(t *testing.T) { require.NoError(t, err) }) t.Run("empty block", func(t *testing.T) { - b := bc.newBlock() + b := e.NewUnsignedBlock(t) + e.SignBlock(b) + require.NoError(t, bc.AddHeaders(&b.Header)) - _, err = bc.GetBlock(b.Hash()) + _, err := bc.GetBlock(b.Hash()) require.NoError(t, err) }) }) } -func (bc *Blockchain) newTestTx(h util.Uint160, script []byte) *transaction.Transaction { - tx := transaction.New(script, 1_000_000) - tx.Nonce = rand.Uint32() - tx.ValidUntilBlock = 100 - tx.Signers = []transaction.Signer{{ - Account: h, - Scopes: transaction.CalledByEntry, - }} - tx.NetworkFee = int64(io.GetVarSize(tx)+200 /* witness */) * bc.FeePerByte() - tx.NetworkFee += 1_000_000 // verification cost - return tx -} +func TestBlockchain_VerifyHashAgainstScript(t *testing.T) { + bc, acc := chain.NewSingle(t) + e := neotest.NewExecutor(t, bc, acc, acc) -func TestVerifyTx(t *testing.T) { - bc := newTestChain(t) - - accs := make([]*wallet.Account, 5) - for i := range accs { - var err error - accs[i], err = wallet.NewAccount() - require.NoError(t, err) + cs, csInvalid := contracts.GetTestContractState(t, pathToInternalContracts, 0, 1, acc.ScriptHash()) + c1 := &neotest.Contract{ + Hash: cs.Hash, + NEF: &cs.NEF, + Manifest: &cs.Manifest, } - - notaryServiceFeePerKey := bc.contracts.Notary.GetNotaryServiceFeePerKey(bc.dao) - - oracleAcc := accs[2] - oraclePubs := keys.PublicKeys{oracleAcc.PrivateKey().PublicKey()} - require.NoError(t, oracleAcc.ConvertMultisig(1, oraclePubs)) - - neoHash := bc.contracts.NEO.Hash - gasHash := bc.contracts.GAS.Hash - w := io.NewBufBinWriter() - for _, sc := range []util.Uint160{neoHash, gasHash} { - for _, a := range accs { - amount := int64(1_000_000) - if sc.Equals(gasHash) { - amount = 1_000_000_000 - } - emit.AppCall(w.BinWriter, sc, "transfer", callflag.All, - neoOwner, a.Contract.ScriptHash(), amount, nil) - emit.Opcodes(w.BinWriter, opcode.ASSERT) - } + c2 := &neotest.Contract{ + Hash: csInvalid.Hash, + NEF: &csInvalid.NEF, + Manifest: &csInvalid.Manifest, } - emit.AppCall(w.BinWriter, gasHash, "transfer", callflag.All, - neoOwner, testchain.CommitteeScriptHash(), int64(1_000_000_000), nil) - emit.Opcodes(w.BinWriter, opcode.ASSERT) - require.NoError(t, w.Err) + e.DeployContract(t, c1, nil) + e.DeployContract(t, c2, nil) - txMove := bc.newTestTx(neoOwner, w.Bytes()) - txMove.SystemFee = 1_000_000_000 - require.NoError(t, testchain.SignTx(bc, txMove)) - b := bc.newBlock(txMove) - require.NoError(t, bc.AddBlock(b)) - - aer, err := bc.GetAppExecResults(txMove.Hash(), trigger.Application) - require.NoError(t, err) - require.Equal(t, 1, len(aer)) - require.Equal(t, aer[0].VMState, vm.HaltState) - - res, err := invokeContractMethodGeneric(bc, 100000000, bc.contracts.Policy.Hash, "blockAccount", true, accs[1].PrivateKey().GetScriptHash().BytesBE()) - require.NoError(t, err) - checkResult(t, res, stackitem.NewBool(true)) - - checkErr := func(t *testing.T, expectedErr error, tx *transaction.Transaction) { - err := bc.VerifyTx(tx) - require.True(t, errors.Is(err, expectedErr), "expected: %v, got: %v", expectedErr, err) - } - - testScript := []byte{byte(opcode.PUSH1)} - h := accs[0].PrivateKey().GetScriptHash() - t.Run("Expired", func(t *testing.T) { - tx := bc.newTestTx(h, testScript) - tx.ValidUntilBlock = 1 - require.NoError(t, accs[0].SignTx(netmode.UnitTestNet, tx)) - checkErr(t, ErrTxExpired, tx) - }) - t.Run("BlockedAccount", func(t *testing.T) { - tx := bc.newTestTx(accs[1].PrivateKey().GetScriptHash(), testScript) - require.NoError(t, accs[1].SignTx(netmode.UnitTestNet, tx)) - err := bc.VerifyTx(tx) - require.True(t, errors.Is(err, ErrPolicy)) - }) - t.Run("InsufficientGas", func(t *testing.T) { - balance := bc.GetUtilityTokenBalance(h) - tx := bc.newTestTx(h, testScript) - tx.SystemFee = balance.Int64() + 1 - require.NoError(t, accs[0].SignTx(netmode.UnitTestNet, tx)) - checkErr(t, ErrInsufficientFunds, tx) - }) - t.Run("TooBigTx", func(t *testing.T) { - script := make([]byte, transaction.MaxTransactionSize) - tx := bc.newTestTx(h, script) - require.NoError(t, accs[0].SignTx(netmode.UnitTestNet, tx)) - checkErr(t, ErrTxTooBig, tx) - }) - t.Run("NetworkFee", func(t *testing.T) { - t.Run("SmallNetworkFee", func(t *testing.T) { - tx := bc.newTestTx(h, testScript) - tx.NetworkFee = 1 - require.NoError(t, accs[0].SignTx(netmode.UnitTestNet, tx)) - checkErr(t, ErrTxSmallNetworkFee, tx) - }) - t.Run("AlmostEnoughNetworkFee", func(t *testing.T) { - tx := bc.newTestTx(h, testScript) - verificationNetFee, calcultedScriptSize := fee.Calculate(bc.GetBaseExecFee(), accs[0].Contract.Script) - expectedSize := io.GetVarSize(tx) + calcultedScriptSize - calculatedNetFee := verificationNetFee + int64(expectedSize)*bc.FeePerByte() - tx.NetworkFee = calculatedNetFee - 1 - require.NoError(t, accs[0].SignTx(netmode.UnitTestNet, tx)) - require.Equal(t, expectedSize, io.GetVarSize(tx)) - checkErr(t, ErrVerificationFailed, tx) - }) - t.Run("EnoughNetworkFee", func(t *testing.T) { - tx := bc.newTestTx(h, testScript) - verificationNetFee, calcultedScriptSize := fee.Calculate(bc.GetBaseExecFee(), accs[0].Contract.Script) - expectedSize := io.GetVarSize(tx) + calcultedScriptSize - calculatedNetFee := verificationNetFee + int64(expectedSize)*bc.FeePerByte() - tx.NetworkFee = calculatedNetFee - require.NoError(t, accs[0].SignTx(netmode.UnitTestNet, tx)) - require.Equal(t, expectedSize, io.GetVarSize(tx)) - require.NoError(t, bc.VerifyTx(tx)) - }) - t.Run("CalculateNetworkFee, signature script", func(t *testing.T) { - tx := bc.newTestTx(h, testScript) - expectedSize := io.GetVarSize(tx) - verificationNetFee, calculatedScriptSize := fee.Calculate(bc.GetBaseExecFee(), accs[0].Contract.Script) - expectedSize += calculatedScriptSize - expectedNetFee := verificationNetFee + int64(expectedSize)*bc.FeePerByte() - tx.NetworkFee = expectedNetFee - require.NoError(t, accs[0].SignTx(netmode.UnitTestNet, tx)) - actualSize := io.GetVarSize(tx) - require.Equal(t, expectedSize, actualSize) - interopCtx := bc.newInteropContext(trigger.Verification, bc.dao, nil, tx) - gasConsumed, err := bc.verifyHashAgainstScript(h, &tx.Scripts[0], interopCtx, -1) - require.NoError(t, err) - require.Equal(t, verificationNetFee, gasConsumed) - require.Equal(t, expectedNetFee, bc.FeePerByte()*int64(actualSize)+gasConsumed) - }) - t.Run("CalculateNetworkFee, multisignature script", func(t *testing.T) { - multisigAcc := accs[4] - pKeys := keys.PublicKeys{multisigAcc.PrivateKey().PublicKey()} - require.NoError(t, multisigAcc.ConvertMultisig(1, pKeys)) - multisigHash := hash.Hash160(multisigAcc.Contract.Script) - tx := bc.newTestTx(multisigHash, testScript) - verificationNetFee, calculatedScriptSize := fee.Calculate(bc.GetBaseExecFee(), multisigAcc.Contract.Script) - expectedSize := io.GetVarSize(tx) + calculatedScriptSize - expectedNetFee := verificationNetFee + int64(expectedSize)*bc.FeePerByte() - tx.NetworkFee = expectedNetFee - require.NoError(t, multisigAcc.SignTx(netmode.UnitTestNet, tx)) - actualSize := io.GetVarSize(tx) - require.Equal(t, expectedSize, actualSize) - interopCtx := bc.newInteropContext(trigger.Verification, bc.dao, nil, tx) - gasConsumed, err := bc.verifyHashAgainstScript(multisigHash, &tx.Scripts[0], interopCtx, -1) - require.NoError(t, err) - require.Equal(t, verificationNetFee, gasConsumed) - require.Equal(t, expectedNetFee, bc.FeePerByte()*int64(actualSize)+gasConsumed) - }) - }) - t.Run("InvalidTxScript", func(t *testing.T) { - tx := bc.newTestTx(h, testScript) - tx.Script = append(tx.Script, 0xff) - require.NoError(t, accs[0].SignTx(netmode.UnitTestNet, tx)) - checkErr(t, ErrInvalidScript, tx) - }) - t.Run("InvalidVerificationScript", func(t *testing.T) { - tx := bc.newTestTx(h, testScript) - verif := []byte{byte(opcode.JMP), 3, 0xff, byte(opcode.PUSHT)} - tx.Signers = append(tx.Signers, transaction.Signer{ - Account: hash.Hash160(verif), - Scopes: transaction.Global, - }) - tx.NetworkFee += 1000000 - require.NoError(t, accs[0].SignTx(netmode.UnitTestNet, tx)) - tx.Scripts = append(tx.Scripts, transaction.Witness{ - InvocationScript: []byte{}, - VerificationScript: verif, - }) - checkErr(t, ErrInvalidVerification, tx) - }) - t.Run("InvalidInvocationScript", func(t *testing.T) { - tx := bc.newTestTx(h, testScript) - verif := []byte{byte(opcode.PUSHT)} - tx.Signers = append(tx.Signers, transaction.Signer{ - Account: hash.Hash160(verif), - Scopes: transaction.Global, - }) - tx.NetworkFee += 1000000 - require.NoError(t, accs[0].SignTx(netmode.UnitTestNet, tx)) - tx.Scripts = append(tx.Scripts, transaction.Witness{ - InvocationScript: []byte{byte(opcode.JMP), 3, 0xff}, - VerificationScript: verif, - }) - checkErr(t, ErrInvalidInvocation, tx) - }) - t.Run("Conflict", func(t *testing.T) { - balance := bc.GetUtilityTokenBalance(h).Int64() - tx := bc.newTestTx(h, testScript) - tx.NetworkFee = balance / 2 - require.NoError(t, accs[0].SignTx(netmode.UnitTestNet, tx)) - require.NoError(t, bc.PoolTx(tx)) - - tx2 := bc.newTestTx(h, testScript) - tx2.NetworkFee = balance / 2 - require.NoError(t, accs[0].SignTx(netmode.UnitTestNet, tx2)) - err := bc.PoolTx(tx2) - require.True(t, errors.Is(err, ErrMemPoolConflict)) - }) - t.Run("InvalidWitnessHash", func(t *testing.T) { - tx := bc.newTestTx(h, testScript) - require.NoError(t, accs[0].SignTx(netmode.UnitTestNet, tx)) - tx.Scripts[0].VerificationScript = []byte{byte(opcode.PUSHT)} - checkErr(t, ErrWitnessHashMismatch, tx) - }) - t.Run("InvalidWitnessSignature", func(t *testing.T) { - tx := bc.newTestTx(h, testScript) - require.NoError(t, accs[0].SignTx(netmode.UnitTestNet, tx)) - tx.Scripts[0].InvocationScript[10] = ^tx.Scripts[0].InvocationScript[10] - checkErr(t, ErrVerificationFailed, tx) - }) - t.Run("InsufficientNetworkFeeForSecondWitness", func(t *testing.T) { - tx := bc.newTestTx(h, testScript) - tx.Signers = append(tx.Signers, transaction.Signer{ - Account: accs[3].PrivateKey().GetScriptHash(), - Scopes: transaction.Global, - }) - require.NoError(t, accs[0].SignTx(netmode.UnitTestNet, tx)) - require.NoError(t, accs[3].SignTx(netmode.UnitTestNet, tx)) - checkErr(t, ErrVerificationFailed, tx) - }) - t.Run("OldTX", func(t *testing.T) { - tx := bc.newTestTx(h, testScript) - require.NoError(t, accs[0].SignTx(netmode.UnitTestNet, tx)) - b := bc.newBlock(tx) - require.NoError(t, bc.AddBlock(b)) - - err := bc.VerifyTx(tx) - require.True(t, errors.Is(err, ErrAlreadyExists)) - }) - t.Run("MemPooledTX", func(t *testing.T) { - tx := bc.newTestTx(h, testScript) - require.NoError(t, accs[0].SignTx(netmode.UnitTestNet, tx)) - require.NoError(t, bc.PoolTx(tx)) - - err := bc.PoolTx(tx) - require.True(t, errors.Is(err, ErrAlreadyExists)) - }) - t.Run("MemPoolOOM", func(t *testing.T) { - bc.memPool = mempool.New(1, 0, false) - tx1 := bc.newTestTx(h, testScript) - tx1.NetworkFee += 10000 // Give it more priority. - require.NoError(t, accs[0].SignTx(netmode.UnitTestNet, tx1)) - require.NoError(t, bc.PoolTx(tx1)) - - tx2 := bc.newTestTx(h, testScript) - require.NoError(t, accs[0].SignTx(netmode.UnitTestNet, tx2)) - err := bc.PoolTx(tx2) - require.True(t, errors.Is(err, ErrOOM)) - }) - t.Run("Attribute", func(t *testing.T) { - t.Run("InvalidHighPriority", func(t *testing.T) { - tx := bc.newTestTx(h, testScript) - tx.Attributes = append(tx.Attributes, transaction.Attribute{Type: transaction.HighPriority}) - require.NoError(t, accs[0].SignTx(netmode.UnitTestNet, tx)) - checkErr(t, ErrInvalidAttribute, tx) - }) - t.Run("ValidHighPriority", func(t *testing.T) { - tx := bc.newTestTx(h, testScript) - tx.Attributes = append(tx.Attributes, transaction.Attribute{Type: transaction.HighPriority}) - tx.NetworkFee += 4_000_000 // multisig check - tx.Signers = []transaction.Signer{{ - Account: testchain.CommitteeScriptHash(), - Scopes: transaction.None, - }} - rawScript := testchain.CommitteeVerificationScript() - require.NoError(t, err) - size := io.GetVarSize(tx) - netFee, sizeDelta := fee.Calculate(bc.GetBaseExecFee(), rawScript) - tx.NetworkFee += netFee - tx.NetworkFee += int64(size+sizeDelta) * bc.FeePerByte() - tx.Scripts = []transaction.Witness{{ - InvocationScript: testchain.SignCommittee(tx), - VerificationScript: rawScript, - }} - require.NoError(t, bc.VerifyTx(tx)) - }) - t.Run("Oracle", func(t *testing.T) { - orc := bc.contracts.Oracle - req := &state.OracleRequest{GasForResponse: 1000_0000} - require.NoError(t, orc.PutRequestInternal(1, req, bc.dao)) - - oracleScript, err := smartcontract.CreateMajorityMultiSigRedeemScript(oraclePubs) - require.NoError(t, err) - oracleHash := hash.Hash160(oracleScript) - - // We need to create new transaction, - // because hashes are cached after signing. - getOracleTx := func(t *testing.T) *transaction.Transaction { - tx := bc.newTestTx(h, orc.GetOracleResponseScript()) - resp := &transaction.OracleResponse{ - ID: 1, - Code: transaction.Success, - Result: []byte{1, 2, 3}, - } - tx.Attributes = []transaction.Attribute{{ - Type: transaction.OracleResponseT, - Value: resp, - }} - tx.NetworkFee += 4_000_000 // multisig check - tx.SystemFee = int64(req.GasForResponse - uint64(tx.NetworkFee)) - tx.Signers = []transaction.Signer{{ - Account: oracleHash, - Scopes: transaction.None, - }} - size := io.GetVarSize(tx) - netFee, sizeDelta := fee.Calculate(bc.GetBaseExecFee(), oracleScript) - tx.NetworkFee += netFee - tx.NetworkFee += int64(size+sizeDelta) * bc.FeePerByte() - return tx - } - - t.Run("NoOracleNodes", func(t *testing.T) { - tx := getOracleTx(t) - require.NoError(t, oracleAcc.SignTx(netmode.UnitTestNet, tx)) - checkErr(t, ErrInvalidAttribute, tx) - }) - - txSetOracle := transaction.New([]byte{byte(opcode.RET)}, 0) // it's a hack, so we don't need a real script - setSigner(txSetOracle, testchain.CommitteeScriptHash()) - txSetOracle.Scripts = []transaction.Witness{{ - InvocationScript: testchain.SignCommittee(txSetOracle), - VerificationScript: testchain.CommitteeVerificationScript(), - }} - bl := block.New(bc.config.StateRootInHeader) - bl.Index = bc.BlockHeight() + 1 - ic := bc.newInteropContext(trigger.All, bc.dao, bl, txSetOracle) - ic.SpawnVM() - ic.VM.LoadScript([]byte{byte(opcode.RET)}) - require.NoError(t, bc.contracts.Designate.DesignateAsRole(ic, noderoles.Oracle, oraclePubs)) - _, err = ic.DAO.Persist() - require.NoError(t, err) - - t.Run("Valid", func(t *testing.T) { - tx := getOracleTx(t) - require.NoError(t, oracleAcc.SignTx(netmode.UnitTestNet, tx)) - require.NoError(t, bc.VerifyTx(tx)) - - t.Run("NativeVerify", func(t *testing.T) { - tx.Signers = append(tx.Signers, transaction.Signer{ - Account: bc.contracts.Oracle.Hash, - Scopes: transaction.None, - }) - tx.Scripts = append(tx.Scripts, transaction.Witness{}) - t.Run("NonZeroVerification", func(t *testing.T) { - w := io.NewBufBinWriter() - emit.Opcodes(w.BinWriter, opcode.ABORT) - emit.Bytes(w.BinWriter, util.Uint160{}.BytesBE()) - emit.Int(w.BinWriter, 0) - emit.String(w.BinWriter, orc.Manifest.Name) - tx.Scripts[len(tx.Scripts)-1].VerificationScript = w.Bytes() - err := bc.VerifyTx(tx) - require.True(t, errors.Is(err, ErrNativeContractWitness), "got: %v", err) - }) - t.Run("Good", func(t *testing.T) { - tx.Scripts[len(tx.Scripts)-1].VerificationScript = nil - require.NoError(t, bc.VerifyTx(tx)) - }) - }) - }) - t.Run("InvalidRequestID", func(t *testing.T) { - tx := getOracleTx(t) - tx.Attributes[0].Value.(*transaction.OracleResponse).ID = 2 - require.NoError(t, oracleAcc.SignTx(netmode.UnitTestNet, tx)) - checkErr(t, ErrInvalidAttribute, tx) - }) - t.Run("InvalidScope", func(t *testing.T) { - tx := getOracleTx(t) - tx.Signers[0].Scopes = transaction.Global - require.NoError(t, oracleAcc.SignTx(netmode.UnitTestNet, tx)) - checkErr(t, ErrInvalidAttribute, tx) - }) - t.Run("InvalidScript", func(t *testing.T) { - tx := getOracleTx(t) - tx.Script = append(tx.Script, byte(opcode.NOP)) - require.NoError(t, oracleAcc.SignTx(netmode.UnitTestNet, tx)) - checkErr(t, ErrInvalidAttribute, tx) - }) - t.Run("InvalidSigner", func(t *testing.T) { - tx := getOracleTx(t) - tx.Signers[0].Account = accs[0].Contract.ScriptHash() - require.NoError(t, accs[0].SignTx(netmode.UnitTestNet, tx)) - checkErr(t, ErrInvalidAttribute, tx) - }) - t.Run("SmallFee", func(t *testing.T) { - tx := getOracleTx(t) - tx.SystemFee = 0 - require.NoError(t, oracleAcc.SignTx(netmode.UnitTestNet, tx)) - checkErr(t, ErrInvalidAttribute, tx) - }) - }) - t.Run("NotValidBefore", func(t *testing.T) { - getNVBTx := func(height uint32) *transaction.Transaction { - tx := bc.newTestTx(h, testScript) - tx.Attributes = append(tx.Attributes, transaction.Attribute{Type: transaction.NotValidBeforeT, Value: &transaction.NotValidBefore{Height: height}}) - tx.NetworkFee += 4_000_000 // multisig check - tx.Signers = []transaction.Signer{{ - Account: testchain.CommitteeScriptHash(), - Scopes: transaction.None, - }} - rawScript := testchain.CommitteeVerificationScript() - require.NoError(t, err) - size := io.GetVarSize(tx) - netFee, sizeDelta := fee.Calculate(bc.GetBaseExecFee(), rawScript) - tx.NetworkFee += netFee - tx.NetworkFee += int64(size+sizeDelta) * bc.FeePerByte() - tx.Scripts = []transaction.Witness{{ - InvocationScript: testchain.SignCommittee(tx), - VerificationScript: rawScript, - }} - return tx - } - t.Run("Disabled", func(t *testing.T) { - tx := getNVBTx(bc.blockHeight + 1) - require.Error(t, bc.VerifyTx(tx)) - }) - t.Run("Enabled", func(t *testing.T) { - bc.config.P2PSigExtensions = true - t.Run("NotYetValid", func(t *testing.T) { - tx := getNVBTx(bc.blockHeight + 1) - require.True(t, errors.Is(bc.VerifyTx(tx), ErrInvalidAttribute)) - }) - t.Run("positive", func(t *testing.T) { - tx := getNVBTx(bc.blockHeight) - require.NoError(t, bc.VerifyTx(tx)) - }) - }) - }) - t.Run("Reserved", func(t *testing.T) { - getReservedTx := func(attrType transaction.AttrType) *transaction.Transaction { - tx := bc.newTestTx(h, testScript) - tx.Attributes = append(tx.Attributes, transaction.Attribute{Type: attrType, Value: &transaction.Reserved{Value: []byte{1, 2, 3}}}) - tx.NetworkFee += 4_000_000 // multisig check - tx.Signers = []transaction.Signer{{ - Account: testchain.CommitteeScriptHash(), - Scopes: transaction.None, - }} - rawScript := testchain.CommitteeVerificationScript() - require.NoError(t, err) - size := io.GetVarSize(tx) - netFee, sizeDelta := fee.Calculate(bc.GetBaseExecFee(), rawScript) - tx.NetworkFee += netFee - tx.NetworkFee += int64(size+sizeDelta) * bc.FeePerByte() - tx.Scripts = []transaction.Witness{{ - InvocationScript: testchain.SignCommittee(tx), - VerificationScript: rawScript, - }} - return tx - } - t.Run("Disabled", func(t *testing.T) { - tx := getReservedTx(transaction.ReservedLowerBound + 3) - require.Error(t, bc.VerifyTx(tx)) - }) - t.Run("Enabled", func(t *testing.T) { - bc.config.ReservedAttributes = true - tx := getReservedTx(transaction.ReservedLowerBound + 3) - require.NoError(t, bc.VerifyTx(tx)) - }) - }) - t.Run("Conflicts", func(t *testing.T) { - getConflictsTx := func(hashes ...util.Uint256) *transaction.Transaction { - tx := bc.newTestTx(h, testScript) - tx.Attributes = make([]transaction.Attribute, len(hashes)) - for i, h := range hashes { - tx.Attributes[i] = transaction.Attribute{ - Type: transaction.ConflictsT, - Value: &transaction.Conflicts{ - Hash: h, - }, - } - } - tx.NetworkFee += 4_000_000 // multisig check - tx.Signers = []transaction.Signer{{ - Account: testchain.CommitteeScriptHash(), - Scopes: transaction.None, - }} - rawScript := testchain.CommitteeVerificationScript() - require.NoError(t, err) - size := io.GetVarSize(tx) - netFee, sizeDelta := fee.Calculate(bc.GetBaseExecFee(), rawScript) - tx.NetworkFee += netFee - tx.NetworkFee += int64(size+sizeDelta) * bc.FeePerByte() - tx.Scripts = []transaction.Witness{{ - InvocationScript: testchain.SignCommittee(tx), - VerificationScript: rawScript, - }} - return tx - } - t.Run("disabled", func(t *testing.T) { - bc.config.P2PSigExtensions = false - tx := getConflictsTx(util.Uint256{1, 2, 3}) - require.Error(t, bc.VerifyTx(tx)) - }) - t.Run("enabled", func(t *testing.T) { - bc.config.P2PSigExtensions = true - t.Run("dummy on-chain conflict", func(t *testing.T) { - tx := bc.newTestTx(h, testScript) - require.NoError(t, accs[0].SignTx(netmode.UnitTestNet, tx)) - conflicting := transaction.New([]byte{byte(opcode.RET)}, 1) - conflicting.Attributes = []transaction.Attribute{ - { - Type: transaction.ConflictsT, - Value: &transaction.Conflicts{ - Hash: tx.Hash(), - }, - }, - } - require.NoError(t, bc.dao.StoreAsTransaction(conflicting, bc.blockHeight, nil)) - require.True(t, errors.Is(bc.VerifyTx(tx), ErrHasConflicts)) - }) - t.Run("attribute on-chain conflict", func(t *testing.T) { - tx := transaction.New([]byte{byte(opcode.PUSH1)}, 0) - tx.ValidUntilBlock = 4242 - tx.Signers = []transaction.Signer{{ - Account: testchain.MultisigScriptHash(), - Scopes: transaction.None, - }} - require.NoError(t, testchain.SignTx(bc, tx)) - b := bc.newBlock(tx) - - require.NoError(t, bc.AddBlock(b)) - txConflict := getConflictsTx(tx.Hash()) - require.Error(t, bc.VerifyTx(txConflict)) - }) - t.Run("positive", func(t *testing.T) { - tx := getConflictsTx(random.Uint256()) - require.NoError(t, bc.VerifyTx(tx)) - }) - }) - }) - t.Run("NotaryAssisted", func(t *testing.T) { - notary, err := wallet.NewAccount() - require.NoError(t, err) - txSetNotary := transaction.New([]byte{byte(opcode.RET)}, 0) - setSigner(txSetNotary, testchain.CommitteeScriptHash()) - txSetNotary.Scripts = []transaction.Witness{{ - InvocationScript: testchain.SignCommittee(txSetNotary), - VerificationScript: testchain.CommitteeVerificationScript(), - }} - bl := block.New(false) - bl.Index = bc.BlockHeight() + 1 - ic := bc.newInteropContext(trigger.All, bc.dao, bl, txSetNotary) - ic.SpawnVM() - ic.VM.LoadScript([]byte{byte(opcode.RET)}) - require.NoError(t, bc.contracts.Designate.DesignateAsRole(ic, noderoles.P2PNotary, keys.PublicKeys{notary.PrivateKey().PublicKey()})) - _, err = ic.DAO.Persist() - require.NoError(t, err) - getNotaryAssistedTx := func(signaturesCount uint8, serviceFee int64) *transaction.Transaction { - tx := bc.newTestTx(h, testScript) - tx.Attributes = append(tx.Attributes, transaction.Attribute{Type: transaction.NotaryAssistedT, Value: &transaction.NotaryAssisted{ - NKeys: signaturesCount, - }}) - tx.NetworkFee += serviceFee // additional fee for NotaryAssisted attribute - tx.NetworkFee += 4_000_000 // multisig check - tx.Signers = []transaction.Signer{{ - Account: testchain.CommitteeScriptHash(), - Scopes: transaction.None, - }, - { - Account: bc.contracts.Notary.Hash, - Scopes: transaction.None, - }, - } - rawScript := testchain.CommitteeVerificationScript() - size := io.GetVarSize(tx) - netFee, sizeDelta := fee.Calculate(bc.GetBaseExecFee(), rawScript) - tx.NetworkFee += netFee - tx.NetworkFee += int64(size+sizeDelta) * bc.FeePerByte() - tx.Scripts = []transaction.Witness{ - { - InvocationScript: testchain.SignCommittee(tx), - VerificationScript: rawScript, - }, - { - InvocationScript: append([]byte{byte(opcode.PUSHDATA1), 64}, notary.PrivateKey().SignHashable(uint32(testchain.Network()), tx)...), - }, - } - return tx - } - t.Run("Disabled", func(t *testing.T) { - bc.config.P2PSigExtensions = false - tx := getNotaryAssistedTx(0, 0) - require.True(t, errors.Is(bc.VerifyTx(tx), ErrInvalidAttribute)) - }) - t.Run("Enabled, insufficient network fee", func(t *testing.T) { - bc.config.P2PSigExtensions = true - tx := getNotaryAssistedTx(1, 0) - require.Error(t, bc.VerifyTx(tx)) - }) - t.Run("Test verify", func(t *testing.T) { - bc.config.P2PSigExtensions = true - t.Run("no NotaryAssisted attribute", func(t *testing.T) { - tx := getNotaryAssistedTx(1, (1+1)*notaryServiceFeePerKey) - tx.Attributes = []transaction.Attribute{} - tx.Signers = []transaction.Signer{ - { - Account: testchain.CommitteeScriptHash(), - Scopes: transaction.None, - }, - { - Account: bc.contracts.Notary.Hash, - Scopes: transaction.None, - }, - } - tx.Scripts = []transaction.Witness{ - { - InvocationScript: testchain.SignCommittee(tx), - VerificationScript: testchain.CommitteeVerificationScript(), - }, - { - InvocationScript: append([]byte{byte(opcode.PUSHDATA1), 64}, notary.PrivateKey().SignHashable(uint32(testchain.Network()), tx)...), - }, - } - require.Error(t, bc.VerifyTx(tx)) - }) - t.Run("no deposit", func(t *testing.T) { - tx := getNotaryAssistedTx(1, (1+1)*notaryServiceFeePerKey) - tx.Signers = []transaction.Signer{ - { - Account: bc.contracts.Notary.Hash, - Scopes: transaction.None, - }, - { - Account: testchain.CommitteeScriptHash(), - Scopes: transaction.None, - }, - } - tx.Scripts = []transaction.Witness{ - { - InvocationScript: append([]byte{byte(opcode.PUSHDATA1), 64}, notary.PrivateKey().SignHashable(uint32(testchain.Network()), tx)...), - }, - { - InvocationScript: testchain.SignCommittee(tx), - VerificationScript: testchain.CommitteeVerificationScript(), - }, - } - require.Error(t, bc.VerifyTx(tx)) - }) - t.Run("bad Notary signer scope", func(t *testing.T) { - tx := getNotaryAssistedTx(1, (1+1)*notaryServiceFeePerKey) - tx.Signers = []transaction.Signer{ - { - Account: testchain.CommitteeScriptHash(), - Scopes: transaction.None, - }, - { - Account: bc.contracts.Notary.Hash, - Scopes: transaction.CalledByEntry, - }, - } - tx.Scripts = []transaction.Witness{ - { - InvocationScript: testchain.SignCommittee(tx), - VerificationScript: testchain.CommitteeVerificationScript(), - }, - { - InvocationScript: append([]byte{byte(opcode.PUSHDATA1), 64}, notary.PrivateKey().SignHashable(uint32(testchain.Network()), tx)...), - }, - } - require.Error(t, bc.VerifyTx(tx)) - }) - t.Run("not signed by Notary", func(t *testing.T) { - tx := getNotaryAssistedTx(1, (1+1)*notaryServiceFeePerKey) - tx.Signers = []transaction.Signer{ - { - Account: testchain.CommitteeScriptHash(), - Scopes: transaction.None, - }, - } - tx.Scripts = []transaction.Witness{ - { - InvocationScript: testchain.SignCommittee(tx), - VerificationScript: testchain.CommitteeVerificationScript(), - }, - } - require.Error(t, bc.VerifyTx(tx)) - }) - t.Run("bad Notary node witness", func(t *testing.T) { - tx := getNotaryAssistedTx(1, (1+1)*notaryServiceFeePerKey) - tx.Signers = []transaction.Signer{ - { - Account: testchain.CommitteeScriptHash(), - Scopes: transaction.None, - }, - { - Account: bc.contracts.Notary.Hash, - Scopes: transaction.None, - }, - } - acc, err := keys.NewPrivateKey() - require.NoError(t, err) - tx.Scripts = []transaction.Witness{ - { - InvocationScript: testchain.SignCommittee(tx), - VerificationScript: testchain.CommitteeVerificationScript(), - }, - { - InvocationScript: append([]byte{byte(opcode.PUSHDATA1), 64}, acc.SignHashable(uint32(testchain.Network()), tx)...), - }, - } - require.Error(t, bc.VerifyTx(tx)) - }) - t.Run("missing payer", func(t *testing.T) { - tx := getNotaryAssistedTx(1, (1+1)*notaryServiceFeePerKey) - tx.Signers = []transaction.Signer{ - { - Account: bc.contracts.Notary.Hash, - Scopes: transaction.None, - }, - } - tx.Scripts = []transaction.Witness{ - { - InvocationScript: append([]byte{byte(opcode.PUSHDATA1), 64}, notary.PrivateKey().SignHashable(uint32(testchain.Network()), tx)...), - }, - } - require.Error(t, bc.VerifyTx(tx)) - }) - t.Run("positive", func(t *testing.T) { - tx := getNotaryAssistedTx(1, (1+1)*notaryServiceFeePerKey) - require.NoError(t, bc.VerifyTx(tx)) - }) - }) - }) - }) - t.Run("Partially-filled transaction", func(t *testing.T) { - bc.config.P2PSigExtensions = true - getPartiallyFilledTx := func(nvb uint32, validUntil uint32) *transaction.Transaction { - tx := bc.newTestTx(h, testScript) - tx.ValidUntilBlock = validUntil - tx.Attributes = []transaction.Attribute{ - { - Type: transaction.NotValidBeforeT, - Value: &transaction.NotValidBefore{Height: nvb}, - }, - { - Type: transaction.NotaryAssistedT, - Value: &transaction.NotaryAssisted{NKeys: 0}, - }, - } - tx.Signers = []transaction.Signer{ - { - Account: bc.contracts.Notary.Hash, - Scopes: transaction.None, - }, - { - Account: testchain.MultisigScriptHash(), - Scopes: transaction.None, - }, - } - size := io.GetVarSize(tx) - netFee, sizeDelta := fee.Calculate(bc.GetBaseExecFee(), testchain.MultisigVerificationScript()) - tx.NetworkFee = netFee + // multisig witness verification price - int64(size)*bc.FeePerByte() + // fee for unsigned size - int64(sizeDelta)*bc.FeePerByte() + // fee for multisig size - 66*bc.FeePerByte() + // fee for Notary signature size (66 bytes for Invocation script and 0 bytes for Verification script) - 2*bc.FeePerByte() + // fee for the length of each script in Notary witness (they are nil, so we did not take them into account during `size` calculation) - notaryServiceFeePerKey + // fee for Notary attribute - fee.Opcode(bc.GetBaseExecFee(), // Notary verification script - opcode.PUSHDATA1, opcode.RET, // invocation script - opcode.PUSH0, opcode.SYSCALL, opcode.RET) + // Neo.Native.Call - nativeprices.NotaryVerificationPrice*bc.GetBaseExecFee() // Notary witness verification price - tx.Scripts = []transaction.Witness{ - { - InvocationScript: append([]byte{byte(opcode.PUSHDATA1), 64}, make([]byte, 64)...), - VerificationScript: []byte{}, - }, - { - InvocationScript: testchain.Sign(tx), - VerificationScript: testchain.MultisigVerificationScript(), - }, - } - return tx - } - - mp := mempool.New(10, 1, false) - verificationF := func(tx *transaction.Transaction, data interface{}) error { - if data.(int) > 5 { - return errors.New("bad data") - } - return nil - } - t.Run("failed pre-verification", func(t *testing.T) { - tx := getPartiallyFilledTx(bc.blockHeight, bc.blockHeight+1) - require.Error(t, bc.PoolTxWithData(tx, 6, mp, bc, verificationF)) // here and below let's use `bc` instead of proper NotaryFeer for the test simplicity. - }) - t.Run("GasLimitExceeded during witness verification", func(t *testing.T) { - tx := getPartiallyFilledTx(bc.blockHeight, bc.blockHeight+1) - tx.NetworkFee-- // to check that NetworkFee was set correctly in getPartiallyFilledTx - tx.Scripts = []transaction.Witness{ - { - InvocationScript: append([]byte{byte(opcode.PUSHDATA1), 64}, make([]byte, 64)...), - VerificationScript: []byte{}, - }, - { - InvocationScript: testchain.Sign(tx), - VerificationScript: testchain.MultisigVerificationScript(), - }, - } - require.Error(t, bc.PoolTxWithData(tx, 5, mp, bc, verificationF)) - }) - t.Run("bad NVB: too big", func(t *testing.T) { - tx := getPartiallyFilledTx(bc.blockHeight+bc.contracts.Notary.GetMaxNotValidBeforeDelta(bc.dao)+1, bc.blockHeight+1) - require.True(t, errors.Is(bc.PoolTxWithData(tx, 5, mp, bc, verificationF), ErrInvalidAttribute)) - }) - t.Run("bad ValidUntilBlock: too small", func(t *testing.T) { - tx := getPartiallyFilledTx(bc.blockHeight, bc.blockHeight+bc.contracts.Notary.GetMaxNotValidBeforeDelta(bc.dao)+1) - require.True(t, errors.Is(bc.PoolTxWithData(tx, 5, mp, bc, verificationF), ErrInvalidAttribute)) - }) - t.Run("good", func(t *testing.T) { - tx := getPartiallyFilledTx(bc.blockHeight, bc.blockHeight+1) - require.NoError(t, bc.PoolTxWithData(tx, 5, mp, bc, verificationF)) - }) - }) -} - -func TestVerifyHashAgainstScript(t *testing.T) { - bc := newTestChain(t) - - cs, csInvalid := getTestContractState(t, 4, 5, random.Uint160()) // sender and IDs are not important for the test - ic := bc.newInteropContext(trigger.Verification, bc.dao, nil, nil) - require.NoError(t, bc.contracts.Management.PutContractState(bc.dao, cs)) - require.NoError(t, bc.contracts.Management.PutContractState(bc.dao, csInvalid)) - - gas := bc.contracts.Policy.GetMaxVerificationGas(ic.DAO) + gas := bc.GetMaxVerificationGAS() t.Run("Contract", func(t *testing.T) { t.Run("Missing", func(t *testing.T) { newH := cs.Hash newH[0] = ^newH[0] w := &transaction.Witness{InvocationScript: []byte{byte(opcode.PUSH4)}} - _, err := bc.verifyHashAgainstScript(newH, w, ic, gas) - require.True(t, errors.Is(err, ErrUnknownVerificationContract)) + _, err := bc.VerifyWitness(newH, nil, w, gas) + require.True(t, errors.Is(err, core.ErrUnknownVerificationContract)) }) t.Run("Invalid", func(t *testing.T) { w := &transaction.Witness{InvocationScript: []byte{byte(opcode.PUSH4)}} - _, err := bc.verifyHashAgainstScript(csInvalid.Hash, w, ic, gas) - require.True(t, errors.Is(err, ErrInvalidVerificationContract)) + _, err := bc.VerifyWitness(csInvalid.Hash, nil, w, gas) + require.True(t, errors.Is(err, core.ErrInvalidVerificationContract)) }) t.Run("ValidSignature", func(t *testing.T) { w := &transaction.Witness{InvocationScript: []byte{byte(opcode.PUSH4)}} - _, err := bc.verifyHashAgainstScript(cs.Hash, w, ic, gas) + _, err := bc.VerifyWitness(cs.Hash, nil, w, gas) require.NoError(t, err) }) t.Run("InvalidSignature", func(t *testing.T) { w := &transaction.Witness{InvocationScript: []byte{byte(opcode.PUSH3)}} - _, err := bc.verifyHashAgainstScript(cs.Hash, w, ic, gas) - require.True(t, errors.Is(err, ErrVerificationFailed)) + _, err := bc.VerifyWitness(cs.Hash, nil, w, gas) + require.True(t, errors.Is(err, core.ErrVerificationFailed)) }) }) t.Run("NotEnoughGas", func(t *testing.T) { @@ -1157,8 +623,8 @@ func TestVerifyHashAgainstScript(t *testing.T) { InvocationScript: []byte{byte(opcode.NOP)}, VerificationScript: verif, } - _, err := bc.verifyHashAgainstScript(hash.Hash160(verif), w, ic, 1) - require.True(t, errors.Is(err, ErrVerificationFailed)) + _, err := bc.VerifyWitness(hash.Hash160(verif), nil, w, 1) + require.True(t, errors.Is(err, core.ErrVerificationFailed)) }) t.Run("NoResult", func(t *testing.T) { verif := []byte{byte(opcode.DROP)} @@ -1166,8 +632,8 @@ func TestVerifyHashAgainstScript(t *testing.T) { InvocationScript: []byte{byte(opcode.PUSH1)}, VerificationScript: verif, } - _, err := bc.verifyHashAgainstScript(hash.Hash160(verif), w, ic, gas) - require.True(t, errors.Is(err, ErrVerificationFailed)) + _, err := bc.VerifyWitness(hash.Hash160(verif), nil, w, gas) + require.True(t, errors.Is(err, core.ErrVerificationFailed)) }) t.Run("BadResult", func(t *testing.T) { verif := make([]byte, 66) @@ -1177,8 +643,8 @@ func TestVerifyHashAgainstScript(t *testing.T) { InvocationScript: []byte{byte(opcode.NOP)}, VerificationScript: verif, } - _, err := bc.verifyHashAgainstScript(hash.Hash160(verif), w, ic, gas) - require.True(t, errors.Is(err, ErrVerificationFailed)) + _, err := bc.VerifyWitness(hash.Hash160(verif), nil, w, gas) + require.True(t, errors.Is(err, core.ErrVerificationFailed)) }) t.Run("TooManyResults", func(t *testing.T) { verif := []byte{byte(opcode.NOP)} @@ -1186,70 +652,63 @@ func TestVerifyHashAgainstScript(t *testing.T) { InvocationScript: []byte{byte(opcode.PUSH1), byte(opcode.PUSH1)}, VerificationScript: verif, } - _, err := bc.verifyHashAgainstScript(hash.Hash160(verif), w, ic, gas) - require.True(t, errors.Is(err, ErrVerificationFailed)) + _, err := bc.VerifyWitness(hash.Hash160(verif), nil, w, gas) + require.True(t, errors.Is(err, core.ErrVerificationFailed)) }) } -func TestIsTxStillRelevant(t *testing.T) { - bc := newTestChain(t) +func TestBlockchain_IsTxStillRelevant(t *testing.T) { + bc, acc := chain.NewSingleWithCustomConfig(t, func(c *config.ProtocolConfiguration) { + c.P2PSigExtensions = true + }) + e := neotest.NewExecutor(t, bc, acc, acc) mp := bc.GetMemPool() - newTx := func(t *testing.T) *transaction.Transaction { - tx := transaction.New([]byte{byte(opcode.RET)}, 100) - tx.ValidUntilBlock = bc.BlockHeight() + 1 - tx.Signers = []transaction.Signer{{ - Account: neoOwner, - Scopes: transaction.CalledByEntry, - }} - return tx - } t.Run("small ValidUntilBlock", func(t *testing.T) { - tx := newTx(t) - require.NoError(t, testchain.SignTx(bc, tx)) + tx := e.PrepareInvocation(t, []byte{byte(opcode.PUSH1)}, []neotest.Signer{acc}, bc.BlockHeight()+1) require.True(t, bc.IsTxStillRelevant(tx, nil, false)) - require.NoError(t, bc.AddBlock(bc.newBlock())) + e.AddNewBlock(t) require.False(t, bc.IsTxStillRelevant(tx, nil, false)) }) t.Run("tx is already persisted", func(t *testing.T) { - tx := newTx(t) - tx.ValidUntilBlock = bc.BlockHeight() + 2 - require.NoError(t, testchain.SignTx(bc, tx)) + tx := e.PrepareInvocation(t, []byte{byte(opcode.PUSH1)}, []neotest.Signer{acc}, bc.BlockHeight()+2) require.True(t, bc.IsTxStillRelevant(tx, nil, false)) - require.NoError(t, bc.AddBlock(bc.newBlock(tx))) + e.AddNewBlock(t, tx) require.False(t, bc.IsTxStillRelevant(tx, nil, false)) }) t.Run("tx with Conflicts attribute", func(t *testing.T) { - tx1 := newTx(t) - require.NoError(t, testchain.SignTx(bc, tx1)) + tx1 := e.PrepareInvocation(t, []byte{byte(opcode.PUSH1)}, []neotest.Signer{acc}, bc.BlockHeight()+5) - tx2 := newTx(t) + tx2 := transaction.New([]byte{byte(opcode.PUSH1)}, 0) + tx2.Nonce = neotest.Nonce() + tx2.ValidUntilBlock = e.Chain.BlockHeight() + 5 tx2.Attributes = []transaction.Attribute{{ Type: transaction.ConflictsT, Value: &transaction.Conflicts{Hash: tx1.Hash()}, }} - require.NoError(t, testchain.SignTx(bc, tx2)) + e.SignTx(t, tx2, -1, acc) require.True(t, bc.IsTxStillRelevant(tx1, mp, false)) - require.NoError(t, bc.verifyAndPoolTx(tx2, mp, bc)) + require.NoError(t, bc.PoolTx(tx2)) require.False(t, bc.IsTxStillRelevant(tx1, mp, false)) }) t.Run("NotValidBefore", func(t *testing.T) { - tx3 := newTx(t) + tx3 := transaction.New([]byte{byte(opcode.PUSH1)}, 0) + tx3.Nonce = neotest.Nonce() tx3.Attributes = []transaction.Attribute{{ Type: transaction.NotValidBeforeT, Value: &transaction.NotValidBefore{Height: bc.BlockHeight() + 1}, }} tx3.ValidUntilBlock = bc.BlockHeight() + 2 - require.NoError(t, testchain.SignTx(bc, tx3)) + e.SignTx(t, tx3, -1, acc) require.False(t, bc.IsTxStillRelevant(tx3, nil, false)) - require.NoError(t, bc.AddBlock(bc.newBlock())) + e.AddNewBlock(t) require.True(t, bc.IsTxStillRelevant(tx3, nil, false)) }) t.Run("contract witness check fails", func(t *testing.T) { @@ -1259,51 +718,51 @@ func TestIsTxStillRelevant(t *testing.T) { "github.com/nspcc-dev/neo-go/pkg/interop/util" ) func Verify() bool { - addr := util.FromAddress("`+address.Uint160ToString(bc.contracts.Ledger.Hash)+`") + addr := util.FromAddress("`+address.Uint160ToString(e.NativeHash(t, nativenames.Ledger))+`") currentHeight := contract.Call(addr, "currentIndex", contract.ReadStates) return currentHeight.(int) < %d }`, bc.BlockHeight()+2) // deploy + next block - txDeploy, h, _, err := testchain.NewDeployTx(bc, "TestVerify.go", neoOwner, strings.NewReader(src), nil) - require.NoError(t, err) - txDeploy.ValidUntilBlock = bc.BlockHeight() + 1 - addSigners(neoOwner, txDeploy) - require.NoError(t, testchain.SignTx(bc, txDeploy)) - require.NoError(t, bc.AddBlock(bc.newBlock(txDeploy))) - - tx := newTx(t) - tx.Signers = append(tx.Signers, transaction.Signer{ - Account: h, - Scopes: transaction.None, + c := neotest.CompileSource(t, acc.ScriptHash(), strings.NewReader(src), &compiler.Options{ + Name: "verification_contract", }) + e.DeployContract(t, c, nil) + + tx := transaction.New([]byte{byte(opcode.PUSH1)}, 0) + tx.Nonce = neotest.Nonce() + tx.ValidUntilBlock = bc.BlockHeight() + 2 + tx.Signers = []transaction.Signer{ + { + Account: c.Hash, + Scopes: transaction.None, + }, + } tx.NetworkFee += 10_000_000 - require.NoError(t, testchain.SignTx(bc, tx)) - tx.Scripts = append(tx.Scripts, transaction.Witness{}) + tx.Scripts = []transaction.Witness{{}} require.True(t, bc.IsTxStillRelevant(tx, mp, false)) - require.NoError(t, bc.AddBlock(bc.newBlock())) + e.AddNewBlock(t) require.False(t, bc.IsTxStillRelevant(tx, mp, false)) }) } -func TestMemPoolRemoval(t *testing.T) { +func TestBlockchain_MemPoolRemoval(t *testing.T) { const added = 16 const notAdded = 32 - bc := newTestChain(t) + bc, acc := chain.NewSingle(t) + e := neotest.NewExecutor(t, bc, acc, acc) + addedTxes := make([]*transaction.Transaction, added) notAddedTxes := make([]*transaction.Transaction, notAdded) for i := range addedTxes { - addedTxes[i] = bc.newTestTx(testchain.MultisigScriptHash(), []byte{byte(opcode.PUSH1)}) - require.NoError(t, testchain.SignTx(bc, addedTxes[i])) + addedTxes[i] = e.PrepareInvocation(t, []byte{byte(opcode.PUSH1)}, []neotest.Signer{acc}, 100) require.NoError(t, bc.PoolTx(addedTxes[i])) } for i := range notAddedTxes { - notAddedTxes[i] = bc.newTestTx(testchain.MultisigScriptHash(), []byte{byte(opcode.PUSH1)}) - require.NoError(t, testchain.SignTx(bc, notAddedTxes[i])) + notAddedTxes[i] = e.PrepareInvocation(t, []byte{byte(opcode.PUSH1)}, []neotest.Signer{acc}, 100) require.NoError(t, bc.PoolTx(notAddedTxes[i])) } - b := bc.newBlock(addedTxes...) - require.NoError(t, bc.AddBlock(b)) mempool := bc.GetMemPool() + e.AddNewBlock(t, addedTxes...) for _, tx := range addedTxes { require.False(t, mempool.ContainsKey(tx.Hash())) } @@ -1312,92 +771,66 @@ func TestMemPoolRemoval(t *testing.T) { } } -func TestHasBlock(t *testing.T) { - bc := newTestChain(t) - blocks, err := bc.genBlocks(50) - require.NoError(t, err) +func TestBlockchain_HasBlock(t *testing.T) { + bc, acc := chain.NewSingle(t) + e := neotest.NewExecutor(t, bc, acc, acc) - // Test unpersisted and persisted access - for j := 0; j < 2; j++ { - for i := 0; i < len(blocks); i++ { - assert.True(t, bc.HasBlock(blocks[i].Hash())) - } - newBlock := bc.newBlock() - assert.False(t, bc.HasBlock(newBlock.Hash())) - _, err = bc.persist(true) - assert.NoError(t, err) + blocks := e.GenerateNewBlocks(t, 10) + + for i := 0; i < len(blocks); i++ { + assert.True(t, bc.HasBlock(blocks[i].Hash())) } + newBlock := e.NewUnsignedBlock(t) + assert.False(t, bc.HasBlock(newBlock.Hash())) } -func TestGetTransaction(t *testing.T) { - bc := newTestChain(t) - tx1 := transaction.New([]byte{byte(opcode.PUSH1)}, 0) - tx1.ValidUntilBlock = 16 - tx1.Signers = []transaction.Signer{{ - Account: testchain.MultisigScriptHash(), - Scopes: transaction.CalledByEntry, - }} - tx2 := transaction.New([]byte{byte(opcode.PUSH2)}, 0) - tx2.ValidUntilBlock = 16 - tx2.Signers = []transaction.Signer{{ - Account: testchain.MultisigScriptHash(), - Scopes: transaction.CalledByEntry, - }} - require.NoError(t, testchain.SignTx(bc, tx1, tx2)) - b1 := bc.newBlock(tx1) +func TestBlockchain_GetTransaction(t *testing.T) { + bc, acc := chain.NewSingle(t) + e := neotest.NewExecutor(t, bc, acc, acc) - assert.Nil(t, bc.AddBlock(b1)) - block := bc.newBlock(tx2) - txSize := io.GetVarSize(tx2) - assert.Nil(t, bc.AddBlock(block)) + tx1 := e.PrepareInvocation(t, []byte{byte(opcode.PUSH1)}, []neotest.Signer{acc}) + e.AddNewBlock(t, tx1) - // Test unpersisted and persisted access - for j := 0; j < 2; j++ { - tx, height, err := bc.GetTransaction(block.Transactions[0].Hash()) - require.Nil(t, err) - assert.Equal(t, block.Index, height) - assert.Equal(t, txSize, tx.Size()) - assert.Equal(t, block.Transactions[0], tx) - assert.Equal(t, 1, io.GetVarSize(tx.Attributes)) - assert.Equal(t, 1, io.GetVarSize(tx.Scripts)) - _, err = bc.persist(true) - assert.NoError(t, err) - } + tx2 := e.PrepareInvocation(t, []byte{byte(opcode.PUSH2)}, []neotest.Signer{acc}) + tx2Size := io.GetVarSize(tx2) + b := e.AddNewBlock(t, tx2) + + tx, height, err := bc.GetTransaction(tx2.Hash()) + require.Nil(t, err) + assert.Equal(t, b.Index, height) + assert.Equal(t, tx2Size, tx.Size()) + assert.Equal(t, b.Transactions[0], tx) } -func TestGetClaimable(t *testing.T) { - bc := newTestChain(t) +func TestBlockchain_GetClaimable(t *testing.T) { + bc, acc := chain.NewSingle(t) + e := neotest.NewExecutor(t, bc, acc, acc) - _, err := bc.genBlocks(10) - require.NoError(t, err) + e.GenerateNewBlocks(t, 10) t.Run("first generation period", func(t *testing.T) { - amount, err := bc.CalculateClaimable(neoOwner, 1) + amount, err := bc.CalculateClaimable(acc.ScriptHash(), 1) require.NoError(t, err) require.EqualValues(t, big.NewInt(5*native.GASFactor/10), amount) }) } -func TestClose(t *testing.T) { - defer func() { - r := recover() - assert.NotNil(t, r) - }() - bc := initTestChain(t, nil, nil) +func TestBlockchain_Close(t *testing.T) { + st := storage.NewMemoryStore() + bc, acc := chain.NewSingleWithCustomConfigAndStore(t, nil, st, false) + e := neotest.NewExecutor(t, bc, acc, acc) go bc.Run() - _, err := bc.genBlocks(10) - require.NoError(t, err) + e.GenerateNewBlocks(t, 10) bc.Close() // It's a hack, but we use internal knowledge of MemoryStore // implementation which makes it completely unusable (up to panicing) // after Close(). - bc.dao.Store.Put([]byte{0}, []byte{1}) - - // This should never be executed. - assert.Nil(t, t) + require.Panics(t, func() { + _ = st.PutChangeSet(map[string][]byte{"0": {1}}, nil) + }) } -func TestSubscriptions(t *testing.T) { +func TestBlockchain_Subscriptions(t *testing.T) { // We use buffering here as a substitute for reader goroutines, events // get queued up and we read them one by one here. const chBufSize = 16 @@ -1406,7 +839,9 @@ func TestSubscriptions(t *testing.T) { notificationCh := make(chan *subscriptions.NotificationEvent, chBufSize) executionCh := make(chan *state.AppExecResult, chBufSize) - bc := newTestChain(t) + bc, acc := chain.NewSingle(t) + e := neotest.NewExecutor(t, bc, acc, acc) + nativeGASHash := e.NativeHash(t, nativenames.Gas) bc.SubscribeForBlocks(blockCh) bc.SubscribeForTransactions(txCh) bc.SubscribeForNotifications(notificationCh) @@ -1417,15 +852,14 @@ func TestSubscriptions(t *testing.T) { assert.Empty(t, blockCh) assert.Empty(t, txCh) - blocks, err := bc.genBlocks(1) - require.NoError(t, err) + generatedB := e.AddNewBlock(t) require.Eventually(t, func() bool { return len(blockCh) != 0 }, time.Second, 10*time.Millisecond) assert.Len(t, notificationCh, 1) // validator bounty assert.Len(t, executionCh, 2) assert.Empty(t, txCh) b := <-blockCh - assert.Equal(t, blocks[0], b) + assert.Equal(t, generatedB, b) assert.Empty(t, blockCh) aer := <-executionCh @@ -1440,11 +874,7 @@ func TestSubscriptions(t *testing.T) { emit.Bytes(script.BinWriter, []byte("yay!")) emit.Syscall(script.BinWriter, interopnames.SystemRuntimeNotify) require.NoError(t, script.Err) - txGood1 := transaction.New(script.Bytes(), 0) - txGood1.Signers = []transaction.Signer{{Account: neoOwner}} - txGood1.Nonce = 1 - txGood1.ValidUntilBlock = 1024 - require.NoError(t, testchain.SignTx(bc, txGood1)) + txGood1 := e.PrepareInvocation(t, script.Bytes(), []neotest.Signer{acc}) // Reset() reuses the script buffer and we need to keep scripts. script = io.NewBufBinWriter() @@ -1452,24 +882,15 @@ func TestSubscriptions(t *testing.T) { emit.Syscall(script.BinWriter, interopnames.SystemRuntimeNotify) emit.Opcodes(script.BinWriter, opcode.THROW) require.NoError(t, script.Err) - txBad := transaction.New(script.Bytes(), 0) - txBad.Signers = []transaction.Signer{{Account: neoOwner}} - txBad.Nonce = 2 - txBad.ValidUntilBlock = 1024 - require.NoError(t, testchain.SignTx(bc, txBad)) + txBad := e.PrepareInvocation(t, script.Bytes(), []neotest.Signer{acc}) script = io.NewBufBinWriter() emit.Bytes(script.BinWriter, []byte("yay! yay! yay!")) emit.Syscall(script.BinWriter, interopnames.SystemRuntimeNotify) require.NoError(t, script.Err) - txGood2 := transaction.New(script.Bytes(), 0) - txGood2.Signers = []transaction.Signer{{Account: neoOwner}} - txGood2.Nonce = 3 - txGood2.ValidUntilBlock = 1024 - require.NoError(t, testchain.SignTx(bc, txGood2)) + txGood2 := e.PrepareInvocation(t, script.Bytes(), []neotest.Signer{acc}) - invBlock := newBlock(bc.config, bc.BlockHeight()+1, bc.CurrentHeaderHash(), txGood1, txBad, txGood2) - require.NoError(t, bc.AddBlock(invBlock)) + invBlock := e.AddNewBlock(t, txGood1, txBad, txGood2) require.Eventually(t, func() bool { return len(blockCh) != 0 && len(txCh) != 0 && @@ -1488,7 +909,7 @@ func TestSubscriptions(t *testing.T) { require.True(t, len(notificationCh) >= 4) for i := 0; i < 4; i++ { notif := <-notificationCh - require.Equal(t, bc.contracts.GAS.Hash, notif.ScriptHash) + require.Equal(t, nativeGASHash, notif.ScriptHash) } // Follow in-block transaction order. @@ -1519,144 +940,12 @@ func TestSubscriptions(t *testing.T) { bc.UnsubscribeFromExecutions(executionCh) // Ensure that new blocks are processed correctly after unsubscription. - _, err = bc.genBlocks(2 * chBufSize) - require.NoError(t, err) + e.GenerateNewBlocks(t, 2*chBufSize) } -func testDumpAndRestore(t *testing.T, dumpF, restoreF func(c *config.Config)) { - if restoreF == nil { - restoreF = dumpF - } - - bc := newTestChainWithCustomCfg(t, dumpF) - - initBasicChain(t, bc) - 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 := newTestChainWithCustomCfg(t, restoreF) - - r := io.NewBinReaderFromBuf(buf) - require.Error(t, chaindump.Restore(bc2, r, 2, 1, nil)) - }) - t.Run("good", func(t *testing.T) { - bc2 := newTestChainWithCustomCfg(t, restoreF) - - 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 TestDumpAndRestore(t *testing.T) { - t.Run("no state root", func(t *testing.T) { - testDumpAndRestore(t, func(c *config.Config) { - c.ProtocolConfiguration.StateRootInHeader = false - }, nil) - }) - t.Run("with state root", func(t *testing.T) { - testDumpAndRestore(t, func(c *config.Config) { - c.ProtocolConfiguration.StateRootInHeader = true - }, nil) - }) - t.Run("remove untraceable", func(t *testing.T) { - // Dump can only be created if all blocks and transactions are present. - testDumpAndRestore(t, nil, func(c *config.Config) { - c.ProtocolConfiguration.MaxTraceableBlocks = 2 - c.ProtocolConfiguration.RemoveUntraceableBlocks = true - }) - }) -} - -func TestRemoveOldTransfers(t *testing.T) { - // Creating proper number of transfers/blocks takes unneccessary time, so emulate - // some DB with stale entries. - bc := newTestChain(t) - h, err := bc.GetHeader(bc.GetHeaderHash(0)) - require.NoError(t, err) - older := h.Timestamp - 1000 - newer := h.Timestamp + 1000 - acc1 := util.Uint160{1} - acc2 := util.Uint160{2} - acc3 := util.Uint160{3} - ttl := state.TokenTransferLog{Raw: []byte{1}} // It's incorrect, but who cares. - - for i := uint32(0); i < 3; i++ { - bc.dao.PutTokenTransferLog(acc1, older, i, false, &ttl) - } - for i := uint32(0); i < 3; i++ { - bc.dao.PutTokenTransferLog(acc2, newer, i, false, &ttl) - } - for i := uint32(0); i < 2; i++ { - bc.dao.PutTokenTransferLog(acc3, older, i, true, &ttl) - } - for i := uint32(0); i < 2; i++ { - bc.dao.PutTokenTransferLog(acc3, newer, i, true, &ttl) - } - - _, err = bc.dao.Persist() - require.NoError(t, err) - _ = bc.removeOldTransfers(0) - - for i := uint32(0); i < 2; i++ { - log, err := bc.dao.GetTokenTransferLog(acc1, older, i, false) - require.NoError(t, err) - require.Equal(t, 0, len(log.Raw)) - } - - log, err := bc.dao.GetTokenTransferLog(acc1, older, 2, false) - require.NoError(t, err) - require.NotEqual(t, 0, len(log.Raw)) - - for i := uint32(0); i < 3; i++ { - log, err = bc.dao.GetTokenTransferLog(acc2, newer, i, false) - require.NoError(t, err) - require.NotEqual(t, 0, len(log.Raw)) - } - - log, err = bc.dao.GetTokenTransferLog(acc3, older, 0, true) - require.NoError(t, err) - require.Equal(t, 0, len(log.Raw)) - - log, err = bc.dao.GetTokenTransferLog(acc3, older, 1, true) - require.NoError(t, err) - require.NotEqual(t, 0, len(log.Raw)) - - for i := uint32(0); i < 2; i++ { - log, err = bc.dao.GetTokenTransferLog(acc3, newer, i, true) - require.NoError(t, err) - require.NotEqual(t, 0, len(log.Raw)) - } -} - -func TestRemoveUntraceable(t *testing.T) { - check := func(t *testing.T, bc *Blockchain, tHash, bHash, sHash util.Uint256, errorExpected bool) { +func TestBlockchain_RemoveUntraceable(t *testing.T) { + neoCommitteeKey := []byte{0xfb, 0xff, 0xff, 0xff, 0x0e} + check := func(t *testing.T, bc *core.Blockchain, tHash, bHash, sHash util.Uint256, errorExpected bool) { _, _, err := bc.GetTransaction(tHash) if errorExpected { require.Error(t, err) @@ -1679,7 +968,7 @@ func TestRemoveUntraceable(t *testing.T) { require.NoError(t, err) if !sHash.Equals(util.Uint256{}) { sm := bc.GetStateModule() - _, err = sm.GetState(sHash, []byte{0xfb, 0xff, 0xff, 0xff, 0x0e}) // NEO committee key. + _, err = sm.GetState(sHash, neoCommitteeKey) if errorExpected { require.Error(t, err) } else { @@ -1688,106 +977,110 @@ func TestRemoveUntraceable(t *testing.T) { } } t.Run("P2PStateExchangeExtensions off", func(t *testing.T) { - bc := newTestChainWithCustomCfg(t, func(c *config.Config) { - c.ProtocolConfiguration.MaxTraceableBlocks = 2 - c.ProtocolConfiguration.GarbageCollectionPeriod = 2 - c.ProtocolConfiguration.RemoveUntraceableBlocks = true + bc, acc := chain.NewSingleWithCustomConfig(t, func(c *config.ProtocolConfiguration) { + c.MaxTraceableBlocks = 2 + c.GarbageCollectionPeriod = 2 + c.RemoveUntraceableBlocks = true }) + e := neotest.NewExecutor(t, bc, acc, acc) + neoValidatorInvoker := e.ValidatorInvoker(e.NativeHash(t, nativenames.Neo)) - tx1, err := testchain.NewTransferFromOwner(bc, bc.contracts.NEO.Hash, util.Uint160{}, 1, 0, bc.BlockHeight()+1) - require.NoError(t, err) - b1 := bc.newBlock(tx1) - require.NoError(t, bc.AddBlock(b1)) + tx1Hash := neoValidatorInvoker.Invoke(t, true, "transfer", acc.ScriptHash(), util.Uint160{1, 2, 3}, 1, nil) tx1Height := bc.BlockHeight() + b1 := e.TopBlock(t) sRoot, err := bc.GetStateModule().GetStateRoot(tx1Height) require.NoError(t, err) - tx2, err := testchain.NewTransferFromOwner(bc, bc.contracts.NEO.Hash, util.Uint160{}, 1, 0, bc.BlockHeight()+1) - require.NoError(t, err) - require.NoError(t, bc.AddBlock(bc.newBlock(tx2))) + neoValidatorInvoker.Invoke(t, true, "transfer", acc.ScriptHash(), util.Uint160{1, 2, 3}, 1, nil) - _, h1, err := bc.GetTransaction(tx1.Hash()) + _, h1, err := bc.GetTransaction(tx1Hash) require.NoError(t, err) require.Equal(t, tx1Height, h1) - check(t, bc, tx1.Hash(), b1.Hash(), sRoot.Root, false) - require.NoError(t, bc.AddBlock(bc.newBlock())) - require.NoError(t, bc.AddBlock(bc.newBlock())) - require.NoError(t, bc.AddBlock(bc.newBlock())) - require.NoError(t, bc.AddBlock(bc.newBlock())) - // Don't wait for Run(). - _, err = bc.persist(true) - require.NoError(t, err) - bc.tryRunGC(0) - check(t, bc, tx1.Hash(), b1.Hash(), sRoot.Root, true) + check(t, bc, tx1Hash, b1.Hash(), sRoot.Root, false) + e.GenerateNewBlocks(t, 4) + + sm := bc.GetStateModule() + require.Eventually(t, func() bool { + _, err = sm.GetState(sRoot.Root, neoCommitteeKey) + return err != nil + }, 2*bcPersistInterval, 10*time.Millisecond) + check(t, bc, tx1Hash, b1.Hash(), sRoot.Root, true) }) t.Run("P2PStateExchangeExtensions on", func(t *testing.T) { - bc := newTestChainWithCustomCfg(t, func(c *config.Config) { - c.ProtocolConfiguration.MaxTraceableBlocks = 2 - c.ProtocolConfiguration.GarbageCollectionPeriod = 2 - c.ProtocolConfiguration.RemoveUntraceableBlocks = true - c.ProtocolConfiguration.P2PStateExchangeExtensions = true - c.ProtocolConfiguration.StateSyncInterval = 2 - c.ProtocolConfiguration.StateRootInHeader = true + bc, acc := chain.NewSingleWithCustomConfig(t, func(c *config.ProtocolConfiguration) { + c.MaxTraceableBlocks = 2 + c.GarbageCollectionPeriod = 2 + c.RemoveUntraceableBlocks = true + c.P2PStateExchangeExtensions = true + c.StateSyncInterval = 2 + c.StateRootInHeader = true }) + e := neotest.NewExecutor(t, bc, acc, acc) + neoValidatorInvoker := e.ValidatorInvoker(e.NativeHash(t, nativenames.Neo)) - tx1, err := testchain.NewTransferFromOwner(bc, bc.contracts.NEO.Hash, util.Uint160{}, 1, 0, bc.BlockHeight()+1) - require.NoError(t, err) - b1 := bc.newBlock(tx1) - require.NoError(t, bc.AddBlock(b1)) + tx1Hash := neoValidatorInvoker.Invoke(t, true, "transfer", acc.ScriptHash(), util.Uint160{1, 2, 3}, 1, nil) tx1Height := bc.BlockHeight() + b1 := e.TopBlock(t) sRoot, err := bc.GetStateModule().GetStateRoot(tx1Height) require.NoError(t, err) - tx2, err := testchain.NewTransferFromOwner(bc, bc.contracts.NEO.Hash, util.Uint160{}, 1, 0, bc.BlockHeight()+1) - require.NoError(t, err) - b2 := bc.newBlock(tx2) - require.NoError(t, bc.AddBlock(b2)) + tx2Hash := neoValidatorInvoker.Invoke(t, true, "transfer", acc.ScriptHash(), util.Uint160{1, 2, 3}, 1, nil) tx2Height := bc.BlockHeight() + b2 := e.TopBlock(t) - _, h1, err := bc.GetTransaction(tx1.Hash()) + _, h1, err := bc.GetTransaction(tx1Hash) require.NoError(t, err) require.Equal(t, tx1Height, h1) - require.NoError(t, bc.AddBlock(bc.newBlock())) - require.NoError(t, bc.AddBlock(bc.newBlock())) - require.NoError(t, bc.AddBlock(bc.newBlock())) + e.GenerateNewBlocks(t, 3) - check(t, bc, tx1.Hash(), b1.Hash(), sRoot.Root, false) - check(t, bc, tx2.Hash(), b2.Hash(), sRoot.Root, false) + check(t, bc, tx1Hash, b1.Hash(), sRoot.Root, false) + check(t, bc, tx2Hash, b2.Hash(), sRoot.Root, false) - require.NoError(t, bc.AddBlock(bc.newBlock())) + e.AddNewBlock(t) - check(t, bc, tx1.Hash(), b1.Hash(), util.Uint256{}, true) - check(t, bc, tx2.Hash(), b2.Hash(), util.Uint256{}, false) - _, h2, err := bc.GetTransaction(tx2.Hash()) + check(t, bc, tx1Hash, b1.Hash(), util.Uint256{}, true) + check(t, bc, tx2Hash, b2.Hash(), util.Uint256{}, false) + _, h2, err := bc.GetTransaction(tx2Hash) require.NoError(t, err) require.Equal(t, tx2Height, h2) }) } -func TestInvalidNotification(t *testing.T) { - bc := newTestChain(t) +func TestBlockchain_InvalidNotification(t *testing.T) { + bc, acc := chain.NewSingle(t) + e := neotest.NewExecutor(t, bc, acc, acc) - cs, _ := getTestContractState(t, 4, 5, random.Uint160()) // sender and IDs are not important for the test - require.NoError(t, bc.contracts.Management.PutContractState(bc.dao, cs)) + cs, _ := contracts.GetTestContractState(t, pathToInternalContracts, 0, 1, acc.ScriptHash()) + e.DeployContract(t, &neotest.Contract{ + Hash: cs.Hash, + NEF: &cs.NEF, + Manifest: &cs.Manifest, + }, nil) - aer, err := invokeContractMethod(bc, 1_00000000, cs.Hash, "invalidStack") - require.NoError(t, err) - require.Equal(t, 2, len(aer.Stack)) - require.Nil(t, aer.Stack[0]) - require.Equal(t, stackitem.InteropT, aer.Stack[1].Type()) + cValidatorInvoker := e.ValidatorInvoker(cs.Hash) + cValidatorInvoker.InvokeAndCheck(t, func(t testing.TB, stack []stackitem.Item) { + require.Equal(t, 1, len(stack)) + require.Nil(t, stack[0]) + }, "invalidStack1") + cValidatorInvoker.Invoke(t, stackitem.NewInterop(nil), "invalidStack2") } // Test that deletion of non-existent doesn't result in error in tx or block addition. -func TestMPTDeleteNoKey(t *testing.T) { - bc := newTestChain(t) +func TestBlockchain_MPTDeleteNoKey(t *testing.T) { + bc, acc := chain.NewSingle(t) + e := neotest.NewExecutor(t, bc, acc, acc) - cs, _ := getTestContractState(t, 4, 5, random.Uint160()) // sender and IDs are not important for the test - require.NoError(t, bc.contracts.Management.PutContractState(bc.dao, cs)) - aer, err := invokeContractMethod(bc, 1_00000000, cs.Hash, "delValue", "non-existent-key") - require.NoError(t, err) - require.Equal(t, vm.HaltState, aer.VMState) + cs, _ := contracts.GetTestContractState(t, pathToInternalContracts, 0, 1, acc.ScriptHash()) + e.DeployContract(t, &neotest.Contract{ + Hash: cs.Hash, + NEF: &cs.NEF, + Manifest: &cs.Manifest, + }, nil) + + cValidatorInvoker := e.ValidatorInvoker(cs.Hash) + cValidatorInvoker.Invoke(t, stackitem.Null{}, "delValue", "non-existent-key") } // Test that UpdateHistory is added to ProtocolConfiguration for all native contracts @@ -1827,407 +1120,857 @@ func TestConfigNativeUpdateHistory(t *testing.T) { } } -func TestBlockchain_InitWithIncompleteStateJump(t *testing.T) { - var ( - stateSyncInterval = 4 - maxTraceable uint32 = 6 - ) - spountCfg := func(c *config.Config) { - c.ProtocolConfiguration.RemoveUntraceableBlocks = true - c.ProtocolConfiguration.StateRootInHeader = true - c.ProtocolConfiguration.P2PStateExchangeExtensions = true - c.ProtocolConfiguration.StateSyncInterval = stateSyncInterval - c.ProtocolConfiguration.MaxTraceableBlocks = maxTraceable - c.ProtocolConfiguration.KeepOnlyLatestState = true - } - bcSpout := newTestChainWithCustomCfg(t, spountCfg) - initBasicChain(t, bcSpout) - - // reach next to the latest state sync point and pretend that we've just restored - stateSyncPoint := (int(bcSpout.BlockHeight())/stateSyncInterval + 1) * stateSyncInterval - for i := bcSpout.BlockHeight() + 1; i <= uint32(stateSyncPoint); i++ { - require.NoError(t, bcSpout.AddBlock(bcSpout.newBlock())) - } - require.Equal(t, uint32(stateSyncPoint), bcSpout.BlockHeight()) - b := bcSpout.newBlock() - require.NoError(t, bcSpout.AddHeaders(&b.Header)) - - // put storage items with STTemp prefix - batch := storage.NewMemCachedStore(bcSpout.dao.Store) - tempPrefix := storage.STTempStorage - if bcSpout.dao.Version.StoragePrefix == tempPrefix { - tempPrefix = storage.STStorage - } - bPrefix := make([]byte, 1) - bPrefix[0] = byte(bcSpout.dao.Version.StoragePrefix) - bcSpout.dao.Store.Seek(storage.SeekRange{Prefix: bPrefix}, func(k, v []byte) bool { - key := slice.Copy(k) - key[0] = byte(tempPrefix) - value := slice.Copy(v) - batch.Put(key, value) - return true +func TestBlockchain_VerifyTx(t *testing.T) { + bc, validator, committee := chain.NewMultiWithCustomConfig(t, func(c *config.ProtocolConfiguration) { + c.P2PSigExtensions = true + c.ReservedAttributes = true }) - _, err := batch.Persist() - require.NoError(t, err) + e := neotest.NewExecutor(t, bc, validator, committee) - checkNewBlockchainErr := func(t *testing.T, cfg func(c *config.Config), store storage.Store, errText string) { - unitTestNetCfg, err := config.Load("../../config", testchain.Network()) + accs := make([]*wallet.Account, 5) + for i := range accs { + var err error + accs[i], err = wallet.NewAccount() require.NoError(t, err) - cfg(&unitTestNetCfg) - log := zaptest.NewLogger(t) - _, err = NewBlockchain(store, unitTestNetCfg.ProtocolConfiguration, log) - if len(errText) != 0 { - require.Error(t, err) - require.True(t, strings.Contains(err.Error(), errText)) - } else { + } + + notaryServiceFeePerKey := bc.GetNotaryServiceFeePerKey() + + oracleAcc := accs[2] + oraclePubs := keys.PublicKeys{oracleAcc.PrivateKey().PublicKey()} + require.NoError(t, oracleAcc.ConvertMultisig(1, oraclePubs)) + + neoHash := e.NativeHash(t, nativenames.Neo) + gasHash := e.NativeHash(t, nativenames.Gas) + policyHash := e.NativeHash(t, nativenames.Policy) + designateHash := e.NativeHash(t, nativenames.Designation) + notaryHash := e.NativeHash(t, nativenames.Notary) + oracleHash := e.NativeHash(t, nativenames.Oracle) + + neoValidatorsInvoker := e.ValidatorInvoker(neoHash) + gasValidatorsInvoker := e.ValidatorInvoker(gasHash) + policySuperInvoker := e.NewInvoker(policyHash, validator, committee) + designateSuperInvoker := e.NewInvoker(designateHash, validator, committee) + neoOwner := validator.ScriptHash() + + neoAmount := int64(1_000_000) + gasAmount := int64(1_000_000_000) + txs := make([]*transaction.Transaction, 0, 2*len(accs)+2) + for _, a := range accs { + txs = append(txs, neoValidatorsInvoker.PrepareInvoke(t, "transfer", neoOwner, a.Contract.ScriptHash(), neoAmount, nil)) + txs = append(txs, gasValidatorsInvoker.PrepareInvoke(t, "transfer", neoOwner, a.Contract.ScriptHash(), gasAmount, nil)) + } + txs = append(txs, neoValidatorsInvoker.PrepareInvoke(t, "transfer", neoOwner, committee.ScriptHash(), neoAmount, nil)) + txs = append(txs, gasValidatorsInvoker.PrepareInvoke(t, "transfer", neoOwner, committee.ScriptHash(), gasAmount, nil)) + e.AddNewBlock(t, txs...) + for _, tx := range txs { + e.CheckHalt(t, tx.Hash(), stackitem.NewBool(true)) + } + policySuperInvoker.Invoke(t, true, "blockAccount", accs[1].PrivateKey().GetScriptHash().BytesBE()) + + checkErr := func(t *testing.T, expectedErr error, tx *transaction.Transaction) { + err := bc.VerifyTx(tx) + require.True(t, errors.Is(err, expectedErr), "expected: %v, got: %v", expectedErr, err) + } + + testScript := []byte{byte(opcode.PUSH1)} + newTestTx := func(t *testing.T, signer util.Uint160, script []byte) *transaction.Transaction { + tx := transaction.New(script, 1_000_000) + tx.Nonce = neotest.Nonce() + tx.ValidUntilBlock = e.Chain.BlockHeight() + 5 + tx.Signers = []transaction.Signer{{ + Account: signer, + Scopes: transaction.CalledByEntry, + }} + tx.NetworkFee = int64(io.GetVarSize(tx)+200 /* witness */) * bc.FeePerByte() + tx.NetworkFee += 1_000_000 // verification cost + return tx + } + + h := accs[0].PrivateKey().GetScriptHash() + t.Run("Expired", func(t *testing.T) { + tx := newTestTx(t, h, testScript) + tx.ValidUntilBlock = 1 + require.NoError(t, accs[0].SignTx(netmode.UnitTestNet, tx)) + checkErr(t, core.ErrTxExpired, tx) + }) + t.Run("BlockedAccount", func(t *testing.T) { + tx := newTestTx(t, accs[1].PrivateKey().GetScriptHash(), testScript) + require.NoError(t, accs[1].SignTx(netmode.UnitTestNet, tx)) + checkErr(t, core.ErrPolicy, tx) + }) + t.Run("InsufficientGas", func(t *testing.T) { + balance := bc.GetUtilityTokenBalance(h) + tx := newTestTx(t, h, testScript) + tx.SystemFee = balance.Int64() + 1 + require.NoError(t, accs[0].SignTx(netmode.UnitTestNet, tx)) + checkErr(t, core.ErrInsufficientFunds, tx) + }) + t.Run("TooBigTx", func(t *testing.T) { + script := make([]byte, transaction.MaxTransactionSize) + tx := newTestTx(t, h, script) + require.NoError(t, accs[0].SignTx(netmode.UnitTestNet, tx)) + checkErr(t, core.ErrTxTooBig, tx) + }) + t.Run("NetworkFee", func(t *testing.T) { + t.Run("SmallNetworkFee", func(t *testing.T) { + tx := newTestTx(t, h, testScript) + tx.NetworkFee = 1 + require.NoError(t, accs[0].SignTx(netmode.UnitTestNet, tx)) + checkErr(t, core.ErrTxSmallNetworkFee, tx) + }) + t.Run("AlmostEnoughNetworkFee", func(t *testing.T) { + tx := newTestTx(t, h, testScript) + verificationNetFee, calcultedScriptSize := fee.Calculate(bc.GetBaseExecFee(), accs[0].Contract.Script) + expectedSize := io.GetVarSize(tx) + calcultedScriptSize + calculatedNetFee := verificationNetFee + int64(expectedSize)*bc.FeePerByte() + tx.NetworkFee = calculatedNetFee - 1 + require.NoError(t, accs[0].SignTx(netmode.UnitTestNet, tx)) + require.Equal(t, expectedSize, io.GetVarSize(tx)) + checkErr(t, core.ErrVerificationFailed, tx) + }) + t.Run("EnoughNetworkFee", func(t *testing.T) { + tx := newTestTx(t, h, testScript) + verificationNetFee, calcultedScriptSize := fee.Calculate(bc.GetBaseExecFee(), accs[0].Contract.Script) + expectedSize := io.GetVarSize(tx) + calcultedScriptSize + calculatedNetFee := verificationNetFee + int64(expectedSize)*bc.FeePerByte() + tx.NetworkFee = calculatedNetFee + require.NoError(t, accs[0].SignTx(netmode.UnitTestNet, tx)) + require.Equal(t, expectedSize, io.GetVarSize(tx)) + require.NoError(t, bc.VerifyTx(tx)) + }) + t.Run("CalculateNetworkFee, signature script", func(t *testing.T) { + tx := newTestTx(t, h, testScript) + expectedSize := io.GetVarSize(tx) + verificationNetFee, calculatedScriptSize := fee.Calculate(bc.GetBaseExecFee(), accs[0].Contract.Script) + expectedSize += calculatedScriptSize + expectedNetFee := verificationNetFee + int64(expectedSize)*bc.FeePerByte() + tx.NetworkFee = expectedNetFee + require.NoError(t, accs[0].SignTx(netmode.UnitTestNet, tx)) + actualSize := io.GetVarSize(tx) + require.Equal(t, expectedSize, actualSize) + gasConsumed, err := bc.VerifyWitness(h, tx, &tx.Scripts[0], -1) require.NoError(t, err) - } - } - boltCfg := func(c *config.Config) { - spountCfg(c) - c.ProtocolConfiguration.KeepOnlyLatestState = true - } - // manually store statejump stage to check statejump recover process - bPrefix[0] = byte(storage.SYSStateJumpStage) - t.Run("invalid RemoveUntraceableBlocks setting", func(t *testing.T) { - bcSpout.dao.Store.Put(bPrefix, []byte{byte(stateJumpStarted)}) - checkNewBlockchainErr(t, func(c *config.Config) { - boltCfg(c) - c.ProtocolConfiguration.RemoveUntraceableBlocks = false - }, bcSpout.dao.Store, "state jump was not completed, but P2PStateExchangeExtensions are disabled or archival node capability is on") + require.Equal(t, verificationNetFee, gasConsumed) + require.Equal(t, expectedNetFee, bc.FeePerByte()*int64(actualSize)+gasConsumed) + }) + t.Run("CalculateNetworkFee, multisignature script", func(t *testing.T) { + multisigAcc := accs[4] + pKeys := keys.PublicKeys{multisigAcc.PrivateKey().PublicKey()} + require.NoError(t, multisigAcc.ConvertMultisig(1, pKeys)) + multisigHash := hash.Hash160(multisigAcc.Contract.Script) + tx := newTestTx(t, multisigHash, testScript) + verificationNetFee, calculatedScriptSize := fee.Calculate(bc.GetBaseExecFee(), multisigAcc.Contract.Script) + expectedSize := io.GetVarSize(tx) + calculatedScriptSize + expectedNetFee := verificationNetFee + int64(expectedSize)*bc.FeePerByte() + tx.NetworkFee = expectedNetFee + require.NoError(t, multisigAcc.SignTx(netmode.UnitTestNet, tx)) + actualSize := io.GetVarSize(tx) + require.Equal(t, expectedSize, actualSize) + gasConsumed, err := bc.VerifyWitness(multisigHash, tx, &tx.Scripts[0], -1) + require.NoError(t, err) + require.Equal(t, verificationNetFee, gasConsumed) + require.Equal(t, expectedNetFee, bc.FeePerByte()*int64(actualSize)+gasConsumed) + }) }) - t.Run("invalid state jump stage format", func(t *testing.T) { - bcSpout.dao.Store.Put(bPrefix, []byte{0x01, 0x02}) - checkNewBlockchainErr(t, boltCfg, bcSpout.dao.Store, "invalid state jump stage format") + t.Run("InvalidTxScript", func(t *testing.T) { + tx := newTestTx(t, h, testScript) + tx.Script = append(tx.Script, 0xff) + require.NoError(t, accs[0].SignTx(netmode.UnitTestNet, tx)) + checkErr(t, core.ErrInvalidScript, tx) }) - t.Run("missing state sync point", func(t *testing.T) { - bcSpout.dao.Store.Put(bPrefix, []byte{byte(stateJumpStarted)}) - checkNewBlockchainErr(t, boltCfg, bcSpout.dao.Store, "failed to get state sync point from the storage") + t.Run("InvalidVerificationScript", func(t *testing.T) { + tx := newTestTx(t, h, testScript) + verif := []byte{byte(opcode.JMP), 3, 0xff, byte(opcode.PUSHT)} + tx.Signers = append(tx.Signers, transaction.Signer{ + Account: hash.Hash160(verif), + Scopes: transaction.Global, + }) + tx.NetworkFee += 1000000 + require.NoError(t, accs[0].SignTx(netmode.UnitTestNet, tx)) + tx.Scripts = append(tx.Scripts, transaction.Witness{ + InvocationScript: []byte{}, + VerificationScript: verif, + }) + checkErr(t, core.ErrInvalidVerification, tx) }) - t.Run("invalid state sync point", func(t *testing.T) { - bcSpout.dao.Store.Put(bPrefix, []byte{byte(stateJumpStarted)}) - point := make([]byte, 4) - binary.LittleEndian.PutUint32(point, uint32(len(bcSpout.headerHashes))) - bcSpout.dao.Store.Put([]byte{byte(storage.SYSStateSyncPoint)}, point) - checkNewBlockchainErr(t, boltCfg, bcSpout.dao.Store, "invalid state sync point") + t.Run("InvalidInvocationScript", func(t *testing.T) { + tx := newTestTx(t, h, testScript) + verif := []byte{byte(opcode.PUSHT)} + tx.Signers = append(tx.Signers, transaction.Signer{ + Account: hash.Hash160(verif), + Scopes: transaction.Global, + }) + tx.NetworkFee += 1000000 + require.NoError(t, accs[0].SignTx(netmode.UnitTestNet, tx)) + tx.Scripts = append(tx.Scripts, transaction.Witness{ + InvocationScript: []byte{byte(opcode.JMP), 3, 0xff}, + VerificationScript: verif, + }) + checkErr(t, core.ErrInvalidInvocation, tx) }) - for _, stage := range []stateJumpStage{stateJumpStarted, newStorageItemsAdded, genesisStateRemoved, 0x03} { - t.Run(fmt.Sprintf("state jump stage %d", stage), func(t *testing.T) { - bcSpout.dao.Store.Put(bPrefix, []byte{byte(stage)}) - point := make([]byte, 4) - binary.LittleEndian.PutUint32(point, uint32(stateSyncPoint)) - bcSpout.dao.Store.Put([]byte{byte(storage.SYSStateSyncPoint)}, point) - var errText string - if stage == 0x03 { - errText = "unknown state jump stage" + t.Run("Conflict", func(t *testing.T) { + balance := bc.GetUtilityTokenBalance(h).Int64() + tx := newTestTx(t, h, testScript) + tx.NetworkFee = balance / 2 + require.NoError(t, accs[0].SignTx(netmode.UnitTestNet, tx)) + require.NoError(t, bc.PoolTx(tx)) + + tx2 := newTestTx(t, h, testScript) + tx2.NetworkFee = balance / 2 + require.NoError(t, accs[0].SignTx(netmode.UnitTestNet, tx2)) + err := bc.PoolTx(tx2) + require.True(t, errors.Is(err, core.ErrMemPoolConflict)) + }) + t.Run("InvalidWitnessHash", func(t *testing.T) { + tx := newTestTx(t, h, testScript) + require.NoError(t, accs[0].SignTx(netmode.UnitTestNet, tx)) + tx.Scripts[0].VerificationScript = []byte{byte(opcode.PUSHT)} + checkErr(t, core.ErrWitnessHashMismatch, tx) + }) + t.Run("InvalidWitnessSignature", func(t *testing.T) { + tx := newTestTx(t, h, testScript) + require.NoError(t, accs[0].SignTx(netmode.UnitTestNet, tx)) + tx.Scripts[0].InvocationScript[10] = ^tx.Scripts[0].InvocationScript[10] + checkErr(t, core.ErrVerificationFailed, tx) + }) + t.Run("InsufficientNetworkFeeForSecondWitness", func(t *testing.T) { + tx := newTestTx(t, h, testScript) + tx.Signers = append(tx.Signers, transaction.Signer{ + Account: accs[3].PrivateKey().GetScriptHash(), + Scopes: transaction.Global, + }) + require.NoError(t, accs[0].SignTx(netmode.UnitTestNet, tx)) + require.NoError(t, accs[3].SignTx(netmode.UnitTestNet, tx)) + checkErr(t, core.ErrVerificationFailed, tx) + }) + t.Run("OldTX", func(t *testing.T) { + tx := newTestTx(t, h, testScript) + require.NoError(t, accs[0].SignTx(netmode.UnitTestNet, tx)) + e.AddNewBlock(t, tx) + + checkErr(t, core.ErrAlreadyExists, tx) + }) + t.Run("MemPooledTX", func(t *testing.T) { + tx := newTestTx(t, h, testScript) + require.NoError(t, accs[0].SignTx(netmode.UnitTestNet, tx)) + require.NoError(t, bc.PoolTx(tx)) + + err := bc.PoolTx(tx) + require.True(t, errors.Is(err, core.ErrAlreadyExists)) + }) + t.Run("MemPoolOOM", func(t *testing.T) { + mp := mempool.New(1, 0, false) + tx1 := newTestTx(t, h, testScript) + tx1.NetworkFee += 10000 // Give it more priority. + require.NoError(t, accs[0].SignTx(netmode.UnitTestNet, tx1)) + require.NoError(t, bc.PoolTx(tx1, mp)) + + tx2 := newTestTx(t, h, testScript) + require.NoError(t, accs[0].SignTx(netmode.UnitTestNet, tx2)) + err := bc.PoolTx(tx2, mp) + require.True(t, errors.Is(err, core.ErrOOM)) + }) + t.Run("Attribute", func(t *testing.T) { + t.Run("InvalidHighPriority", func(t *testing.T) { + tx := newTestTx(t, h, testScript) + tx.Attributes = append(tx.Attributes, transaction.Attribute{Type: transaction.HighPriority}) + require.NoError(t, accs[0].SignTx(netmode.UnitTestNet, tx)) + checkErr(t, core.ErrInvalidAttribute, tx) + }) + t.Run("ValidHighPriority", func(t *testing.T) { + tx := newTestTx(t, h, testScript) + tx.Attributes = append(tx.Attributes, transaction.Attribute{Type: transaction.HighPriority}) + tx.NetworkFee += 4_000_000 // multisig check + tx.Signers = []transaction.Signer{{ + Account: committee.ScriptHash(), + Scopes: transaction.None, + }} + rawScript := committee.Script() + size := io.GetVarSize(tx) + netFee, sizeDelta := fee.Calculate(bc.GetBaseExecFee(), rawScript) + tx.NetworkFee += netFee + tx.NetworkFee += int64(size+sizeDelta) * bc.FeePerByte() + tx.Scripts = []transaction.Witness{{ + InvocationScript: committee.SignHashable(uint32(netmode.UnitTestNet), tx), + VerificationScript: rawScript, + }} + require.NoError(t, bc.VerifyTx(tx)) + }) + t.Run("Oracle", func(t *testing.T) { + cs := contracts.GetOracleContractState(t, pathToInternalContracts, validator.ScriptHash(), 0) + e.DeployContract(t, &neotest.Contract{ + Hash: cs.Hash, + NEF: &cs.NEF, + Manifest: &cs.Manifest, + }, nil) + cInvoker := e.ValidatorInvoker(cs.Hash) + + const gasForResponse int64 = 10_000_000 + putOracleRequest(t, cInvoker, "https://get.1234", new(string), "handle", []byte{}, gasForResponse) + + oracleScript, err := smartcontract.CreateMajorityMultiSigRedeemScript(oraclePubs) + require.NoError(t, err) + oracleMultisigHash := hash.Hash160(oracleScript) + + respScript := native.CreateOracleResponseScript(oracleHash) + + // We need to create new transaction, + // because hashes are cached after signing. + getOracleTx := func(t *testing.T) *transaction.Transaction { + tx := transaction.New(respScript, 1000_0000) + tx.Nonce = neotest.Nonce() + tx.ValidUntilBlock = bc.BlockHeight() + 1 + resp := &transaction.OracleResponse{ + ID: 0, + Code: transaction.Success, + Result: []byte{1, 2, 3}, + } + tx.Attributes = []transaction.Attribute{{ + Type: transaction.OracleResponseT, + Value: resp, + }} + tx.NetworkFee += 4_000_000 // multisig check + tx.SystemFee = gasForResponse - tx.NetworkFee + tx.Signers = []transaction.Signer{{ + Account: oracleMultisigHash, + Scopes: transaction.None, + }} + size := io.GetVarSize(tx) + netFee, sizeDelta := fee.Calculate(bc.GetBaseExecFee(), oracleScript) + tx.NetworkFee += netFee + tx.NetworkFee += int64(size+sizeDelta) * bc.FeePerByte() + return tx } - checkNewBlockchainErr(t, spountCfg, bcSpout.dao.Store, errText) - }) - } -} -func TestChainWithVolatileNumOfValidators(t *testing.T) { - bc := newTestChainWithCustomCfg(t, func(c *config.Config) { - c.ProtocolConfiguration.ValidatorsCount = 0 - c.ProtocolConfiguration.CommitteeHistory = map[uint32]int{ - 0: 1, - 4: 4, - 24: 6, - } - c.ProtocolConfiguration.ValidatorsHistory = map[uint32]int{ - 0: 1, - 4: 4, - } - require.NoError(t, c.ProtocolConfiguration.Validate()) - }) - require.Equal(t, uint32(0), bc.BlockHeight()) + t.Run("NoOracleNodes", func(t *testing.T) { + tx := getOracleTx(t) + require.NoError(t, oracleAcc.SignTx(netmode.UnitTestNet, tx)) + checkErr(t, core.ErrInvalidAttribute, tx) + }) - priv0 := testchain.PrivateKeyByID(0) - - vals, err := bc.GetValidators() - require.NoError(t, err) - script, err := smartcontract.CreateDefaultMultiSigRedeemScript(vals) - require.NoError(t, err) - curWit := transaction.Witness{ - VerificationScript: script, - } - for i := 1; i < 26; i++ { - comm, err := bc.GetCommittee() - require.NoError(t, err) - if i < 5 { - require.Equal(t, 1, len(comm)) - } else if i < 25 { - require.Equal(t, 4, len(comm)) - } else { - require.Equal(t, 6, len(comm)) - } - // Mimic consensus. - if bc.config.ShouldUpdateCommitteeAt(uint32(i)) { - vals, err = bc.GetValidators() - } else { - vals, err = bc.GetNextBlockValidators() - } - require.NoError(t, err) - if i < 4 { - require.Equalf(t, 1, len(vals), "at %d", i) - } else { - require.Equalf(t, 4, len(vals), "at %d", i) - } - require.NoError(t, err) - script, err := smartcontract.CreateDefaultMultiSigRedeemScript(vals) - require.NoError(t, err) - nextWit := transaction.Witness{ - VerificationScript: script, - } - b := &block.Block{ - Header: block.Header{ - NextConsensus: nextWit.ScriptHash(), - Script: curWit, - }, - } - curWit = nextWit - b.PrevHash = bc.GetHeaderHash(i - 1) - b.Timestamp = uint64(time.Now().UTC().Unix())*1000 + uint64(i) - b.Index = uint32(i) - b.RebuildMerkleRoot() - if i < 5 { - signa := priv0.SignHashable(uint32(bc.config.Magic), b) - b.Script.InvocationScript = append([]byte{byte(opcode.PUSHDATA1), byte(len(signa))}, signa...) - } else { - b.Script.InvocationScript = testchain.Sign(b) - } - err = bc.AddBlock(b) - require.NoErrorf(t, err, "at %d", i) - } -} - -func setSigner(tx *transaction.Transaction, h util.Uint160) { - tx.Signers = []transaction.Signer{{ - Account: h, - Scopes: transaction.Global, - }} -} - -func TestBlockchain_StartFromExistingDB(t *testing.T) { - ps, path := newLevelDBForTestingWithPath(t, "") - customConfig := func(c *config.Config) { - c.ProtocolConfiguration.StateRootInHeader = true // Need for P2PStateExchangeExtensions check. - } - bc := initTestChain(t, ps, customConfig) - go bc.Run() - initBasicChain(t, bc) - require.True(t, bc.BlockHeight() > 5, "ensure that basic chain is correctly initialised") - - // Information for further tests. - h := bc.BlockHeight() - cryptoLibHash, err := bc.GetNativeContractScriptHash(nativenames.CryptoLib) - require.NoError(t, err) - cryptoLibState := bc.GetContractState(cryptoLibHash) - require.NotNil(t, cryptoLibState) - var ( - managementID = -1 - managementContractPrefix = 8 - ) - - bc.Close() // Ensure persist is done and persistent store is properly closed. - - newPS := func(t *testing.T) storage.Store { - ps, _ = newLevelDBForTestingWithPath(t, path) - t.Cleanup(func() { require.NoError(t, ps.Close()) }) - return ps - } - t.Run("mismatch storage version", func(t *testing.T) { - ps = newPS(t) - cache := storage.NewMemCachedStore(ps) // Extra wrapper to avoid good DB corruption. - d := dao.NewSimple(cache, bc.config.StateRootInHeader, bc.config.P2PStateExchangeExtensions) - d.PutVersion(dao.Version{ - Value: "0.0.0", - }) - _, err := d.Persist() // Persist to `cache` wrapper. - require.NoError(t, err) - _, err = initTestChainNoCheck(t, cache, customConfig) - require.Error(t, err) - require.True(t, strings.Contains(err.Error(), "storage version mismatch")) - }) - t.Run("mismatch StateRootInHeader", func(t *testing.T) { - ps = newPS(t) - _, err := initTestChainNoCheck(t, ps, func(c *config.Config) { - customConfig(c) - c.ProtocolConfiguration.StateRootInHeader = false - }) - require.Error(t, err) - require.True(t, strings.Contains(err.Error(), "StateRootInHeader setting mismatch")) - }) - t.Run("mismatch P2PSigExtensions", func(t *testing.T) { - ps = newPS(t) - _, err := initTestChainNoCheck(t, ps, func(c *config.Config) { - customConfig(c) - c.ProtocolConfiguration.P2PSigExtensions = false - }) - require.Error(t, err) - require.True(t, strings.Contains(err.Error(), "P2PSigExtensions setting mismatch")) - }) - t.Run("mismatch P2PStateExchangeExtensions", func(t *testing.T) { - ps = newPS(t) - _, err := initTestChainNoCheck(t, ps, func(c *config.Config) { - customConfig(c) - c.ProtocolConfiguration.StateRootInHeader = true - c.ProtocolConfiguration.P2PStateExchangeExtensions = true - }) - require.Error(t, err) - require.True(t, strings.Contains(err.Error(), "P2PStateExchangeExtensions setting mismatch")) - }) - t.Run("mismatch KeepOnlyLatestState", func(t *testing.T) { - ps = newPS(t) - _, err := initTestChainNoCheck(t, ps, func(c *config.Config) { - customConfig(c) - c.ProtocolConfiguration.KeepOnlyLatestState = true - }) - require.Error(t, err) - require.True(t, strings.Contains(err.Error(), "KeepOnlyLatestState setting mismatch")) - }) - t.Run("corrupted headers", func(t *testing.T) { - ps = newPS(t) - - // Corrupt headers hashes batch. - cache := storage.NewMemCachedStore(ps) // Extra wrapper to avoid good DB corruption. - key := make([]byte, 5) - key[0] = byte(storage.IXHeaderHashList) - binary.BigEndian.PutUint32(key[1:], 1) - cache.Put(key, []byte{1, 2, 3}) - - _, err := initTestChainNoCheck(t, cache, customConfig) - require.Error(t, err) - require.True(t, strings.Contains(err.Error(), "failed to read batch of 2000")) - }) - t.Run("corrupted current header height", func(t *testing.T) { - ps = newPS(t) - - // Remove current header. - cache := storage.NewMemCachedStore(ps) // Extra wrapper to avoid good DB corruption. - cache.Delete([]byte{byte(storage.SYSCurrentHeader)}) - - _, err := initTestChainNoCheck(t, cache, customConfig) - require.Error(t, err) - require.True(t, strings.Contains(err.Error(), "failed to retrieve current header")) - }) - t.Run("missing last batch of 2000 headers and missing last header", func(t *testing.T) { - ps = newPS(t) - - // Remove latest headers hashes batch and current header. - cache := storage.NewMemCachedStore(ps) // Extra wrapper to avoid good DB corruption. - cache.Delete([]byte{byte(storage.IXHeaderHashList)}) - currHeaderInfo, err := cache.Get([]byte{byte(storage.SYSCurrentHeader)}) - require.NoError(t, err) - currHeaderHash, err := util.Uint256DecodeBytesLE(currHeaderInfo[:32]) - require.NoError(t, err) - cache.Delete(append([]byte{byte(storage.DataExecutable)}, currHeaderHash.BytesBE()...)) - - _, err = initTestChainNoCheck(t, cache, customConfig) - require.Error(t, err) - require.True(t, strings.Contains(err.Error(), "could not get header")) - }) - t.Run("missing last block", func(t *testing.T) { - ps = newPS(t) - - // Remove current block from storage. - cache := storage.NewMemCachedStore(ps) // Extra wrapper to avoid good DB corruption. - cache.Delete([]byte{byte(storage.SYSCurrentBlock)}) - - _, err := initTestChainNoCheck(t, cache, customConfig) - require.Error(t, err) - require.True(t, strings.Contains(err.Error(), "failed to retrieve current block height")) - }) - t.Run("missing last stateroot", func(t *testing.T) { - ps = newPS(t) - - // Remove latest stateroot from storage. - cache := storage.NewMemCachedStore(ps) // Extra wrapper to avoid good DB corruption. - key := make([]byte, 5) - key[0] = byte(storage.DataMPTAux) - binary.BigEndian.PutUint32(key, h) - cache.Delete(key) - - _, err := initTestChainNoCheck(t, cache, customConfig) - require.Error(t, err) - require.True(t, strings.Contains(err.Error(), "can't init MPT at height")) - }) - t.Run("failed native Management initialisation", func(t *testing.T) { - ps = newPS(t) - - // Corrupt serialised CryptoLib state. - cache := storage.NewMemCachedStore(ps) // Extra wrapper to avoid good DB corruption. - key := make([]byte, 1+4+1+20) - key[0] = byte(storage.STStorage) - binary.LittleEndian.PutUint32(key[1:], uint32(managementID)) - key[5] = byte(managementContractPrefix) - copy(key[6:], cryptoLibHash.BytesBE()) - cache.Put(key, []byte{1, 2, 3}) - - _, err := initTestChainNoCheck(t, cache, customConfig) - require.Error(t, err) - require.True(t, strings.Contains(err.Error(), "can't init cache for Management native contract")) - }) - t.Run("invalid native contract deactivation", func(t *testing.T) { - ps = newPS(t) - _, err := initTestChainNoCheck(t, ps, func(c *config.Config) { - customConfig(c) - c.ProtocolConfiguration.NativeUpdateHistories = map[string][]uint32{ - nativenames.Policy: {0}, - nativenames.Neo: {0}, - nativenames.Gas: {0}, - nativenames.Designation: {0}, - nativenames.StdLib: {0}, - nativenames.Management: {0}, - nativenames.Oracle: {0}, - nativenames.Ledger: {0}, - nativenames.Notary: {0}, - nativenames.CryptoLib: {h + 10}, + keys := make([]interface{}, 0, len(oraclePubs)) + for _, p := range oraclePubs { + keys = append(keys, p.Bytes()) } + designateSuperInvoker.Invoke(t, stackitem.Null{}, "designateAsRole", + int64(roles.Oracle), keys) + + t.Run("Valid", func(t *testing.T) { + tx := getOracleTx(t) + require.NoError(t, oracleAcc.SignTx(netmode.UnitTestNet, tx)) + require.NoError(t, bc.VerifyTx(tx)) + + t.Run("NativeVerify", func(t *testing.T) { + tx.Signers = append(tx.Signers, transaction.Signer{ + Account: oracleHash, + Scopes: transaction.None, + }) + tx.Scripts = append(tx.Scripts, transaction.Witness{}) + t.Run("NonZeroVerification", func(t *testing.T) { + w := io.NewBufBinWriter() + emit.Opcodes(w.BinWriter, opcode.ABORT) + emit.Bytes(w.BinWriter, util.Uint160{}.BytesBE()) + emit.Int(w.BinWriter, 0) + emit.String(w.BinWriter, nativenames.Oracle) + tx.Scripts[len(tx.Scripts)-1].VerificationScript = w.Bytes() + err := bc.VerifyTx(tx) + require.True(t, errors.Is(err, core.ErrNativeContractWitness), "got: %v", err) + }) + t.Run("Good", func(t *testing.T) { + tx.Scripts[len(tx.Scripts)-1].VerificationScript = nil + require.NoError(t, bc.VerifyTx(tx)) + }) + }) + }) + t.Run("InvalidRequestID", func(t *testing.T) { + tx := getOracleTx(t) + tx.Attributes[0].Value.(*transaction.OracleResponse).ID = 2 + require.NoError(t, oracleAcc.SignTx(netmode.UnitTestNet, tx)) + checkErr(t, core.ErrInvalidAttribute, tx) + }) + t.Run("InvalidScope", func(t *testing.T) { + tx := getOracleTx(t) + tx.Signers[0].Scopes = transaction.Global + require.NoError(t, oracleAcc.SignTx(netmode.UnitTestNet, tx)) + checkErr(t, core.ErrInvalidAttribute, tx) + }) + t.Run("InvalidScript", func(t *testing.T) { + tx := getOracleTx(t) + tx.Script = append(tx.Script, byte(opcode.NOP)) + require.NoError(t, oracleAcc.SignTx(netmode.UnitTestNet, tx)) + checkErr(t, core.ErrInvalidAttribute, tx) + }) + t.Run("InvalidSigner", func(t *testing.T) { + tx := getOracleTx(t) + tx.Signers[0].Account = accs[0].Contract.ScriptHash() + require.NoError(t, accs[0].SignTx(netmode.UnitTestNet, tx)) + checkErr(t, core.ErrInvalidAttribute, tx) + }) + t.Run("SmallFee", func(t *testing.T) { + tx := getOracleTx(t) + tx.SystemFee = 0 + require.NoError(t, oracleAcc.SignTx(netmode.UnitTestNet, tx)) + checkErr(t, core.ErrInvalidAttribute, tx) + }) + }) + t.Run("NotValidBefore", func(t *testing.T) { + getNVBTx := func(e *neotest.Executor, height uint32) *transaction.Transaction { + tx := newTestTx(t, h, testScript) + tx.Attributes = append(tx.Attributes, transaction.Attribute{Type: transaction.NotValidBeforeT, Value: &transaction.NotValidBefore{Height: height}}) + tx.NetworkFee += 4_000_000 // multisig check + tx.Signers = []transaction.Signer{{ + Account: e.Validator.ScriptHash(), + Scopes: transaction.None, + }} + size := io.GetVarSize(tx) + rawScript := e.Validator.Script() + netFee, sizeDelta := fee.Calculate(e.Chain.GetBaseExecFee(), rawScript) + tx.NetworkFee += netFee + tx.NetworkFee += int64(size+sizeDelta) * e.Chain.FeePerByte() + tx.Scripts = []transaction.Witness{{ + InvocationScript: e.Validator.SignHashable(uint32(netmode.UnitTestNet), tx), + VerificationScript: rawScript, + }} + return tx + } + t.Run("Disabled", func(t *testing.T) { + bcBad, validatorBad, committeeBad := chain.NewMultiWithCustomConfig(t, func(c *config.ProtocolConfiguration) { + c.P2PSigExtensions = false + c.ReservedAttributes = false + }) + eBad := neotest.NewExecutor(t, bcBad, validatorBad, committeeBad) + tx := getNVBTx(eBad, bcBad.BlockHeight()) + err := bcBad.VerifyTx(tx) + require.Error(t, err) + require.True(t, strings.Contains(err.Error(), "invalid attribute: NotValidBefore attribute was found, but P2PSigExtensions are disabled")) + }) + t.Run("Enabled", func(t *testing.T) { + t.Run("NotYetValid", func(t *testing.T) { + tx := getNVBTx(e, bc.BlockHeight()+1) + require.True(t, errors.Is(bc.VerifyTx(tx), core.ErrInvalidAttribute)) + }) + t.Run("positive", func(t *testing.T) { + tx := getNVBTx(e, bc.BlockHeight()) + require.NoError(t, bc.VerifyTx(tx)) + }) + }) + }) + t.Run("Reserved", func(t *testing.T) { + getReservedTx := func(e *neotest.Executor, attrType transaction.AttrType) *transaction.Transaction { + tx := newTestTx(t, h, testScript) + tx.Attributes = append(tx.Attributes, transaction.Attribute{Type: attrType, Value: &transaction.Reserved{Value: []byte{1, 2, 3}}}) + tx.NetworkFee += 4_000_000 // multisig check + tx.Signers = []transaction.Signer{{ + Account: e.Validator.ScriptHash(), + Scopes: transaction.None, + }} + rawScript := e.Validator.Script() + size := io.GetVarSize(tx) + netFee, sizeDelta := fee.Calculate(e.Chain.GetBaseExecFee(), rawScript) + tx.NetworkFee += netFee + tx.NetworkFee += int64(size+sizeDelta) * e.Chain.FeePerByte() + tx.Scripts = []transaction.Witness{{ + InvocationScript: e.Validator.SignHashable(uint32(netmode.UnitTestNet), tx), + VerificationScript: rawScript, + }} + return tx + } + t.Run("Disabled", func(t *testing.T) { + bcBad, validatorBad, committeeBad := chain.NewMultiWithCustomConfig(t, func(c *config.ProtocolConfiguration) { + c.P2PSigExtensions = false + c.ReservedAttributes = false + }) + eBad := neotest.NewExecutor(t, bcBad, validatorBad, committeeBad) + tx := getReservedTx(eBad, transaction.ReservedLowerBound+3) + err := bcBad.VerifyTx(tx) + require.Error(t, err) + require.True(t, strings.Contains(err.Error(), "invalid attribute: attribute of reserved type was found, but ReservedAttributes are disabled")) + }) + t.Run("Enabled", func(t *testing.T) { + tx := getReservedTx(e, transaction.ReservedLowerBound+3) + require.NoError(t, bc.VerifyTx(tx)) + }) + }) + t.Run("Conflicts", func(t *testing.T) { + getConflictsTx := func(e *neotest.Executor, hashes ...util.Uint256) *transaction.Transaction { + tx := newTestTx(t, h, testScript) + tx.Attributes = make([]transaction.Attribute, len(hashes)) + for i, h := range hashes { + tx.Attributes[i] = transaction.Attribute{ + Type: transaction.ConflictsT, + Value: &transaction.Conflicts{ + Hash: h, + }, + } + } + tx.NetworkFee += 4_000_000 // multisig check + tx.Signers = []transaction.Signer{{ + Account: e.Validator.ScriptHash(), + Scopes: transaction.None, + }} + rawScript := e.Validator.Script() + size := io.GetVarSize(tx) + netFee, sizeDelta := fee.Calculate(e.Chain.GetBaseExecFee(), rawScript) + tx.NetworkFee += netFee + tx.NetworkFee += int64(size+sizeDelta) * e.Chain.FeePerByte() + tx.Scripts = []transaction.Witness{{ + InvocationScript: e.Validator.SignHashable(uint32(netmode.UnitTestNet), tx), + VerificationScript: rawScript, + }} + return tx + } + t.Run("disabled", func(t *testing.T) { + bcBad, validatorBad, committeeBad := chain.NewMultiWithCustomConfig(t, func(c *config.ProtocolConfiguration) { + c.P2PSigExtensions = false + c.ReservedAttributes = false + }) + eBad := neotest.NewExecutor(t, bcBad, validatorBad, committeeBad) + tx := getConflictsTx(eBad, util.Uint256{1, 2, 3}) + err := bcBad.VerifyTx(tx) + require.Error(t, err) + require.True(t, strings.Contains(err.Error(), "invalid attribute: Conflicts attribute was found, but P2PSigExtensions are disabled")) + }) + t.Run("enabled", func(t *testing.T) { + t.Run("dummy on-chain conflict", func(t *testing.T) { + tx := newTestTx(t, h, testScript) + require.NoError(t, accs[0].SignTx(netmode.UnitTestNet, tx)) + conflicting := transaction.New([]byte{byte(opcode.RET)}, 1000_0000) + conflicting.ValidUntilBlock = bc.BlockHeight() + 1 + conflicting.Signers = []transaction.Signer{ + { + Account: validator.ScriptHash(), + Scopes: transaction.CalledByEntry, + }, + } + conflicting.Attributes = []transaction.Attribute{ + { + Type: transaction.ConflictsT, + Value: &transaction.Conflicts{ + Hash: tx.Hash(), + }, + }, + } + conflicting.NetworkFee = 1000_0000 + require.NoError(t, validator.SignTx(netmode.UnitTestNet, conflicting)) + e.AddNewBlock(t, conflicting) + require.True(t, errors.Is(bc.VerifyTx(tx), core.ErrHasConflicts)) + }) + t.Run("attribute on-chain conflict", func(t *testing.T) { + tx := neoValidatorsInvoker.Invoke(t, stackitem.NewBool(true), "transfer", neoOwner, neoOwner, 1, nil) + txConflict := getConflictsTx(e, tx) + require.Error(t, bc.VerifyTx(txConflict)) + }) + t.Run("positive", func(t *testing.T) { + tx := getConflictsTx(e, random.Uint256()) + require.NoError(t, bc.VerifyTx(tx)) + }) + }) + }) + t.Run("NotaryAssisted", func(t *testing.T) { + notary, err := wallet.NewAccount() + require.NoError(t, err) + designateSuperInvoker.Invoke(t, stackitem.Null{}, "designateAsRole", + int64(roles.P2PNotary), []interface{}{notary.PrivateKey().PublicKey().Bytes()}) + txSetNotary := transaction.New([]byte{byte(opcode.RET)}, 0) + txSetNotary.Signers = []transaction.Signer{ + { + Account: committee.ScriptHash(), + Scopes: transaction.Global, + }, + } + txSetNotary.Scripts = []transaction.Witness{{ + InvocationScript: e.Committee.SignHashable(uint32(netmode.UnitTestNet), txSetNotary), + VerificationScript: e.Committee.Script(), + }} + + getNotaryAssistedTx := func(e *neotest.Executor, signaturesCount uint8, serviceFee int64) *transaction.Transaction { + tx := newTestTx(t, h, testScript) + tx.Attributes = append(tx.Attributes, transaction.Attribute{Type: transaction.NotaryAssistedT, Value: &transaction.NotaryAssisted{ + NKeys: signaturesCount, + }}) + tx.NetworkFee += serviceFee // additional fee for NotaryAssisted attribute + tx.NetworkFee += 4_000_000 // multisig check + tx.Signers = []transaction.Signer{{ + Account: e.CommitteeHash, + Scopes: transaction.None, + }, + { + Account: notaryHash, + Scopes: transaction.None, + }, + } + rawScript := committee.Script() + size := io.GetVarSize(tx) + netFee, sizeDelta := fee.Calculate(e.Chain.GetBaseExecFee(), rawScript) + tx.NetworkFee += netFee + tx.NetworkFee += int64(size+sizeDelta) * e.Chain.FeePerByte() + tx.Scripts = []transaction.Witness{ + { + InvocationScript: committee.SignHashable(uint32(netmode.UnitTestNet), tx), + VerificationScript: rawScript, + }, + { + InvocationScript: append([]byte{byte(opcode.PUSHDATA1), 64}, notary.PrivateKey().SignHashable(uint32(netmode.UnitTestNet), tx)...), + }, + } + return tx + } + t.Run("Disabled", func(t *testing.T) { + bcBad, validatorBad, committeeBad := chain.NewMultiWithCustomConfig(t, func(c *config.ProtocolConfiguration) { + c.P2PSigExtensions = false + c.ReservedAttributes = false + }) + eBad := neotest.NewExecutor(t, bcBad, validatorBad, committeeBad) + tx := transaction.New(testScript, 1_000_000) + tx.Nonce = neotest.Nonce() + tx.ValidUntilBlock = e.Chain.BlockHeight() + 5 + tx.Attributes = append(tx.Attributes, transaction.Attribute{Type: transaction.NotaryAssistedT, Value: &transaction.NotaryAssisted{NKeys: 0}}) + tx.NetworkFee = 1_0000_0000 + eBad.SignTx(t, tx, 1_0000_0000, eBad.Committee) + err := bcBad.VerifyTx(tx) + require.Error(t, err) + require.True(t, strings.Contains(err.Error(), "invalid attribute: NotaryAssisted attribute was found, but P2PSigExtensions are disabled")) + }) + t.Run("Enabled, insufficient network fee", func(t *testing.T) { + tx := getNotaryAssistedTx(e, 1, 0) + require.Error(t, bc.VerifyTx(tx)) + }) + t.Run("Test verify", func(t *testing.T) { + t.Run("no NotaryAssisted attribute", func(t *testing.T) { + tx := getNotaryAssistedTx(e, 1, (1+1)*notaryServiceFeePerKey) + tx.Attributes = []transaction.Attribute{} + tx.Signers = []transaction.Signer{ + { + Account: committee.ScriptHash(), + Scopes: transaction.None, + }, + { + Account: notaryHash, + Scopes: transaction.None, + }, + } + tx.Scripts = []transaction.Witness{ + { + InvocationScript: committee.SignHashable(uint32(netmode.UnitTestNet), tx), + VerificationScript: committee.Script(), + }, + { + InvocationScript: append([]byte{byte(opcode.PUSHDATA1), 64}, notary.PrivateKey().SignHashable(uint32(netmode.UnitTestNet), tx)...), + }, + } + require.Error(t, bc.VerifyTx(tx)) + }) + t.Run("no deposit", func(t *testing.T) { + tx := getNotaryAssistedTx(e, 1, (1+1)*notaryServiceFeePerKey) + tx.Signers = []transaction.Signer{ + { + Account: notaryHash, + Scopes: transaction.None, + }, + { + Account: committee.ScriptHash(), + Scopes: transaction.None, + }, + } + tx.Scripts = []transaction.Witness{ + { + InvocationScript: append([]byte{byte(opcode.PUSHDATA1), 64}, notary.PrivateKey().SignHashable(uint32(netmode.UnitTestNet), tx)...), + }, + { + InvocationScript: committee.SignHashable(uint32(netmode.UnitTestNet), tx), + VerificationScript: committee.Script(), + }, + } + require.Error(t, bc.VerifyTx(tx)) + }) + t.Run("bad Notary signer scope", func(t *testing.T) { + tx := getNotaryAssistedTx(e, 1, (1+1)*notaryServiceFeePerKey) + tx.Signers = []transaction.Signer{ + { + Account: committee.ScriptHash(), + Scopes: transaction.None, + }, + { + Account: notaryHash, + Scopes: transaction.CalledByEntry, + }, + } + tx.Scripts = []transaction.Witness{ + { + InvocationScript: committee.SignHashable(uint32(netmode.UnitTestNet), tx), + VerificationScript: committee.Script(), + }, + { + InvocationScript: append([]byte{byte(opcode.PUSHDATA1), 64}, notary.PrivateKey().SignHashable(uint32(netmode.UnitTestNet), tx)...), + }, + } + require.Error(t, bc.VerifyTx(tx)) + }) + t.Run("not signed by Notary", func(t *testing.T) { + tx := getNotaryAssistedTx(e, 1, (1+1)*notaryServiceFeePerKey) + tx.Signers = []transaction.Signer{ + { + Account: committee.ScriptHash(), + Scopes: transaction.None, + }, + } + tx.Scripts = []transaction.Witness{ + { + InvocationScript: committee.SignHashable(uint32(netmode.UnitTestNet), tx), + VerificationScript: committee.Script(), + }, + } + require.Error(t, bc.VerifyTx(tx)) + }) + t.Run("bad Notary node witness", func(t *testing.T) { + tx := getNotaryAssistedTx(e, 1, (1+1)*notaryServiceFeePerKey) + tx.Signers = []transaction.Signer{ + { + Account: committee.ScriptHash(), + Scopes: transaction.None, + }, + { + Account: notaryHash, + Scopes: transaction.None, + }, + } + acc, err := keys.NewPrivateKey() + require.NoError(t, err) + tx.Scripts = []transaction.Witness{ + { + InvocationScript: committee.SignHashable(uint32(netmode.UnitTestNet), tx), + VerificationScript: committee.Script(), + }, + { + InvocationScript: append([]byte{byte(opcode.PUSHDATA1), 64}, acc.SignHashable(uint32(netmode.UnitTestNet), tx)...), + }, + } + require.Error(t, bc.VerifyTx(tx)) + }) + t.Run("missing payer", func(t *testing.T) { + tx := getNotaryAssistedTx(e, 1, (1+1)*notaryServiceFeePerKey) + tx.Signers = []transaction.Signer{ + { + Account: notaryHash, + Scopes: transaction.None, + }, + } + tx.Scripts = []transaction.Witness{ + { + InvocationScript: append([]byte{byte(opcode.PUSHDATA1), 64}, notary.PrivateKey().SignHashable(uint32(netmode.UnitTestNet), tx)...), + }, + } + require.Error(t, bc.VerifyTx(tx)) + }) + t.Run("positive", func(t *testing.T) { + tx := getNotaryAssistedTx(e, 1, (1+1)*notaryServiceFeePerKey) + require.NoError(t, bc.VerifyTx(tx)) + }) + }) }) - require.Error(t, err) - require.True(t, strings.Contains(err.Error(), fmt.Sprintf("native contract %s is already stored, but marked as inactive for height %d in config", nativenames.CryptoLib, h))) }) - t.Run("invalid native contract activation", func(t *testing.T) { - ps = newPS(t) + t.Run("Partially-filled transaction", func(t *testing.T) { + getPartiallyFilledTx := func(nvb uint32, validUntil uint32) *transaction.Transaction { + tx := newTestTx(t, h, testScript) + tx.ValidUntilBlock = validUntil + tx.Attributes = []transaction.Attribute{ + { + Type: transaction.NotValidBeforeT, + Value: &transaction.NotValidBefore{Height: nvb}, + }, + { + Type: transaction.NotaryAssistedT, + Value: &transaction.NotaryAssisted{NKeys: 0}, + }, + } + tx.Signers = []transaction.Signer{ + { + Account: notaryHash, + Scopes: transaction.None, + }, + { + Account: validator.ScriptHash(), + Scopes: transaction.None, + }, + } + size := io.GetVarSize(tx) + netFee, sizeDelta := fee.Calculate(bc.GetBaseExecFee(), validator.Script()) + tx.NetworkFee = netFee + // multisig witness verification price + int64(size)*bc.FeePerByte() + // fee for unsigned size + int64(sizeDelta)*bc.FeePerByte() + // fee for multisig size + 66*bc.FeePerByte() + // fee for Notary signature size (66 bytes for Invocation script and 0 bytes for Verification script) + 2*bc.FeePerByte() + // fee for the length of each script in Notary witness (they are nil, so we did not take them into account during `size` calculation) + notaryServiceFeePerKey + // fee for Notary attribute + fee.Opcode(bc.GetBaseExecFee(), // Notary verification script + opcode.PUSHDATA1, opcode.RET, // invocation script + opcode.PUSH0, opcode.SYSCALL, opcode.RET) + // Neo.Native.Call + nativeprices.NotaryVerificationPrice*bc.GetBaseExecFee() // Notary witness verification price + tx.Scripts = []transaction.Witness{ + { + InvocationScript: append([]byte{byte(opcode.PUSHDATA1), 64}, make([]byte, 64)...), + VerificationScript: []byte{}, + }, + { + InvocationScript: validator.SignHashable(uint32(netmode.UnitTestNet), tx), + VerificationScript: validator.Script(), + }, + } + return tx + } - // Remove CryptoLib from storage. - cache := storage.NewMemCachedStore(ps) // Extra wrapper to avoid good DB corruption. - key := make([]byte, 1+4+1+20) - key[0] = byte(storage.STStorage) - binary.LittleEndian.PutUint32(key[1:], uint32(managementID)) - key[5] = byte(managementContractPrefix) - copy(key[6:], cryptoLibHash.BytesBE()) - cache.Delete(key) - - _, err := initTestChainNoCheck(t, cache, customConfig) - require.Error(t, err) - require.True(t, strings.Contains(err.Error(), fmt.Sprintf("native contract %s is not stored, but should be active at height %d according to config", nativenames.CryptoLib, h))) - }) - t.Run("stored and autogenerated native contract's states mismatch", func(t *testing.T) { - ps = newPS(t) - - // Change stored CryptoLib state. - cache := storage.NewMemCachedStore(ps) // Extra wrapper to avoid good DB corruption. - key := make([]byte, 1+4+1+20) - key[0] = byte(storage.STStorage) - binary.LittleEndian.PutUint32(key[1:], uint32(managementID)) - key[5] = byte(managementContractPrefix) - copy(key[6:], cryptoLibHash.BytesBE()) - cs := *cryptoLibState - cs.ID = -123 - csBytes, err := stackitem.SerializeConvertible(&cs) - require.NoError(t, err) - cache.Put(key, csBytes) - - _, err = initTestChainNoCheck(t, cache, customConfig) - require.Error(t, err) - require.True(t, strings.Contains(err.Error(), fmt.Sprintf("native %s: version mismatch (stored contract state differs from autogenerated one)", nativenames.CryptoLib))) - }) - - t.Run("good", func(t *testing.T) { - ps = newPS(t) - _, err := initTestChainNoCheck(t, ps, customConfig) - require.NoError(t, err) + mp := mempool.New(10, 1, false) + verificationF := func(tx *transaction.Transaction, data interface{}) error { + if data.(int) > 5 { + return errors.New("bad data") + } + return nil + } + t.Run("failed pre-verification", func(t *testing.T) { + tx := getPartiallyFilledTx(bc.BlockHeight(), bc.BlockHeight()+1) + require.Error(t, bc.PoolTxWithData(tx, 6, mp, bc, verificationF)) // here and below let's use `bc` instead of proper NotaryFeer for the test simplicity. + }) + t.Run("GasLimitExceeded during witness verification", func(t *testing.T) { + tx := getPartiallyFilledTx(bc.BlockHeight(), bc.BlockHeight()+1) + tx.NetworkFee-- // to check that NetworkFee was set correctly in getPartiallyFilledTx + tx.Scripts = []transaction.Witness{ + { + InvocationScript: append([]byte{byte(opcode.PUSHDATA1), 64}, make([]byte, 64)...), + VerificationScript: []byte{}, + }, + { + InvocationScript: validator.SignHashable(uint32(netmode.UnitTestNet), tx), + VerificationScript: validator.Script(), + }, + } + require.Error(t, bc.PoolTxWithData(tx, 5, mp, bc, verificationF)) + }) + t.Run("bad NVB: too big", func(t *testing.T) { + tx := getPartiallyFilledTx(bc.BlockHeight()+bc.GetMaxNotValidBeforeDelta()+1, bc.BlockHeight()+1) + require.True(t, errors.Is(bc.PoolTxWithData(tx, 5, mp, bc, verificationF), core.ErrInvalidAttribute)) + }) + t.Run("bad ValidUntilBlock: too small", func(t *testing.T) { + tx := getPartiallyFilledTx(bc.BlockHeight(), bc.BlockHeight()+bc.GetMaxNotValidBeforeDelta()+1) + require.True(t, errors.Is(bc.PoolTxWithData(tx, 5, mp, bc, verificationF), core.ErrInvalidAttribute)) + }) + t.Run("good", func(t *testing.T) { + tx := getPartiallyFilledTx(bc.BlockHeight(), bc.BlockHeight()+1) + require.NoError(t, bc.PoolTxWithData(tx, 5, mp, bc, verificationF)) + }) }) } + +func TestBlockchain_Bug1728(t *testing.T) { + bc, acc := chain.NewSingle(t) + e := neotest.NewExecutor(t, bc, acc, acc) + managementInvoker := e.ValidatorInvoker(e.NativeHash(t, nativenames.Management)) + + src := `package example + import "github.com/nspcc-dev/neo-go/pkg/interop/runtime" + func init() { if true { } else { } } + func _deploy(_ interface{}, isUpdate bool) { + runtime.Log("Deploy") + }` + c := neotest.CompileSource(t, acc.ScriptHash(), strings.NewReader(src), &compiler.Options{Name: "TestContract"}) + managementInvoker.DeployContract(t, c, nil) +} diff --git a/pkg/core/helper_test.go b/pkg/core/helper_test.go index 927932bf3..63134688a 100644 --- a/pkg/core/helper_test.go +++ b/pkg/core/helper_test.go @@ -1,55 +1,21 @@ package core import ( - "encoding/base64" - "encoding/hex" - "encoding/json" - "fmt" - "math/big" - "os" - "path" - "path/filepath" - "strings" "testing" "time" "github.com/nspcc-dev/neo-go/internal/testchain" - "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/blockchainer" - "github.com/nspcc-dev/neo-go/pkg/core/chaindump" - "github.com/nspcc-dev/neo-go/pkg/core/fee" - "github.com/nspcc-dev/neo-go/pkg/core/native" - "github.com/nspcc-dev/neo-go/pkg/core/native/noderoles" - "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/encoding/address" - "github.com/nspcc-dev/neo-go/pkg/encoding/fixedn" - "github.com/nspcc-dev/neo-go/pkg/io" - "github.com/nspcc-dev/neo-go/pkg/rpc/client/nns" "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/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/nspcc-dev/neo-go/pkg/wallet" "github.com/stretchr/testify/require" "go.uber.org/zap" "go.uber.org/zap/zaptest" ) -// multisig address which possess all NEO. -var neoOwner = testchain.MultisigScriptHash() - -// examplesPrefix is a prefix of the example smart-contracts. -const examplesPrefix = "../../examples/" - // newTestChain should be called before newBlock invocation to properly setup // global state. func newTestChain(t testing.TB) *Blockchain { @@ -67,38 +33,6 @@ func newTestChainWithCustomCfgAndStore(t testing.TB, st storage.Store, f func(*c return chain } -func newLevelDBForTesting(t testing.TB) storage.Store { - newLevelStore, _ := newLevelDBForTestingWithPath(t, "") - return newLevelStore -} - -func newLevelDBForTestingWithPath(t testing.TB, dbPath string) (storage.Store, string) { - if dbPath == "" { - dbPath = t.TempDir() - } - dbOptions := storage.LevelDBOptions{ - DataDirectoryPath: dbPath, - } - newLevelStore, err := storage.NewLevelDBStore(dbOptions) - require.Nil(t, err, "NewLevelDBStore error") - return newLevelStore, dbPath -} - -func newBoltStoreForTesting(t testing.TB) storage.Store { - boltDBStore, _ := newBoltStoreForTestingWithPath(t, "") - return boltDBStore -} - -func newBoltStoreForTestingWithPath(t testing.TB, dbPath string) (storage.Store, string) { - if dbPath == "" { - d := t.TempDir() - dbPath = filepath.Join(d, "test_bolt_db") - } - boltDBStore, err := storage.NewBoltDBStore(storage.BoltDBOptions{FilePath: dbPath}) - require.NoError(t, err) - return boltDBStore, dbPath -} - func initTestChain(t testing.TB, st storage.Store, f func(*config.Config)) *Blockchain { chain, err := initTestChainNoCheck(t, st, f) require.NoError(t, err) @@ -192,646 +126,3 @@ func (bc *Blockchain) genBlocks(n int) ([]*block.Block, error) { } return blocks, nil } - -func TestBug1728(t *testing.T) { - src := `package example - import "github.com/nspcc-dev/neo-go/pkg/interop/runtime" - func init() { if true { } else { } } - func _deploy(_ interface{}, isUpdate bool) { - runtime.Log("Deploy") - }` - nf, di, err := compiler.CompileWithOptions("foo.go", strings.NewReader(src), nil) - require.NoError(t, err) - m, err := di.ConvertToManifest(&compiler.Options{Name: "TestContract"}) - require.NoError(t, err) - - rawManifest, err := json.Marshal(m) - require.NoError(t, err) - rawNef, err := nf.Bytes() - require.NoError(t, err) - - bc := newTestChain(t) - - aer, err := invokeContractMethod(bc, 10000000000, - bc.contracts.Management.Hash, "deploy", rawNef, rawManifest) - require.NoError(t, err) - require.Equal(t, aer.VMState, vm.HaltState) -} - -// This function generates "../rpc/testdata/testblocks.acc" file which contains data -// for RPC unit tests. It also is a nice integration test. -// To generate new "../rpc/testdata/testblocks.acc", follow the steps: -// 1. Set saveChain down below to true -// 2. Run tests with `$ make test` -func TestCreateBasicChain(t *testing.T) { - const saveChain = false - const prefix = "../rpc/server/testdata/" - - bc := newTestChain(t) - initBasicChain(t, bc) - - if saveChain { - outStream, err := os.Create(prefix + "testblocks.acc") - require.NoError(t, err) - t.Cleanup(func() { - outStream.Close() - }) - - writer := io.NewBinWriterFromIO(outStream) - writer.WriteU32LE(bc.BlockHeight()) - err = chaindump.Dump(bc, writer, 1, bc.BlockHeight()) - require.NoError(t, err) - } - - priv0 := testchain.PrivateKeyByID(0) - priv1 := testchain.PrivateKeyByID(1) - priv0ScriptHash := priv0.GetScriptHash() - acc0 := wallet.NewAccountFromPrivateKey(priv0) - - // Prepare some transaction for future submission. - txSendRaw := newNEP17Transfer(bc.contracts.NEO.Hash, priv0ScriptHash, priv1.GetScriptHash(), int64(fixedn.Fixed8FromInt64(1000))) - txSendRaw.ValidUntilBlock = bc.config.MaxValidUntilBlockIncrement - txSendRaw.Nonce = 0x1234 - txSendRaw.Signers = []transaction.Signer{{ - Account: priv0ScriptHash, - Scopes: transaction.CalledByEntry, - AllowedContracts: nil, - AllowedGroups: nil, - }} - require.NoError(t, addNetworkFee(bc, txSendRaw, acc0)) - require.NoError(t, acc0.SignTx(testchain.Network(), txSendRaw)) - bw := io.NewBufBinWriter() - txSendRaw.EncodeBinary(bw.BinWriter) - t.Logf("sendrawtransaction: \n\tbase64: %s\n\tHash LE: %s", base64.StdEncoding.EncodeToString(bw.Bytes()), txSendRaw.Hash().StringLE()) - require.False(t, saveChain) -} - -func initBasicChain(t *testing.T, bc *Blockchain) { - const prefix = "../rpc/server/testdata/" - // Increase in case if you need more blocks - const validUntilBlock = 1200 - - // To be incremented after each created transaction to keep chain constant. - var testNonce uint32 = 1 - - // Use as nonce when new transaction is created to avoid random data in tests. - getNextNonce := func() uint32 { - testNonce++ - return testNonce - } - - const neoAmount = 99999000 - - gasHash := bc.contracts.GAS.Hash - neoHash := bc.contracts.NEO.Hash - policyHash := bc.contracts.Policy.Hash - notaryHash := bc.contracts.Notary.Hash - 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", bc.GetHeaderHash(0).StringLE()) - - priv0 := testchain.PrivateKeyByID(0) - priv0ScriptHash := priv0.GetScriptHash() - priv1 := testchain.PrivateKeyByID(1) - priv1ScriptHash := priv1.GetScriptHash() - acc0 := wallet.NewAccountFromPrivateKey(priv0) - acc1 := wallet.NewAccountFromPrivateKey(priv1) - - deployContractFromPriv0 := func(t *testing.T, path, contractName string, configPath *string, expectedID int32) (util.Uint256, util.Uint256, util.Uint160) { - txDeploy, _ := newDeployTx(t, bc, priv0ScriptHash, path, contractName, configPath) - txDeploy.Nonce = getNextNonce() - txDeploy.ValidUntilBlock = validUntilBlock - require.NoError(t, addNetworkFee(bc, txDeploy, acc0)) - require.NoError(t, acc0.SignTx(testchain.Network(), txDeploy)) - b := bc.newBlock(txDeploy) - require.NoError(t, bc.AddBlock(b)) // block #11 - checkTxHalt(t, bc, txDeploy.Hash()) - sh, err := bc.GetContractScriptHash(expectedID) - require.NoError(t, err) - return b.Hash(), txDeploy.Hash(), sh - } - - require.Equal(t, big.NewInt(5000_0000), bc.GetUtilityTokenBalance(priv0ScriptHash)) // gas bounty - - // Block #1: move 1000 GAS and neoAmount NEO to priv0. - txMoveNeo, err := testchain.NewTransferFromOwner(bc, neoHash, priv0ScriptHash, neoAmount, getNextNonce(), validUntilBlock) - require.NoError(t, err) - // Move some GAS to one simple account. - txMoveGas, err := testchain.NewTransferFromOwner(bc, gasHash, priv0ScriptHash, int64(fixedn.Fixed8FromInt64(1000)), - getNextNonce(), validUntilBlock) - require.NoError(t, err) - b := bc.newBlock(txMoveNeo, txMoveGas) - require.NoError(t, bc.AddBlock(b)) - checkTxHalt(t, bc, txMoveGas.Hash()) - checkTxHalt(t, bc, txMoveNeo.Hash()) - 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()) - - require.True(t, bc.GetUtilityTokenBalance(priv0ScriptHash).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. - cfgPath := prefix + "test_contract.yml" - block2H, txDeployH, cHash := deployContractFromPriv0(t, prefix+"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. - script := io.NewBufBinWriter() - emit.AppCall(script.BinWriter, cHash, "putValue", callflag.All, "testkey", "testvalue") - txInv := transaction.New(script.Bytes(), 1*native.GASFactor) - txInv.Nonce = getNextNonce() - txInv.ValidUntilBlock = validUntilBlock - txInv.Signers = []transaction.Signer{{Account: priv0ScriptHash}} - require.NoError(t, addNetworkFee(bc, txInv, acc0)) - require.NoError(t, acc0.SignTx(testchain.Network(), txInv)) - b = bc.newBlock(txInv) - require.NoError(t, bc.AddBlock(b)) - checkTxHalt(t, bc, txInv.Hash()) - t.Logf("txInv: %s", txInv.Hash().StringLE()) - - // Block #4: transfer 0.0000_1 NEO from priv0 to priv1. - txNeo0to1 := newNEP17Transfer(neoHash, priv0ScriptHash, priv1ScriptHash, 1000) - txNeo0to1.Nonce = getNextNonce() - txNeo0to1.ValidUntilBlock = validUntilBlock - txNeo0to1.Signers = []transaction.Signer{ - { - Account: priv0ScriptHash, - Scopes: transaction.CalledByEntry, - AllowedContracts: nil, - AllowedGroups: nil, - }, - } - require.NoError(t, addNetworkFee(bc, txNeo0to1, acc0)) - require.NoError(t, acc0.SignTx(testchain.Network(), txNeo0to1)) - b = bc.newBlock(txNeo0to1) - require.NoError(t, bc.AddBlock(b)) - checkTxHalt(t, bc, txNeo0to1.Hash()) - - // Block #5: initialize rubles contract and transfer 1000 rubles from the contract to priv0. - w := io.NewBufBinWriter() - emit.AppCall(w.BinWriter, cHash, "init", callflag.All) - initTx := transaction.New(w.Bytes(), 1*native.GASFactor) - initTx.Nonce = getNextNonce() - initTx.ValidUntilBlock = validUntilBlock - initTx.Signers = []transaction.Signer{{Account: priv0ScriptHash}} - require.NoError(t, addNetworkFee(bc, initTx, acc0)) - require.NoError(t, acc0.SignTx(testchain.Network(), initTx)) - transferTx := newNEP17Transfer(cHash, cHash, priv0ScriptHash, 1000) - transferTx.Nonce = getNextNonce() - transferTx.ValidUntilBlock = validUntilBlock - transferTx.Signers = []transaction.Signer{ - { - Account: priv0ScriptHash, - Scopes: transaction.CalledByEntry, - AllowedContracts: nil, - AllowedGroups: nil, - }, - } - require.NoError(t, addNetworkFee(bc, transferTx, acc0)) - transferTx.SystemFee += 1000000 - require.NoError(t, acc0.SignTx(testchain.Network(), transferTx)) - b = bc.newBlock(initTx, transferTx) - require.NoError(t, bc.AddBlock(b)) - checkTxHalt(t, bc, initTx.Hash()) - checkTxHalt(t, bc, transferTx.Hash()) - t.Logf("recieveRublesTx: %v", transferTx.Hash().StringLE()) - - // Block #6: transfer 123 rubles from priv0 to priv1 - transferTx = newNEP17Transfer(cHash, priv0.GetScriptHash(), priv1ScriptHash, 123) - transferTx.Nonce = getNextNonce() - transferTx.ValidUntilBlock = validUntilBlock - transferTx.Signers = []transaction.Signer{ - { - Account: priv0ScriptHash, - Scopes: transaction.CalledByEntry, - AllowedContracts: nil, - AllowedGroups: nil, - }, - } - require.NoError(t, addNetworkFee(bc, transferTx, acc0)) - transferTx.SystemFee += 1000000 - require.NoError(t, acc0.SignTx(testchain.Network(), transferTx)) - b = bc.newBlock(transferTx) - require.NoError(t, bc.AddBlock(b)) - checkTxHalt(t, bc, transferTx.Hash()) - t.Logf("sendRublesTx: %v", transferTx.Hash().StringLE()) - - // Block #7: push verification contract into the chain. - verifyPath := filepath.Join(prefix, "verify", "verification_contract.go") - _, _, _ = deployContractFromPriv0(t, verifyPath, "Verify", nil, 2) - - // Block #8: deposit some GAS to notary contract for priv0. - transferTx = newNEP17Transfer(gasHash, priv0.GetScriptHash(), notaryHash, 10_0000_0000, priv0.GetScriptHash(), int64(bc.BlockHeight()+1000)) - transferTx.Nonce = getNextNonce() - transferTx.ValidUntilBlock = validUntilBlock - transferTx.Signers = []transaction.Signer{ - { - Account: priv0ScriptHash, - Scopes: transaction.CalledByEntry, - }, - } - require.NoError(t, addNetworkFee(bc, transferTx, acc0)) - transferTx.SystemFee += 10_0000 - require.NoError(t, acc0.SignTx(testchain.Network(), transferTx)) - b = bc.newBlock(transferTx) - require.NoError(t, bc.AddBlock(b)) - checkTxHalt(t, bc, transferTx.Hash()) - t.Logf("notaryDepositTxPriv0: %v", transferTx.Hash().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)) - bc.setNodesByRole(t, true, noderoles.P2PNotary, keys.PublicKeys{ntr.Accounts[0].PrivateKey().PublicKey()}) - 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(prefix, "verify_args", "verification_with_args_contract.go") - _, _, _ = deployContractFromPriv0(t, verifyPath, "VerifyWithArgs", nil, 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 - - // Block #12: transfer funds to committee for futher NS record registration. - transferFundsToCommittee(t, bc) // block #12 - - // Block #13: add `.com` root to NNS. - res, err := invokeContractMethodGeneric(bc, -1, - nsHash, "addRoot", true, "com") // block #13 - require.NoError(t, err) - checkResult(t, res, stackitem.Null{}) - - // Block #14: register `neo.com` via NNS. - res, err = invokeContractMethodGeneric(bc, -1, - nsHash, "register", acc0, "neo.com", priv0ScriptHash) // block #14 - require.NoError(t, err) - checkResult(t, res, stackitem.NewBool(true)) - 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. - res, err = invokeContractMethodGeneric(bc, -1, nsHash, - "setRecord", acc0, "neo.com", int64(nns.A), "1.2.3.4") // block #15 - require.NoError(t, err) - checkResult(t, res, stackitem.Null{}) - - // Block #16: invoke `test_contract.go`: put new value with the same key to check `getstate` RPC call - script.Reset() - emit.AppCall(script.BinWriter, cHash, "putValue", callflag.All, "testkey", "newtestvalue") - // Invoke `test_contract.go`: put values to check `findstates` RPC call - emit.AppCall(script.BinWriter, cHash, "putValue", callflag.All, "aa", "v1") - emit.AppCall(script.BinWriter, cHash, "putValue", callflag.All, "aa10", "v2") - emit.AppCall(script.BinWriter, cHash, "putValue", callflag.All, "aa50", "v3") - txInv = transaction.New(script.Bytes(), 1*native.GASFactor) - txInv.Nonce = getNextNonce() - txInv.ValidUntilBlock = validUntilBlock - txInv.Signers = []transaction.Signer{{Account: priv0ScriptHash}} - require.NoError(t, addNetworkFee(bc, txInv, acc0)) - require.NoError(t, acc0.SignTx(testchain.Network(), txInv)) - b = bc.newBlock(txInv) - require.NoError(t, bc.AddBlock(b)) // block #16 - checkTxHalt(t, bc, txInv.Hash()) - - // 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 - - // 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} - txGas0toNFS := newNEP17Transfer(gasHash, priv0ScriptHash, nfsHash, 10_0000_0000, containerID.BytesBE(), objectID.BytesBE()) - txGas0toNFS.SystemFee += 4000_0000 - txGas0toNFS.Nonce = getNextNonce() - txGas0toNFS.ValidUntilBlock = validUntilBlock - txGas0toNFS.Signers = []transaction.Signer{ - { - Account: priv0ScriptHash, - Scopes: transaction.CalledByEntry, - }, - } - require.NoError(t, addNetworkFee(bc, txGas0toNFS, acc0)) - require.NoError(t, acc0.SignTx(testchain.Network(), txGas0toNFS)) - b = bc.newBlock(txGas0toNFS) - require.NoError(t, bc.AddBlock(b)) // block #18 - checkTxHalt(t, bc, txGas0toNFS.Hash()) - aer, _ := bc.GetAppExecResults(txGas0toNFS.Hash(), trigger.Application) - require.Equal(t, 2, len(aer[0].Events)) // GAS transfer + NFSO transfer - tokenID, err = aer[0].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. - script.Reset() - emit.AppCall(script.BinWriter, nfsHash, "transfer", callflag.All, priv0ScriptHash, priv1ScriptHash, 25, tokenID, nil) - emit.Opcodes(script.BinWriter, opcode.ASSERT) - require.NoError(t, script.Err) - txNFS0to1 := transaction.New(script.Bytes(), 1*native.GASFactor) - txNFS0to1.Nonce = getNextNonce() - txNFS0to1.ValidUntilBlock = validUntilBlock - txNFS0to1.Signers = []transaction.Signer{{Account: priv0ScriptHash, Scopes: transaction.CalledByEntry}} - require.NoError(t, addNetworkFee(bc, txNFS0to1, acc0)) - require.NoError(t, acc0.SignTx(testchain.Network(), txNFS0to1)) - b = bc.newBlock(txNFS0to1) - require.NoError(t, bc.AddBlock(b)) // block #19 - checkTxHalt(t, bc, txNFS0to1.Hash()) - - // Block #20: transfer 1000 GAS to priv1. - txMoveGas, err = testchain.NewTransferFromOwner(bc, gasHash, priv1ScriptHash, int64(fixedn.Fixed8FromInt64(1000)), - getNextNonce(), validUntilBlock) - require.NoError(t, err) - require.NoError(t, bc.AddBlock(bc.newBlock(txMoveGas))) - checkTxHalt(t, bc, txMoveGas.Hash()) // block #20 - - // Block #21: transfer 0.05 NFSO from priv1 back to priv0. - script.Reset() - emit.AppCall(script.BinWriter, nfsHash, "transfer", callflag.All, priv1ScriptHash, priv0.GetScriptHash(), 5, tokenID, nil) - emit.Opcodes(script.BinWriter, opcode.ASSERT) - require.NoError(t, script.Err) - txNFS1to0 := transaction.New(script.Bytes(), 1*native.GASFactor) - txNFS1to0.Nonce = getNextNonce() - txNFS1to0.ValidUntilBlock = validUntilBlock - txNFS1to0.Signers = []transaction.Signer{{Account: priv1ScriptHash, Scopes: transaction.CalledByEntry}} - require.NoError(t, addNetworkFee(bc, txNFS1to0, acc0)) - require.NoError(t, acc1.SignTx(testchain.Network(), txNFS1to0)) - b = bc.newBlock(txNFS1to0) - require.NoError(t, bc.AddBlock(b)) // block #21 - checkTxHalt(t, bc, txNFS1to0.Hash()) - - // Compile contract to test `invokescript` RPC call - invokePath := filepath.Join(prefix, "invoke", "invokescript_contract.go") - invokeCfg := filepath.Join(prefix, "invoke", "invoke.yml") - _, _ = newDeployTx(t, bc, priv0ScriptHash, invokePath, "ContractForInvokescriptTest", &invokeCfg) -} - -func newNEP17Transfer(sc, from, to util.Uint160, amount int64, additionalArgs ...interface{}) *transaction.Transaction { - return newNEP17TransferWithAssert(sc, from, to, amount, true, additionalArgs...) -} - -func newNEP17TransferWithAssert(sc, from, to util.Uint160, amount int64, needAssert bool, additionalArgs ...interface{}) *transaction.Transaction { - w := io.NewBufBinWriter() - emit.AppCall(w.BinWriter, sc, "transfer", callflag.All, from, to, amount, additionalArgs) - if needAssert { - emit.Opcodes(w.BinWriter, opcode.ASSERT) - } - if w.Err != nil { - panic(fmt.Errorf("failed to create NEP-17 transfer transaction: %w", w.Err)) - } - - script := w.Bytes() - return transaction.New(script, 11000000) -} - -func newDeployTx(t *testing.T, bc *Blockchain, sender util.Uint160, name, ctrName string, cfgName *string) (*transaction.Transaction, util.Uint160) { - tx, h, avm, err := testchain.NewDeployTx(bc, name, sender, nil, cfgName) - require.NoError(t, err) - t.Logf("contract (%s): \n\tHash: %s\n\tAVM: %s", name, h.StringLE(), base64.StdEncoding.EncodeToString(avm)) - return tx, h -} - -func addSigners(sender util.Uint160, txs ...*transaction.Transaction) { - for _, tx := range txs { - tx.Signers = []transaction.Signer{{ - Account: sender, - Scopes: transaction.Global, - AllowedContracts: nil, - AllowedGroups: nil, - }} - } -} - -func addNetworkFee(bc *Blockchain, tx *transaction.Transaction, sender *wallet.Account) error { - size := io.GetVarSize(tx) - netFee, sizeDelta := fee.Calculate(bc.GetBaseExecFee(), sender.Contract.Script) - tx.NetworkFee += netFee - size += sizeDelta - for _, cosigner := range tx.Signers { - contract := bc.GetContractState(cosigner.Account) - if contract != nil { - netFee, sizeDelta = fee.Calculate(bc.GetBaseExecFee(), contract.NEF.Script) - tx.NetworkFee += netFee - size += sizeDelta - } - } - tx.NetworkFee += int64(size) * bc.FeePerByte() - return nil -} - -// Signer can be either bool or *wallet.Account. -// In the first case `true` means sign by committee, `false` means sign by validators. -func prepareContractMethodInvokeGeneric(chain *Blockchain, sysfee int64, - hash util.Uint160, method string, signer interface{}, args ...interface{}) (*transaction.Transaction, error) { - w := io.NewBufBinWriter() - emit.AppCall(w.BinWriter, hash, method, callflag.All, args...) - if w.Err != nil { - return nil, w.Err - } - script := w.Bytes() - tx := transaction.New(script, 0) - tx.ValidUntilBlock = chain.blockHeight + 1 - var err error - switch s := signer.(type) { - case bool: - if s { - addSigners(testchain.CommitteeScriptHash(), tx) - setTxSystemFee(chain, sysfee, tx) - err = testchain.SignTxCommittee(chain, tx) - } else { - addSigners(neoOwner, tx) - setTxSystemFee(chain, sysfee, tx) - err = testchain.SignTx(chain, tx) - } - case *wallet.Account: - signTxWithAccounts(chain, sysfee, tx, s) - case []*wallet.Account: - signTxWithAccounts(chain, sysfee, tx, s...) - default: - panic("invalid signer") - } - if err != nil { - return nil, err - } - return tx, nil -} - -func setTxSystemFee(bc *Blockchain, sysFee int64, tx *transaction.Transaction) { - if sysFee >= 0 { - tx.SystemFee = sysFee - return - } - - lastBlock := bc.topBlock.Load().(*block.Block) - b := &block.Block{ - Header: block.Header{ - Index: lastBlock.Index + 1, - Timestamp: lastBlock.Timestamp + 1000, - }, - Transactions: []*transaction.Transaction{tx}, - } - - ttx := *tx // prevent setting 'hash' field - ic := bc.GetTestVM(trigger.Application, &ttx, b) - defer ic.Finalize() - - ic.VM.LoadWithFlags(tx.Script, callflag.All) - _ = ic.VM.Run() - tx.SystemFee = ic.VM.GasConsumed() -} - -func signTxWithAccounts(chain *Blockchain, sysFee int64, tx *transaction.Transaction, accs ...*wallet.Account) { - scope := transaction.CalledByEntry - for _, acc := range accs { - accH, _ := address.StringToUint160(acc.Address) - tx.Signers = append(tx.Signers, transaction.Signer{ - Account: accH, - Scopes: scope, - }) - scope = transaction.Global - } - setTxSystemFee(chain, sysFee, tx) - size := io.GetVarSize(tx) - for _, acc := range accs { - if acc.Contract.Deployed { - // don't need precise calculation for tests - tx.NetworkFee += 1000_0000 - continue - } - netFee, sizeDelta := fee.Calculate(chain.GetBaseExecFee(), acc.Contract.Script) - size += sizeDelta - tx.NetworkFee += netFee - } - tx.NetworkFee += int64(size) * chain.FeePerByte() - - for _, acc := range accs { - if err := acc.SignTx(testchain.Network(), tx); err != nil { - panic(err) - } - } -} - -func persistBlock(chain *Blockchain, txs ...*transaction.Transaction) ([]*state.AppExecResult, error) { - b := chain.newBlock(txs...) - err := chain.AddBlock(b) - if err != nil { - return nil, err - } - - aers := make([]*state.AppExecResult, len(txs)) - for i, tx := range txs { - res, err := chain.GetAppExecResults(tx.Hash(), trigger.Application) - if err != nil { - return nil, err - } - aers[i] = &res[0] - } - return aers, nil -} - -func invokeContractMethod(chain *Blockchain, sysfee int64, hash util.Uint160, method string, args ...interface{}) (*state.AppExecResult, error) { - return invokeContractMethodGeneric(chain, sysfee, hash, method, false, args...) -} - -func invokeContractMethodGeneric(chain *Blockchain, sysfee int64, hash util.Uint160, method string, - signer interface{}, args ...interface{}) (*state.AppExecResult, error) { - tx, err := prepareContractMethodInvokeGeneric(chain, sysfee, hash, - method, signer, args...) - if err != nil { - return nil, err - } - aers, err := persistBlock(chain, tx) - if err != nil { - return nil, err - } - return aers[0], nil -} - -func transferTokenFromMultisigAccountCheckOK(t *testing.T, chain *Blockchain, to, tokenHash util.Uint160, amount int64, additionalArgs ...interface{}) { - transferTx := transferTokenFromMultisigAccount(t, chain, to, tokenHash, amount, additionalArgs...) - res, err := chain.GetAppExecResults(transferTx.Hash(), trigger.Application) - require.NoError(t, err) - require.Equal(t, vm.HaltState, res[0].VMState) - require.Equal(t, 0, len(res[0].Stack)) -} - -func transferTokenFromMultisigAccount(t *testing.T, chain *Blockchain, to, tokenHash util.Uint160, amount int64, additionalArgs ...interface{}) *transaction.Transaction { - return transferTokenFromMultisigAccountWithAssert(t, chain, to, tokenHash, amount, true, additionalArgs...) -} - -func transferTokenFromMultisigAccountWithAssert(t *testing.T, chain *Blockchain, to, tokenHash util.Uint160, amount int64, needAssert bool, additionalArgs ...interface{}) *transaction.Transaction { - transferTx := newNEP17TransferWithAssert(tokenHash, testchain.MultisigScriptHash(), to, amount, needAssert, additionalArgs...) - transferTx.SystemFee = 100000000 - transferTx.ValidUntilBlock = chain.BlockHeight() + 1 - addSigners(neoOwner, transferTx) - require.NoError(t, testchain.SignTx(chain, transferTx)) - b := chain.newBlock(transferTx) - require.NoError(t, chain.AddBlock(b)) - return transferTx -} - -func checkResult(t *testing.T, result *state.AppExecResult, expected stackitem.Item) { - require.Equal(t, vm.HaltState, result.VMState, result.FaultException) - require.Equal(t, 1, len(result.Stack)) - require.Equal(t, expected, result.Stack[0]) -} - -func checkTxHalt(t testing.TB, bc *Blockchain, h util.Uint256) { - aer, err := bc.GetAppExecResults(h, trigger.Application) - require.NoError(t, err) - require.Equal(t, 1, len(aer)) - require.Equal(t, vm.HaltState, aer[0].VMState, aer[0].FaultException) -} - -func checkFAULTState(t *testing.T, result *state.AppExecResult) { - require.Equal(t, vm.FaultState, result.VMState) -} - -func checkBalanceOf(t *testing.T, chain *Blockchain, addr util.Uint160, expected int) { - balance := chain.GetUtilityTokenBalance(addr) - require.Equal(t, int64(expected), balance.Int64()) -} - -type NotaryFeerStub struct { - bc blockchainer.Blockchainer -} - -func (f NotaryFeerStub) FeePerByte() int64 { return f.bc.FeePerByte() } -func (f NotaryFeerStub) GetUtilityTokenBalance(acc util.Uint160) *big.Int { - return f.bc.GetNotaryBalance(acc) -} -func (f NotaryFeerStub) BlockHeight() uint32 { return f.bc.BlockHeight() } -func (f NotaryFeerStub) P2PSigExtensionsEnabled() bool { return f.bc.P2PSigExtensionsEnabled() } -func NewNotaryFeerStub(bc blockchainer.Blockchainer) NotaryFeerStub { - return NotaryFeerStub{ - bc: bc, - } -} diff --git a/pkg/core/interop_system_test.go b/pkg/core/interop_system_core_test.go similarity index 60% rename from pkg/core/interop_system_test.go rename to pkg/core/interop_system_core_test.go index 07771e534..5224c0e4c 100644 --- a/pkg/core/interop_system_test.go +++ b/pkg/core/interop_system_core_test.go @@ -1,24 +1,20 @@ package core import ( - "encoding/json" "errors" "fmt" - "math" "math/big" - "os" "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/internal/testchain" "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/dao" "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/interopnames" "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" @@ -29,7 +25,6 @@ import ( "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" "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" @@ -39,10 +34,11 @@ import ( "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/nspcc-dev/neo-go/pkg/wallet" "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) { @@ -72,29 +68,6 @@ func TestRuntimeGetRandomCompatibility(t *testing.T) { require.Equal(t, "217172703763162599519098299724476526911", ic.VM.Estack().Pop().BigInt().String()) } -func TestRuntimeGetRandomDifferentTransactions(t *testing.T) { - bc := newTestChain(t) - b, _ := bc.GetBlock(bc.GetHeaderHash(0)) - - tx1 := transaction.New([]byte{byte(opcode.PUSH1)}, 0) - ic1 := bc.newInteropContext(trigger.Application, bc.dao.GetWrapped(), b, tx1) - ic1.VM = vm.New() - ic1.VM.LoadScript(tx1.Script) - - tx2 := transaction.New([]byte{byte(opcode.PUSH2)}, 0) - ic2 := bc.newInteropContext(trigger.Application, bc.dao.GetWrapped(), b, tx2) - ic2.VM = vm.New() - ic2.VM.LoadScript(tx2.Script) - - require.NoError(t, runtime.GetRandom(ic1)) - require.NoError(t, runtime.GetRandom(ic2)) - require.NotEqual(t, ic1.VM.Estack().Pop().BigInt(), ic2.VM.Estack().Pop().BigInt()) - - require.NoError(t, runtime.GetRandom(ic1)) - require.NoError(t, runtime.GetRandom(ic2)) - require.NotEqual(t, ic1.VM.Estack().Pop().BigInt(), ic2.VM.Estack().Pop().BigInt()) -} - func getSharpTestTx(sender util.Uint160) *transaction.Transaction { tx := transaction.New([]byte{byte(opcode.PUSH2)}, 0) tx.Nonce = 0 @@ -116,81 +89,6 @@ func getSharpTestGenesis(t *testing.T) *block.Block { return b } -func TestContractCreateAccount(t *testing.T) { - v, ic, _ := createVM(t) - t.Run("Good", func(t *testing.T) { - priv, err := keys.NewPrivateKey() - require.NoError(t, err) - pub := priv.PublicKey() - v.Estack().PushVal(pub.Bytes()) - require.NoError(t, contractCreateStandardAccount(ic)) - - value := v.Estack().Pop().Bytes() - u, err := util.Uint160DecodeBytesBE(value) - require.NoError(t, err) - require.Equal(t, pub.GetScriptHash(), u) - }) - t.Run("InvalidKey", func(t *testing.T) { - v.Estack().PushVal([]byte{1, 2, 3}) - require.Error(t, contractCreateStandardAccount(ic)) - }) -} - -func TestContractCreateMultisigAccount(t *testing.T) { - v, ic, _ := createVM(t) - t.Run("Good", func(t *testing.T) { - m, n := 3, 5 - pubs := make(keys.PublicKeys, n) - arr := make([]stackitem.Item, n) - for i := range pubs { - pk, err := keys.NewPrivateKey() - require.NoError(t, err) - pubs[i] = pk.PublicKey() - arr[i] = stackitem.Make(pubs[i].Bytes()) - } - v.Estack().PushVal(stackitem.Make(arr)) - v.Estack().PushVal(m) - require.NoError(t, contractCreateMultisigAccount(ic)) - - expected, err := smartcontract.CreateMultiSigRedeemScript(m, pubs) - require.NoError(t, err) - value := v.Estack().Pop().Bytes() - u, err := util.Uint160DecodeBytesBE(value) - require.NoError(t, err) - require.Equal(t, hash.Hash160(expected), u) - }) - t.Run("InvalidKey", func(t *testing.T) { - v.Estack().PushVal(stackitem.Make([]stackitem.Item{stackitem.Make([]byte{1, 2, 3})})) - v.Estack().PushVal(1) - require.Error(t, contractCreateMultisigAccount(ic)) - }) - t.Run("Invalid m", func(t *testing.T) { - pk, err := keys.NewPrivateKey() - require.NoError(t, err) - v.Estack().PushVal(stackitem.Make([]stackitem.Item{stackitem.Make(pk.PublicKey().Bytes())})) - v.Estack().PushVal(2) - require.Error(t, contractCreateMultisigAccount(ic)) - }) - t.Run("m overflows int64", func(t *testing.T) { - pk, err := keys.NewPrivateKey() - require.NoError(t, err) - v.Estack().PushVal(stackitem.Make([]stackitem.Item{stackitem.Make(pk.PublicKey().Bytes())})) - m := big.NewInt(math.MaxInt64) - m.Add(m, big.NewInt(1)) - v.Estack().PushVal(stackitem.NewBigInteger(m)) - require.Error(t, contractCreateMultisigAccount(ic)) - }) -} - -func TestRuntimeGasLeft(t *testing.T) { - v, ic, _ := createVM(t) - - v.GasLimit = 100 - v.AddGas(58) - require.NoError(t, runtime.GasLeft(ic)) - require.EqualValues(t, 42, v.Estack().Pop().BigInt().Int64()) -} - func TestRuntimeGetNotifications(t *testing.T) { v, ic, _ := createVM(t) @@ -235,7 +133,7 @@ func TestRuntimeGetNotifications(t *testing.T) { func TestRuntimeGetInvocationCounter(t *testing.T) { v, ic, bc := createVM(t) - cs, _ := getTestContractState(t, 4, 5, random.Uint160()) // sender and IDs are not important for the test + 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 @@ -661,432 +559,6 @@ func createVMAndContractState(t testing.TB) (*vm.VM, *state.Contract, *interop.C return v, contractState, context, chain } -var ( - helper1ContractNEFPath = filepath.Join("test_data", "management_helper", "management_helper1.nef") - helper1ContractManifestPath = filepath.Join("test_data", "management_helper", "management_helper1.manifest.json") - helper2ContractNEFPath = filepath.Join("test_data", "management_helper", "management_helper2.nef") - helper2ContractManifestPath = filepath.Join("test_data", "management_helper", "management_helper2.manifest.json") -) - -// TestGenerateManagementHelperContracts generates 2 helper contracts second of which is -// allowed to call the first. It uses test chain to define Management and StdLib -// native hashes and saves generated NEF and manifest to ../test_data/management_contract folder. -// Set `saveState` flag to true and run the test to rewrite NEF and manifest files. -func TestGenerateManagementHelperContracts(t *testing.T) { - const saveState = false - - bc := newTestChain(t) - mgmtHash := bc.contracts.Management.Hash - stdHash := bc.contracts.Std.Hash - neoHash := bc.contracts.NEO.Hash - singleChainValidator := testchain.PrivateKey(testchain.IDToOrder(0)) - acc := wallet.NewAccountFromPrivateKey(singleChainValidator) - require.NoError(t, acc.ConvertMultisig(1, keys.PublicKeys{singleChainValidator.PublicKey()})) - singleChainValidatorHash := acc.Contract.ScriptHash() - - w := io.NewBufBinWriter() - emit.Opcodes(w.BinWriter, opcode.ABORT) - addOff := w.Len() - emit.Opcodes(w.BinWriter, opcode.ADD, opcode.RET) - addMultiOff := w.Len() - emit.Opcodes(w.BinWriter, opcode.ADD, opcode.ADD, opcode.RET) - ret7Off := w.Len() - emit.Opcodes(w.BinWriter, opcode.PUSH7, opcode.RET) - dropOff := w.Len() - emit.Opcodes(w.BinWriter, opcode.DROP, opcode.RET) - initOff := w.Len() - emit.Opcodes(w.BinWriter, opcode.INITSSLOT, 1, opcode.PUSH3, opcode.STSFLD0, opcode.RET) - add3Off := w.Len() - emit.Opcodes(w.BinWriter, opcode.LDSFLD0, opcode.ADD, opcode.RET) - invalidRetOff := w.Len() - emit.Opcodes(w.BinWriter, opcode.PUSH1, opcode.PUSH2, opcode.RET) - justRetOff := w.Len() - emit.Opcodes(w.BinWriter, opcode.RET) - verifyOff := w.Len() - emit.Opcodes(w.BinWriter, opcode.LDSFLD0, opcode.SUB, - opcode.CONVERT, opcode.Opcode(stackitem.BooleanT), opcode.RET) - deployOff := w.Len() - emit.Opcodes(w.BinWriter, opcode.SWAP, opcode.JMPIF, 2+8+1+1+1+1+39+3) - emit.String(w.BinWriter, "create") // 8 bytes - emit.Int(w.BinWriter, 2) // 1 byte - emit.Opcodes(w.BinWriter, opcode.PACK) // 1 byte - emit.Int(w.BinWriter, 1) // 1 byte (args count for `serialize`) - emit.Opcodes(w.BinWriter, opcode.PACK) // 1 byte (pack args into array for `serialize`) - emit.AppCallNoArgs(w.BinWriter, stdHash, "serialize", callflag.All) // 39 bytes - emit.Opcodes(w.BinWriter, opcode.CALL, 3+8+1+1+1+1+39+3, opcode.RET) - emit.String(w.BinWriter, "update") // 8 bytes - emit.Int(w.BinWriter, 2) // 1 byte - emit.Opcodes(w.BinWriter, opcode.PACK) // 1 byte - emit.Int(w.BinWriter, 1) // 1 byte (args count for `serialize`) - emit.Opcodes(w.BinWriter, opcode.PACK) // 1 byte (pack args into array for `serialize`) - emit.AppCallNoArgs(w.BinWriter, stdHash, "serialize", callflag.All) // 39 bytes - emit.Opcodes(w.BinWriter, opcode.CALL, 3, opcode.RET) - putValOff := w.Len() - emit.String(w.BinWriter, "initial") - emit.Syscall(w.BinWriter, interopnames.SystemStorageGetContext) - emit.Syscall(w.BinWriter, interopnames.SystemStoragePut) - emit.Opcodes(w.BinWriter, opcode.RET) - getValOff := w.Len() - emit.String(w.BinWriter, "initial") - emit.Syscall(w.BinWriter, interopnames.SystemStorageGetContext) - emit.Syscall(w.BinWriter, interopnames.SystemStorageGet) - emit.Opcodes(w.BinWriter, opcode.RET) - delValOff := w.Len() - emit.Syscall(w.BinWriter, interopnames.SystemStorageGetContext) - emit.Syscall(w.BinWriter, interopnames.SystemStorageDelete) - emit.Opcodes(w.BinWriter, opcode.RET) - onNEP17PaymentOff := w.Len() - emit.Syscall(w.BinWriter, interopnames.SystemRuntimeGetCallingScriptHash) - emit.Int(w.BinWriter, 4) - emit.Opcodes(w.BinWriter, opcode.PACK) - emit.String(w.BinWriter, "LastPayment") - emit.Syscall(w.BinWriter, interopnames.SystemRuntimeNotify) - emit.Opcodes(w.BinWriter, opcode.RET) - onNEP11PaymentOff := w.Len() - emit.Syscall(w.BinWriter, interopnames.SystemRuntimeGetCallingScriptHash) - emit.Int(w.BinWriter, 5) - emit.Opcodes(w.BinWriter, opcode.PACK) - emit.String(w.BinWriter, "LostPayment") - emit.Syscall(w.BinWriter, interopnames.SystemRuntimeNotify) - emit.Opcodes(w.BinWriter, opcode.RET) - update3Off := w.Len() - emit.Int(w.BinWriter, 3) - emit.Opcodes(w.BinWriter, opcode.JMP, 2+1) - updateOff := w.Len() - emit.Int(w.BinWriter, 2) - emit.Opcodes(w.BinWriter, opcode.PACK) - emit.AppCallNoArgs(w.BinWriter, mgmtHash, "update", callflag.All) - emit.Opcodes(w.BinWriter, opcode.DROP) - emit.Opcodes(w.BinWriter, opcode.RET) - destroyOff := w.Len() - emit.AppCall(w.BinWriter, mgmtHash, "destroy", callflag.All) - emit.Opcodes(w.BinWriter, opcode.DROP) - emit.Opcodes(w.BinWriter, opcode.RET) - invalidStackOff := w.Len() - emit.Opcodes(w.BinWriter, opcode.NEWARRAY0, opcode.DUP, opcode.DUP, opcode.APPEND) // recursive array - emit.Syscall(w.BinWriter, interopnames.SystemStorageGetReadOnlyContext) // interop item - emit.Opcodes(w.BinWriter, opcode.RET) - callT0Off := w.Len() - emit.Opcodes(w.BinWriter, opcode.CALLT, 0, 0, opcode.PUSH1, opcode.ADD, opcode.RET) - callT1Off := w.Len() - emit.Opcodes(w.BinWriter, opcode.CALLT, 1, 0, opcode.RET) - callT2Off := w.Len() - emit.Opcodes(w.BinWriter, opcode.CALLT, 0, 0, opcode.RET) - burnGasOff := w.Len() - emit.Syscall(w.BinWriter, interopnames.SystemRuntimeBurnGas) - emit.Opcodes(w.BinWriter, opcode.RET) - invocCounterOff := w.Len() - emit.Syscall(w.BinWriter, interopnames.SystemRuntimeGetInvocationCounter) - emit.Opcodes(w.BinWriter, opcode.RET) - - script := w.Bytes() - m := manifest.NewManifest("TestMain") - m.ABI.Methods = []manifest.Method{ - { - Name: "add", - Offset: addOff, - Parameters: []manifest.Parameter{ - manifest.NewParameter("addend1", smartcontract.IntegerType), - manifest.NewParameter("addend2", smartcontract.IntegerType), - }, - ReturnType: smartcontract.IntegerType, - }, - { - Name: "add", - Offset: addMultiOff, - Parameters: []manifest.Parameter{ - manifest.NewParameter("addend1", smartcontract.IntegerType), - manifest.NewParameter("addend2", smartcontract.IntegerType), - manifest.NewParameter("addend3", smartcontract.IntegerType), - }, - ReturnType: smartcontract.IntegerType, - }, - { - Name: "ret7", - Offset: ret7Off, - Parameters: []manifest.Parameter{}, - ReturnType: smartcontract.IntegerType, - }, - { - Name: "drop", - Offset: dropOff, - ReturnType: smartcontract.VoidType, - }, - { - Name: manifest.MethodInit, - Offset: initOff, - ReturnType: smartcontract.VoidType, - }, - { - Name: "add3", - Offset: add3Off, - Parameters: []manifest.Parameter{ - manifest.NewParameter("addend", smartcontract.IntegerType), - }, - ReturnType: smartcontract.IntegerType, - }, - { - Name: "invalidReturn", - Offset: invalidRetOff, - ReturnType: smartcontract.IntegerType, - }, - { - Name: "justReturn", - Offset: justRetOff, - ReturnType: smartcontract.VoidType, - }, - { - Name: manifest.MethodVerify, - Offset: verifyOff, - ReturnType: smartcontract.BoolType, - }, - { - Name: manifest.MethodDeploy, - Offset: deployOff, - Parameters: []manifest.Parameter{ - manifest.NewParameter("data", smartcontract.AnyType), - manifest.NewParameter("isUpdate", smartcontract.BoolType), - }, - ReturnType: smartcontract.VoidType, - }, - { - Name: "getValue", - Offset: getValOff, - ReturnType: smartcontract.StringType, - }, - { - Name: "putValue", - Offset: putValOff, - Parameters: []manifest.Parameter{ - manifest.NewParameter("value", smartcontract.StringType), - }, - ReturnType: smartcontract.VoidType, - }, - { - Name: "delValue", - Offset: delValOff, - Parameters: []manifest.Parameter{ - manifest.NewParameter("key", smartcontract.StringType), - }, - ReturnType: smartcontract.VoidType, - }, - { - Name: manifest.MethodOnNEP11Payment, - Offset: onNEP11PaymentOff, - Parameters: []manifest.Parameter{ - manifest.NewParameter("from", smartcontract.Hash160Type), - manifest.NewParameter("amount", smartcontract.IntegerType), - manifest.NewParameter("tokenid", smartcontract.ByteArrayType), - manifest.NewParameter("data", smartcontract.AnyType), - }, - ReturnType: smartcontract.VoidType, - }, - { - Name: manifest.MethodOnNEP17Payment, - Offset: onNEP17PaymentOff, - Parameters: []manifest.Parameter{ - manifest.NewParameter("from", smartcontract.Hash160Type), - manifest.NewParameter("amount", smartcontract.IntegerType), - manifest.NewParameter("data", smartcontract.AnyType), - }, - ReturnType: smartcontract.VoidType, - }, - { - Name: "update", - Offset: updateOff, - Parameters: []manifest.Parameter{ - manifest.NewParameter("nef", smartcontract.ByteArrayType), - manifest.NewParameter("manifest", smartcontract.ByteArrayType), - }, - ReturnType: smartcontract.VoidType, - }, - { - Name: "update", - Offset: update3Off, - Parameters: []manifest.Parameter{ - manifest.NewParameter("nef", smartcontract.ByteArrayType), - manifest.NewParameter("manifest", smartcontract.ByteArrayType), - manifest.NewParameter("data", smartcontract.AnyType), - }, - ReturnType: smartcontract.VoidType, - }, - { - Name: "destroy", - Offset: destroyOff, - ReturnType: smartcontract.VoidType, - }, - { - Name: "invalidStack", - Offset: invalidStackOff, - ReturnType: smartcontract.VoidType, - }, - { - Name: "callT0", - Offset: callT0Off, - Parameters: []manifest.Parameter{ - manifest.NewParameter("address", smartcontract.Hash160Type), - }, - ReturnType: smartcontract.IntegerType, - }, - { - Name: "callT1", - Offset: callT1Off, - ReturnType: smartcontract.IntegerType, - }, - { - Name: "callT2", - Offset: callT2Off, - ReturnType: smartcontract.IntegerType, - }, - { - Name: "burnGas", - Offset: burnGasOff, - Parameters: []manifest.Parameter{ - manifest.NewParameter("amount", smartcontract.IntegerType), - }, - ReturnType: smartcontract.VoidType, - }, - { - Name: "invocCounter", - Offset: invocCounterOff, - ReturnType: smartcontract.IntegerType, - }, - } - m.Permissions = make([]manifest.Permission, 2) - m.Permissions[0].Contract.Type = manifest.PermissionHash - m.Permissions[0].Contract.Value = bc.contracts.NEO.Hash - m.Permissions[0].Methods.Add("balanceOf") - - m.Permissions[1].Contract.Type = manifest.PermissionHash - m.Permissions[1].Contract.Value = util.Uint160{} - m.Permissions[1].Methods.Add("method") - - // Generate NEF file. - ne, err := nef.NewFile(script) - if err != nil { - panic(err) - } - ne.Tokens = []nef.MethodToken{ - { - Hash: neoHash, - Method: "balanceOf", - ParamCount: 1, - HasReturn: true, - CallFlag: callflag.ReadStates, - }, - { - Hash: util.Uint160{}, - Method: "method", - HasReturn: true, - CallFlag: callflag.ReadStates, - }, - } - ne.Checksum = ne.CalculateChecksum() - - // Write first NEF file. - bytes, err := ne.Bytes() - require.NoError(t, err) - if saveState { - err = os.WriteFile(helper1ContractNEFPath, bytes, os.ModePerm) - require.NoError(t, err) - } - - // Write first manifest file. - mData, err := json.Marshal(m) - require.NoError(t, err) - if saveState { - err = os.WriteFile(helper1ContractManifestPath, mData, os.ModePerm) - require.NoError(t, err) - } - - // Create hash of the first contract assuming that sender is single-chain validator. - h := state.CreateContractHash(singleChainValidatorHash, ne.Checksum, m.Name) - - currScript := []byte{byte(opcode.RET)} - m = manifest.NewManifest("TestAux") - perm := manifest.NewPermission(manifest.PermissionHash, h) - perm.Methods.Add("add") - perm.Methods.Add("drop") - perm.Methods.Add("add3") - perm.Methods.Add("invalidReturn") - perm.Methods.Add("justReturn") - perm.Methods.Add("getValue") - m.Permissions = append(m.Permissions, *perm) - ne, err = nef.NewFile(currScript) - if err != nil { - panic(err) - } - - // Write second NEF file. - bytes, err = ne.Bytes() - require.NoError(t, err) - if saveState { - err = os.WriteFile(helper2ContractNEFPath, bytes, os.ModePerm) - require.NoError(t, err) - } - - // Write second manifest file. - mData, err = json.Marshal(m) - require.NoError(t, err) - if saveState { - err = os.WriteFile(helper2ContractManifestPath, mData, os.ModePerm) - require.NoError(t, err) - } - - require.False(t, saveState) -} - -// getTestContractState returns 2 contracts second of which is allowed to call the first. -func getTestContractState(t *testing.T, id1, id2 int32, sender2 util.Uint160) (*state.Contract, *state.Contract) { - errNotFound := errors.New("auto-generated oracle contract is not found, use TestGenerateOracleContract to regenerate") - - neBytes, err := os.ReadFile(helper1ContractNEFPath) - require.NoError(t, err, fmt.Errorf("nef1: %w", errNotFound)) - ne, err := nef.FileFromBytes(neBytes) - require.NoError(t, err) - - mBytes, err := os.ReadFile(helper1ContractManifestPath) - require.NoError(t, err, fmt.Errorf("manifest1: %w", errNotFound)) - m := &manifest.Manifest{} - err = json.Unmarshal(mBytes, m) - require.NoError(t, err) - - cs1 := &state.Contract{ - ContractBase: state.ContractBase{ - NEF: ne, - Manifest: *m, - ID: id1, - }, - } - - neBytes, err = os.ReadFile(helper2ContractNEFPath) - require.NoError(t, err, fmt.Errorf("nef2: %w", errNotFound)) - ne, err = nef.FileFromBytes(neBytes) - require.NoError(t, err) - - mBytes, err = os.ReadFile(helper2ContractManifestPath) - require.NoError(t, err, fmt.Errorf("manifest2: %w", errNotFound)) - m = &manifest.Manifest{} - err = json.Unmarshal(mBytes, m) - require.NoError(t, err) - - // Retrieve hash of the first contract from the permissions of the second contract. - require.Equal(t, 1, len(m.Permissions)) - require.Equal(t, manifest.PermissionHash, m.Permissions[0].Contract.Type) - cs1.Hash = m.Permissions[0].Contract.Hash() - - cs2 := &state.Contract{ - ContractBase: state.ContractBase{ - NEF: ne, - Manifest: *m, - ID: id2, - Hash: state.CreateContractHash(sender2, ne.Checksum, m.Name), - }, - } - - return cs1, cs2 -} - func loadScript(ic *interop.Context, script []byte, args ...interface{}) { ic.SpawnVM() ic.VM.LoadScriptWithFlags(script, callflag.AllowCall) @@ -1108,7 +580,7 @@ func loadScriptWithHashAndFlags(ic *interop.Context, script []byte, hash util.Ui func TestContractCall(t *testing.T) { _, ic, bc := createVM(t) - cs, currCs := getTestContractState(t, 4, 5, random.Uint160()) // sender and IDs are not important for the test + 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)) @@ -1496,80 +968,3 @@ func TestRuntimeCheckWitness(t *testing.T) { }) }) } - -func TestLoadToken(t *testing.T) { - bc := newTestChain(t) - - cs, _ := getTestContractState(t, 4, 5, random.Uint160()) // sender and IDs are not important for the test - require.NoError(t, bc.contracts.Management.PutContractState(bc.dao, cs)) - - t.Run("good", func(t *testing.T) { - aer, err := invokeContractMethod(bc, 1_00000000, cs.Hash, "callT0", neoOwner.BytesBE()) - require.NoError(t, err) - realBalance, _ := bc.GetGoverningTokenBalance(neoOwner) - checkResult(t, aer, stackitem.Make(realBalance.Int64()+1)) - }) - t.Run("invalid param count", func(t *testing.T) { - aer, err := invokeContractMethod(bc, 1_00000000, cs.Hash, "callT2") - require.NoError(t, err) - checkFAULTState(t, aer) - }) - t.Run("invalid contract", func(t *testing.T) { - aer, err := invokeContractMethod(bc, 1_00000000, cs.Hash, "callT1") - require.NoError(t, err) - checkFAULTState(t, aer) - }) -} - -func TestRuntimeGetNetwork(t *testing.T) { - bc := newTestChain(t) - - w := io.NewBufBinWriter() - emit.Syscall(w.BinWriter, interopnames.SystemRuntimeGetNetwork) - require.NoError(t, w.Err) - - tx := transaction.New(w.Bytes(), 10_000) - tx.ValidUntilBlock = bc.BlockHeight() + 1 - addSigners(neoOwner, tx) - require.NoError(t, testchain.SignTx(bc, tx)) - - require.NoError(t, bc.AddBlock(bc.newBlock(tx))) - - aer, err := bc.GetAppExecResults(tx.Hash(), trigger.Application) - require.NoError(t, err) - checkResult(t, &aer[0], stackitem.Make(uint32(bc.config.Magic))) -} - -func TestRuntimeBurnGas(t *testing.T) { - bc := newTestChain(t) - - cs, _ := getTestContractState(t, 4, 5, random.Uint160()) // sender and IDs are not important for the test - require.NoError(t, bc.contracts.Management.PutContractState(bc.dao, cs)) - - const sysFee = 2_000000 - - t.Run("good", func(t *testing.T) { - aer, err := invokeContractMethod(bc, sysFee, cs.Hash, "burnGas", int64(1)) - require.NoError(t, err) - require.Equal(t, vm.HaltState, aer.VMState) - - t.Run("gas limit exceeded", func(t *testing.T) { - aer, err = invokeContractMethod(bc, aer.GasConsumed, cs.Hash, "burnGas", int64(2)) - require.NoError(t, err) - require.Equal(t, vm.FaultState, aer.VMState) - }) - }) - t.Run("too big integer", func(t *testing.T) { - gas := big.NewInt(math.MaxInt64) - gas.Add(gas, big.NewInt(1)) - - aer, err := invokeContractMethod(bc, sysFee, cs.Hash, "burnGas", gas) - require.NoError(t, err) - checkFAULTState(t, aer) - }) - t.Run("zero GAS", func(t *testing.T) { - aer, err := invokeContractMethod(bc, sysFee, cs.Hash, "burnGas", int64(0)) - require.NoError(t, err) - checkFAULTState(t, aer) - }) -} diff --git a/pkg/core/interop_system_neotest_test.go b/pkg/core/interop_system_neotest_test.go new file mode 100644 index 000000000..f2ac39226 --- /dev/null +++ b/pkg/core/interop_system_neotest_test.go @@ -0,0 +1,245 @@ +package core_test + +import ( + "encoding/json" + "math" + "math/big" + "testing" + + "github.com/nspcc-dev/neo-go/internal/contracts" + "github.com/nspcc-dev/neo-go/pkg/core/interop" + "github.com/nspcc-dev/neo-go/pkg/core/interop/interopnames" + "github.com/nspcc-dev/neo-go/pkg/core/native/nativenames" + "github.com/nspcc-dev/neo-go/pkg/core/transaction" + "github.com/nspcc-dev/neo-go/pkg/crypto/hash" + "github.com/nspcc-dev/neo-go/pkg/crypto/keys" + "github.com/nspcc-dev/neo-go/pkg/io" + "github.com/nspcc-dev/neo-go/pkg/neotest" + "github.com/nspcc-dev/neo-go/pkg/neotest/chain" + "github.com/nspcc-dev/neo-go/pkg/smartcontract" + "github.com/nspcc-dev/neo-go/pkg/util" + "github.com/nspcc-dev/neo-go/pkg/vm/emit" + "github.com/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) + + w := io.NewBufBinWriter() + emit.Syscall(w.BinWriter, interopnames.SystemRuntimeGetRandom) + require.NoError(t, w.Err) + script := w.Bytes() + + tx1 := e.PrepareInvocation(t, script, []neotest.Signer{e.Validator}, bc.BlockHeight()+1) + tx2 := e.PrepareInvocation(t, script, []neotest.Signer{e.Validator}, bc.BlockHeight()+1) + e.AddNewBlock(t, tx1, tx2) + e.CheckHalt(t, tx1.Hash()) + e.CheckHalt(t, tx2.Hash()) + + res1 := e.GetTxExecResult(t, tx1.Hash()) + res2 := e.GetTxExecResult(t, tx2.Hash()) + + r1, err := res1.Stack[0].TryInteger() + require.NoError(t, err) + r2, err := res2.Stack[0].TryInteger() + require.NoError(t, err) + require.NotEqual(t, r1, r2) +} + +func TestSystemContractCreateStandardAccount(t *testing.T) { + bc, acc := chain.NewSingle(t) + e := neotest.NewExecutor(t, bc, acc, acc) + w := io.NewBufBinWriter() + + t.Run("Good", func(t *testing.T) { + priv, err := keys.NewPrivateKey() + require.NoError(t, err) + pub := priv.PublicKey() + + emit.Bytes(w.BinWriter, pub.Bytes()) + emit.Syscall(w.BinWriter, interopnames.SystemContractCreateStandardAccount) + require.NoError(t, w.Err) + script := w.Bytes() + + tx := e.PrepareInvocation(t, script, []neotest.Signer{e.Validator}, bc.BlockHeight()+1) + e.AddNewBlock(t, tx) + e.CheckHalt(t, tx.Hash()) + + res := e.GetTxExecResult(t, tx.Hash()) + value := res.Stack[0].Value().([]byte) + u, err := util.Uint160DecodeBytesBE(value) + require.NoError(t, err) + require.Equal(t, pub.GetScriptHash(), u) + }) + t.Run("InvalidKey", func(t *testing.T) { + w.Reset() + emit.Bytes(w.BinWriter, []byte{1, 2, 3}) + emit.Syscall(w.BinWriter, interopnames.SystemContractCreateStandardAccount) + require.NoError(t, w.Err) + script := w.Bytes() + + tx := e.PrepareInvocation(t, script, []neotest.Signer{e.Validator}, bc.BlockHeight()+1) + e.AddNewBlock(t, tx) + e.CheckFault(t, tx.Hash(), "invalid prefix 1") + }) +} + +func TestSystemContractCreateMultisigAccount(t *testing.T) { + bc, acc := chain.NewSingle(t) + e := neotest.NewExecutor(t, bc, acc, acc) + w := io.NewBufBinWriter() + + createScript := func(t *testing.T, pubs []interface{}, m int) []byte { + w.Reset() + emit.Array(w.BinWriter, pubs...) + emit.Int(w.BinWriter, int64(m)) + emit.Syscall(w.BinWriter, interopnames.SystemContractCreateMultisigAccount) + require.NoError(t, w.Err) + return w.Bytes() + } + t.Run("Good", func(t *testing.T) { + m, n := 3, 5 + pubs := make(keys.PublicKeys, n) + arr := make([]interface{}, n) + for i := range pubs { + pk, err := keys.NewPrivateKey() + require.NoError(t, err) + pubs[i] = pk.PublicKey() + arr[i] = pubs[i].Bytes() + } + script := createScript(t, arr, m) + + txH := e.InvokeScript(t, script, []neotest.Signer{acc}) + e.CheckHalt(t, txH) + res := e.GetTxExecResult(t, txH) + value := res.Stack[0].Value().([]byte) + u, err := util.Uint160DecodeBytesBE(value) + require.NoError(t, err) + expected, err := smartcontract.CreateMultiSigRedeemScript(m, pubs) + require.NoError(t, err) + require.Equal(t, hash.Hash160(expected), u) + }) + t.Run("InvalidKey", func(t *testing.T) { + script := createScript(t, []interface{}{[]byte{1, 2, 3}}, 1) + e.InvokeScriptCheckFAULT(t, script, []neotest.Signer{acc}, "invalid prefix 1") + }) + t.Run("Invalid m", func(t *testing.T) { + pk, err := keys.NewPrivateKey() + require.NoError(t, err) + script := createScript(t, []interface{}{pk.PublicKey().Bytes()}, 2) + e.InvokeScriptCheckFAULT(t, script, []neotest.Signer{acc}, "length of the signatures (2) is higher then the number of public keys") + }) + t.Run("m overflows int32", func(t *testing.T) { + pk, err := keys.NewPrivateKey() + require.NoError(t, err) + m := big.NewInt(math.MaxInt32) + m.Add(m, big.NewInt(1)) + w.Reset() + emit.Array(w.BinWriter, pk.Bytes()) + emit.BigInt(w.BinWriter, m) + emit.Syscall(w.BinWriter, interopnames.SystemContractCreateMultisigAccount) + require.NoError(t, w.Err) + e.InvokeScriptCheckFAULT(t, w.Bytes(), []neotest.Signer{acc}, "m must be positive and fit int32") + }) +} + +func TestSystemRuntimeGasLeft(t *testing.T) { + const runtimeGasLeftPrice = 1 << 4 + + 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) + managementInvoker := e.ValidatorInvoker(e.NativeHash(t, nativenames.Management)) + + cs, _ := contracts.GetTestContractState(t, pathToInternalContracts, 0, 1, acc.ScriptHash()) + rawManifest, err := json.Marshal(cs.Manifest) + require.NoError(t, err) + rawNef, err := cs.NEF.Bytes() + require.NoError(t, err) + tx := managementInvoker.PrepareInvoke(t, "deploy", rawNef, rawManifest) + e.AddNewBlock(t, tx) + e.CheckHalt(t, tx.Hash()) + cInvoker := e.ValidatorInvoker(cs.Hash) + + t.Run("good", func(t *testing.T) { + realBalance, _ := bc.GetGoverningTokenBalance(acc.ScriptHash()) + cInvoker.Invoke(t, stackitem.NewBigInteger(big.NewInt(realBalance.Int64()+1)), "callT0", acc.ScriptHash()) + }) + t.Run("invalid param count", func(t *testing.T) { + cInvoker.InvokeFail(t, "method not found: callT2/1", "callT2", acc.ScriptHash()) + }) + t.Run("invalid contract", func(t *testing.T) { + cInvoker.InvokeFail(t, "token contract 0000000000000000000000000000000000000000 not found: key not found", "callT1") + }) +} + +func 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 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)) + }) +} diff --git a/pkg/core/native/interop.go b/pkg/core/native/interop.go index 077a18aab..c97850138 100644 --- a/pkg/core/native/interop.go +++ b/pkg/core/native/interop.go @@ -17,14 +17,15 @@ func Call(ic *interop.Context) error { return fmt.Errorf("native contract of version %d is not active", version) } var c interop.Contract + curr := ic.VM.GetCurrentScriptHash() for _, ctr := range ic.Natives { - if ctr.Metadata().Hash == ic.VM.GetCurrentScriptHash() { + if ctr.Metadata().Hash == curr { c = ctr break } } if c == nil { - return fmt.Errorf("native contract %d not found", version) + return fmt.Errorf("native contract %s (version %d) not found", curr.StringLE(), version) } history := c.Metadata().UpdateHistory if len(history) == 0 { diff --git a/pkg/core/native/management.go b/pkg/core/native/management.go index 6e0d3c836..f61c640d3 100644 --- a/pkg/core/native/management.go +++ b/pkg/core/native/management.go @@ -200,7 +200,7 @@ func (m *Management) getNefAndManifestFromItems(ic *interop.Context, args []stac gas := ic.Chain.GetStoragePrice() * int64(len(nefBytes)+len(manifestBytes)) if isDeploy { - fee := m.GetMinimumDeploymentFee(ic.DAO) + fee := m.minimumDeploymentFee(ic.DAO) if fee > gas { gas = fee } @@ -400,11 +400,11 @@ func (m *Management) Destroy(d *dao.Simple, hash util.Uint160) error { } func (m *Management) getMinimumDeploymentFee(ic *interop.Context, args []stackitem.Item) stackitem.Item { - return stackitem.NewBigInteger(big.NewInt(m.GetMinimumDeploymentFee(ic.DAO))) + return stackitem.NewBigInteger(big.NewInt(m.minimumDeploymentFee(ic.DAO))) } -// GetMinimumDeploymentFee returns the minimum required fee for contract deploy. -func (m *Management) GetMinimumDeploymentFee(dao *dao.Simple) int64 { +// minimumDeploymentFee returns the minimum required fee for contract deploy. +func (m *Management) minimumDeploymentFee(dao *dao.Simple) int64 { return getIntWithKey(m.ID, dao, keyMinimumDeploymentFee) } diff --git a/pkg/core/native/native_test/common_test.go b/pkg/core/native/native_test/common_test.go index da37b24b8..bf9c900ce 100644 --- a/pkg/core/native/native_test/common_test.go +++ b/pkg/core/native/native_test/common_test.go @@ -90,7 +90,7 @@ func setNodesByRole(t *testing.T, designateInvoker *neotest.ContractInvoker, ok func checkNodeRoles(t *testing.T, designateInvoker *neotest.ContractInvoker, ok bool, r noderoles.Role, index uint32, res keys.PublicKeys) { if ok { - designateInvoker.InvokeAndCheck(t, func(t *testing.T, stack []stackitem.Item) { + designateInvoker.InvokeAndCheck(t, func(t testing.TB, stack []stackitem.Item) { require.Equal(t, 1, len(stack)) arr := stack[0].Value().([]stackitem.Item) require.Equal(t, len(res), len(arr)) diff --git a/pkg/core/native/native_test/ledger_test.go b/pkg/core/native/native_test/ledger_test.go index a869a9de9..ab71dbee0 100644 --- a/pkg/core/native/native_test/ledger_test.go +++ b/pkg/core/native/native_test/ledger_test.go @@ -82,7 +82,7 @@ func TestLedger_GetTransactionFromBlock(t *testing.T) { ledgerInvoker.Invoke(t, e.Chain.BlockHeight(), "currentIndex") // Adds a block. b := e.GetBlockByIndex(t, int(e.Chain.BlockHeight())) - check := func(t *testing.T, stack []stackitem.Item) { + check := func(t testing.TB, stack []stackitem.Item) { require.Equal(t, 1, len(stack)) actual, ok := stack[0].Value().([]stackitem.Item) require.True(t, ok) diff --git a/pkg/core/native/native_test/management_test.go b/pkg/core/native/native_test/management_test.go index eb4fa8872..c34b1c8d8 100644 --- a/pkg/core/native/native_test/management_test.go +++ b/pkg/core/native/native_test/management_test.go @@ -3,17 +3,14 @@ package native_test import ( "bytes" "encoding/json" - "errors" "fmt" - "os" - "path/filepath" "testing" - "github.com/nspcc-dev/neo-go/pkg/core/storage" - + "github.com/nspcc-dev/neo-go/internal/contracts" "github.com/nspcc-dev/neo-go/pkg/core/chaindump" "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/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" @@ -23,20 +20,12 @@ import ( "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/util" "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 ( - helper1ContractNEFPath = filepath.Join("..", "..", "test_data", "management_helper", "management_helper1.nef") - helper1ContractManifestPath = filepath.Join("..", "..", "test_data", "management_helper", "management_helper1.manifest.json") - helper2ContractNEFPath = filepath.Join("..", "..", "test_data", "management_helper", "management_helper2.nef") - helper2ContractManifestPath = filepath.Join("..", "..", "test_data", "management_helper", "management_helper2.manifest.json") -) - func newManagementClient(t *testing.T) *neotest.ContractInvoker { return newNativeClient(t, nativenames.Management) } @@ -45,62 +34,11 @@ func TestManagement_MinimumDeploymentFee(t *testing.T) { testGetSet(t, newManagementClient(t), "MinimumDeploymentFee", 10_00000000, 0, 0) } -// getTestContractState returns 2 contracts second of which is allowed to call the first. -func getTestContractState(t *testing.T, id1, id2 int32, sender2 util.Uint160) (*state.Contract, *state.Contract) { - errNotFound := errors.New("auto-generated oracle contract is not found, use TestGenerateOracleContract to regenerate") - - neBytes, err := os.ReadFile(helper1ContractNEFPath) - require.NoError(t, err, fmt.Errorf("nef1: %w", errNotFound)) - ne, err := nef.FileFromBytes(neBytes) - require.NoError(t, err) - - mBytes, err := os.ReadFile(helper1ContractManifestPath) - require.NoError(t, err, fmt.Errorf("manifest1: %w", errNotFound)) - m := &manifest.Manifest{} - err = json.Unmarshal(mBytes, m) - require.NoError(t, err) - - cs1 := &state.Contract{ - ContractBase: state.ContractBase{ - NEF: ne, - Manifest: *m, - ID: id1, - }, - } - - neBytes, err = os.ReadFile(helper2ContractNEFPath) - require.NoError(t, err, fmt.Errorf("nef2: %w", errNotFound)) - ne, err = nef.FileFromBytes(neBytes) - require.NoError(t, err) - - mBytes, err = os.ReadFile(helper2ContractManifestPath) - require.NoError(t, err, fmt.Errorf("manifest2: %w", errNotFound)) - m = &manifest.Manifest{} - err = json.Unmarshal(mBytes, m) - require.NoError(t, err) - - // Retrieve hash of the first contract from the permissions of the second contract. - require.Equal(t, 1, len(m.Permissions)) - require.Equal(t, manifest.PermissionHash, m.Permissions[0].Contract.Type) - cs1.Hash = m.Permissions[0].Contract.Hash() - - cs2 := &state.Contract{ - ContractBase: state.ContractBase{ - NEF: ne, - Manifest: *m, - ID: id2, - Hash: state.CreateContractHash(sender2, ne.Checksum, m.Name), - }, - } - - return cs1, cs2 -} - func TestManagement_ContractDeploy(t *testing.T) { c := newManagementClient(t) managementInvoker := c.WithSigners(c.Committee) - cs1, _ := getTestContractState(t, 1, 2, c.Committee.ScriptHash()) + cs1, _ := contracts.GetTestContractState(t, pathToInternalContracts, 1, 2, c.Committee.ScriptHash()) manifestBytes, err := json.Marshal(cs1.Manifest) require.NoError(t, err) nefBytes, err := cs1.NEF.Bytes() @@ -301,7 +239,7 @@ func TestManagement_StartFromHeight(t *testing.T) { c := e.CommitteeInvoker(e.NativeHash(t, nativenames.Management)) managementInvoker := c.WithSigners(c.Committee) - cs1, _ := getTestContractState(t, 1, 2, c.CommitteeHash) + cs1, _ := contracts.GetTestContractState(t, pathToInternalContracts, 1, 2, c.CommitteeHash) manifestBytes, err := json.Marshal(cs1.Manifest) require.NoError(t, err) nefBytes, err := cs1.NEF.Bytes() @@ -329,7 +267,7 @@ func TestManagement_DeployManifestOverflow(t *testing.T) { c := newManagementClient(t) managementInvoker := c.WithSigners(c.Committee) - cs1, _ := getTestContractState(t, 1, 2, c.CommitteeHash) + cs1, _ := contracts.GetTestContractState(t, pathToInternalContracts, 1, 2, c.CommitteeHash) manif1, err := json.Marshal(cs1.Manifest) require.NoError(t, err) nef1, err := nef.NewFile(cs1.NEF.Script) @@ -359,7 +297,7 @@ func TestManagement_ContractDeployAndUpdateWithParameter(t *testing.T) { c := newManagementClient(t) managementInvoker := c.WithSigners(c.Committee) - cs1, _ := getTestContractState(t, 1, 2, c.CommitteeHash) + cs1, _ := contracts.GetTestContractState(t, pathToInternalContracts, 1, 2, c.CommitteeHash) cs1.Manifest.Permissions = []manifest.Permission{*manifest.NewPermission(manifest.PermissionWildcard)} cs1.ID = 1 cs1.Hash = state.CreateContractHash(c.CommitteeHash, cs1.NEF.Checksum, cs1.Manifest.Name) @@ -400,7 +338,7 @@ func TestManagement_ContractUpdate(t *testing.T) { c := newManagementClient(t) managementInvoker := c.WithSigners(c.Committee) - cs1, _ := getTestContractState(t, 1, 2, c.CommitteeHash) + cs1, _ := contracts.GetTestContractState(t, pathToInternalContracts, 1, 2, c.CommitteeHash) // Allow calling management contract. cs1.Manifest.Permissions = []manifest.Permission{*manifest.NewPermission(manifest.PermissionWildcard)} manifestBytes, err := json.Marshal(cs1.Manifest) @@ -535,7 +473,7 @@ func TestManagement_GetContract(t *testing.T) { c := newManagementClient(t) managementInvoker := c.WithSigners(c.Committee) - cs1, _ := getTestContractState(t, 1, 2, c.CommitteeHash) + cs1, _ := contracts.GetTestContractState(t, pathToInternalContracts, 1, 2, c.CommitteeHash) manifestBytes, err := json.Marshal(cs1.Manifest) require.NoError(t, err) nefBytes, err := cs1.NEF.Bytes() @@ -560,7 +498,7 @@ func TestManagement_ContractDestroy(t *testing.T) { c := newManagementClient(t) managementInvoker := c.WithSigners(c.Committee) - cs1, _ := getTestContractState(t, 1, 2, c.CommitteeHash) + cs1, _ := contracts.GetTestContractState(t, pathToInternalContracts, 1, 2, c.CommitteeHash) // Allow calling management contract. cs1.Manifest.Permissions = []manifest.Permission{*manifest.NewPermission(manifest.PermissionWildcard)} manifestBytes, err := json.Marshal(cs1.Manifest) diff --git a/pkg/core/native/native_test/neo_test.go b/pkg/core/native/native_test/neo_test.go index 6da70d52e..d31dd4bab 100644 --- a/pkg/core/native/native_test/neo_test.go +++ b/pkg/core/native/native_test/neo_test.go @@ -7,6 +7,7 @@ import ( "sort" "testing" + "github.com/nspcc-dev/neo-go/internal/contracts" "github.com/nspcc-dev/neo-go/internal/random" "github.com/nspcc-dev/neo-go/pkg/core/native" "github.com/nspcc-dev/neo-go/pkg/core/native/nativenames" @@ -276,7 +277,7 @@ func TestNEO_TransferOnPayment(t *testing.T) { e := neoValidatorsInvoker.Executor managementValidatorsInvoker := e.ValidatorInvoker(e.NativeHash(t, nativenames.Management)) - cs, _ := getTestContractState(t, 1, 2, e.CommitteeHash) + cs, _ := contracts.GetTestContractState(t, pathToInternalContracts, 1, 2, e.CommitteeHash) cs.Hash = state.CreateContractHash(e.Validator.ScriptHash(), cs.NEF.Checksum, cs.Manifest.Name) // set proper hash manifB, err := json.Marshal(cs.Manifest) require.NoError(t, err) diff --git a/pkg/core/native/native_test/oracle_test.go b/pkg/core/native/native_test/oracle_test.go index 8b7ae6a4a..77247f80b 100644 --- a/pkg/core/native/native_test/oracle_test.go +++ b/pkg/core/native/native_test/oracle_test.go @@ -2,32 +2,28 @@ package native_test import ( "encoding/json" - "errors" - "fmt" "math" "math/big" - "os" "path/filepath" "strings" "testing" + "github.com/nspcc-dev/neo-go/internal/contracts" "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/core/state" "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/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/util" "github.com/nspcc-dev/neo-go/pkg/vm/emit" "github.com/nspcc-dev/neo-go/pkg/vm/stackitem" "github.com/stretchr/testify/require" ) +var pathToInternalContracts = filepath.Join("..", "..", "..", "..", "internal", "contracts") + func newOracleClient(t *testing.T) *neotest.ContractInvoker { return newNativeClient(t, nativenames.Oracle) } @@ -36,36 +32,6 @@ func TestGetSetPrice(t *testing.T) { testGetSet(t, newOracleClient(t), "Price", native.DefaultOracleRequestPrice, 1, math.MaxInt64) } -// getOracleContractState reads pre-compiled oracle contract generated by -// TestGenerateOracleContract and returns its state. -func getOracleContractState(t *testing.T, sender util.Uint160, id int32) *state.Contract { - var ( - oracleContractNEFPath = filepath.Join("..", "..", "test_data", "oracle_contract", "oracle.nef") - oracleContractManifestPath = filepath.Join("..", "..", "test_data", "oracle_contract", "oracle.manifest.json") - ) - errNotFound := errors.New("auto-generated oracle contract is not found, use TestGenerateOracleContract to regenerate") - - neBytes, err := os.ReadFile(oracleContractNEFPath) - require.NoError(t, err, fmt.Errorf("nef: %w", errNotFound)) - ne, err := nef.FileFromBytes(neBytes) - require.NoError(t, err) - - mBytes, err := os.ReadFile(oracleContractManifestPath) - require.NoError(t, err, fmt.Errorf("manifest: %w", errNotFound)) - m := &manifest.Manifest{} - err = json.Unmarshal(mBytes, m) - require.NoError(t, err) - - return &state.Contract{ - ContractBase: state.ContractBase{ - NEF: ne, - Hash: state.CreateContractHash(sender, ne.Checksum, m.Name), - Manifest: *m, - ID: id, - }, - } -} - func putOracleRequest(t *testing.T, oracleInvoker *neotest.ContractInvoker, url string, filter *string, cb string, userData []byte, gas int64, errStr ...string) { var filtItem interface{} @@ -86,7 +52,7 @@ func TestOracle_Request(t *testing.T) { designationCommitteeInvoker := e.CommitteeInvoker(e.NativeHash(t, nativenames.Designation)) gasCommitteeInvoker := e.CommitteeInvoker(e.NativeHash(t, nativenames.Gas)) - cs := getOracleContractState(t, e.Validator.ScriptHash(), 1) + cs := contracts.GetOracleContractState(t, pathToInternalContracts, e.Validator.ScriptHash(), 1) nBytes, err := cs.NEF.Bytes() require.NoError(t, err) mBytes, err := json.Marshal(cs.Manifest) diff --git a/pkg/core/native/oracle.go b/pkg/core/native/oracle.go index 44eee0418..3730769d3 100644 --- a/pkg/core/native/oracle.go +++ b/pkg/core/native/oracle.go @@ -86,9 +86,7 @@ func newOracle() *Oracle { o := &Oracle{ContractMD: *interop.NewContractMD(nativenames.Oracle, oracleContractID)} defer o.UpdateHash() - w := io.NewBufBinWriter() - emit.AppCall(w.BinWriter, o.Hash, "finish", callflag.All) - o.oracleScript = w.Bytes() + o.oracleScript = CreateOracleResponseScript(o.Hash) desc := newDescriptor("request", smartcontract.VoidType, manifest.NewParameter("url", smartcontract.StringType), @@ -526,3 +524,14 @@ func (o *Oracle) updateCache(d *dao.Simple) error { orc.AddRequests(reqs) return nil } + +// CreateOracleResponseScript returns script that is used to create native Oracle +// response. +func CreateOracleResponseScript(nativeOracleHash util.Uint160) []byte { + w := io.NewBufBinWriter() + emit.AppCall(w.BinWriter, nativeOracleHash, "finish", callflag.All) + if w.Err != nil { + panic(fmt.Errorf("failed to create Oracle response script: %w", w.Err)) + } + return w.Bytes() +} diff --git a/pkg/core/native_contract_test.go b/pkg/core/native_contract_test.go index 563f40b9b..2be2c46b4 100644 --- a/pkg/core/native_contract_test.go +++ b/pkg/core/native_contract_test.go @@ -1,323 +1,164 @@ -package core +package core_test import ( - "errors" - "math/big" + "encoding/json" + "fmt" + "strings" "testing" - "github.com/nspcc-dev/neo-go/internal/random" - "github.com/nspcc-dev/neo-go/pkg/core/dao" + "github.com/nspcc-dev/neo-go/internal/contracts" + "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/core/interop/contract" + "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/storage" - "github.com/nspcc-dev/neo-go/pkg/smartcontract" + "github.com/nspcc-dev/neo-go/pkg/crypto/hash" + "github.com/nspcc-dev/neo-go/pkg/neotest" + "github.com/nspcc-dev/neo-go/pkg/neotest/chain" "github.com/nspcc-dev/neo-go/pkg/smartcontract/callflag" - "github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest" "github.com/nspcc-dev/neo-go/pkg/smartcontract/trigger" "github.com/nspcc-dev/neo-go/pkg/util" - "github.com/nspcc-dev/neo-go/pkg/vm" "github.com/nspcc-dev/neo-go/pkg/vm/opcode" "github.com/nspcc-dev/neo-go/pkg/vm/stackitem" "github.com/stretchr/testify/require" ) -type testNative struct { - meta interop.ContractMD - blocks chan uint32 -} - -func (tn *testNative) Initialize(_ *interop.Context) error { - return nil -} - -func (tn *testNative) Metadata() *interop.ContractMD { - return &tn.meta -} - -func (tn *testNative) OnPersist(ic *interop.Context) error { - select { - case tn.blocks <- ic.Block.Index: - return nil - default: - return errors.New("can't send index") - } -} - -func (tn *testNative) PostPersist(ic *interop.Context) error { - return nil -} - -var _ interop.Contract = (*testNative)(nil) - -// registerNative registers native contract in the blockchain. -func (bc *Blockchain) registerNative(c interop.Contract) { - bc.contracts.Contracts = append(bc.contracts.Contracts, c) - bc.config.NativeUpdateHistories[c.Metadata().Name] = c.Metadata().UpdateHistory -} - -const ( - testSumCPUFee = 1 << 15 // same as contract.Call - testSumStorageFee = 200 -) - -func newTestNative() *testNative { - cMD := interop.NewContractMD("Test.Native.Sum", 0) - cMD.UpdateHistory = []uint32{0} - tn := &testNative{ - meta: *cMD, - blocks: make(chan uint32, 1), - } - defer tn.meta.UpdateHash() - - desc := &manifest.Method{ - Name: "sum", - Parameters: []manifest.Parameter{ - manifest.NewParameter("addend1", smartcontract.IntegerType), - manifest.NewParameter("addend2", smartcontract.IntegerType), - }, - ReturnType: smartcontract.IntegerType, - Safe: true, - } - md := &interop.MethodAndPrice{ - Func: tn.sum, - CPUFee: testSumCPUFee, - StorageFee: testSumStorageFee, - RequiredFlags: callflag.NoneFlag, - } - tn.meta.AddMethod(md, desc) - - desc = &manifest.Method{ - Name: "callOtherContractNoReturn", - Parameters: []manifest.Parameter{ - manifest.NewParameter("contractHash", smartcontract.Hash160Type), - manifest.NewParameter("method", smartcontract.StringType), - manifest.NewParameter("arg", smartcontract.ArrayType), - }, - ReturnType: smartcontract.VoidType, - Safe: true, - } - md = &interop.MethodAndPrice{ - Func: tn.callOtherContractNoReturn, - CPUFee: testSumCPUFee, - RequiredFlags: callflag.NoneFlag} - tn.meta.AddMethod(md, desc) - - desc = &manifest.Method{ - Name: "callOtherContractWithReturn", - Parameters: []manifest.Parameter{ - manifest.NewParameter("contractHash", smartcontract.Hash160Type), - manifest.NewParameter("method", smartcontract.StringType), - manifest.NewParameter("arg", smartcontract.ArrayType), - }, - ReturnType: smartcontract.IntegerType, - } - md = &interop.MethodAndPrice{ - Func: tn.callOtherContractWithReturn, - CPUFee: testSumCPUFee, - RequiredFlags: callflag.NoneFlag} - tn.meta.AddMethod(md, desc) - - return tn -} - -func (tn *testNative) sum(_ *interop.Context, args []stackitem.Item) stackitem.Item { - s1, err := args[0].TryInteger() - if err != nil { - panic(err) - } - s2, err := args[1].TryInteger() - if err != nil { - panic(err) - } - return stackitem.NewBigInteger(s1.Add(s1, s2)) -} - -func toUint160(item stackitem.Item) util.Uint160 { - bs, err := item.TryBytes() - if err != nil { - panic(err) - } - u, err := util.Uint160DecodeBytesBE(bs) - if err != nil { - panic(err) - } - return u -} - -func (tn *testNative) call(ic *interop.Context, args []stackitem.Item, hasReturn bool) { - cs, err := ic.GetContract(toUint160(args[0])) - if err != nil { - panic(err) - } - bs, err := args[1].TryBytes() - if err != nil { - panic(err) - } - err = contract.CallFromNative(ic, tn.meta.Hash, cs, string(bs), args[2].Value().([]stackitem.Item), hasReturn) - if err != nil { - panic(err) - } -} - -func (tn *testNative) callOtherContractNoReturn(ic *interop.Context, args []stackitem.Item) stackitem.Item { - tn.call(ic, args, false) - return stackitem.Null{} -} - -func (tn *testNative) callOtherContractWithReturn(ic *interop.Context, args []stackitem.Item) stackitem.Item { - tn.call(ic, args, true) - bi := ic.VM.Estack().Pop().BigInt() - return stackitem.Make(bi.Add(bi, big.NewInt(1))) -} - func TestNativeContract_Invoke(t *testing.T) { - chain := newTestChain(t) + const ( + transferCPUFee = 1 << 17 + transferStorageFee = 50 + systemContractCallPrice = 1 << 15 + ) + bc, validator, committee := chain.NewMulti(t) + e := neotest.NewExecutor(t, bc, validator, committee) + gasHash := e.NativeHash(t, nativenames.Gas) - tn := newTestNative() - chain.registerNative(tn) + baseExecFee := bc.GetBaseExecFee() + price := fee.Opcode(baseExecFee, opcode.SYSCALL, // System.Contract.Call + opcode.PUSHDATA1, // contract hash (20 byte) + opcode.PUSHDATA1, // method + opcode.PUSH15, // call flags + // `transfer` args: + opcode.PUSHDATA1, // from + opcode.PUSHDATA1, // to + opcode.PUSH1, // amount + opcode.PUSHNULL, // data + // end args + opcode.PUSH4, // amount of args + opcode.PACK, // pack args + ) + price += systemContractCallPrice*baseExecFee + // System.Contract.Call price + transferCPUFee*baseExecFee + // `transfer` itself + transferStorageFee*bc.GetStoragePrice() // `transfer` storage price - err := chain.contracts.Management.PutContractState(chain.dao, &state.Contract{ - ContractBase: state.ContractBase{ - ID: 1, - NEF: tn.meta.NEF, - Hash: tn.meta.Hash, - Manifest: tn.meta.Manifest, - }, - }) - require.NoError(t, err) + tx := e.NewUnsignedTx(t, gasHash, "transfer", validator.ScriptHash(), validator.ScriptHash(), 1, nil) + e.SignTx(t, tx, -1, validator) + e.AddNewBlock(t, tx) + e.CheckHalt(t, tx.Hash(), stackitem.Make(true)) - // System.Contract.Call + "sum" itself + opcodes for pushing arguments. - price := int64(testSumCPUFee * chain.GetBaseExecFee() * 2) - price += testSumStorageFee * chain.GetStoragePrice() - price += 3 * fee.Opcode(chain.GetBaseExecFee(), opcode.PUSHINT8) - price += 2 * fee.Opcode(chain.GetBaseExecFee(), opcode.SYSCALL, opcode.PUSHDATA1, opcode.PUSHINT8) - price += fee.Opcode(chain.GetBaseExecFee(), opcode.PACK) - res, err := invokeContractMethod(chain, price, tn.Metadata().Hash, "sum", int64(14), int64(28)) - require.NoError(t, err) - checkResult(t, res, stackitem.Make(42)) - _, err = chain.persist(false) - require.NoError(t, err) - - select { - case index := <-tn.blocks: - require.Equal(t, chain.blockHeight, index) - default: - require.Fail(t, "onPersist wasn't called") - } - - // Enough for Call and other opcodes, but not enough for "sum" call. - res, err = invokeContractMethod(chain, price-1, tn.Metadata().Hash, "sum", int64(14), int64(28)) - require.NoError(t, err) - checkFAULTState(t, res) + // Enough for Call and other opcodes, but not enough for "transfer" call. + tx = e.NewUnsignedTx(t, gasHash, "transfer", validator.ScriptHash(), validator.ScriptHash(), 1, nil) + e.SignTx(t, tx, price-1, validator) + e.AddNewBlock(t, tx) + e.CheckFault(t, tx.Hash(), "gas limit exceeded") } func TestNativeContract_InvokeInternal(t *testing.T) { - chain := newTestChain(t) - - tn := newTestNative() - chain.registerNative(tn) - - err := chain.contracts.Management.PutContractState(chain.dao, &state.Contract{ - ContractBase: state.ContractBase{ - ID: 1, - NEF: tn.meta.NEF, - Manifest: tn.meta.Manifest, - }, - }) - require.NoError(t, err) - - d := dao.NewSimple(storage.NewMemoryStore(), chain.config.StateRootInHeader, chain.config.P2PSigExtensions) - ic := chain.newInteropContext(trigger.Application, d, nil, nil) - - sumOffset := 0 - for _, md := range tn.Metadata().Methods { - if md.MD.Name == "sum" { - sumOffset = md.MD.Offset - break - } - } + bc, validator, committee := chain.NewMulti(t) + e := neotest.NewExecutor(t, bc, validator, committee) + clState := bc.GetContractState(e.NativeHash(t, nativenames.CryptoLib)) + require.NotNil(t, clState) + md := clState.Manifest.ABI.GetMethod("ripemd160", 1) + require.NotNil(t, md) t.Run("fail, bad current script hash", func(t *testing.T) { + ic := bc.GetTestVM(trigger.Application, nil, nil) v := ic.SpawnVM() - v.LoadScriptWithHash(tn.Metadata().NEF.Script, util.Uint160{1, 2, 3}, callflag.All) - v.Estack().PushVal(14) - v.Estack().PushVal(28) - v.Context().Jump(sumOffset) + fakeH := util.Uint160{1, 2, 3} + v.LoadScriptWithHash(clState.NEF.Script, fakeH, callflag.All) + input := []byte{1, 2, 3, 4} + v.Estack().PushVal(input) + v.Context().Jump(md.Offset) - // it's prohibited to call natives directly - require.Error(t, v.Run()) + // Bad current script hash + err := v.Run() + require.Error(t, err) + require.True(t, strings.Contains(err.Error(), fmt.Sprintf("native contract %s (version 0) not found", fakeH.StringLE())), err.Error()) }) t.Run("fail, bad NativeUpdateHistory height", func(t *testing.T) { - tn.Metadata().UpdateHistory = []uint32{chain.blockHeight + 1} + bcBad, validatorBad, committeeBad := chain.NewMultiWithCustomConfig(t, func(c *config.ProtocolConfiguration) { + c.NativeUpdateHistories = map[string][]uint32{ + nativenames.Policy: {0}, + nativenames.Neo: {0}, + nativenames.Gas: {0}, + nativenames.Designation: {0}, + nativenames.StdLib: {0}, + nativenames.Management: {0}, + nativenames.Oracle: {0}, + nativenames.Ledger: {0}, + nativenames.CryptoLib: {1}, + } + }) + eBad := neotest.NewExecutor(t, bcBad, validatorBad, committeeBad) + + ic := bcBad.GetTestVM(trigger.Application, nil, nil) v := ic.SpawnVM() - v.LoadScriptWithHash(tn.Metadata().NEF.Script, tn.Metadata().Hash, callflag.All) - v.Estack().PushVal(14) - v.Estack().PushVal(28) - v.Context().Jump(sumOffset) + v.LoadScriptWithHash(clState.NEF.Script, clState.Hash, callflag.All) // hash is not affected by native update history + input := []byte{1, 2, 3, 4} + v.Estack().PushVal(input) + v.Context().Jump(md.Offset) - // it's prohibited to call natives before NativeUpdateHistory[0] height - require.Error(t, v.Run()) + // It's prohibited to call natives before NativeUpdateHistory[0] height. + err := v.Run() + require.Error(t, err) + require.True(t, strings.Contains(err.Error(), "native contract CryptoLib is active after height = 1")) - // set the value back to 0 - tn.Metadata().UpdateHistory = []uint32{0} + // Add new block => CryptoLib should be active now. + eBad.AddNewBlock(t) + ic = bcBad.GetTestVM(trigger.Application, nil, nil) + v = ic.SpawnVM() + v.LoadScriptWithHash(clState.NEF.Script, clState.Hash, callflag.All) // hash is not affected by native update history + v.Estack().PushVal(input) + v.Context().Jump(md.Offset) + + require.NoError(t, v.Run()) + value := v.Estack().Pop().Bytes() + require.Equal(t, hash.RipeMD160(input).BytesBE(), value) }) t.Run("success", func(t *testing.T) { + ic := bc.GetTestVM(trigger.Application, nil, nil) v := ic.SpawnVM() - v.LoadScriptWithHash(tn.Metadata().NEF.Script, tn.Metadata().Hash, callflag.All) - v.Estack().PushVal(14) - v.Estack().PushVal(28) - v.Context().Jump(sumOffset) + v.LoadScriptWithHash(clState.NEF.Script, clState.Hash, callflag.All) + input := []byte{1, 2, 3, 4} + v.Estack().PushVal(input) + v.Context().Jump(md.Offset) + require.NoError(t, v.Run()) - value := v.Estack().Pop().BigInt() - require.Equal(t, int64(42), value.Int64()) + value := v.Estack().Pop().Bytes() + require.Equal(t, hash.RipeMD160(input).BytesBE(), value) }) } func TestNativeContract_InvokeOtherContract(t *testing.T) { - chain := newTestChain(t) + bc, validator, committee := chain.NewMulti(t) + e := neotest.NewExecutor(t, bc, validator, committee) + managementInvoker := e.ValidatorInvoker(e.NativeHash(t, nativenames.Management)) + gasInvoker := e.ValidatorInvoker(e.NativeHash(t, nativenames.Gas)) - tn := newTestNative() - chain.registerNative(tn) - - err := chain.contracts.Management.PutContractState(chain.dao, &state.Contract{ - ContractBase: state.ContractBase{ - ID: 1, - Hash: tn.meta.Hash, - NEF: tn.meta.NEF, - Manifest: tn.meta.Manifest, - }, - }) + cs, _ := contracts.GetTestContractState(t, pathToInternalContracts, 1, 2, validator.ScriptHash()) + cs.Hash = state.CreateContractHash(validator.ScriptHash(), cs.NEF.Checksum, cs.Manifest.Name) // set proper hash + manifB, err := json.Marshal(cs.Manifest) require.NoError(t, err) + nefB, err := cs.NEF.Bytes() + require.NoError(t, err) + si, err := cs.ToStackItem() + require.NoError(t, err) + managementInvoker.Invoke(t, si, "deploy", nefB, manifB) - var drainTN = func(t *testing.T) { - select { - case <-tn.blocks: - default: - require.Fail(t, "testNative didn't send us block") - } - } - - cs, _ := getTestContractState(t, 4, 5, random.Uint160()) // sender and IDs are not important for the test - require.NoError(t, chain.contracts.Management.PutContractState(chain.dao, cs)) - - baseFee := chain.GetBaseExecFee() t.Run("non-native, no return", func(t *testing.T) { - res, err := invokeContractMethod(chain, testSumCPUFee*baseFee*4+10000, tn.Metadata().Hash, "callOtherContractNoReturn", cs.Hash, "justReturn", []interface{}{}) - require.NoError(t, err) - drainTN(t) - require.Equal(t, vm.HaltState, res.VMState, res.FaultException) - checkResult(t, res, stackitem.Null{}) // simple call is done with EnsureNotEmpty - }) - t.Run("non-native, with return", func(t *testing.T) { - res, err := invokeContractMethod(chain, testSumCPUFee*baseFee*4+10000, tn.Metadata().Hash, - "callOtherContractWithReturn", cs.Hash, "ret7", []interface{}{}) - require.NoError(t, err) - drainTN(t) - checkResult(t, res, stackitem.Make(8)) + // `onNEP17Payment` will be invoked on test contract from GAS contract. + gasInvoker.Invoke(t, true, "transfer", validator.ScriptHash(), cs.Hash, 1, nil) }) } diff --git a/pkg/core/native_designate_test.go b/pkg/core/native_designate_test.go index d8a5cb44c..3dd68f05f 100644 --- a/pkg/core/native_designate_test.go +++ b/pkg/core/native_designate_test.go @@ -10,69 +10,11 @@ import ( "github.com/nspcc-dev/neo-go/pkg/core/native/noderoles" "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/smartcontract/callflag" "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/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 (bc *Blockchain) setNodesByRole(t *testing.T, ok bool, r noderoles.Role, nodes keys.PublicKeys) { - w := io.NewBufBinWriter() - for _, pub := range nodes { - emit.Bytes(w.BinWriter, pub.Bytes()) - } - emit.Int(w.BinWriter, int64(len(nodes))) - emit.Opcodes(w.BinWriter, opcode.PACK) - emit.Int(w.BinWriter, int64(r)) - emit.Int(w.BinWriter, 2) - emit.Opcodes(w.BinWriter, opcode.PACK) - emit.AppCallNoArgs(w.BinWriter, bc.contracts.Designate.Hash, "designateAsRole", callflag.All) - require.NoError(t, w.Err) - tx := transaction.New(w.Bytes(), 0) - tx.NetworkFee = 10_000_000 - tx.SystemFee = 10_000_000 - tx.ValidUntilBlock = 100 - tx.Signers = []transaction.Signer{ - { - Account: testchain.MultisigScriptHash(), - Scopes: transaction.None, - }, - { - Account: testchain.CommitteeScriptHash(), - Scopes: transaction.CalledByEntry, - }, - } - require.NoError(t, testchain.SignTx(bc, tx)) - tx.Scripts = append(tx.Scripts, transaction.Witness{ - InvocationScript: testchain.SignCommittee(tx), - VerificationScript: testchain.CommitteeVerificationScript(), - }) - require.NoError(t, bc.AddBlock(bc.newBlock(tx))) - - aer, err := bc.GetAppExecResults(tx.Hash(), trigger.Application) - require.NoError(t, err) - require.Equal(t, 1, len(aer)) - if ok { - require.Equal(t, vm.HaltState, aer[0].VMState) - require.Equal(t, 1, len(aer[0].Events)) - - ev := aer[0].Events[0] - require.Equal(t, bc.contracts.Designate.Hash, ev.ScriptHash) - require.Equal(t, native.DesignationEventName, ev.Name) - require.Equal(t, []stackitem.Item{ - stackitem.Make(int64(r)), - stackitem.Make(bc.BlockHeight()), - }, ev.Item.Value().([]stackitem.Item)) - } else { - require.Equal(t, vm.FaultState, aer[0].VMState) - require.Equal(t, 0, len(aer[0].Events)) - } -} - func TestDesignate_DesignateAsRole(t *testing.T) { bc := newTestChain(t) diff --git a/pkg/core/native_management_test.go b/pkg/core/native_management_test.go index d03009af0..e38a8cf96 100644 --- a/pkg/core/native_management_test.go +++ b/pkg/core/native_management_test.go @@ -1,39 +1,33 @@ -package core +package core_test import ( "testing" - "github.com/nspcc-dev/neo-go/pkg/core/storage" + "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" + "github.com/nspcc-dev/neo-go/pkg/neotest/chain" "github.com/nspcc-dev/neo-go/pkg/util" "github.com/stretchr/testify/require" ) -type memoryStore struct { - *storage.MemoryStore -} - -func (memoryStore) Close() error { return nil } - -func TestMinimumDeploymentFee(t *testing.T) { - chain := newTestChain(t) - - t.Run("get, internal method", func(t *testing.T) { - n := chain.contracts.Management.GetMinimumDeploymentFee(chain.dao) - require.Equal(t, 10_00000000, int(n)) - }) -} - func TestManagement_GetNEP17Contracts(t *testing.T) { t.Run("empty chain", func(t *testing.T) { - chain := newTestChain(t) - require.ElementsMatch(t, []util.Uint160{chain.contracts.NEO.Hash, chain.contracts.GAS.Hash}, chain.contracts.Management.GetNEP17Contracts()) + bc, validators, committee := chain.NewMulti(t) + e := neotest.NewExecutor(t, bc, validators, committee) + + require.ElementsMatch(t, []util.Uint160{e.NativeHash(t, nativenames.Neo), + e.NativeHash(t, nativenames.Gas)}, bc.GetNEP17Contracts()) }) - t.Run("test chain", func(t *testing.T) { - chain := newTestChain(t) - initBasicChain(t, chain) - rublesHash, err := chain.GetContractScriptHash(1) - require.NoError(t, err) - require.ElementsMatch(t, []util.Uint160{chain.contracts.NEO.Hash, chain.contracts.GAS.Hash, rublesHash}, chain.contracts.Management.GetNEP17Contracts()) + 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 + }) + e := neotest.NewExecutor(t, bc, validators, committee) + initBasicChain(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_neo_test.go b/pkg/core/native_neo_test.go index b54a6f3f5..9c0c7b43c 100644 --- a/pkg/core/native_neo_test.go +++ b/pkg/core/native_neo_test.go @@ -1,20 +1,17 @@ -package core +package core_test import ( "fmt" "math/big" + "path/filepath" "testing" - "github.com/nspcc-dev/neo-go/internal/testchain" + "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/io" - "github.com/nspcc-dev/neo-go/pkg/smartcontract/callflag" - "github.com/nspcc-dev/neo-go/pkg/smartcontract/trigger" - "github.com/nspcc-dev/neo-go/pkg/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/neotest" + "github.com/nspcc-dev/neo-go/pkg/neotest/chain" "github.com/nspcc-dev/neo-go/pkg/wallet" "github.com/stretchr/testify/require" ) @@ -40,96 +37,88 @@ func BenchmarkNEO_GetGASPerVote(t *testing.B) { } } -func benchmarkGasPerVote(t *testing.B, ps storage.Store, nRewardRecords int, rewardDistance int) { - bc := newTestChainWithCustomCfgAndStore(t, ps, nil) - - neo := bc.contracts.NEO - tx := transaction.New([]byte{byte(opcode.PUSH1)}, 0) - ic := bc.newInteropContext(trigger.Application, bc.dao, nil, tx) - ic.SpawnVM() - ic.Block = bc.newBlock(tx) - - advanceChain := func(t *testing.B, count int) { - for i := 0; i < count; i++ { - require.NoError(t, bc.AddBlock(bc.newBlock())) - ic.Block.Index++ - } +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 := testchain.CommitteeSize() - accs := make([]*wallet.Account, sz) + sz := len(cfg.StandbyCommittee) + voters := make([]*wallet.Account, sz) candidates := make(keys.PublicKeys, sz) - txs := make([]*transaction.Transaction, 0, len(accs)) + 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() - accs[i], err = wallet.NewAccount() + voters[i], err = wallet.NewAccount() require.NoError(t, err) - require.NoError(t, neo.RegisterCandidateInternal(ic, candidates[i])) + registerTx := neoSuperInvoker.PrepareInvoke(t, "registerCandidate", candidates[i].Bytes()) + txs = append(txs, registerTx) - to := accs[i].Contract.ScriptHash() - w := io.NewBufBinWriter() - emit.AppCall(w.BinWriter, bc.contracts.NEO.Hash, "transfer", callflag.All, - neoOwner.BytesBE(), to.BytesBE(), - big.NewInt(int64(sz-i)*1000000).Int64(), nil) - emit.Opcodes(w.BinWriter, opcode.ASSERT) - emit.AppCall(w.BinWriter, bc.contracts.GAS.Hash, "transfer", callflag.All, - neoOwner.BytesBE(), to.BytesBE(), - int64(1_000_000_000), nil) - emit.Opcodes(w.BinWriter, opcode.ASSERT) - require.NoError(t, w.Err) - tx := transaction.New(w.Bytes(), 1000_000_000) - tx.ValidUntilBlock = bc.BlockHeight() + 1 - setSigner(tx, testchain.MultisigScriptHash()) - require.NoError(t, testchain.SignTx(bc, tx)) - txs = append(txs, tx) + 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) } - require.NoError(t, bc.AddBlock(bc.newBlock(txs...))) + e.AddNewBlock(t, txs...) for _, tx := range txs { - checkTxHalt(t, bc, tx.Hash()) + e.CheckHalt(t, tx.Hash()) } + voteTxs := make([]*transaction.Transaction, 0, sz) for i := 0; i < sz; i++ { - priv := accs[i].PrivateKey() + priv := voters[i].PrivateKey() h := priv.GetScriptHash() - setSigner(tx, h) - ic.VM.Load(priv.PublicKey().GetVerificationScript()) - require.NoError(t, neo.VoteInternal(ic, h, candidates[i])) + 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()) } - _, err := ic.DAO.Persist() - require.NoError(t, err) // Collect set of nRewardRecords reward records for each voter. - advanceChain(t, nRewardRecords*testchain.CommitteeSize()) + e.GenerateNewBlocks(t, len(cfg.StandbyCommittee)) // Transfer some more NEO to first voter to update his balance height. - to := accs[0].Contract.ScriptHash() - w := io.NewBufBinWriter() - emit.AppCall(w.BinWriter, bc.contracts.NEO.Hash, "transfer", callflag.All, - neoOwner.BytesBE(), to.BytesBE(), int64(1), nil) - emit.Opcodes(w.BinWriter, opcode.ASSERT) - require.NoError(t, w.Err) - tx = transaction.New(w.Bytes(), 1000_000_000) - tx.ValidUntilBlock = bc.BlockHeight() + 1 - setSigner(tx, testchain.MultisigScriptHash()) - require.NoError(t, testchain.SignTx(bc, tx)) - require.NoError(t, bc.AddBlock(bc.newBlock(tx))) - - aer, err := bc.GetAppExecResults(tx.Hash(), trigger.Application) - require.NoError(t, err) - require.Equal(t, 1, len(aer)) - require.Equal(t, vm.HaltState, aer[0].VMState, aer[0].FaultException) + 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. - advanceChain(t, rewardDistance) + e.GenerateNewBlocks(t, rewardDistance) end := bc.BlockHeight() t.ResetTimer() t.ReportAllocs() t.StartTimer() for i := 0; i < t.N; i++ { - _, err := neo.CalculateBonus(ic.DAO, to, end) + _, err := bc.CalculateClaimable(to, end) require.NoError(t, err) } t.StopTimer() diff --git a/pkg/core/native_policy_test.go b/pkg/core/native_policy_test.go index 8e06d6571..211f7476c 100644 --- a/pkg/core/native_policy_test.go +++ b/pkg/core/native_policy_test.go @@ -1,54 +1,67 @@ -package core +package core_test import ( + "fmt" "testing" - "github.com/nspcc-dev/neo-go/internal/random" - "github.com/nspcc-dev/neo-go/internal/testchain" "github.com/nspcc-dev/neo-go/pkg/core/interop" "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/neotest" + "github.com/nspcc-dev/neo-go/pkg/neotest/chain" "github.com/stretchr/testify/require" ) -func transferFundsToCommittee(t *testing.T, chain *Blockchain) { - transferTokenFromMultisigAccount(t, chain, testchain.CommitteeScriptHash(), - chain.contracts.GAS.Hash, 1000_00000000) -} - -func TestFeePerByte(t *testing.T) { - chain := newTestChain(t) +func TestPolicy_FeePerByte(t *testing.T) { + bc, _, _ := chain.NewMulti(t) t.Run("get, internal method", func(t *testing.T) { - n := chain.contracts.Policy.GetFeePerByteInternal(chain.dao) + n := bc.FeePerByte() require.Equal(t, 1000, int(n)) }) } -func TestExecFeeFactor(t *testing.T) { - chain := newTestChain(t) +func TestPolicy_ExecFeeFactor(t *testing.T) { + bc, _, _ := chain.NewMulti(t) t.Run("get, internal method", func(t *testing.T) { - n := chain.contracts.Policy.GetExecFeeFactorInternal(chain.dao) + n := bc.GetBaseExecFee() require.EqualValues(t, interop.DefaultBaseExecFee, n) }) } -func TestStoragePrice(t *testing.T) { - chain := newTestChain(t) +func TestPolicy_StoragePrice(t *testing.T) { + bc, validators, committee := chain.NewMulti(t) + e := neotest.NewExecutor(t, bc, validators, committee) t.Run("get, internal method", func(t *testing.T) { - n := chain.contracts.Policy.GetStoragePriceInternal(chain.dao) + e.AddNewBlock(t) // avoid default value got from Blockchain. + + n := bc.GetStoragePrice() require.Equal(t, int64(native.DefaultStoragePrice), n) }) } -func TestBlockedAccounts(t *testing.T) { - chain := newTestChain(t) - transferTokenFromMultisigAccount(t, chain, testchain.CommitteeScriptHash(), - chain.contracts.GAS.Hash, 100_00000000) +func TestPolicy_BlockedAccounts(t *testing.T) { + bc, validators, committee := chain.NewMulti(t) + e := neotest.NewExecutor(t, bc, validators, committee) + policyHash := e.NativeHash(t, nativenames.Policy) + policySuperInvoker := e.NewInvoker(policyHash, validators, committee) + unlucky := e.NewAccount(t, 5_0000_0000) + policyUnluckyInvoker := e.NewInvoker(policyHash, unlucky) + + // Block unlucky account. + policySuperInvoker.Invoke(t, true, "blockAccount", unlucky.ScriptHash()) + + // Transaction from blocked account shouldn't be accepted. t.Run("isBlocked, internal method", func(t *testing.T) { - isBlocked := chain.contracts.Policy.IsBlockedInternal(chain.dao, random.Uint160()) - require.Equal(t, false, isBlocked) + tx := policyUnluckyInvoker.PrepareInvoke(t, "getStoragePrice") + b := e.NewUnsignedBlock(t, tx) + e.SignBlock(b) + expectedErr := fmt.Sprintf("transaction %s failed to verify: not allowed by policy: account %s is blocked", tx.Hash().StringLE(), unlucky.ScriptHash().StringLE()) + err := e.Chain.AddBlock(b) + require.Error(t, err) + require.Equal(t, expectedErr, err.Error()) }) } diff --git a/pkg/core/notary_test.go b/pkg/core/notary_test.go index 236bae50b..46f2e38db 100644 --- a/pkg/core/notary_test.go +++ b/pkg/core/notary_test.go @@ -1,8 +1,9 @@ -package core +package core_test import ( "errors" "fmt" + "math/big" "math/rand" "path" "path/filepath" @@ -10,29 +11,32 @@ import ( "testing" "time" - "github.com/nspcc-dev/neo-go/internal/testchain" "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/mempool" - "github.com/nspcc-dev/neo-go/pkg/core/native/noderoles" + "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/interop/native/roles" "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/network" "github.com/nspcc-dev/neo-go/pkg/network/payload" "github.com/nspcc-dev/neo-go/pkg/services/notary" "github.com/nspcc-dev/neo-go/pkg/smartcontract" - "github.com/nspcc-dev/neo-go/pkg/smartcontract/trigger" "github.com/nspcc-dev/neo-go/pkg/util" "github.com/nspcc-dev/neo-go/pkg/vm/opcode" + "github.com/nspcc-dev/neo-go/pkg/vm/stackitem" "github.com/nspcc-dev/neo-go/pkg/wallet" "github.com/stretchr/testify/require" "go.uber.org/zap/zaptest" ) -var notaryModulePath = filepath.Join("..", "services", "notary") - -func getTestNotary(t *testing.T, bc *Blockchain, walletPath, pass string, onTx func(tx *transaction.Transaction) error) (*wallet.Account, *notary.Notary, *mempool.Pool) { +func getTestNotary(t *testing.T, bc *core.Blockchain, walletPath, pass string, onTx func(tx *transaction.Transaction) error) (*wallet.Account, *notary.Notary, *mempool.Pool) { mainCfg := config.P2PNotary{ Enabled: true, UnlockWallet: config.Wallet{ @@ -46,7 +50,7 @@ func getTestNotary(t *testing.T, bc *Blockchain, walletPath, pass string, onTx f Log: zaptest.NewLogger(t), } mp := mempool.New(10, 1, true) - ntr, err := notary.NewNotary(cfg, testchain.Network(), mp, onTx) + ntr, err := notary.NewNotary(cfg, netmode.UnitTestNet, mp, onTx) require.NoError(t, err) w, err := wallet.NewWalletFromFile(path.Join(notaryModulePath, walletPath)) @@ -68,7 +72,14 @@ func dupNotaryRequest(t *testing.T, p *payload.P2PNotaryRequest) *payload.P2PNot } func TestNotary(t *testing.T) { - bc := newTestChain(t) + bc, validators, committee := chain.NewMultiWithCustomConfig(t, func(c *config.ProtocolConfiguration) { + c.P2PSigExtensions = true + }) + e := neotest.NewExecutor(t, bc, validators, committee) + notaryHash := e.NativeHash(t, nativenames.Notary) + designationSuperInvoker := e.NewInvoker(e.NativeHash(t, nativenames.Designation), validators, committee) + gasValidatorInvoker := e.ValidatorInvoker(e.NativeHash(t, nativenames.Gas)) + var ( nonce uint32 nvbDiffFallback uint32 = 20 @@ -145,8 +156,9 @@ func TestNotary(t *testing.T) { mp1.StopSubscriptions() }) - notaryNodes := keys.PublicKeys{acc1.PrivateKey().PublicKey(), acc2.PrivateKey().PublicKey()} - bc.setNodesByRole(t, true, noderoles.P2PNotary, notaryNodes) + notaryNodes := []interface{}{acc1.PrivateKey().PublicKey().Bytes(), acc2.PrivateKey().PublicKey().Bytes()} + designationSuperInvoker.Invoke(t, stackitem.Null{}, "designateAsRole", + int64(roles.P2PNotary), notaryNodes) type requester struct { accounts []*wallet.Account @@ -193,7 +205,7 @@ func TestNotary(t *testing.T) { VerificationScript: []byte{}, }, } - err = requester.SignTx(testchain.Network(), fallback) + err = requester.SignTx(netmode.UnitTestNet, fallback) require.NoError(t, err) return fallback } @@ -251,7 +263,7 @@ func TestNotary(t *testing.T) { for j := range main.Scripts { main.Scripts[j].VerificationScript = verificationScripts[j] if i == j { - main.Scripts[j].InvocationScript = append([]byte{byte(opcode.PUSHDATA1), 64}, acc.PrivateKey().SignHashable(uint32(testchain.Network()), main)...) + main.Scripts[j].InvocationScript = append([]byte{byte(opcode.PUSHDATA1), 64}, acc.PrivateKey().SignHashable(uint32(netmode.UnitTestNet), main)...) } } main.Scripts = append(main.Scripts, transaction.Witness{}) // empty Notary witness @@ -296,12 +308,11 @@ func TestNotary(t *testing.T) { require.Equal(t, io.GetVarSize(completedTx), completedTx.Size()) for i := 0; i < len(completedTx.Scripts)-1; i++ { - interopCtx := bc.newInteropContext(trigger.Verification, bc.dao, nil, completedTx) - _, err := bc.verifyHashAgainstScript(completedTx.Signers[i].Account, &completedTx.Scripts[i], interopCtx, -1) + _, err := bc.VerifyWitness(completedTx.Signers[i].Account, completedTx, &completedTx.Scripts[i], -1) require.NoError(t, err) } require.Equal(t, transaction.Witness{ - InvocationScript: append([]byte{byte(opcode.PUSHDATA1), 64}, acc1.PrivateKey().SignHashable(uint32(testchain.Network()), requests[0].MainTransaction)...), + InvocationScript: append([]byte{byte(opcode.PUSHDATA1), 64}, acc1.PrivateKey().SignHashable(uint32(netmode.UnitTestNet), requests[0].MainTransaction)...), VerificationScript: []byte{}, }, completedTx.Scripts[len(completedTx.Scripts)-1]) } else { @@ -316,15 +327,14 @@ func TestNotary(t *testing.T) { require.Equal(t, 2, len(completedTx.Signers)) require.Equal(t, 2, len(completedTx.Scripts)) require.Equal(t, transaction.Witness{ - InvocationScript: append([]byte{byte(opcode.PUSHDATA1), 64}, acc1.PrivateKey().SignHashable(uint32(testchain.Network()), req.FallbackTransaction)...), + InvocationScript: append([]byte{byte(opcode.PUSHDATA1), 64}, acc1.PrivateKey().SignHashable(uint32(netmode.UnitTestNet), req.FallbackTransaction)...), VerificationScript: []byte{}, }, completedTx.Scripts[0]) // check that tx size was updated require.Equal(t, io.GetVarSize(completedTx), completedTx.Size()) - interopCtx := bc.newInteropContext(trigger.Verification, bc.dao, nil, completedTx) - _, err := bc.verifyHashAgainstScript(completedTx.Signers[1].Account, &completedTx.Scripts[1], interopCtx, -1) + _, err := bc.VerifyWitness(completedTx.Signers[1].Account, completedTx, &completedTx.Scripts[1], -1) require.NoError(t, err) } else { completedTx := getCompletedTx(t, false, req.FallbackTransaction.Hash()) @@ -483,7 +493,8 @@ func TestNotary(t *testing.T) { checkFallbackTxs(t, r, false) ntr1.UpdateNotaryNodes(keys.PublicKeys{randomAcc.PublicKey()}) setFinalizeWithError(false) - require.NoError(t, bc.AddBlock(bc.newBlock())) + + e.AddNewBlock(t) checkMainTx(t, requesters, r, 1, false) checkFallbackTxs(t, r, false) // set account back for the next tests @@ -494,11 +505,11 @@ func TestNotary(t *testing.T) { requests, requesters := checkCompleteStandardRequest(t, 3, false) // check PostPersist with finalisation error setFinalizeWithError(true) - require.NoError(t, bc.AddBlock(bc.newBlock())) + e.AddNewBlock(t) checkMainTx(t, requesters, requests, len(requests), false) // check PostPersist without finalisation error setFinalizeWithError(false) - require.NoError(t, bc.AddBlock(bc.newBlock())) + e.AddNewBlock(t) checkMainTx(t, requesters, requests, len(requests), true) // PostPersist: complete main transaction, multisignature account @@ -507,12 +518,12 @@ func TestNotary(t *testing.T) { checkFallbackTxs(t, requests, false) // check PostPersist with finalisation error setFinalizeWithError(true) - require.NoError(t, bc.AddBlock(bc.newBlock())) + e.AddNewBlock(t) checkMainTx(t, requesters, requests, len(requests), false) checkFallbackTxs(t, requests, false) // check PostPersist without finalisation error setFinalizeWithError(false) - require.NoError(t, bc.AddBlock(bc.newBlock())) + e.AddNewBlock(t) checkMainTx(t, requesters, requests, len(requests), true) checkFallbackTxs(t, requests, false) @@ -521,15 +532,15 @@ func TestNotary(t *testing.T) { requests, requesters = checkCompleteStandardRequest(t, 3, false) checkFallbackTxs(t, requests, false) // make fallbacks valid - _, err = bc.genBlocks(int(nvbDiffFallback)) + e.GenerateNewBlocks(t, int(nvbDiffFallback)) require.NoError(t, err) // check PostPersist for valid fallbacks with finalisation error - require.NoError(t, bc.AddBlock(bc.newBlock())) + e.AddNewBlock(t) checkMainTx(t, requesters, requests, len(requests), false) checkFallbackTxs(t, requests, false) // check PostPersist for valid fallbacks without finalisation error setFinalizeWithError(false) - require.NoError(t, bc.AddBlock(bc.newBlock())) + e.AddNewBlock(t) checkMainTx(t, requesters, requests, len(requests), false) checkFallbackTxs(t, requests, true) @@ -540,15 +551,15 @@ func TestNotary(t *testing.T) { requests, requesters = checkCompleteMultisigRequest(t, nSigs, nKeys, false) checkFallbackTxs(t, requests, false) // make fallbacks valid - _, err = bc.genBlocks(int(nvbDiffFallback)) + e.GenerateNewBlocks(t, int(nvbDiffFallback)) require.NoError(t, err) // check PostPersist for valid fallbacks with finalisation error - require.NoError(t, bc.AddBlock(bc.newBlock())) + e.AddNewBlock(t) checkMainTx(t, requesters, requests, len(requests), false) checkFallbackTxs(t, requests, false) // check PostPersist for valid fallbacks without finalisation error setFinalizeWithError(false) - require.NoError(t, bc.AddBlock(bc.newBlock())) + e.AddNewBlock(t) checkMainTx(t, requesters, requests, len(requests), false) checkFallbackTxs(t, requests[:nSigs], true) // the rest of fallbacks should also be applied even if the main tx was already constructed by the moment they were sent @@ -559,14 +570,14 @@ func TestNotary(t *testing.T) { requests, requesters = checkCompleteStandardRequest(t, 5, false) checkFallbackTxs(t, requests, false) // make fallbacks valid - _, err = bc.genBlocks(int(nvbDiffFallback)) + e.GenerateNewBlocks(t, int(nvbDiffFallback)) require.NoError(t, err) // some of fallbacks should fail finalisation unluckies = []*payload.P2PNotaryRequest{requests[0], requests[4]} lucky := requests[1:4] setChoosy(true) // check PostPersist for lucky fallbacks - require.NoError(t, bc.AddBlock(bc.newBlock())) + e.AddNewBlock(t) checkMainTx(t, requesters, requests, len(requests), false) checkFallbackTxs(t, lucky, true) checkFallbackTxs(t, unluckies, false) @@ -574,7 +585,7 @@ func TestNotary(t *testing.T) { setChoosy(false) setFinalizeWithError(false) // check PostPersist for unlucky fallbacks - require.NoError(t, bc.AddBlock(bc.newBlock())) + e.AddNewBlock(t) checkMainTx(t, requesters, requests, len(requests), false) checkFallbackTxs(t, lucky, true) checkFallbackTxs(t, unluckies, true) @@ -585,19 +596,19 @@ func TestNotary(t *testing.T) { requests, requesters = checkCompleteStandardRequest(t, 5, false, 1, 2, 3, 4, 5) checkFallbackTxs(t, requests, false) // generate blocks to reach the most earlier fallback's NVB - _, err = bc.genBlocks(int(nvbDiffFallback)) + e.GenerateNewBlocks(t, int(nvbDiffFallback)) require.NoError(t, err) // check PostPersist for valid fallbacks without finalisation error // Add block before allowing tx to finalize to exclude race condition when // main transaction is finalized between `finalizeWithError` restore and adding new block. - require.NoError(t, bc.AddBlock(bc.newBlock())) + e.AddNewBlock(t) mtx.RLock() start := len(completedTxes) mtx.RUnlock() setFinalizeWithError(false) for i := range requests { if i != 0 { - require.NoError(t, bc.AddBlock(bc.newBlock())) + e.AddNewBlock(t) } require.Eventually(t, func() bool { mtx.RLock() @@ -615,13 +626,13 @@ func TestNotary(t *testing.T) { requests, requesters = checkCompleteStandardRequest(t, 4, false) checkFallbackTxs(t, requests, false) // make fallbacks valid and remove one fallback - _, err = bc.genBlocks(int(nvbDiffFallback)) + e.GenerateNewBlocks(t, int(nvbDiffFallback)) require.NoError(t, err) ntr1.UpdateNotaryNodes(keys.PublicKeys{randomAcc.PublicKey()}) ntr1.OnRequestRemoval(requests[3]) // non of the fallbacks should be completed setFinalizeWithError(false) - require.NoError(t, bc.AddBlock(bc.newBlock())) + e.AddNewBlock(t) checkMainTx(t, requesters, requests, len(requests), false) checkFallbackTxs(t, requests, false) // set account back for the next tests @@ -633,13 +644,13 @@ func TestNotary(t *testing.T) { requests, requesters = checkCompleteStandardRequest(t, 4, false) checkFallbackTxs(t, requests, false) // make fallbacks valid and remove one fallback - _, err = bc.genBlocks(int(nvbDiffFallback)) + e.GenerateNewBlocks(t, int(nvbDiffFallback)) require.NoError(t, err) unlucky := requests[3] ntr1.OnRequestRemoval(unlucky) // rest of the fallbacks should be completed setFinalizeWithError(false) - require.NoError(t, bc.AddBlock(bc.newBlock())) + e.AddNewBlock(t) checkMainTx(t, requesters, requests, len(requests), false) checkFallbackTxs(t, requests[:3], true) require.Nil(t, completedTxes[unlucky.FallbackTransaction.Hash()]) @@ -648,20 +659,20 @@ func TestNotary(t *testing.T) { setFinalizeWithError(true) requests, requesters = checkCompleteStandardRequest(t, 4, false) // remove all fallbacks - _, err = bc.genBlocks(int(nvbDiffFallback)) + e.GenerateNewBlocks(t, int(nvbDiffFallback)) require.NoError(t, err) for i := range requests { ntr1.OnRequestRemoval(requests[i]) } // then the whole request should be removed, i.e. there are no completed transactions setFinalizeWithError(false) - require.NoError(t, bc.AddBlock(bc.newBlock())) + e.AddNewBlock(t) checkMainTx(t, requesters, requests, len(requests), false) checkFallbackTxs(t, requests, false) // OnRequestRemoval: signature request, remove unexisting fallback ntr1.OnRequestRemoval(requests[0]) - require.NoError(t, bc.AddBlock(bc.newBlock())) + e.AddNewBlock(t) checkMainTx(t, requesters, requests, len(requests), false) checkFallbackTxs(t, requests, false) @@ -673,13 +684,13 @@ func TestNotary(t *testing.T) { checkMainTx(t, requesters, requests, len(requests), false) checkFallbackTxs(t, requests, false) // make fallbacks valid and remove the last fallback - _, err = bc.genBlocks(int(nvbDiffFallback)) + e.GenerateNewBlocks(t, int(nvbDiffFallback)) require.NoError(t, err) unlucky = requests[nSigs-1] ntr1.OnRequestRemoval(unlucky) // then (m-1) out of n fallbacks should be completed setFinalizeWithError(false) - require.NoError(t, bc.AddBlock(bc.newBlock())) + e.AddNewBlock(t) checkMainTx(t, requesters, requests, len(requests), false) checkFallbackTxs(t, requests[:nSigs-1], true) require.Nil(t, completedTxes[unlucky.FallbackTransaction.Hash()]) @@ -690,20 +701,20 @@ func TestNotary(t *testing.T) { setFinalizeWithError(true) requests, requesters = checkCompleteMultisigRequest(t, nSigs, nKeys, false) // make fallbacks valid and then remove all of them - _, err = bc.genBlocks(int(nvbDiffFallback)) + e.GenerateNewBlocks(t, int(nvbDiffFallback)) require.NoError(t, err) for i := range requests { ntr1.OnRequestRemoval(requests[i]) } // then the whole request should be removed, i.e. there are no completed transactions setFinalizeWithError(false) - require.NoError(t, bc.AddBlock(bc.newBlock())) + e.AddNewBlock(t) checkMainTx(t, requesters, requests, len(requests), false) checkFallbackTxs(t, requests, false) // // OnRequestRemoval: multisignature request, remove unexisting fallbac, i.e. there still shouldn't be any completed transactions after this ntr1.OnRequestRemoval(requests[0]) - require.NoError(t, bc.AddBlock(bc.newBlock())) + e.AddNewBlock(t) checkMainTx(t, requesters, requests, len(requests), false) checkFallbackTxs(t, requests, false) @@ -712,11 +723,11 @@ func TestNotary(t *testing.T) { requester1, _ := wallet.NewAccount() requester2, _ := wallet.NewAccount() amount := int64(100_0000_0000) - feer := NewNotaryFeerStub(bc) - transferTokenFromMultisigAccountCheckOK(t, bc, bc.GetNotaryContractScriptHash(), bc.contracts.GAS.Hash, amount, requester1.PrivateKey().PublicKey().GetScriptHash(), int64(bc.BlockHeight()+50)) - checkBalanceOf(t, bc, bc.contracts.Notary.Hash, int(amount)) - transferTokenFromMultisigAccountCheckOK(t, bc, bc.GetNotaryContractScriptHash(), bc.contracts.GAS.Hash, amount, requester2.PrivateKey().PublicKey().GetScriptHash(), int64(bc.BlockHeight()+50)) - checkBalanceOf(t, bc, bc.contracts.Notary.Hash, int(2*amount)) + gasValidatorInvoker.Invoke(t, true, "transfer", e.Validator.ScriptHash(), bc.GetNotaryContractScriptHash(), amount, []interface{}{requester1.PrivateKey().PublicKey().GetScriptHash(), int64(bc.BlockHeight() + 50)}) + e.CheckGASBalance(t, notaryHash, big.NewInt(amount)) + gasValidatorInvoker.Invoke(t, true, "transfer", e.Validator.ScriptHash(), bc.GetNotaryContractScriptHash(), amount, []interface{}{requester2.PrivateKey().PublicKey().GetScriptHash(), int64(bc.BlockHeight() + 50)}) + e.CheckGASBalance(t, notaryHash, big.NewInt(2*amount)) + // create request for 2 standard signatures => main tx should be completed after the second request is added to the pool requests = createMixedRequest([]requester{ { @@ -728,6 +739,7 @@ func TestNotary(t *testing.T) { typ: notary.Signature, }, }) + feer := network.NewNotaryFeer(bc) require.NoError(t, mp1.Add(requests[0].FallbackTransaction, feer, requests[0])) require.NoError(t, mp1.Add(requests[1].FallbackTransaction, feer, requests[1])) require.Eventually(t, func() bool { diff --git a/pkg/core/oracle_test.go b/pkg/core/oracle_test.go index b33bf78dd..5529c39ca 100644 --- a/pkg/core/oracle_test.go +++ b/pkg/core/oracle_test.go @@ -1,13 +1,13 @@ -package core +package core_test import ( "bytes" + "encoding/binary" "encoding/json" "errors" "fmt" gio "io" "net/http" - "os" "path" "path/filepath" "strings" @@ -15,168 +15,41 @@ import ( "testing" "time" + "github.com/nspcc-dev/neo-go/internal/contracts" "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/interop/interopnames" - "github.com/nspcc-dev/neo-go/pkg/core/native/noderoles" + "github.com/nspcc-dev/neo-go/pkg/core" + "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/keys" - "github.com/nspcc-dev/neo-go/pkg/io" + "github.com/nspcc-dev/neo-go/pkg/interop/native/roles" + "github.com/nspcc-dev/neo-go/pkg/neotest" + "github.com/nspcc-dev/neo-go/pkg/neotest/chain" "github.com/nspcc-dev/neo-go/pkg/services/oracle" - "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/nef" "github.com/nspcc-dev/neo-go/pkg/util" - "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/util/slice" + "github.com/nspcc-dev/neo-go/pkg/vm/stackitem" "github.com/nspcc-dev/neo-go/pkg/wallet" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.uber.org/zap/zaptest" ) -var ( - oracleModulePath = filepath.Join("..", "services", "oracle") - oracleContractNEFPath = filepath.Join("test_data", "oracle_contract", "oracle.nef") - oracleContractManifestPath = filepath.Join("test_data", "oracle_contract", "oracle.manifest.json") -) +var oracleModulePath = filepath.Join("..", "services", "oracle") -// TestGenerateOracleContract generates helper contract that is able to call -// native Oracle contract and has callback method. It uses test chain to define -// Oracle and StdLib native hashes and saves generated NEF and manifest to ... folder. -// Set `saveState` flag to true and run the test to rewrite NEF and manifest files. -func TestGenerateOracleContract(t *testing.T) { - const saveState = false - - bc := newTestChain(t) - oracleHash := bc.contracts.Oracle.Hash - stdHash := bc.contracts.Std.Hash - - w := io.NewBufBinWriter() - emit.Int(w.BinWriter, 5) - emit.Opcodes(w.BinWriter, opcode.PACK) - emit.Int(w.BinWriter, int64(callflag.All)) - emit.String(w.BinWriter, "request") - emit.Bytes(w.BinWriter, oracleHash.BytesBE()) - emit.Syscall(w.BinWriter, interopnames.SystemContractCall) - emit.Opcodes(w.BinWriter, opcode.DROP) - emit.Opcodes(w.BinWriter, opcode.RET) - - // `handle` method aborts if len(userData) == 2 and does NOT perform witness checks - // for the sake of contract code simplicity (the contract is used in multiple testchains). - offset := w.Len() - - emit.Opcodes(w.BinWriter, opcode.OVER) - emit.Opcodes(w.BinWriter, opcode.SIZE) - emit.Int(w.BinWriter, 2) - emit.Instruction(w.BinWriter, opcode.JMPNE, []byte{3}) - emit.Opcodes(w.BinWriter, opcode.ABORT) - emit.Int(w.BinWriter, 4) // url, userData, code, result - emit.Opcodes(w.BinWriter, opcode.PACK) - emit.Int(w.BinWriter, 1) // 1 byte (args count for `serialize`) - emit.Opcodes(w.BinWriter, opcode.PACK) // 1 byte (pack args into array for `serialize`) - emit.AppCallNoArgs(w.BinWriter, stdHash, "serialize", callflag.All) // 39 bytes - emit.String(w.BinWriter, "lastOracleResponse") - emit.Syscall(w.BinWriter, interopnames.SystemStorageGetContext) - emit.Syscall(w.BinWriter, interopnames.SystemStoragePut) - emit.Opcodes(w.BinWriter, opcode.RET) - - m := manifest.NewManifest("TestOracle") - m.ABI.Methods = []manifest.Method{ - { - Name: "requestURL", - Offset: 0, - Parameters: []manifest.Parameter{ - manifest.NewParameter("url", smartcontract.StringType), - manifest.NewParameter("filter", smartcontract.StringType), - manifest.NewParameter("callback", smartcontract.StringType), - manifest.NewParameter("userData", smartcontract.AnyType), - manifest.NewParameter("gasForResponse", smartcontract.IntegerType), - }, - ReturnType: smartcontract.VoidType, - }, - { - Name: "handle", - Offset: offset, - Parameters: []manifest.Parameter{ - manifest.NewParameter("url", smartcontract.StringType), - manifest.NewParameter("userData", smartcontract.AnyType), - manifest.NewParameter("code", smartcontract.IntegerType), - manifest.NewParameter("result", smartcontract.ByteArrayType), - }, - ReturnType: smartcontract.VoidType, - }, - } - - perm := manifest.NewPermission(manifest.PermissionHash, oracleHash) - perm.Methods.Add("request") - m.Permissions = append(m.Permissions, *perm) - - // Generate NEF file. - script := w.Bytes() - ne, err := nef.NewFile(script) - require.NoError(t, err) - - // Write NEF file. - bytes, err := ne.Bytes() - require.NoError(t, err) - if saveState { - err = os.WriteFile(oracleContractNEFPath, bytes, os.ModePerm) - require.NoError(t, err) - } - - // Write manifest file. - mData, err := json.Marshal(m) - require.NoError(t, err) - if saveState { - err = os.WriteFile(oracleContractManifestPath, mData, os.ModePerm) - require.NoError(t, err) - } - - require.False(t, saveState) -} - -// getOracleContractState reads pre-compiled oracle contract generated by -// TestGenerateOracleContract and returns its state. -func getOracleContractState(t *testing.T, sender util.Uint160, id int32) *state.Contract { - errNotFound := errors.New("auto-generated oracle contract is not found, use TestGenerateOracleContract to regenerate") - - neBytes, err := os.ReadFile(oracleContractNEFPath) - require.NoError(t, err, fmt.Errorf("nef: %w", errNotFound)) - ne, err := nef.FileFromBytes(neBytes) - require.NoError(t, err) - - mBytes, err := os.ReadFile(oracleContractManifestPath) - require.NoError(t, err, fmt.Errorf("manifest: %w", errNotFound)) - m := &manifest.Manifest{} - err = json.Unmarshal(mBytes, m) - require.NoError(t, err) - - return &state.Contract{ - ContractBase: state.ContractBase{ - NEF: ne, - Hash: state.CreateContractHash(sender, ne.Checksum, m.Name), - Manifest: *m, - ID: id, - }, - } -} - -func putOracleRequest(t *testing.T, h util.Uint160, bc *Blockchain, +func putOracleRequest(t *testing.T, oracleValidatorInvoker *neotest.ContractInvoker, url string, filter *string, cb string, userData []byte, gas int64) util.Uint256 { var filtItem interface{} if filter != nil { filtItem = *filter } - res, err := invokeContractMethod(bc, gas+50_000_000+5_000_000, h, "requestURL", - url, filtItem, cb, userData, gas) - require.NoError(t, err) - return res.Container + return oracleValidatorInvoker.Invoke(t, stackitem.Null{}, "requestURL", url, filtItem, cb, userData, gas) } -func getOracleConfig(t *testing.T, bc *Blockchain, w, pass string, returnOracleRedirectionErrOn func(address string) bool) oracle.Config { +func getOracleConfig(t *testing.T, bc *core.Blockchain, w, pass string, returnOracleRedirectionErrOn func(address string) bool) oracle.Config { return oracle.Config{ Log: zaptest.NewLogger(t), Network: netmode.UnitTestNet, @@ -193,7 +66,7 @@ func getOracleConfig(t *testing.T, bc *Blockchain, w, pass string, returnOracleR } } -func getTestOracle(t *testing.T, bc *Blockchain, walletPath, pass string) ( +func getTestOracle(t *testing.T, bc *core.Blockchain, walletPath, pass string) ( *wallet.Account, *oracle.Oracle, map[uint64]*responseWithSig, @@ -217,7 +90,19 @@ func getTestOracle(t *testing.T, bc *Blockchain, walletPath, pass string) ( // Compatibility test from C# code. // https://github.com/neo-project/neo-modules/blob/master/tests/Neo.Plugins.OracleService.Tests/UT_OracleService.cs#L61 func TestCreateResponseTx(t *testing.T) { - bc := newTestChain(t) + bc, validator, committee := chain.NewMulti(t) + e := neotest.NewExecutor(t, bc, validator, committee) + managementInvoker := e.ValidatorInvoker(e.NativeHash(t, nativenames.Management)) + + cs := contracts.GetOracleContractState(t, pathToInternalContracts, validator.ScriptHash(), 0) + 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) require.Equal(t, int64(30), bc.GetBaseExecFee()) require.Equal(t, int64(1000), bc.FeePerByte()) @@ -236,10 +121,10 @@ func TestCreateResponseTx(t *testing.T) { Code: transaction.Success, Result: []byte{0}, } - require.NoError(t, bc.contracts.Oracle.PutRequestInternal(1, req, bc.dao)) + cInvoker.Invoke(t, stackitem.Null{}, "requestURL", req.URL, *req.Filter, req.CallbackMethod, req.UserData, int64(req.GasForResponse)) orc.UpdateOracleNodes(keys.PublicKeys{acc.PrivateKey().PublicKey()}) bc.SetOracle(orc) - tx, err := orc.CreateResponseTx(int64(req.GasForResponse), 1, resp) + tx, err = orc.CreateResponseTx(int64(req.GasForResponse), 1, resp) require.NoError(t, err) assert.Equal(t, 166, tx.Size()) assert.Equal(t, int64(2198650), tx.NetworkFee) @@ -247,7 +132,7 @@ func TestCreateResponseTx(t *testing.T) { } func TestOracle_InvalidWallet(t *testing.T) { - bc := newTestChain(t) + bc, _, _ := chain.NewMulti(t) _, err := oracle.NewOracle(getOracleConfig(t, bc, "./testdata/oracle1.json", "invalid", nil)) require.Error(t, err) @@ -257,45 +142,65 @@ func TestOracle_InvalidWallet(t *testing.T) { } func TestOracle(t *testing.T) { - bc := newTestChain(t) + bc, validator, committee := chain.NewMulti(t) + e := neotest.NewExecutor(t, bc, validator, committee) + managementInvoker := e.ValidatorInvoker(e.NativeHash(t, nativenames.Management)) + designationSuperInvoker := e.NewInvoker(e.NativeHash(t, nativenames.Designation), validator, committee) + nativeOracleH := e.NativeHash(t, nativenames.Oracle) + nativeOracleID := e.NativeID(t, nativenames.Oracle) - oracleCtr := bc.contracts.Oracle acc1, orc1, m1, ch1 := getTestOracle(t, bc, "./testdata/oracle1.json", "one") acc2, orc2, m2, ch2 := getTestOracle(t, bc, "./testdata/oracle2.json", "two") oracleNodes := keys.PublicKeys{acc1.PrivateKey().PublicKey(), acc2.PrivateKey().PublicKey()} // Must be set in native contract for tx verification. - bc.setNodesByRole(t, true, noderoles.Oracle, oracleNodes) + designationSuperInvoker.Invoke(t, stackitem.Null{}, "designateAsRole", + int64(roles.Oracle), []interface{}{oracleNodes[0].Bytes(), oracleNodes[1].Bytes()}) orc1.UpdateOracleNodes(oracleNodes.Copy()) orc2.UpdateOracleNodes(oracleNodes.Copy()) - orcNative := bc.contracts.Oracle - md, ok := orcNative.GetMethod(manifest.MethodVerify, -1) - require.True(t, ok) - orc1.UpdateNativeContract(orcNative.NEF.Script, orcNative.GetOracleResponseScript(), orcNative.Hash, md.MD.Offset) - orc2.UpdateNativeContract(orcNative.NEF.Script, orcNative.GetOracleResponseScript(), orcNative.Hash, md.MD.Offset) + nativeOracleState := bc.GetContractState(nativeOracleH) + require.NotNil(t, nativeOracleState) + md := nativeOracleState.Manifest.ABI.GetMethod(manifest.MethodVerify, -1) + require.NotNil(t, md) + oracleRespScript := native.CreateOracleResponseScript(nativeOracleH) + orc1.UpdateNativeContract(nativeOracleState.NEF.Script, slice.Copy(oracleRespScript), nativeOracleH, md.Offset) + orc2.UpdateNativeContract(nativeOracleState.NEF.Script, slice.Copy(oracleRespScript), nativeOracleH, md.Offset) - cs := getOracleContractState(t, util.Uint160{}, 42) - require.NoError(t, bc.contracts.Management.PutContractState(bc.dao, cs)) + cs := contracts.GetOracleContractState(t, pathToInternalContracts, validator.ScriptHash(), 0) + 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) - putOracleRequest(t, cs.Hash, bc, "https://get.1234", nil, "handle", []byte{}, 10_000_000) - putOracleRequest(t, cs.Hash, bc, "https://get.1234", nil, "handle", []byte{}, 10_000_000) - putOracleRequest(t, cs.Hash, bc, "https://get.timeout", nil, "handle", []byte{}, 10_000_000) - putOracleRequest(t, cs.Hash, bc, "https://get.notfound", nil, "handle", []byte{}, 10_000_000) - putOracleRequest(t, cs.Hash, bc, "https://get.forbidden", nil, "handle", []byte{}, 10_000_000) - putOracleRequest(t, cs.Hash, bc, "https://private.url", nil, "handle", []byte{}, 10_000_000) - putOracleRequest(t, cs.Hash, bc, "https://get.big", nil, "handle", []byte{}, 10_000_000) - putOracleRequest(t, cs.Hash, bc, "https://get.maxallowed", nil, "handle", []byte{}, 10_000_000) - putOracleRequest(t, cs.Hash, bc, "https://get.maxallowed", nil, "handle", []byte{}, 100_000_000) + putOracleRequest(t, cInvoker, "https://get.1234", nil, "handle", []byte{}, 10_000_000) + putOracleRequest(t, cInvoker, "https://get.1234", nil, "handle", []byte{}, 10_000_000) + putOracleRequest(t, cInvoker, "https://get.timeout", nil, "handle", []byte{}, 10_000_000) + putOracleRequest(t, cInvoker, "https://get.notfound", nil, "handle", []byte{}, 10_000_000) + putOracleRequest(t, cInvoker, "https://get.forbidden", nil, "handle", []byte{}, 10_000_000) + putOracleRequest(t, cInvoker, "https://private.url", nil, "handle", []byte{}, 10_000_000) + putOracleRequest(t, cInvoker, "https://get.big", nil, "handle", []byte{}, 10_000_000) + putOracleRequest(t, cInvoker, "https://get.maxallowed", nil, "handle", []byte{}, 10_000_000) + putOracleRequest(t, cInvoker, "https://get.maxallowed", nil, "handle", []byte{}, 100_000_000) flt := "$.Values[1]" - putOracleRequest(t, cs.Hash, bc, "https://get.filter", &flt, "handle", []byte{}, 10_000_000) - putOracleRequest(t, cs.Hash, bc, "https://get.filterinv", &flt, "handle", []byte{}, 10_000_000) + putOracleRequest(t, cInvoker, "https://get.filter", &flt, "handle", []byte{}, 10_000_000) + putOracleRequest(t, cInvoker, "https://get.filterinv", &flt, "handle", []byte{}, 10_000_000) - putOracleRequest(t, cs.Hash, bc, "https://get.invalidcontent", nil, "handle", []byte{}, 10_000_000) + putOracleRequest(t, cInvoker, "https://get.invalidcontent", nil, "handle", []byte{}, 10_000_000) checkResp := func(t *testing.T, id uint64, resp *transaction.OracleResponse) *state.OracleRequest { - req, err := oracleCtr.GetRequestInternal(bc.dao, id) - require.NoError(t, err) + // Use a hack to get request from Oracle contract, because we can't use GetRequestInternal directly. + requestKey := make([]byte, 9) + requestKey[0] = 7 // prefixRequest from native Oracle contract + binary.BigEndian.PutUint64(requestKey[1:], id) + si := bc.GetStorageItem(nativeOracleID, requestKey) + require.NotNil(t, si) + req := new(state.OracleRequest) + require.NoError(t, stackitem.DeserializeConvertible(si, req)) reqs := map[uint64]*state.OracleRequest{id: req} orc1.ProcessRequestsInternal(reqs) @@ -328,7 +233,7 @@ func TestOracle(t *testing.T) { actualHash := cp.Hash() require.Equal(t, actualHash, cachedHash, "transaction hash was changed during ") - require.NoError(t, bc.verifyAndPoolTx(tx, bc.GetMemPool(), bc)) + require.NoError(t, bc.PoolTx(tx)) } t.Run("NormalRequest", func(t *testing.T) { @@ -436,21 +341,34 @@ func TestOracle(t *testing.T) { } func TestOracleFull(t *testing.T) { - bc := initTestChain(t, nil, nil) + bc, validator, committee := chain.NewMultiWithCustomConfigAndStore(t, nil, nil, false) + e := neotest.NewExecutor(t, bc, validator, committee) + designationSuperInvoker := e.NewInvoker(e.NativeHash(t, nativenames.Designation), validator, committee) + acc, orc, _, _ := getTestOracle(t, bc, "./testdata/oracle2.json", "two") mp := bc.GetMemPool() orc.OnTransaction = func(tx *transaction.Transaction) error { return mp.Add(tx, bc) } bc.SetOracle(orc) - cs := getOracleContractState(t, util.Uint160{}, 42) - require.NoError(t, bc.contracts.Management.PutContractState(bc.dao, cs)) - go bc.Run() orc.Start() - t.Cleanup(orc.Shutdown) + t.Cleanup(func() { + orc.Shutdown() + bc.Close() + }) - bc.setNodesByRole(t, true, noderoles.Oracle, keys.PublicKeys{acc.PrivateKey().PublicKey()}) - putOracleRequest(t, cs.Hash, bc, "https://get.1234", new(string), "handle", []byte{}, 10_000_000) + designationSuperInvoker.Invoke(t, stackitem.Null{}, "designateAsRole", + int64(roles.Oracle), []interface{}{acc.PrivateKey().PublicKey().Bytes()}) + + cs := contracts.GetOracleContractState(t, pathToInternalContracts, validator.ScriptHash(), 0) + e.DeployContract(t, &neotest.Contract{ + Hash: cs.Hash, + NEF: &cs.NEF, + Manifest: &cs.Manifest, + }, nil) + cInvoker := e.ValidatorInvoker(cs.Hash) + + putOracleRequest(t, cInvoker, "https://get.1234", new(string), "handle", []byte{}, 10_000_000) require.Eventually(t, func() bool { return mp.Count() == 1 }, time.Second*3, time.Millisecond*200) @@ -461,17 +379,20 @@ func TestOracleFull(t *testing.T) { } func TestNotYetRunningOracle(t *testing.T) { - bc := initTestChain(t, nil, nil) + bc, validator, committee := chain.NewMultiWithCustomConfigAndStore(t, nil, nil, false) + e := neotest.NewExecutor(t, bc, validator, committee) + designationSuperInvoker := e.NewInvoker(e.NativeHash(t, nativenames.Designation), validator, committee) + acc, orc, _, _ := getTestOracle(t, bc, "./testdata/oracle2.json", "two") mp := bc.GetMemPool() orc.OnTransaction = func(tx *transaction.Transaction) error { return mp.Add(tx, bc) } bc.SetOracle(orc) - cs := getOracleContractState(t, util.Uint160{}, 42) - require.NoError(t, bc.contracts.Management.PutContractState(bc.dao, cs)) - go bc.Run() - bc.setNodesByRole(t, true, noderoles.Oracle, keys.PublicKeys{acc.PrivateKey().PublicKey()}) + t.Cleanup(bc.Close) + + designationSuperInvoker.Invoke(t, stackitem.Null{}, "designateAsRole", + int64(roles.Oracle), []interface{}{acc.PrivateKey().PublicKey().Bytes()}) var req state.OracleRequest var reqs = make(map[uint64]*state.OracleRequest) diff --git a/pkg/core/stateroot_test.go b/pkg/core/stateroot_test.go index 29777f457..3a958def5 100644 --- a/pkg/core/stateroot_test.go +++ b/pkg/core/stateroot_test.go @@ -1,6 +1,7 @@ -package core +package core_test import ( + "crypto/elliptic" "errors" "path/filepath" "sort" @@ -10,18 +11,25 @@ import ( "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" + "github.com/nspcc-dev/neo-go/pkg/core" + "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/core/state" + corestate "github.com/nspcc-dev/neo-go/pkg/core/stateroot" "github.com/nspcc-dev/neo-go/pkg/core/storage" "github.com/nspcc-dev/neo-go/pkg/core/transaction" "github.com/nspcc-dev/neo-go/pkg/crypto/hash" "github.com/nspcc-dev/neo-go/pkg/crypto/keys" + "github.com/nspcc-dev/neo-go/pkg/interop/native/roles" "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/network/payload" "github.com/nspcc-dev/neo-go/pkg/services/stateroot" "github.com/nspcc-dev/neo-go/pkg/smartcontract" "github.com/nspcc-dev/neo-go/pkg/util" "github.com/nspcc-dev/neo-go/pkg/vm/emit" + "github.com/nspcc-dev/neo-go/pkg/vm/stackitem" "github.com/nspcc-dev/neo-go/pkg/wallet" "github.com/stretchr/testify/require" "go.uber.org/atomic" @@ -70,95 +78,113 @@ func newMajorityMultisigWithGAS(t *testing.T, n int) (util.Uint160, keys.PublicK } func TestStateRoot(t *testing.T) { - bc := newTestChain(t) + bc, validator, committee := chain.NewMulti(t) + e := neotest.NewExecutor(t, bc, validator, committee) + designationSuperInvoker := e.NewInvoker(e.NativeHash(t, nativenames.Designation), validator, committee) + gasValidatorInvoker := e.ValidatorInvoker(e.NativeHash(t, nativenames.Gas)) h, pubs, accs := newMajorityMultisigWithGAS(t, 2) - bc.setNodesByRole(t, true, noderoles.StateValidator, pubs) + validatorNodes := []interface{}{pubs[0].Bytes(), pubs[1].Bytes()} + designationSuperInvoker.Invoke(t, stackitem.Null{}, "designateAsRole", + int64(roles.StateValidator), validatorNodes) updateIndex := bc.BlockHeight() - transferTokenFromMultisigAccount(t, bc, h, bc.contracts.GAS.Hash, 1_0000_0000) + + gasValidatorInvoker.Invoke(t, true, "transfer", validator.ScriptHash(), h, 1_0000_0000, nil) tmpDir := t.TempDir() w := createAndWriteWallet(t, accs[0], filepath.Join(tmpDir, "w"), "pass") cfg := createStateRootConfig(w.Path(), "pass") - srv, err := stateroot.New(cfg, bc.stateRoot, zaptest.NewLogger(t), bc, nil) + srMod := bc.GetStateModule().(*corestate.Module) // Take full responsibility here. + srv, err := stateroot.New(cfg, srMod, zaptest.NewLogger(t), bc, nil) require.NoError(t, err) - require.EqualValues(t, 0, bc.stateRoot.CurrentValidatedHeight()) - r, err := bc.stateRoot.GetStateRoot(bc.BlockHeight()) + require.EqualValues(t, 0, bc.GetStateModule().CurrentValidatedHeight()) + r, err := bc.GetStateModule().GetStateRoot(bc.BlockHeight()) require.NoError(t, err) - require.Equal(t, r.Root, bc.stateRoot.CurrentLocalStateRoot()) + require.Equal(t, r.Root, bc.GetStateModule().CurrentLocalStateRoot()) t.Run("invalid message", func(t *testing.T) { require.Error(t, srv.OnPayload(&payload.Extensible{Data: []byte{42}})) - require.EqualValues(t, 0, bc.stateRoot.CurrentValidatedHeight()) + require.EqualValues(t, 0, bc.GetStateModule().CurrentValidatedHeight()) }) t.Run("drop zero index", func(t *testing.T) { - r, err := bc.stateRoot.GetStateRoot(0) + r, err := bc.GetStateModule().GetStateRoot(0) require.NoError(t, err) data, err := testserdes.EncodeBinary(stateroot.NewMessage(stateroot.RootT, r)) require.NoError(t, err) require.NoError(t, srv.OnPayload(&payload.Extensible{Data: data})) - require.EqualValues(t, 0, bc.stateRoot.CurrentValidatedHeight()) + require.EqualValues(t, 0, bc.GetStateModule().CurrentValidatedHeight()) }) t.Run("invalid height", func(t *testing.T) { - r, err := bc.stateRoot.GetStateRoot(1) + r, err := bc.GetStateModule().GetStateRoot(1) require.NoError(t, err) r.Index = 10 data := testSignStateRoot(t, r, pubs, accs...) require.Error(t, srv.OnPayload(&payload.Extensible{Data: data})) - require.EqualValues(t, 0, bc.stateRoot.CurrentValidatedHeight()) + require.EqualValues(t, 0, bc.GetStateModule().CurrentValidatedHeight()) }) t.Run("invalid signer", func(t *testing.T) { accInv, err := wallet.NewAccount() require.NoError(t, err) pubs := keys.PublicKeys{accInv.PrivateKey().PublicKey()} require.NoError(t, accInv.ConvertMultisig(1, pubs)) - transferTokenFromMultisigAccount(t, bc, accInv.Contract.ScriptHash(), bc.contracts.GAS.Hash, 1_0000_0000) - r, err := bc.stateRoot.GetStateRoot(1) + gasValidatorInvoker.Invoke(t, true, "transfer", validator.ScriptHash(), accInv.Contract.ScriptHash(), 1_0000_0000, nil) + r, err := bc.GetStateModule().GetStateRoot(1) require.NoError(t, err) data := testSignStateRoot(t, r, pubs, accInv) err = srv.OnPayload(&payload.Extensible{Data: data}) - require.True(t, errors.Is(err, ErrWitnessHashMismatch), "got: %v", err) - require.EqualValues(t, 0, bc.stateRoot.CurrentValidatedHeight()) + require.True(t, errors.Is(err, core.ErrWitnessHashMismatch), "got: %v", err) + require.EqualValues(t, 0, bc.GetStateModule().CurrentValidatedHeight()) }) - r, err = bc.stateRoot.GetStateRoot(updateIndex + 1) + r, err = bc.GetStateModule().GetStateRoot(updateIndex + 1) require.NoError(t, err) data := testSignStateRoot(t, r, pubs, accs...) require.NoError(t, srv.OnPayload(&payload.Extensible{Data: data})) - require.EqualValues(t, 2, bc.stateRoot.CurrentValidatedHeight()) + require.EqualValues(t, 2, bc.GetStateModule().CurrentValidatedHeight()) - r, err = bc.stateRoot.GetStateRoot(updateIndex + 1) + r, err = bc.GetStateModule().GetStateRoot(updateIndex + 1) require.NoError(t, err) require.NotEqual(t, 0, len(r.Witness)) require.Equal(t, h, r.Witness[0].ScriptHash()) } +type memoryStore struct { + *storage.MemoryStore +} + +func (memoryStore) Close() error { return nil } + func TestStateRootInitNonZeroHeight(t *testing.T) { st := memoryStore{storage.NewMemoryStore()} h, pubs, accs := newMajorityMultisigWithGAS(t, 2) var root util.Uint256 t.Run("init", func(t *testing.T) { // this is in a separate test to do proper cleanup - bc := newTestChainWithCustomCfgAndStore(t, st, nil) - bc.setNodesByRole(t, true, noderoles.StateValidator, pubs) - transferTokenFromMultisigAccount(t, bc, h, bc.contracts.GAS.Hash, 1_0000_0000) + bc, validator, committee := chain.NewMultiWithCustomConfigAndStore(t, nil, st, true) + e := neotest.NewExecutor(t, bc, validator, committee) + designationSuperInvoker := e.NewInvoker(e.NativeHash(t, nativenames.Designation), validator, committee) + gasValidatorInvoker := e.ValidatorInvoker(e.NativeHash(t, nativenames.Gas)) + + validatorNodes := []interface{}{pubs[0].Bytes(), pubs[1].Bytes()} + designationSuperInvoker.Invoke(t, stackitem.Null{}, "designateAsRole", + int64(roles.StateValidator), validatorNodes) + gasValidatorInvoker.Invoke(t, true, "transfer", validator.ScriptHash(), h, 1_0000_0000, nil) - _, err := persistBlock(bc) - require.NoError(t, err) tmpDir := t.TempDir() w := createAndWriteWallet(t, accs[0], filepath.Join(tmpDir, "w"), "pass") cfg := createStateRootConfig(w.Path(), "pass") - srv, err := stateroot.New(cfg, bc.stateRoot, zaptest.NewLogger(t), bc, nil) + srMod := bc.GetStateModule().(*corestate.Module) // Take full responsibility here. + srv, err := stateroot.New(cfg, srMod, zaptest.NewLogger(t), bc, nil) require.NoError(t, err) - r, err := bc.stateRoot.GetStateRoot(2) + r, err := bc.GetStateModule().GetStateRoot(2) require.NoError(t, err) data := testSignStateRoot(t, r, pubs, accs...) require.NoError(t, srv.OnPayload(&payload.Extensible{Data: data})) - require.EqualValues(t, 2, bc.stateRoot.CurrentValidatedHeight()) - root = bc.stateRoot.CurrentLocalStateRoot() + require.EqualValues(t, 2, bc.GetStateModule().CurrentValidatedHeight()) + root = bc.GetStateModule().CurrentLocalStateRoot() }) - bc2 := newTestChainWithCustomCfgAndStore(t, st, nil) + bc2, _, _ := chain.NewMultiWithCustomConfigAndStore(t, nil, st, true) srv := bc2.GetStateModule() require.EqualValues(t, 2, srv.CurrentValidatedHeight()) require.Equal(t, root, srv.CurrentLocalStateRoot()) @@ -186,7 +212,22 @@ func createStateRootConfig(walletPath, password string) config.StateRoot { func TestStateRootFull(t *testing.T) { tmpDir := t.TempDir() - bc := newTestChain(t) + bc, validator, committee := chain.NewMulti(t) + e := neotest.NewExecutor(t, bc, validator, committee) + designationSuperInvoker := e.NewInvoker(e.NativeHash(t, nativenames.Designation), validator, committee) + gasValidatorInvoker := e.ValidatorInvoker(e.NativeHash(t, nativenames.Gas)) + + getDesignatedByRole := func(t *testing.T, h uint32) keys.PublicKeys { + res, err := designationSuperInvoker.TestInvoke(t, "getDesignatedByRole", int64(noderoles.StateValidator), h) + require.NoError(t, err) + nodes := res.Pop().Value().([]stackitem.Item) + pubs := make(keys.PublicKeys, len(nodes)) + for i, node := range nodes { + pubs[i], err = keys.NewPublicKeyFromBytes(node.Value().([]byte), elliptic.P256()) + require.NoError(t, err) + } + return pubs + } h, pubs, accs := newMajorityMultisigWithGAS(t, 2) w := createAndWriteWallet(t, accs[1], filepath.Join(tmpDir, "wallet2"), "two") @@ -194,7 +235,8 @@ func TestStateRootFull(t *testing.T) { var lastValidated atomic.Value var lastHeight atomic.Uint32 - srv, err := stateroot.New(cfg, bc.stateRoot, zaptest.NewLogger(t), bc, func(ep *payload.Extensible) { + srMod := bc.GetStateModule().(*corestate.Module) // Take full responsibility here. + srv, err := stateroot.New(cfg, srMod, zaptest.NewLogger(t), bc, func(ep *payload.Extensible) { lastHeight.Store(ep.ValidBlockStart) lastValidated.Store(ep) }) @@ -202,16 +244,17 @@ func TestStateRootFull(t *testing.T) { srv.Start() t.Cleanup(srv.Shutdown) - bc.setNodesByRole(t, true, noderoles.StateValidator, pubs) - transferTokenFromMultisigAccount(t, bc, h, bc.contracts.GAS.Hash, 1_0000_0000) + validatorNodes := []interface{}{pubs[0].Bytes(), pubs[1].Bytes()} + designationSuperInvoker.Invoke(t, stackitem.Null{}, "designateAsRole", + int64(roles.StateValidator), validatorNodes) + gasValidatorInvoker.Invoke(t, true, "transfer", validator.ScriptHash(), h, 1_0000_0000, nil) require.Eventually(t, func() bool { return lastHeight.Load() == 2 }, time.Second, time.Millisecond) - checkVoteBroadcasted(t, bc, lastValidated.Load().(*payload.Extensible), 2, 1) - _, err = persistBlock(bc) - require.NoError(t, err) + checkVoteBroadcasted(t, bc, lastValidated.Load().(*payload.Extensible), 2, 1, getDesignatedByRole) + e.AddNewBlock(t) require.Eventually(t, func() bool { return lastHeight.Load() == 3 }, time.Second, time.Millisecond) - checkVoteBroadcasted(t, bc, lastValidated.Load().(*payload.Extensible), 3, 1) + checkVoteBroadcasted(t, bc, lastValidated.Load().(*payload.Extensible), 3, 1, getDesignatedByRole) - r, err := bc.stateRoot.GetStateRoot(2) + r, err := bc.GetStateModule().GetStateRoot(2) require.NoError(t, err) require.NoError(t, srv.AddSignature(2, 0, accs[0].PrivateKey().SignHashable(uint32(netmode.UnitTestNet), r))) require.NotNil(t, lastValidated.Load().(*payload.Extensible)) @@ -220,7 +263,7 @@ func TestStateRootFull(t *testing.T) { require.NoError(t, testserdes.DecodeBinary(lastValidated.Load().(*payload.Extensible).Data, msg)) require.NotEqual(t, stateroot.RootT, msg.Type) // not a sender for this root - r, err = bc.stateRoot.GetStateRoot(3) + r, err = bc.GetStateModule().GetStateRoot(3) require.NoError(t, err) require.Error(t, srv.AddSignature(2, 0, accs[0].PrivateKey().SignHashable(uint32(netmode.UnitTestNet), r))) require.NoError(t, srv.AddSignature(3, 0, accs[0].PrivateKey().SignHashable(uint32(netmode.UnitTestNet), r))) @@ -235,8 +278,8 @@ func TestStateRootFull(t *testing.T) { require.Equal(t, r.Root, actual.Root) } -func checkVoteBroadcasted(t *testing.T, bc *Blockchain, p *payload.Extensible, - height uint32, valIndex byte) { +func checkVoteBroadcasted(t *testing.T, bc *core.Blockchain, p *payload.Extensible, + height uint32, valIndex byte, getDesignatedByRole func(t *testing.T, h uint32) keys.PublicKeys) { require.NotNil(t, p) m := new(stateroot.Message) require.NoError(t, testserdes.DecodeBinary(p.Data, m)) @@ -249,8 +292,7 @@ func checkVoteBroadcasted(t *testing.T, bc *Blockchain, p *payload.Extensible, require.Equal(t, height, vote.Height) require.Equal(t, int32(valIndex), vote.ValidatorIndex) - pubs, _, err := bc.contracts.Designate.GetDesignatedByRole(bc.dao, noderoles.StateValidator, bc.BlockHeight()) - require.NoError(t, err) + pubs := getDesignatedByRole(t, bc.BlockHeight()) require.True(t, len(pubs) > int(valIndex)) require.True(t, pubs[valIndex].VerifyHashable(vote.Signature, uint32(netmode.UnitTestNet), r)) } diff --git a/pkg/core/statesync_test.go b/pkg/core/statesync_test.go index 2be3b01a9..4162c921b 100644 --- a/pkg/core/statesync_test.go +++ b/pkg/core/statesync_test.go @@ -1,13 +1,14 @@ -package core +package core_test import ( "testing" - "time" "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" "github.com/nspcc-dev/neo-go/pkg/core/storage" + "github.com/nspcc-dev/neo-go/pkg/neotest" + "github.com/nspcc-dev/neo-go/pkg/neotest/chain" "github.com/nspcc-dev/neo-go/pkg/util" "github.com/nspcc-dev/neo-go/pkg/util/slice" "github.com/stretchr/testify/require" @@ -18,40 +19,41 @@ func TestStateSyncModule_Init(t *testing.T) { stateSyncInterval = 2 maxTraceable uint32 = 3 ) - spoutCfg := func(c *config.Config) { - c.ProtocolConfiguration.StateRootInHeader = true - c.ProtocolConfiguration.P2PStateExchangeExtensions = true - c.ProtocolConfiguration.StateSyncInterval = stateSyncInterval - c.ProtocolConfiguration.MaxTraceableBlocks = maxTraceable + spoutCfg := func(c *config.ProtocolConfiguration) { + c.StateRootInHeader = true + c.P2PStateExchangeExtensions = true + c.StateSyncInterval = stateSyncInterval + c.MaxTraceableBlocks = maxTraceable } - bcSpout := newTestChainWithCustomCfg(t, spoutCfg) + bcSpout, validators, committee := chain.NewMultiWithCustomConfig(t, spoutCfg) + e := neotest.NewExecutor(t, bcSpout, validators, committee) for i := 0; i <= 2*stateSyncInterval+int(maxTraceable)+1; i++ { - require.NoError(t, bcSpout.AddBlock(bcSpout.newBlock())) + e.AddNewBlock(t) } - boltCfg := func(c *config.Config) { + boltCfg := func(c *config.ProtocolConfiguration) { spoutCfg(c) - c.ProtocolConfiguration.KeepOnlyLatestState = true - c.ProtocolConfiguration.RemoveUntraceableBlocks = true + c.KeepOnlyLatestState = true + c.RemoveUntraceableBlocks = true } t.Run("error: module disabled by config", func(t *testing.T) { - bcBolt := newTestChainWithCustomCfg(t, func(c *config.Config) { + bcBolt, _, _ := chain.NewMultiWithCustomConfig(t, func(c *config.ProtocolConfiguration) { boltCfg(c) - c.ProtocolConfiguration.RemoveUntraceableBlocks = false + c.RemoveUntraceableBlocks = false }) module := bcBolt.GetStateSyncModule() require.Error(t, module.Init(bcSpout.BlockHeight())) // module inactive (non-archival node) }) t.Run("inactive: spout chain is too low to start state sync process", func(t *testing.T) { - bcBolt := newTestChainWithCustomCfg(t, boltCfg) + bcBolt, _, _ := chain.NewMultiWithCustomConfig(t, boltCfg) module := bcBolt.GetStateSyncModule() require.NoError(t, module.Init(uint32(2*stateSyncInterval-1))) require.False(t, module.IsActive()) }) t.Run("inactive: bolt chain height is close enough to spout chain height", func(t *testing.T) { - bcBolt := newTestChainWithCustomCfg(t, boltCfg) + bcBolt, _, _ := chain.NewMultiWithCustomConfig(t, boltCfg) for i := 1; i < int(bcSpout.BlockHeight())-stateSyncInterval; i++ { b, err := bcSpout.GetBlock(bcSpout.GetHeaderHash(i)) require.NoError(t, err) @@ -63,15 +65,16 @@ func TestStateSyncModule_Init(t *testing.T) { }) t.Run("error: bolt chain is too low to start state sync process", func(t *testing.T) { - bcBolt := newTestChainWithCustomCfg(t, boltCfg) - require.NoError(t, bcBolt.AddBlock(bcBolt.newBlock())) + bcBolt, validatorsBolt, committeeBolt := chain.NewMultiWithCustomConfig(t, boltCfg) + eBolt := neotest.NewExecutor(t, bcBolt, validatorsBolt, committeeBolt) + eBolt.AddNewBlock(t) module := bcBolt.GetStateSyncModule() require.Error(t, module.Init(uint32(3*stateSyncInterval))) }) t.Run("initialized: no previous state sync point", func(t *testing.T) { - bcBolt := newTestChainWithCustomCfg(t, boltCfg) + bcBolt, _, _ := chain.NewMultiWithCustomConfig(t, boltCfg) module := bcBolt.GetStateSyncModule() require.NoError(t, module.Init(bcSpout.BlockHeight())) @@ -82,7 +85,7 @@ func TestStateSyncModule_Init(t *testing.T) { }) t.Run("error: outdated state sync point in the storage", func(t *testing.T) { - bcBolt := newTestChainWithCustomCfg(t, boltCfg) + bcBolt, _, _ := chain.NewMultiWithCustomConfig(t, boltCfg) module := bcBolt.GetStateSyncModule() require.NoError(t, module.Init(bcSpout.BlockHeight())) @@ -91,7 +94,7 @@ func TestStateSyncModule_Init(t *testing.T) { }) t.Run("initialized: valid previous state sync point in the storage", func(t *testing.T) { - bcBolt := newTestChainWithCustomCfg(t, boltCfg) + bcBolt, _, _ := chain.NewMultiWithCustomConfig(t, boltCfg) module := bcBolt.GetStateSyncModule() require.NoError(t, module.Init(bcSpout.BlockHeight())) @@ -104,7 +107,8 @@ func TestStateSyncModule_Init(t *testing.T) { }) t.Run("initialization from headers/blocks/mpt synced stages", func(t *testing.T) { - bcBolt := newTestChainWithCustomCfg(t, boltCfg) + bcBolt, validatorsBolt, committeeBolt := chain.NewMultiWithCustomConfig(t, boltCfg) + eBolt := neotest.NewExecutor(t, bcBolt, validatorsBolt, committeeBolt) module := bcBolt.GetStateSyncModule() require.NoError(t, module.Init(bcSpout.BlockHeight())) @@ -263,7 +267,7 @@ func TestStateSyncModule_Init(t *testing.T) { // add one more block to the restored chain and start new module: the module should recognise state sync is completed // and regular blocks processing was started - require.NoError(t, bcBolt.AddBlock(bcBolt.newBlock())) + eBolt.AddNewBlock(t) module = bcBolt.GetStateSyncModule() require.NoError(t, module.Init(bcSpout.BlockHeight())) require.False(t, module.IsActive()) @@ -282,27 +286,31 @@ func TestStateSyncModule_RestoreBasicChain(t *testing.T) { maxTraceable uint32 = 6 stateSyncPoint = 20 ) - spoutCfg := func(c *config.Config) { - c.ProtocolConfiguration.StateRootInHeader = true - c.ProtocolConfiguration.P2PStateExchangeExtensions = true - c.ProtocolConfiguration.StateSyncInterval = stateSyncInterval - c.ProtocolConfiguration.MaxTraceableBlocks = maxTraceable + spoutCfg := func(c *config.ProtocolConfiguration) { + c.StateRootInHeader = true + c.P2PStateExchangeExtensions = true + c.StateSyncInterval = stateSyncInterval + c.MaxTraceableBlocks = maxTraceable + c.P2PSigExtensions = true // `initBasicChain` assumes Notary is enabled. } - bcSpout := newTestChainWithCustomCfg(t, spoutCfg) - initBasicChain(t, bcSpout) + 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) // make spout chain higher that latest state sync point (add several blocks up to stateSyncPoint+2) - require.NoError(t, bcSpout.AddBlock(bcSpout.newBlock())) + e.AddNewBlock(t) require.Equal(t, stateSyncPoint+2, int(bcSpout.BlockHeight())) - boltCfg := func(c *config.Config) { + boltCfg := func(c *config.ProtocolConfiguration) { spoutCfg(c) - c.ProtocolConfiguration.KeepOnlyLatestState = true - c.ProtocolConfiguration.RemoveUntraceableBlocks = true + c.KeepOnlyLatestState = true + c.RemoveUntraceableBlocks = true } - bcBoltStore := memoryStore{storage.NewMemoryStore()} - bcBolt := initTestChain(t, bcBoltStore, boltCfg) - go bcBolt.Run() + bcBoltStore := storage.NewMemoryStore() + bcBolt, _, _ := chain.NewMultiWithCustomConfigAndStore(t, boltCfg, bcBoltStore, false) + go bcBolt.Run() // Will close it manually at the end. module := bcBolt.GetStateSyncModule() t.Run("error: add headers before initialisation", func(t *testing.T) { @@ -421,9 +429,9 @@ func TestStateSyncModule_RestoreBasicChain(t *testing.T) { require.Equal(t, bcSpout.BlockHeight(), bcBolt.BlockHeight()) // compare storage states - fetchStorage := func(bc *Blockchain) []storage.KeyValue { + fetchStorage := func(ps storage.Store, storagePrefix byte) []storage.KeyValue { var kv []storage.KeyValue - bc.dao.Store.Seek(storage.SeekRange{Prefix: []byte{byte(bc.dao.Version.StoragePrefix)}}, func(k, v []byte) bool { + ps.Seek(storage.SeekRange{Prefix: []byte{storagePrefix}}, func(k, v []byte) bool { key := slice.Copy(k) value := slice.Copy(v) if key[0] == byte(storage.STTempStorage) { @@ -437,25 +445,19 @@ func TestStateSyncModule_RestoreBasicChain(t *testing.T) { }) return kv } - expected := fetchStorage(bcSpout) - actual := fetchStorage(bcBolt) + // Both blockchains are running, so we need to wait until recent changes will be persisted + // to the underlying backend store. Close blockchains to ensure persist was completed. + bcSpout.Close() + bcBolt.Close() + expected := fetchStorage(bcSpoutStore, byte(storage.STStorage)) + actual := fetchStorage(bcBoltStore, byte(storage.STTempStorage)) require.ElementsMatch(t, expected, actual) // no temp items should be left - require.Eventually(t, func() bool { - var haveItems bool - bcBolt.dao.Store.Seek(storage.SeekRange{Prefix: []byte{byte(storage.STStorage)}}, func(_, _ []byte) bool { - haveItems = true - return false - }) - return !haveItems - }, time.Second*5, time.Millisecond*100) - bcBolt.Close() - - // Check restoring with new prefix. - bcBolt = initTestChain(t, bcBoltStore, boltCfg) - go bcBolt.Run() - defer bcBolt.Close() - require.Equal(t, storage.STTempStorage, bcBolt.dao.Version.StoragePrefix) - require.Equal(t, storage.STTempStorage, bcBolt.persistent.Version.StoragePrefix) + var haveItems bool + bcBoltStore.Seek(storage.SeekRange{Prefix: []byte{byte(storage.STStorage)}}, func(_, _ []byte) bool { + haveItems = true + return false + }) + require.False(t, haveItems) } diff --git a/pkg/core/test_data/management_helper/README.md b/pkg/core/test_data/management_helper/README.md deleted file mode 100644 index 853e322f6..000000000 --- a/pkg/core/test_data/management_helper/README.md +++ /dev/null @@ -1,9 +0,0 @@ -## Management helper contracts - -Management helper contracts NEF and manifest files are generated automatically by -`TestGenerateManagementHelperContracts` and are used in tests. Do not modify these files manually. -To regenerate these files: - -1. Open `TestGenerateManagementHelperContracts` and set `saveState` flag to `true`. -2. Run `TestGenerateManagementHelperContracts`. -3. Set `saveState` back to `false`. \ No newline at end of file diff --git a/pkg/core/test_data/management_helper/management_helper2.manifest.json b/pkg/core/test_data/management_helper/management_helper2.manifest.json deleted file mode 100755 index fb2614616..000000000 --- a/pkg/core/test_data/management_helper/management_helper2.manifest.json +++ /dev/null @@ -1 +0,0 @@ -{"name":"TestAux","abi":{"methods":[],"events":[]},"features":{},"groups":[],"permissions":[{"contract":"0x00ecaa2f079b65e3b31572e4c2c160a1abd02997","methods":["add","drop","add3","invalidReturn","justReturn","getValue"]}],"supportedstandards":[],"trusts":[],"extra":null} \ No newline at end of file diff --git a/pkg/core/test_data/oracle_contract/README.md b/pkg/core/test_data/oracle_contract/README.md deleted file mode 100644 index 83fad42a7..000000000 --- a/pkg/core/test_data/oracle_contract/README.md +++ /dev/null @@ -1,9 +0,0 @@ -## Oracle helper contract - -Oracle helper contract NEF and manifest files are generated automatically by -`TestGenerateOracleContract` and are used in tests. Do not modify these files manually. -To regenerate these files: - -1. Open `TestGenerateOracleContract` and set `saveState` flag to `true`. -2. Run `TestGenerateOracleContract`. -3. Set `saveState` back to `false`. \ No newline at end of file diff --git a/pkg/neotest/basic.go b/pkg/neotest/basic.go index 750d72288..7904914e2 100644 --- a/pkg/neotest/basic.go +++ b/pkg/neotest/basic.go @@ -36,7 +36,7 @@ type Executor struct { } // NewExecutor creates new executor instance from provided blockchain and committee. -func NewExecutor(t *testing.T, bc blockchainer.Blockchainer, validator, committee Signer) *Executor { +func NewExecutor(t testing.TB, bc blockchainer.Blockchainer, validator, committee Signer) *Executor { checkMultiSigner(t, validator) checkMultiSigner(t, committee) @@ -50,21 +50,28 @@ func NewExecutor(t *testing.T, bc blockchainer.Blockchainer, validator, committe } // TopBlock returns block with the highest index. -func (e *Executor) TopBlock(t *testing.T) *block.Block { +func (e *Executor) TopBlock(t testing.TB) *block.Block { b, err := e.Chain.GetBlock(e.Chain.GetHeaderHash(int(e.Chain.BlockHeight()))) require.NoError(t, err) return b } // NativeHash returns native contract hash by name. -func (e *Executor) NativeHash(t *testing.T, name string) util.Uint160 { +func (e *Executor) NativeHash(t testing.TB, name string) util.Uint160 { h, err := e.Chain.GetNativeContractScriptHash(name) require.NoError(t, err) return h } +// ContractHash returns contract hash by ID. +func (e *Executor) ContractHash(t testing.TB, id int32) util.Uint160 { + h, err := e.Chain.GetContractScriptHash(id) + require.NoError(t, err) + return h +} + // NativeID returns native contract ID by name. -func (e *Executor) NativeID(t *testing.T, name string) int32 { +func (e *Executor) NativeID(t testing.TB, name string) int32 { h := e.NativeHash(t, name) cs := e.Chain.GetContractState(h) require.NotNil(t, cs) @@ -72,7 +79,7 @@ func (e *Executor) NativeID(t *testing.T, name string) int32 { } // NewUnsignedTx creates new unsigned transaction which invokes method of contract with hash. -func (e *Executor) NewUnsignedTx(t *testing.T, hash util.Uint160, method string, args ...interface{}) *transaction.Transaction { +func (e *Executor) NewUnsignedTx(t testing.TB, hash util.Uint160, method string, args ...interface{}) *transaction.Transaction { w := io.NewBufBinWriter() emit.AppCall(w.BinWriter, hash, method, callflag.All, args...) require.NoError(t, w.Err) @@ -86,14 +93,14 @@ func (e *Executor) NewUnsignedTx(t *testing.T, hash util.Uint160, method string, // NewTx creates new transaction which invokes contract method. // Transaction is signed with signer. -func (e *Executor) NewTx(t *testing.T, signers []Signer, +func (e *Executor) NewTx(t testing.TB, signers []Signer, hash util.Uint160, method string, args ...interface{}) *transaction.Transaction { tx := e.NewUnsignedTx(t, hash, method, args...) return e.SignTx(t, tx, -1, signers...) } // SignTx signs a transaction using provided signers. -func (e *Executor) SignTx(t *testing.T, tx *transaction.Transaction, sysFee int64, signers ...Signer) *transaction.Transaction { +func (e *Executor) SignTx(t testing.TB, tx *transaction.Transaction, sysFee int64, signers ...Signer) *transaction.Transaction { for _, acc := range signers { tx.Signers = append(tx.Signers, transaction.Signer{ Account: acc.ScriptHash(), @@ -111,7 +118,7 @@ func (e *Executor) SignTx(t *testing.T, tx *transaction.Transaction, sysFee int6 // NewAccount returns new signer holding 100.0 GAS (or given amount is specified). // This method advances the chain by one block with a transfer transaction. -func (e *Executor) NewAccount(t *testing.T, expectedGASBalance ...int64) Signer { +func (e *Executor) NewAccount(t testing.TB, expectedGASBalance ...int64) Signer { acc, err := wallet.NewAccount() require.NoError(t, err) @@ -131,8 +138,16 @@ func (e *Executor) NewAccount(t *testing.T, expectedGASBalance ...int64) Signer // precalculated contract hash matches the actual one. // data is an optional argument to `_deploy`. // Returns hash of the deploy transaction. -func (e *Executor) DeployContract(t *testing.T, c *Contract, data interface{}) util.Uint256 { - tx := e.NewDeployTx(t, e.Chain, c, data) +func (e *Executor) DeployContract(t testing.TB, c *Contract, data interface{}) util.Uint256 { + return e.DeployContractBy(t, e.Validator, c, data) +} + +// DeployContractBy compiles and deploys contract to bc using provided signer. +// It also checks that precalculated contract hash matches the actual one. +// data is an optional argument to `_deploy`. +// Returns hash of the deploy transaction. +func (e *Executor) DeployContractBy(t testing.TB, signer Signer, c *Contract, data interface{}) util.Uint256 { + tx := NewDeployTxBy(t, e.Chain, signer, c, data) e.AddNewBlock(t, tx) e.CheckHalt(t, tx.Hash()) @@ -148,9 +163,9 @@ func (e *Executor) DeployContract(t *testing.T, c *Contract, data interface{}) u return tx.Hash() } -// DeployContractCheckFAULT compiles and deploys contract to bc. It checks that deploy -// transaction FAULTed with the specified error. -func (e *Executor) DeployContractCheckFAULT(t *testing.T, c *Contract, data interface{}, errMessage string) { +// DeployContractCheckFAULT compiles and deploys contract to bc using validator +// account. It checks that deploy transaction FAULTed with the specified error. +func (e *Executor) DeployContractCheckFAULT(t testing.TB, c *Contract, data interface{}, errMessage string) { tx := e.NewDeployTx(t, e.Chain, c, data) e.AddNewBlock(t, tx) e.CheckFault(t, tx.Hash(), errMessage) @@ -158,31 +173,41 @@ func (e *Executor) DeployContractCheckFAULT(t *testing.T, c *Contract, data inte // InvokeScript adds transaction with the specified script to the chain and // returns its hash. It does no faults check. -func (e *Executor) InvokeScript(t *testing.T, script []byte, signers []Signer) util.Uint256 { - tx := transaction.New(script, 0) - tx.Nonce = Nonce() - tx.ValidUntilBlock = e.Chain.BlockHeight() + 1 - e.SignTx(t, tx, -1, signers...) +func (e *Executor) InvokeScript(t testing.TB, script []byte, signers []Signer) util.Uint256 { + tx := e.PrepareInvocation(t, script, signers) e.AddNewBlock(t, tx) return tx.Hash() } +// PrepareInvocation creates transaction with the specified script and signs it +// by the provided signer. +func (e *Executor) PrepareInvocation(t testing.TB, script []byte, signers []Signer, validUntilBlock ...uint32) *transaction.Transaction { + tx := transaction.New(script, 0) + tx.Nonce = Nonce() + tx.ValidUntilBlock = e.Chain.BlockHeight() + 1 + if len(validUntilBlock) != 0 { + tx.ValidUntilBlock = validUntilBlock[0] + } + e.SignTx(t, tx, -1, signers...) + return tx +} + // InvokeScriptCheckHALT adds transaction with the specified script to the chain // and checks it's HALTed with the specified items on stack. -func (e *Executor) InvokeScriptCheckHALT(t *testing.T, script []byte, signers []Signer, stack ...stackitem.Item) { +func (e *Executor) InvokeScriptCheckHALT(t testing.TB, script []byte, signers []Signer, stack ...stackitem.Item) { hash := e.InvokeScript(t, script, signers) e.CheckHalt(t, hash, stack...) } // InvokeScriptCheckFAULT adds transaction with the specified script to the // chain and checks it's FAULTed with the specified error. -func (e *Executor) InvokeScriptCheckFAULT(t *testing.T, script []byte, signers []Signer, errMessage string) { +func (e *Executor) InvokeScriptCheckFAULT(t testing.TB, script []byte, signers []Signer, errMessage string) { hash := e.InvokeScript(t, script, signers) e.CheckFault(t, hash, errMessage) } // CheckHalt checks that transaction persisted with HALT state. -func (e *Executor) CheckHalt(t *testing.T, h util.Uint256, stack ...stackitem.Item) *state.AppExecResult { +func (e *Executor) CheckHalt(t testing.TB, h util.Uint256, stack ...stackitem.Item) *state.AppExecResult { aer, err := e.Chain.GetAppExecResults(h, trigger.Application) require.NoError(t, err) require.Equal(t, vm.HaltState, aer[0].VMState, aer[0].FaultException) @@ -194,7 +219,7 @@ func (e *Executor) CheckHalt(t *testing.T, h util.Uint256, stack ...stackitem.It // CheckFault checks that transaction persisted with FAULT state. // Raised exception is also checked to contain s as a substring. -func (e *Executor) CheckFault(t *testing.T, h util.Uint256, s string) { +func (e *Executor) CheckFault(t testing.TB, h util.Uint256, s string) { aer, err := e.Chain.GetAppExecResults(h, trigger.Application) require.NoError(t, err) require.Equal(t, vm.FaultState, aer[0].VMState) @@ -204,7 +229,7 @@ func (e *Executor) CheckFault(t *testing.T, h util.Uint256, s string) { // CheckTxNotificationEvent checks that specified event was emitted at the specified position // during transaction script execution. Negative index corresponds to backwards enumeration. -func (e *Executor) CheckTxNotificationEvent(t *testing.T, h util.Uint256, index int, expected state.NotificationEvent) { +func (e *Executor) CheckTxNotificationEvent(t testing.TB, h util.Uint256, index int, expected state.NotificationEvent) { aer, err := e.Chain.GetAppExecResults(h, trigger.Application) require.NoError(t, err) l := len(aer[0].Events) @@ -216,13 +241,24 @@ func (e *Executor) CheckTxNotificationEvent(t *testing.T, h util.Uint256, index } // CheckGASBalance ensures that provided account owns specified amount of GAS. -func (e *Executor) CheckGASBalance(t *testing.T, acc util.Uint160, expected *big.Int) { +func (e *Executor) CheckGASBalance(t testing.TB, acc util.Uint160, expected *big.Int) { actual := e.Chain.GetUtilityTokenBalance(acc) require.Equal(t, expected, actual, fmt.Errorf("invalid GAS balance: expected %s, got %s", expected.String(), actual.String())) } +// EnsureGASBalance ensures that provided account owns amount of GAS that satisfies provided condition. +func (e *Executor) EnsureGASBalance(t testing.TB, acc util.Uint160, isOk func(balance *big.Int) bool) { + actual := e.Chain.GetUtilityTokenBalance(acc) + require.True(t, isOk(actual), fmt.Errorf("invalid GAS balance: got %s, condition is not satisfied", actual.String())) +} + // NewDeployTx returns new deployment tx for contract signed by committee. -func (e *Executor) NewDeployTx(t *testing.T, bc blockchainer.Blockchainer, c *Contract, data interface{}) *transaction.Transaction { +func (e *Executor) NewDeployTx(t testing.TB, bc blockchainer.Blockchainer, c *Contract, data interface{}) *transaction.Transaction { + return NewDeployTxBy(t, bc, e.Validator, c, data) +} + +// NewDeployTxBy returns new deployment tx for contract signed by the specified signer. +func NewDeployTxBy(t testing.TB, bc blockchainer.Blockchainer, signer Signer, c *Contract, data interface{}) *transaction.Transaction { rawManifest, err := json.Marshal(c.Manifest) require.NoError(t, err) @@ -237,11 +273,11 @@ func (e *Executor) NewDeployTx(t *testing.T, bc blockchainer.Blockchainer, c *Co tx.Nonce = Nonce() tx.ValidUntilBlock = bc.BlockHeight() + 1 tx.Signers = []transaction.Signer{{ - Account: e.Validator.ScriptHash(), + Account: signer.ScriptHash(), Scopes: transaction.Global, }} - addNetworkFee(bc, tx, e.Validator) - require.NoError(t, e.Validator.SignTx(netmode.UnitTestNet, tx)) + addNetworkFee(bc, tx, signer) + require.NoError(t, signer.SignTx(netmode.UnitTestNet, tx)) return tx } @@ -266,7 +302,7 @@ func addNetworkFee(bc blockchainer.Blockchainer, tx *transaction.Transaction, si } // NewUnsignedBlock creates new unsigned block from txs. -func (e *Executor) NewUnsignedBlock(t *testing.T, txs ...*transaction.Transaction) *block.Block { +func (e *Executor) NewUnsignedBlock(t testing.TB, txs ...*transaction.Transaction) *block.Block { lastBlock := e.TopBlock(t) b := &block.Block{ Header: block.Header{ @@ -289,7 +325,7 @@ func (e *Executor) NewUnsignedBlock(t *testing.T, txs ...*transaction.Transactio } // AddNewBlock creates a new block from provided transactions and adds it on bc. -func (e *Executor) AddNewBlock(t *testing.T, txs ...*transaction.Transaction) *block.Block { +func (e *Executor) AddNewBlock(t testing.TB, txs ...*transaction.Transaction) *block.Block { b := e.NewUnsignedBlock(t, txs...) e.SignBlock(b) require.NoError(t, e.Chain.AddBlock(b)) @@ -297,10 +333,12 @@ func (e *Executor) AddNewBlock(t *testing.T, txs ...*transaction.Transaction) *b } // GenerateNewBlocks adds specified number of empty blocks to the chain. -func (e *Executor) GenerateNewBlocks(t *testing.T, count int) { +func (e *Executor) GenerateNewBlocks(t testing.TB, count int) []*block.Block { + blocks := make([]*block.Block, count) for i := 0; i < count; i++ { - e.AddNewBlock(t) + blocks[i] = e.AddNewBlock(t) } + return blocks } // SignBlock add validators signature to b. @@ -311,7 +349,7 @@ func (e *Executor) SignBlock(b *block.Block) *block.Block { } // AddBlockCheckHalt is a convenient wrapper over AddBlock and CheckHalt. -func (e *Executor) AddBlockCheckHalt(t *testing.T, txs ...*transaction.Transaction) *block.Block { +func (e *Executor) AddBlockCheckHalt(t testing.TB, txs ...*transaction.Transaction) *block.Block { b := e.AddNewBlock(t, txs...) for _, tx := range txs { e.CheckHalt(t, tx.Hash()) @@ -344,14 +382,14 @@ func TestInvoke(bc blockchainer.Blockchainer, tx *transaction.Transaction) (*vm. } // GetTransaction returns transaction and its height by the specified hash. -func (e *Executor) GetTransaction(t *testing.T, h util.Uint256) (*transaction.Transaction, uint32) { +func (e *Executor) GetTransaction(t testing.TB, h util.Uint256) (*transaction.Transaction, uint32) { tx, height, err := e.Chain.GetTransaction(h) require.NoError(t, err) return tx, height } // GetBlockByIndex returns block by the specified index. -func (e *Executor) GetBlockByIndex(t *testing.T, idx int) *block.Block { +func (e *Executor) GetBlockByIndex(t testing.TB, idx int) *block.Block { h := e.Chain.GetHeaderHash(idx) require.NotEmpty(t, h) b, err := e.Chain.GetBlock(h) @@ -360,7 +398,7 @@ func (e *Executor) GetBlockByIndex(t *testing.T, idx int) *block.Block { } // GetTxExecResult returns application execution results for the specified transaction. -func (e *Executor) GetTxExecResult(t *testing.T, h util.Uint256) *state.AppExecResult { +func (e *Executor) GetTxExecResult(t testing.TB, h util.Uint256) *state.AppExecResult { aer, err := e.Chain.GetAppExecResults(h, trigger.Application) require.NoError(t, err) require.Equal(t, 1, len(aer)) diff --git a/pkg/neotest/chain/chain.go b/pkg/neotest/chain/chain.go index 5cbec396b..a98b4c10b 100644 --- a/pkg/neotest/chain/chain.go +++ b/pkg/neotest/chain/chain.go @@ -121,15 +121,14 @@ func init() { // this package. MemoryStore is used as the backend storage, so all of the chain // contents is always in RAM. The Signer returned is validator (and committee at // the same time). -func NewSingle(t *testing.T) (*core.Blockchain, neotest.Signer) { +func NewSingle(t testing.TB) (*core.Blockchain, neotest.Signer) { return NewSingleWithCustomConfig(t, nil) } // NewSingleWithCustomConfig is similar to NewSingle, but allows to override the // default configuration. -func NewSingleWithCustomConfig(t *testing.T, f func(*config.ProtocolConfiguration)) (*core.Blockchain, neotest.Signer) { - st := storage.NewMemoryStore() - return NewSingleWithCustomConfigAndStore(t, f, st, true) +func NewSingleWithCustomConfig(t testing.TB, f func(*config.ProtocolConfiguration)) (*core.Blockchain, neotest.Signer) { + return NewSingleWithCustomConfigAndStore(t, f, nil, true) } // NewSingleWithCustomConfigAndStore is similar to NewSingleWithCustomConfig, but @@ -137,7 +136,7 @@ func NewSingleWithCustomConfig(t *testing.T, f func(*config.ProtocolConfiguratio // Run method is called on the Blockchain instance, if not then it's caller's // responsibility to do that before using the chain and its caller's responsibility // also to properly Close the chain when done. -func NewSingleWithCustomConfigAndStore(t *testing.T, f func(cfg *config.ProtocolConfiguration), st storage.Store, run bool) (*core.Blockchain, neotest.Signer) { +func NewSingleWithCustomConfigAndStore(t testing.TB, f func(cfg *config.ProtocolConfiguration), st storage.Store, run bool) (*core.Blockchain, neotest.Signer) { protoCfg := config.ProtocolConfiguration{ Magic: netmode.UnitTestNet, MaxTraceableBlocks: MaxTraceableBlocks, @@ -150,6 +149,9 @@ func NewSingleWithCustomConfigAndStore(t *testing.T, f func(cfg *config.Protocol if f != nil { f(&protoCfg) } + if st == nil { + st = storage.NewMemoryStore() + } log := zaptest.NewLogger(t) bc, err := core.NewBlockchain(st, protoCfg, log) require.NoError(t, err) @@ -163,13 +165,34 @@ func NewSingleWithCustomConfigAndStore(t *testing.T, f func(cfg *config.Protocol // NewMulti creates new blockchain instance with four validators and six // committee members, otherwise not differring much from NewSingle. The // second value returned contains validators Signer, the third -- committee one. -func NewMulti(t *testing.T) (*core.Blockchain, neotest.Signer, neotest.Signer) { +func NewMulti(t testing.TB) (*core.Blockchain, neotest.Signer, neotest.Signer) { return NewMultiWithCustomConfig(t, nil) } // NewMultiWithCustomConfig is similar to NewMulti except it allows to override the // default configuration. -func NewMultiWithCustomConfig(t *testing.T, f func(*config.ProtocolConfiguration)) (*core.Blockchain, neotest.Signer, neotest.Signer) { +func NewMultiWithCustomConfig(t testing.TB, f func(*config.ProtocolConfiguration)) (*core.Blockchain, neotest.Signer, neotest.Signer) { + return NewMultiWithCustomConfigAndStore(t, f, nil, true) +} + +// NewMultiWithCustomConfigAndStore is similar to NewMultiWithCustomConfig, but +// also allows to override backend Store being used. The last parameter controls if +// Run method is called on the Blockchain instance, if not then it's caller's +// responsibility to do that before using the chain and its caller's responsibility +// also to properly Close the chain when done. +func NewMultiWithCustomConfigAndStore(t testing.TB, f func(*config.ProtocolConfiguration), st storage.Store, run bool) (*core.Blockchain, neotest.Signer, neotest.Signer) { + bc, validator, committee, err := NewMultiWithCustomConfigAndStoreNoCheck(t, f, st) + require.NoError(t, err) + if run { + go bc.Run() + t.Cleanup(bc.Close) + } + return bc, validator, committee +} + +// NewMultiWithCustomConfigAndStoreNoCheck is similar to NewMultiWithCustomConfig, +// but do not perform Blockchain run and do not check Blockchain constructor error. +func NewMultiWithCustomConfigAndStoreNoCheck(t testing.TB, f func(*config.ProtocolConfiguration), st storage.Store) (*core.Blockchain, neotest.Signer, neotest.Signer, error) { protoCfg := config.ProtocolConfiguration{ Magic: netmode.UnitTestNet, MaxTraceableBlocks: MaxTraceableBlocks, @@ -182,12 +205,11 @@ func NewMultiWithCustomConfig(t *testing.T, f func(*config.ProtocolConfiguration if f != nil { f(&protoCfg) } + if st == nil { + st = storage.NewMemoryStore() + } - st := storage.NewMemoryStore() log := zaptest.NewLogger(t) bc, err := core.NewBlockchain(st, protoCfg, log) - require.NoError(t, err) - go bc.Run() - t.Cleanup(bc.Close) - return bc, neotest.NewMultiSigner(multiValidatorAcc...), neotest.NewMultiSigner(multiCommitteeAcc...) + return bc, neotest.NewMultiSigner(multiValidatorAcc...), neotest.NewMultiSigner(multiCommitteeAcc...), err } diff --git a/pkg/neotest/client.go b/pkg/neotest/client.go index 7767b02c8..ba8aaf695 100644 --- a/pkg/neotest/client.go +++ b/pkg/neotest/client.go @@ -19,6 +19,15 @@ type ContractInvoker struct { Signers []Signer } +// NewInvoker creates new ContractInvoker for contract with hash h and specified signers. +func (e *Executor) NewInvoker(h util.Uint160, signers ...Signer) *ContractInvoker { + return &ContractInvoker{ + Executor: e, + Hash: h, + Signers: signers, + } +} + // CommitteeInvoker creates new ContractInvoker for contract with hash h and committee multisignature signer. func (e *Executor) CommitteeInvoker(h util.Uint160) *ContractInvoker { return &ContractInvoker{ @@ -38,7 +47,7 @@ func (e *Executor) ValidatorInvoker(h util.Uint160) *ContractInvoker { } // TestInvoke creates test VM and invokes method with args. -func (c *ContractInvoker) TestInvoke(t *testing.T, method string, args ...interface{}) (*vm.Stack, error) { +func (c *ContractInvoker) TestInvoke(t testing.TB, method string, args ...interface{}) (*vm.Stack, error) { tx := c.PrepareInvokeNoSign(t, method, args...) b := c.NewUnsignedBlock(t, tx) ic := c.Chain.GetTestVM(trigger.Application, tx, b) @@ -57,18 +66,18 @@ func (c *ContractInvoker) WithSigners(signers ...Signer) *ContractInvoker { } // PrepareInvoke creates new invocation transaction. -func (c *ContractInvoker) PrepareInvoke(t *testing.T, method string, args ...interface{}) *transaction.Transaction { +func (c *ContractInvoker) PrepareInvoke(t testing.TB, method string, args ...interface{}) *transaction.Transaction { return c.Executor.NewTx(t, c.Signers, c.Hash, method, args...) } // PrepareInvokeNoSign creates new unsigned invocation transaction. -func (c *ContractInvoker) PrepareInvokeNoSign(t *testing.T, method string, args ...interface{}) *transaction.Transaction { +func (c *ContractInvoker) PrepareInvokeNoSign(t testing.TB, method string, args ...interface{}) *transaction.Transaction { return c.Executor.NewUnsignedTx(t, c.Hash, method, args...) } // Invoke invokes method with args, persists transaction and checks the result. // Returns transaction hash. -func (c *ContractInvoker) Invoke(t *testing.T, result interface{}, method string, args ...interface{}) util.Uint256 { +func (c *ContractInvoker) Invoke(t testing.TB, result interface{}, method string, args ...interface{}) util.Uint256 { tx := c.PrepareInvoke(t, method, args...) c.AddNewBlock(t, tx) c.CheckHalt(t, tx.Hash(), stackitem.Make(result)) @@ -77,7 +86,7 @@ func (c *ContractInvoker) Invoke(t *testing.T, result interface{}, method string // InvokeAndCheck invokes method with args, persists transaction and checks the result // using provided function. Returns transaction hash. -func (c *ContractInvoker) InvokeAndCheck(t *testing.T, checkResult func(t *testing.T, stack []stackitem.Item), method string, args ...interface{}) util.Uint256 { +func (c *ContractInvoker) InvokeAndCheck(t testing.TB, checkResult func(t testing.TB, stack []stackitem.Item), method string, args ...interface{}) util.Uint256 { tx := c.PrepareInvoke(t, method, args...) c.AddNewBlock(t, tx) aer, err := c.Chain.GetAppExecResults(tx.Hash(), trigger.Application) @@ -90,7 +99,7 @@ func (c *ContractInvoker) InvokeAndCheck(t *testing.T, checkResult func(t *testi } // InvokeWithFeeFail is like InvokeFail but sets custom system fee for the transaction. -func (c *ContractInvoker) InvokeWithFeeFail(t *testing.T, message string, sysFee int64, method string, args ...interface{}) util.Uint256 { +func (c *ContractInvoker) InvokeWithFeeFail(t testing.TB, message string, sysFee int64, method string, args ...interface{}) util.Uint256 { tx := c.PrepareInvokeNoSign(t, method, args...) c.Executor.SignTx(t, tx, sysFee, c.Signers...) c.AddNewBlock(t, tx) @@ -100,7 +109,7 @@ func (c *ContractInvoker) InvokeWithFeeFail(t *testing.T, message string, sysFee // InvokeFail invokes method with args, persists transaction and checks the error message. // Returns transaction hash. -func (c *ContractInvoker) InvokeFail(t *testing.T, message string, method string, args ...interface{}) { +func (c *ContractInvoker) InvokeFail(t testing.TB, message string, method string, args ...interface{}) { tx := c.PrepareInvoke(t, method, args...) c.AddNewBlock(t, tx) c.CheckFault(t, tx.Hash(), message) diff --git a/pkg/neotest/compile.go b/pkg/neotest/compile.go index 1fff8b961..4679ec269 100644 --- a/pkg/neotest/compile.go +++ b/pkg/neotest/compile.go @@ -25,7 +25,7 @@ type Contract struct { var contracts = make(map[string]*Contract) // CompileSource compiles contract from reader and returns it's NEF, manifest and hash. -func CompileSource(t *testing.T, sender util.Uint160, src io.Reader, opts *compiler.Options) *Contract { +func CompileSource(t testing.TB, sender util.Uint160, src io.Reader, opts *compiler.Options) *Contract { // nef.NewFile() cares about version a lot. config.Version = "neotest" @@ -43,7 +43,7 @@ func CompileSource(t *testing.T, sender util.Uint160, src io.Reader, opts *compi } // CompileFile compiles contract from file and returns it's NEF, manifest and hash. -func CompileFile(t *testing.T, sender util.Uint160, srcPath string, configPath string) *Contract { +func CompileFile(t testing.TB, sender util.Uint160, srcPath string, configPath string) *Contract { if c, ok := contracts[srcPath]; ok { return c } @@ -66,6 +66,8 @@ func CompileFile(t *testing.T, sender util.Uint160, srcPath string, configPath s o.Permissions[i] = manifest.Permission(conf.Permissions[i]) } o.SafeMethods = conf.SafeMethods + o.Overloads = conf.Overloads + o.SourceURL = conf.SourceURL m, err := compiler.CreateManifest(di, o) require.NoError(t, err) diff --git a/pkg/neotest/signer.go b/pkg/neotest/signer.go index e9580626c..e9f8ba9e3 100644 --- a/pkg/neotest/signer.go +++ b/pkg/neotest/signer.go @@ -162,7 +162,7 @@ func (m multiSigner) Single(n int) SingleSigner { return NewSingleSigner(wallet.NewAccountFromPrivateKey(m.accounts[n].PrivateKey())) } -func checkMultiSigner(t *testing.T, s Signer) { +func checkMultiSigner(t testing.TB, s Signer) { ms, ok := s.(multiSigner) require.True(t, ok, "expected to be a multi-signer") diff --git a/pkg/rpc/server/server_test.go b/pkg/rpc/server/server_test.go index 71eab16d8..96d79ebf9 100644 --- a/pkg/rpc/server/server_test.go +++ b/pkg/rpc/server/server_test.go @@ -61,17 +61,17 @@ type rpcTestCase struct { check func(t *testing.T, e *executor, result interface{}) } -const genesisBlockHash = "a4ae00f6ac7496cac14e709fbf8b8ecb4c9831d8a6ee396056af9350fcf22671" -const testContractHash = "1ab08f5508edafa6f28e3db3227442a9e70aac52" -const deploymentTxHash = "017c9edb217477aeb3e0c35462361209fdb7bf104dc8e285e2385af8713926b4" +const genesisBlockHash = "f42e2ae74bbea6aa1789fdc4efa35ad55b04335442637c091eafb5b0e779dae7" +const testContractHash = "2db7d679c538ace5f00495c9e9d8ea95f1e0f5a5" +const deploymentTxHash = "496bccb5cb0a008ef9b7a32c459e508ef24fbb0830f82bac9162afa4ca804839" const ( - verifyContractHash = "7deef31e5c616e157cdf02a5446f36d0a4eead52" + verifyContractHash = "06ed5314c2e4cb103029a60b86d46afa2fb8f67c" verifyContractAVM = "VwIAQS1RCDBwDBTunqIsJ+NL0BSPxBCOCPdOj1BIskrZMCQE2zBxaBPOStkoJATbKGlK2SgkBNsol0A=" - verifyWithArgsContractHash = "6df009754ce475a6a5730c9e488f80e8e47bc1f1" - nnsContractHash = "1a7530a4c6cfdd40ffed40775aa5453febab24c0" + verifyWithArgsContractHash = "0dce75f52adb1a4c5c6eaa6a34eb26db2e5b3781" + nnsContractHash = "ee92563903e4efd53565784080b2dbdc5c37e21f" nnsToken1ID = "6e656f2e636f6d" - nfsoContractHash = "aaf8913c501e25c42877e79f04cb7c2c1ab47e57" + nfsoContractHash = "5f9ebd6b001b54c7bc70f96e0412fcf415dfe09f" nfsoToken1ID = "7e244ffd6aa85fb1579d2ed22e9b761ab62e3486" invokescriptContractAVM = "VwIADBQBDAMOBQYMDQIODw0DDgcJAAAAAErZMCQE2zBwaEH4J+yMqiYEEUAMFA0PAwIJAAIBAwcDBAUCAQAOBgwJStkwJATbMHFpQfgn7IyqJgQSQBNA" ) @@ -279,7 +279,7 @@ var rpcTestCases = map[string][]rpcTestCase{ result: func(e *executor) interface{} { return &map[string]interface{}{ "name": "neo.com", - "expiration": "HrL+G4YB", + "expiration": "lhbLRl0B", } }, }, @@ -882,7 +882,7 @@ var rpcTestCases = map[string][]rpcTestCase{ name: "positive, with notifications", params: `["` + nnsContractHash + `", "transfer", [{"type":"Hash160", "value":"0x0bcd2978634d961c24f5aea0802297ff128724d6"},{"type":"String", "value":"neo.com"},{"type":"Any", "value":null}],["0xb248508f4ef7088e10c48f14d04be3272ca29eee"]]`, result: func(e *executor) interface{} { - script := []byte{0x0b, 0x0c, 0x07, 0x6e, 0x65, 0x6f, 0x2e, 0x63, 0x6f, 0x6d, 0x0c, 0x14, 0xd6, 0x24, 0x87, 0x12, 0xff, 0x97, 0x22, 0x80, 0xa0, 0xae, 0xf5, 0x24, 0x1c, 0x96, 0x4d, 0x63, 0x78, 0x29, 0xcd, 0x0b, 0x13, 0xc0, 0x1f, 0x0c, 0x08, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x66, 0x65, 0x72, 0x0c, 0x14, 0xc0, 0x24, 0xab, 0xeb, 0x3f, 0x45, 0xa5, 0x5a, 0x77, 0x40, 0xed, 0xff, 0x40, 0xdd, 0xcf, 0xc6, 0xa4, 0x30, 0x75, 0x1a, 0x41, 0x62, 0x7d, 0x5b, 0x52} + script := []byte{0x0b, 0x0c, 0x07, 0x6e, 0x65, 0x6f, 0x2e, 0x63, 0x6f, 0x6d, 0x0c, 0x14, 0xd6, 0x24, 0x87, 0x12, 0xff, 0x97, 0x22, 0x80, 0xa0, 0xae, 0xf5, 0x24, 0x1c, 0x96, 0x4d, 0x63, 0x78, 0x29, 0xcd, 0xb, 0x13, 0xc0, 0x1f, 0xc, 0x8, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x66, 0x65, 0x72, 0xc, 0x14, 0x1f, 0xe2, 0x37, 0x5c, 0xdc, 0xdb, 0xb2, 0x80, 0x40, 0x78, 0x65, 0x35, 0xd5, 0xef, 0xe4, 0x3, 0x39, 0x56, 0x92, 0xee, 0x41, 0x62, 0x7d, 0x5b, 0x52} return &result.Invoke{ State: "HALT", GasConsumed: 32167260, @@ -915,7 +915,7 @@ var rpcTestCases = map[string][]rpcTestCase{ chg := []storage.Operation{{ State: "Changed", Key: []byte{0xfa, 0xff, 0xff, 0xff, 0xb}, - Value: []byte{0xe8, 0x80, 0x64, 0xcb, 0x53, 0x79, 0x12}, + Value: []byte{0x1e, 0xb, 0xca, 0xeb, 0x53, 0x79, 0x12}, }, { State: "Added", Key: []byte{0xfb, 0xff, 0xff, 0xff, 0x14, 0xd6, 0x24, 0x87, 0x12, 0xff, 0x97, 0x22, 0x80, 0xa0, 0xae, 0xf5, 0x24, 0x1c, 0x96, 0x4d, 0x63, 0x78, 0x29, 0xcd, 0xb}, @@ -927,7 +927,7 @@ var rpcTestCases = map[string][]rpcTestCase{ }, { State: "Changed", Key: []byte{0xfa, 0xff, 0xff, 0xff, 0x14, 0xee, 0x9e, 0xa2, 0x2c, 0x27, 0xe3, 0x4b, 0xd0, 0x14, 0x8f, 0xc4, 0x10, 0x8e, 0x8, 0xf7, 0x4e, 0x8f, 0x50, 0x48, 0xb2}, - Value: []byte{0x41, 0x01, 0x21, 0x05, 0x9e, 0x0b, 0x0b, 0x18, 0x0b}, + Value: []byte{0x41, 0x01, 0x21, 0x05, 0xf6, 0x99, 0x28, 0x2d, 0xb}, }} // Can be returned in any order. assert.ElementsMatch(t, chg, res.Diagnostics.Changes) @@ -937,7 +937,7 @@ var rpcTestCases = map[string][]rpcTestCase{ name: "positive, verbose", params: `["` + nnsContractHash + `", "resolve", [{"type":"String", "value":"neo.com"},{"type":"Integer","value":1}], [], true]`, result: func(e *executor) interface{} { - script := []byte{0x11, 0xc, 0x7, 0x6e, 0x65, 0x6f, 0x2e, 0x63, 0x6f, 0x6d, 0x12, 0xc0, 0x1f, 0xc, 0x7, 0x72, 0x65, 0x73, 0x6f, 0x6c, 0x76, 0x65, 0xc, 0x14, 0xc0, 0x24, 0xab, 0xeb, 0x3f, 0x45, 0xa5, 0x5a, 0x77, 0x40, 0xed, 0xff, 0x40, 0xdd, 0xcf, 0xc6, 0xa4, 0x30, 0x75, 0x1a, 0x41, 0x62, 0x7d, 0x5b, 0x52} + script := []byte{0x11, 0xc, 0x7, 0x6e, 0x65, 0x6f, 0x2e, 0x63, 0x6f, 0x6d, 0x12, 0xc0, 0x1f, 0xc, 0x7, 0x72, 0x65, 0x73, 0x6f, 0x6c, 0x76, 0x65, 0xc, 0x14, 0x1f, 0xe2, 0x37, 0x5c, 0xdc, 0xdb, 0xb2, 0x80, 0x40, 0x78, 0x65, 0x35, 0xd5, 0xef, 0xe4, 0x3, 0x39, 0x56, 0x92, 0xee, 0x41, 0x62, 0x7d, 0x5b, 0x52} stdHash, _ := e.chain.GetNativeContractScriptHash(nativenames.StdLib) cryptoHash, _ := e.chain.GetNativeContractScriptHash(nativenames.CryptoLib) return &result.Invoke{ @@ -1205,12 +1205,12 @@ var rpcTestCases = map[string][]rpcTestCase{ "sendrawtransaction": { { name: "positive", - params: `["ADQSAADA2KcAAAAAABDiEgAAAAAAgBYAAAHunqIsJ+NL0BSPxBCOCPdOj1BIsgEAYBDAAwDodkgXAAAADBQRJlu0FyUAQb4E6PokDjj1fB5WmwwU7p6iLCfjS9AUj8QQjgj3To9QSLIUwB8MCHRyYW5zZmVyDBT1Y+pAvCg9TQ4FxI6jBbPyoHNA70FifVtSOQFCDEBRp0p08GFA2rYC/Xrol8DIhXEMfVMbUJEYer1RqZSatmTjUJE9fnZtDGkQEX/zQ7yOhbnIPAZIrllUTuUBskhUKAwhArNiK/QBe9/jF8WK7V9MdT8ga324lgRvp9d0u8S/f43CQVbnsyc="]`, + params: `["ABsAAACWP5gAAAAAAEDaEgAAAAAAFgAAAAHunqIsJ+NL0BSPxBCOCPdOj1BIsoAAXgsDAOh2SBcAAAAMFBEmW7QXJQBBvgTo+iQOOPV8HlabDBTunqIsJ+NL0BSPxBCOCPdOj1BIshTAHwwIdHJhbnNmZXIMFPVj6kC8KD1NDgXEjqMFs/Kgc0DvQWJ9W1IBQgxAOv87rSn7OV7Y/wuVE58QaSz0o0wv37hWY08RZFP2kYYgSPvemZiT69wf6QeAUTABJ1JosxgIUory9vXv0kkpXSgMIQKzYiv0AXvf4xfFiu1fTHU/IGt9uJYEb6fXdLvEv3+NwkFW57Mn"]`, result: func(e *executor) interface{} { return &result.RelayResult{} }, check: func(t *testing.T, e *executor, inv interface{}) { res, ok := inv.(*result.RelayResult) require.True(t, ok) - expectedHash := "8ea251d812fbbdecaebfc164fb6afbd78b7db94f7dacb69421cd5d4e364522d2" + expectedHash := "acc3e13102c211068d06ff64034d6f7e2d4db00c1703d0dec8afa73560664fe1" assert.Equal(t, expectedHash, res.Hash.StringLE()) }, }, @@ -1945,12 +1945,12 @@ func testRPCProtocol(t *testing.T, doRPCCall func(string, string, *testing.T) [] require.NoError(t, json.Unmarshal(res, actual)) checkNep17TransfersAux(t, e, actual, sent, rcvd) } - t.Run("time frame only", func(t *testing.T) { testNEP17T(t, 4, 5, 0, 0, []int{14, 15, 16, 17}, []int{3, 4}) }) + t.Run("time frame only", func(t *testing.T) { testNEP17T(t, 4, 5, 0, 0, []int{17, 18, 19, 20}, []int{3, 4}) }) t.Run("no res", func(t *testing.T) { testNEP17T(t, 100, 100, 0, 0, []int{}, []int{}) }) - t.Run("limit", func(t *testing.T) { testNEP17T(t, 1, 7, 3, 0, []int{11, 12}, []int{2}) }) - t.Run("limit 2", func(t *testing.T) { testNEP17T(t, 4, 5, 2, 0, []int{14}, []int{3}) }) - t.Run("limit with page", func(t *testing.T) { testNEP17T(t, 1, 7, 3, 1, []int{13, 14}, []int{3}) }) - t.Run("limit with page 2", func(t *testing.T) { testNEP17T(t, 1, 7, 3, 2, []int{15, 16}, []int{4}) }) + t.Run("limit", func(t *testing.T) { testNEP17T(t, 1, 7, 3, 0, []int{14, 15}, []int{2}) }) + t.Run("limit 2", func(t *testing.T) { testNEP17T(t, 4, 5, 2, 0, []int{17}, []int{3}) }) + t.Run("limit with page", func(t *testing.T) { testNEP17T(t, 1, 7, 3, 1, []int{16, 17}, []int{3}) }) + t.Run("limit with page 2", func(t *testing.T) { testNEP17T(t, 1, 7, 3, 2, []int{18, 19}, []int{4}) }) }) } @@ -2086,7 +2086,7 @@ func checkNep17Balances(t *testing.T, e *executor, acc interface{}) { }, { Asset: e.chain.UtilityTokenHash(), - Amount: "46748035310", + Amount: "47102293830", LastUpdated: 19, }}, Address: testchain.PrivateKeyByID(0).GetScriptHash().StringLE(), @@ -2198,7 +2198,7 @@ func checkNep11TransfersAux(t *testing.T, e *executor, acc interface{}, sent, rc } func checkNep17Transfers(t *testing.T, e *executor, acc interface{}) { - checkNep17TransfersAux(t, e, acc, []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19}, []int{0, 1, 2, 3, 4, 5, 6, 7, 8}) + checkNep17TransfersAux(t, e, acc, []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22}, []int{0, 1, 2, 3, 4, 5, 6, 7, 8}) } func checkNep17TransfersAux(t *testing.T, e *executor, acc interface{}, sent, rcvd []int) { @@ -2224,8 +2224,11 @@ func checkNep17TransfersAux(t *testing.T, e *executor, acc interface{}, sent, rc blockPutNewTestValue, err := e.chain.GetBlock(e.chain.GetHeaderHash(16)) // invoke `put` method of `test_contract.go` with `testkey`, `newtestvalue` args require.NoError(t, err) - require.Equal(t, 1, len(blockPutNewTestValue.Transactions)) + require.Equal(t, 4, len(blockPutNewTestValue.Transactions)) txPutNewTestValue := blockPutNewTestValue.Transactions[0] + txPutValue1 := blockPutNewTestValue.Transactions[1] // invoke `put` method of `test_contract.go` with `aa`, `v1` args + txPutValue2 := blockPutNewTestValue.Transactions[2] // invoke `put` method of `test_contract.go` with `aa10`, `v2` args + txPutValue3 := blockPutNewTestValue.Transactions[3] // invoke `put` method of `test_contract.go` with `aa50`, `v3` args blockSetRecord, err := e.chain.GetBlock(e.chain.GetHeaderHash(15)) // add type A record to `neo.com` domain via NNS require.NoError(t, err) @@ -2336,6 +2339,30 @@ func checkNep17TransfersAux(t *testing.T, e *executor, acc interface{}, sent, rc Index: 17, TxHash: blockDeploy5.Hash(), }, + { + Timestamp: blockPutNewTestValue.Timestamp, + Asset: e.chain.UtilityTokenHash(), + Address: "", // burn + Amount: big.NewInt(txPutValue3.SystemFee + txPutValue3.NetworkFee).String(), + Index: 16, + TxHash: blockPutNewTestValue.Hash(), + }, + { + Timestamp: blockPutNewTestValue.Timestamp, + Asset: e.chain.UtilityTokenHash(), + Address: "", // burn + Amount: big.NewInt(txPutValue2.SystemFee + txPutValue2.NetworkFee).String(), + Index: 16, + TxHash: blockPutNewTestValue.Hash(), + }, + { + Timestamp: blockPutNewTestValue.Timestamp, + Asset: e.chain.UtilityTokenHash(), + Address: "", // burn + Amount: big.NewInt(txPutValue1.SystemFee + txPutValue1.NetworkFee).String(), + Index: 16, + TxHash: blockPutNewTestValue.Hash(), + }, { Timestamp: blockPutNewTestValue.Timestamp, Asset: e.chain.UtilityTokenHash(), diff --git a/pkg/rpc/server/testdata/testblocks.acc b/pkg/rpc/server/testdata/testblocks.acc index f004f9cf5..243844ae2 100644 Binary files a/pkg/rpc/server/testdata/testblocks.acc and b/pkg/rpc/server/testdata/testblocks.acc differ diff --git a/pkg/rpc/server/testdata/verify/verification_contract.yml b/pkg/rpc/server/testdata/verify/verification_contract.yml new file mode 100644 index 000000000..5d18c74a7 --- /dev/null +++ b/pkg/rpc/server/testdata/verify/verification_contract.yml @@ -0,0 +1,2 @@ +name: "Verify" +sourceurl: https://github.com/nspcc-dev/neo-go/ diff --git a/pkg/rpc/server/testdata/verify_args/verification_with_args_contract.yml b/pkg/rpc/server/testdata/verify_args/verification_with_args_contract.yml new file mode 100644 index 000000000..2c70e5a7c --- /dev/null +++ b/pkg/rpc/server/testdata/verify_args/verification_with_args_contract.yml @@ -0,0 +1,2 @@ +name: "Verify with args" +sourceurl: https://github.com/nspcc-dev/neo-go/