From 233fca0c1e780da71b989f6a5e7c2a655cb25131 Mon Sep 17 00:00:00 2001 From: Evgeniy Stratonikov Date: Sat, 23 Oct 2021 14:28:03 +0300 Subject: [PATCH 1/7] neotest: add contract testing framework Signed-off-by: Evgeniy Stratonikov --- .gitignore | 5 +- pkg/neotest/account.go | 8 ++ pkg/neotest/basic.go | 272 +++++++++++++++++++++++++++++++++++++ pkg/neotest/chain/chain.go | 50 +++++++ pkg/neotest/compile.go | 85 ++++++++++++ 5 files changed, 417 insertions(+), 3 deletions(-) create mode 100644 pkg/neotest/account.go create mode 100644 pkg/neotest/basic.go create mode 100644 pkg/neotest/chain/chain.go create mode 100644 pkg/neotest/compile.go diff --git a/.gitignore b/.gitignore index 89e285f84..cdaca8c9e 100644 --- a/.gitignore +++ b/.gitignore @@ -28,9 +28,8 @@ bin/ *~ TAGS -# leveldb -chains/ -chain/ +# storage +/chains # patch *.orig diff --git a/pkg/neotest/account.go b/pkg/neotest/account.go new file mode 100644 index 000000000..00c95a7a0 --- /dev/null +++ b/pkg/neotest/account.go @@ -0,0 +1,8 @@ +package neotest + +var _nonce uint32 + +func nonce() uint32 { + _nonce++ + return _nonce +} diff --git a/pkg/neotest/basic.go b/pkg/neotest/basic.go new file mode 100644 index 000000000..b374e0d53 --- /dev/null +++ b/pkg/neotest/basic.go @@ -0,0 +1,272 @@ +package neotest + +import ( + "encoding/json" + "strings" + "testing" + + "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/blockchainer" + "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/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/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/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" +) + +// Executor is a wrapper over chain state. +type Executor struct { + Chain blockchainer.Blockchainer + Committee *wallet.Account + CommitteeHash util.Uint160 + Contracts map[string]*Contract +} + +// NewExecutor creates new executor instance from provided blockchain and committee. +func NewExecutor(t *testing.T, bc blockchainer.Blockchainer, committee *wallet.Account) *Executor { + require.Equal(t, 1, len(bc.GetConfig().StandbyCommittee)) + + return &Executor{ + Chain: bc, + Committee: committee, + CommitteeHash: committee.Contract.ScriptHash(), + Contracts: make(map[string]*Contract), + } +} + +// TopBlock returns block with the highest index. +func (e *Executor) TopBlock(t *testing.T) *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 { + h, err := e.Chain.GetNativeContractScriptHash(name) + require.NoError(t, err) + return h +} + +// 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 { + w := io.NewBufBinWriter() + emit.AppCall(w.BinWriter, hash, method, callflag.All, args...) + require.NoError(t, w.Err) + + script := w.Bytes() + tx := transaction.New(script, 0) + tx.Nonce = nonce() + tx.ValidUntilBlock = e.Chain.BlockHeight() + 1 + return tx +} + +// NewTx creates new transaction which invokes contract method. +// Transaction is signed with signer. +func (e *Executor) NewTx(t *testing.T, signer interface{}, + hash util.Uint160, method string, args ...interface{}) *transaction.Transaction { + tx := e.NewUnsignedTx(t, hash, method, args...) + return e.SignTx(t, tx, -1, signer) +} + +// SignTx signs a transaction using provided signers. +// signers can be either *wallet.Account or []*wallet.Account. +func (e *Executor) SignTx(t *testing.T, tx *transaction.Transaction, sysFee int64, signers interface{}) *transaction.Transaction { + switch s := signers.(type) { + case *wallet.Account: + tx.Signers = append(tx.Signers, transaction.Signer{ + Account: s.Contract.ScriptHash(), + Scopes: transaction.Global, + }) + addNetworkFee(e.Chain, tx, s) + addSystemFee(e.Chain, tx, sysFee) + require.NoError(t, s.SignTx(e.Chain.GetConfig().Magic, tx)) + case []*wallet.Account: + for _, acc := range s { + tx.Signers = append(tx.Signers, transaction.Signer{ + Account: acc.Contract.ScriptHash(), + Scopes: transaction.Global, + }) + } + for _, acc := range s { + addNetworkFee(e.Chain, tx, acc) + } + addSystemFee(e.Chain, tx, sysFee) + for _, acc := range s { + require.NoError(t, acc.SignTx(e.Chain.GetConfig().Magic, tx)) + } + default: + panic("invalid signer") + } + + return tx +} + +// NewAccount returns new account holding 100.0 GAS. This method advances the chain +// by one block with a transfer transaction. +func (e *Executor) NewAccount(t *testing.T) *wallet.Account { + acc, err := wallet.NewAccount() + require.NoError(t, err) + + tx := e.NewTx(t, e.Committee, + e.NativeHash(t, nativenames.Gas), "transfer", + e.Committee.Contract.ScriptHash(), acc.Contract.ScriptHash(), int64(100_0000_0000), nil) + e.AddNewBlock(t, tx) + e.CheckHalt(t, tx.Hash()) + return acc +} + +// DeployContract compiles and deploys contract to bc. +// 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) + e.AddNewBlock(t, tx) + e.CheckHalt(t, tx.Hash()) + return tx.Hash() +} + +// CheckHalt checks that transaction persisted with HALT state. +func (e *Executor) CheckHalt(t *testing.T, 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) + if len(stack) != 0 { + require.Equal(t, stack, aer[0].Stack) + } + return &aer[0] +} + +// 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) { + aer, err := e.Chain.GetAppExecResults(h, trigger.Application) + require.NoError(t, err) + require.Equal(t, vm.FaultState, aer[0].VMState) + require.True(t, strings.Contains(aer[0].FaultException, s), + "expected: %s, got: %s", s, aer[0].FaultException) +} + +// 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 { + rawManifest, err := json.Marshal(c.Manifest) + require.NoError(t, err) + + neb, err := c.NEF.Bytes() + require.NoError(t, err) + + buf := io.NewBufBinWriter() + emit.AppCall(buf.BinWriter, bc.ManagementContractHash(), "deploy", callflag.All, neb, rawManifest, data) + require.NoError(t, buf.Err) + + tx := transaction.New(buf.Bytes(), 100*native.GASFactor) + tx.Nonce = nonce() + tx.ValidUntilBlock = bc.BlockHeight() + 1 + tx.Signers = []transaction.Signer{{ + Account: e.Committee.Contract.ScriptHash(), + Scopes: transaction.Global, + }} + addNetworkFee(bc, tx, e.Committee) + require.NoError(t, e.Committee.SignTx(netmode.UnitTestNet, tx)) + return tx +} + +func addSystemFee(bc blockchainer.Blockchainer, tx *transaction.Transaction, sysFee int64) { + if sysFee >= 0 { + tx.SystemFee = sysFee + return + } + v, _ := TestInvoke(bc, tx) // ignore error to support failing transactions + tx.SystemFee = v.GasConsumed() +} + +func addNetworkFee(bc blockchainer.Blockchainer, tx *transaction.Transaction, sender *wallet.Account) { + baseFee := bc.GetPolicer().GetBaseExecFee() + size := io.GetVarSize(tx) + netFee, sizeDelta := fee.Calculate(baseFee, sender.Contract.Script) + tx.NetworkFee += netFee + size += sizeDelta + tx.NetworkFee += int64(size) * bc.FeePerByte() +} + +// NewUnsignedBlock creates new unsigned block from txs. +func (e *Executor) NewUnsignedBlock(t *testing.T, txs ...*transaction.Transaction) *block.Block { + lastBlock := e.TopBlock(t) + b := &block.Block{ + Header: block.Header{ + NextConsensus: e.Committee.Contract.ScriptHash(), + Script: transaction.Witness{ + VerificationScript: e.Committee.Contract.Script, + }, + Timestamp: lastBlock.Timestamp + 1, + }, + Transactions: txs, + } + if e.Chain.GetConfig().StateRootInHeader { + b.StateRootEnabled = true + b.PrevStateRoot = e.Chain.GetStateModule().CurrentLocalStateRoot() + } + b.PrevHash = lastBlock.Hash() + b.Index = e.Chain.BlockHeight() + 1 + b.RebuildMerkleRoot() + return b +} + +// 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 { + b := e.NewUnsignedBlock(t, txs...) + e.SignBlock(b) + require.NoError(t, e.Chain.AddBlock(b)) + return b +} + +// SignBlock add validators signature to b. +func (e *Executor) SignBlock(b *block.Block) *block.Block { + sign := e.Committee.PrivateKey().SignHashable(uint32(e.Chain.GetConfig().Magic), b) + b.Script.InvocationScript = append([]byte{byte(opcode.PUSHDATA1), 64}, sign...) + return b +} + +// AddBlockCheckHalt is a convenient wrapper over AddNewBlock and CheckHalt. +func (e *Executor) AddBlockCheckHalt(t *testing.T, bc blockchainer.Blockchainer, txs ...*transaction.Transaction) *block.Block { + b := e.AddNewBlock(t, txs...) + for _, tx := range txs { + e.CheckHalt(t, tx.Hash()) + } + return b +} + +// TestInvoke creates a test VM with dummy block and executes transaction in it. +func TestInvoke(bc blockchainer.Blockchainer, tx *transaction.Transaction) (*vm.VM, error) { + lastBlock, err := bc.GetBlock(bc.GetHeaderHash(int(bc.BlockHeight()))) + if err != nil { + return nil, err + } + b := &block.Block{ + Header: block.Header{ + Index: bc.BlockHeight() + 1, + Timestamp: lastBlock.Timestamp + 1, + }, + } + + // `GetTestVM` as well as `Run` can use transaction hash which will set cached value. + // This is unwanted behaviour so we explicitly copy transaction to perform execution. + ttx := *tx + v, f := bc.GetTestVM(trigger.Application, &ttx, b) + defer f() + + v.LoadWithFlags(tx.Script, callflag.All) + err = v.Run() + return v, err +} diff --git a/pkg/neotest/chain/chain.go b/pkg/neotest/chain/chain.go new file mode 100644 index 000000000..24d79393f --- /dev/null +++ b/pkg/neotest/chain/chain.go @@ -0,0 +1,50 @@ +package chain + +import ( + "encoding/hex" + "testing" + + "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/storage" + "github.com/nspcc-dev/neo-go/pkg/crypto/keys" + "github.com/nspcc-dev/neo-go/pkg/wallet" + "github.com/stretchr/testify/require" + "go.uber.org/zap/zaptest" +) + +const validatorWIF = "KxyjQ8eUa4FHt3Gvioyt1Wz29cTUrE4eTqX3yFSk1YFCsPL8uNsY" + +// committeeAcc is an account used to sign tx as a committee. +var committeeAcc *wallet.Account + +func init() { + committeeAcc, _ = wallet.NewAccountFromWIF(validatorWIF) + pubs := keys.PublicKeys{committeeAcc.PrivateKey().PublicKey()} + err := committeeAcc.ConvertMultisig(1, pubs) + if err != nil { + panic(err) + } +} + +// NewSingle creates new blockchain instance with a single validator and +// setups cleanup functions. +func NewSingle(t *testing.T) (*core.Blockchain, *wallet.Account) { + protoCfg := config.ProtocolConfiguration{ + Magic: netmode.UnitTestNet, + SecondsPerBlock: 1, + StandbyCommittee: []string{hex.EncodeToString(committeeAcc.PrivateKey().PublicKey().Bytes())}, + ValidatorsCount: 1, + VerifyBlocks: true, + VerifyTransactions: true, + } + + 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, committeeAcc +} diff --git a/pkg/neotest/compile.go b/pkg/neotest/compile.go new file mode 100644 index 000000000..cd9831c8d --- /dev/null +++ b/pkg/neotest/compile.go @@ -0,0 +1,85 @@ +package neotest + +import ( + "io" + "testing" + + "github.com/nspcc-dev/neo-go/cli/smartcontract" + "github.com/nspcc-dev/neo-go/pkg/compiler" + "github.com/nspcc-dev/neo-go/pkg/config" + "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" +) + +// Contract contains contract info for deployment. +type Contract struct { + Hash util.Uint160 + NEF *nef.File + Manifest *manifest.Manifest +} + +// contracts caches compiled contracts from FS across multiple tests. +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 { + // nef.NewFile() cares about version a lot. + config.Version = "neotest" + + avm, di, err := compiler.CompileWithDebugInfo(opts.Name, src) + require.NoError(t, err) + + ne, err := nef.NewFile(avm) + require.NoError(t, err) + + m, err := compiler.CreateManifest(di, opts) + require.NoError(t, err) + + return &Contract{ + Hash: state.CreateContractHash(sender, ne.Checksum, m.Name), + NEF: ne, + Manifest: m, + } +} + +// 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 { + if c, ok := contracts[srcPath]; ok { + return c + } + + // nef.NewFile() cares about version a lot. + config.Version = "neotest" + + avm, di, err := compiler.CompileWithDebugInfo(srcPath, nil) + require.NoError(t, err) + + ne, err := nef.NewFile(avm) + require.NoError(t, err) + + conf, err := smartcontract.ParseContractConfig(configPath) + require.NoError(t, err) + + o := &compiler.Options{} + o.Name = conf.Name + o.ContractEvents = conf.Events + o.ContractSupportedStandards = conf.SupportedStandards + o.Permissions = make([]manifest.Permission, len(conf.Permissions)) + for i := range conf.Permissions { + o.Permissions[i] = manifest.Permission(conf.Permissions[i]) + } + o.SafeMethods = conf.SafeMethods + m, err := compiler.CreateManifest(di, o) + require.NoError(t, err) + + c := &Contract{ + Hash: state.CreateContractHash(sender, ne.Checksum, m.Name), + NEF: ne, + Manifest: m, + } + contracts[srcPath] = c + return c +} From e3625152c60ce0c6435e70d131311283b119cb9b Mon Sep 17 00:00:00 2001 From: Evgeniy Stratonikov Date: Sat, 23 Oct 2021 14:28:25 +0300 Subject: [PATCH 2/7] core: move NNS test out of core Signed-off-by: Evgeniy Stratonikov --- .../tests/nonnative_name_service_test.go | 463 +++++++++++++++++ pkg/core/helper_test.go | 25 +- pkg/core/native_neo_test.go | 7 + pkg/core/nonnative_name_service_test.go | 470 ------------------ 4 files changed, 474 insertions(+), 491 deletions(-) create mode 100644 examples/nft-nd-nns/tests/nonnative_name_service_test.go delete mode 100644 pkg/core/nonnative_name_service_test.go diff --git a/examples/nft-nd-nns/tests/nonnative_name_service_test.go b/examples/nft-nd-nns/tests/nonnative_name_service_test.go new file mode 100644 index 000000000..1b23bb862 --- /dev/null +++ b/examples/nft-nd-nns/tests/nonnative_name_service_test.go @@ -0,0 +1,463 @@ +package tests + +import ( + "strings" + "testing" + + nns "github.com/nspcc-dev/neo-go/examples/nft-nd-nns" + "github.com/nspcc-dev/neo-go/pkg/compiler" + "github.com/nspcc-dev/neo-go/pkg/core/interop/interopnames" + "github.com/nspcc-dev/neo-go/pkg/core/transaction" + "github.com/nspcc-dev/neo-go/pkg/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/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/nspcc-dev/neo-go/pkg/wallet" + "github.com/stretchr/testify/require" +) + +func newExecutorWithNS(t *testing.T) (*neotest.Executor, util.Uint160) { + bc, acc := chain.NewSingle(t) + e := neotest.NewExecutor(t, bc, acc) + c := neotest.CompileFile(t, e.CommitteeHash, "..", "../nns.yml") + e.DeployContract(t, c, nil) + + h, err := e.Chain.GetContractScriptHash(1) + require.NoError(t, err) + require.Equal(t, c.Hash, h) + return e, c.Hash +} + +// +//func TestNameService_Price(t *testing.T) { +// e, nsHash := newExecutorWithNS(t) +// +// testGetSet(t, e.Chain, nsHash, "Price", +// defaultNameServiceDomainPrice, 0, 10000_00000000) +//} + +func TestNonfungible(t *testing.T) { + e, nsHash := newExecutorWithNS(t) + + acc := e.NewAccount(t) + testNameServiceInvokeAux(t, e, nsHash, defaultNameServiceSysfee, acc, "symbol", "NNS") + testNameServiceInvokeAux(t, e, nsHash, defaultNameServiceSysfee, acc, "decimals", 0) + testNameServiceInvokeAux(t, e, nsHash, defaultNameServiceSysfee, acc, "totalSupply", 0) +} + +func TestAddRoot(t *testing.T) { + e, nsHash := newExecutorWithNS(t) + + t.Run("invalid format", func(t *testing.T) { + testNameServiceInvoke(t, e, nsHash, "addRoot", nil, "") + }) + t.Run("not signed by committee", func(t *testing.T) { + acc := e.NewAccount(t) + tx := e.PrepareInvoke(t, acc, nsHash, "addRoot", "some") + e.AddBlock(t, tx) + e.CheckFault(t, tx.Hash(), "not witnessed by committee") + }) + + testNameServiceInvoke(t, e, nsHash, "addRoot", stackitem.Null{}, "some") + t.Run("already exists", func(t *testing.T) { + testNameServiceInvoke(t, e, nsHash, "addRoot", nil, "some") + }) +} + +func TestExpiration(t *testing.T) { + e, nsHash := newExecutorWithNS(t) + bc := e.Chain + + acc := e.NewAccount(t) + + testNameServiceInvoke(t, e, nsHash, "addRoot", stackitem.Null{}, "com") + testNameServiceInvokeAux(t, e, nsHash, defaultRegisterSysfee, acc, "register", + true, "first.com", acc.Contract.ScriptHash()) + + testNameServiceInvokeAux(t, e, nsHash, defaultNameServiceSysfee, acc, + "setRecord", stackitem.Null{}, "first.com", int64(nns.TXT), "sometext") + b1 := e.TopBlock(t) + + tx := e.PrepareInvoke(t, acc, nsHash, "register", "second.com", acc.Contract.ScriptHash()) + b2 := e.NewBlock(t, tx) + b2.Index = b1.Index + 1 + b2.PrevHash = b1.Hash() + b2.Timestamp = b1.Timestamp + 10000 + require.NoError(t, bc.AddBlock(e.SignBlock(b2))) + e.CheckHalt(t, tx.Hash()) + + tx = e.PrepareInvoke(t, acc, nsHash, "isAvailable", "first.com") + b3 := e.NewBlock(t, tx) + b3.Index = b2.Index + 1 + b3.PrevHash = b2.Hash() + b3.Timestamp = b1.Timestamp + (millisecondsInYear + 1) + require.NoError(t, bc.AddBlock(e.SignBlock(b3))) + e.CheckHalt(t, tx.Hash(), stackitem.NewBool(true)) + + tx = e.PrepareInvoke(t, acc, nsHash, "isAvailable", "second.com") + b4 := e.NewBlock(t, tx) + b4.Index = b3.Index + 1 + b4.PrevHash = b3.Hash() + b4.Timestamp = b3.Timestamp + 1000 + require.NoError(t, bc.AddBlock(e.SignBlock(b4))) + e.CheckHalt(t, tx.Hash(), stackitem.NewBool(false)) + + tx = e.PrepareInvoke(t, acc, nsHash, "getRecord", "first.com", int64(nns.TXT)) + b5 := e.NewBlock(t, tx) + b5.Index = b4.Index + 1 + b5.PrevHash = b4.Hash() + b5.Timestamp = b4.Timestamp + 1000 + require.NoError(t, bc.AddBlock(e.SignBlock(b5))) + e.CheckFault(t, tx.Hash(), "name has expired") +} + +const millisecondsInYear = 365 * 24 * 3600 * 1000 + +func TestRegisterAndRenew(t *testing.T) { + e, nsHash := newExecutorWithNS(t) + + testNameServiceInvoke(t, e, nsHash, "isAvailable", nil, "neo.com") + testNameServiceInvoke(t, e, nsHash, "addRoot", stackitem.Null{}, "org") + testNameServiceInvoke(t, e, nsHash, "isAvailable", nil, "neo.com") + testNameServiceInvoke(t, e, nsHash, "addRoot", stackitem.Null{}, "com") + testNameServiceInvoke(t, e, nsHash, "isAvailable", true, "neo.com") + testNameServiceInvoke(t, e, nsHash, "register", nil, "neo.org", e.CommitteeHash) + testNameServiceInvoke(t, e, nsHash, "register", nil, "docs.neo.org", e.CommitteeHash) + testNameServiceInvoke(t, e, nsHash, "register", nil, "\nneo.com'", e.CommitteeHash) + testNameServiceInvoke(t, e, nsHash, "register", nil, "neo.com\n", e.CommitteeHash) + testNameServiceInvoke(t, e, nsHash, "register", nil, "neo.com", e.CommitteeHash) + testNameServiceInvokeAux(t, e, nsHash, defaultNameServiceDomainPrice, e.Committee, "register", + nil, "neo.com", e.CommitteeHash) + + testNameServiceInvoke(t, e, nsHash, "isAvailable", true, "neo.com") + testNameServiceInvoke(t, e, nsHash, "balanceOf", 0, e.CommitteeHash) + testNameServiceInvokeAux(t, e, nsHash, defaultRegisterSysfee, e.Committee, "register", + true, "neo.com", e.CommitteeHash) + topBlock := e.TopBlock(t) + expectedExpiration := topBlock.Timestamp + millisecondsInYear + testNameServiceInvokeAux(t, e, nsHash, defaultRegisterSysfee, e.Committee, "register", + false, "neo.com", e.CommitteeHash) + testNameServiceInvoke(t, e, nsHash, "isAvailable", false, "neo.com") + + props := stackitem.NewMap() + props.Add(stackitem.Make("name"), stackitem.Make("neo.com")) + props.Add(stackitem.Make("expiration"), stackitem.Make(expectedExpiration)) + testNameServiceInvoke(t, e, nsHash, "properties", props, "neo.com") + testNameServiceInvoke(t, e, nsHash, "balanceOf", 1, e.CommitteeHash) + testNameServiceInvoke(t, e, nsHash, "ownerOf", e.CommitteeHash.BytesBE(), []byte("neo.com")) + + t.Run("invalid token ID", func(t *testing.T) { + testNameServiceInvoke(t, e, nsHash, "properties", nil, "not.exists") + testNameServiceInvoke(t, e, nsHash, "ownerOf", nil, "not.exists") + testNameServiceInvoke(t, e, nsHash, "properties", nil, []interface{}{}) + testNameServiceInvoke(t, e, nsHash, "ownerOf", nil, []interface{}{}) + }) + + // Renew + expectedExpiration += millisecondsInYear + testNameServiceInvokeAux(t, e, nsHash, 100_0000_0000, e.Committee, "renew", expectedExpiration, "neo.com") + + props.Add(stackitem.Make("expiration"), stackitem.Make(expectedExpiration)) + testNameServiceInvoke(t, e, nsHash, "properties", props, "neo.com") +} + +func TestSetGetRecord(t *testing.T) { + e, nsHash := newExecutorWithNS(t) + + acc := e.NewAccount(t) + testNameServiceInvoke(t, e, nsHash, "addRoot", stackitem.Null{}, "com") + + t.Run("set before register", func(t *testing.T) { + testNameServiceInvoke(t, e, nsHash, "setRecord", nil, "neo.com", int64(nns.TXT), "sometext") + }) + testNameServiceInvokeAux(t, e, nsHash, defaultRegisterSysfee, e.Committee, "register", + true, "neo.com", e.CommitteeHash) + t.Run("invalid parameters", func(t *testing.T) { + testNameServiceInvoke(t, e, nsHash, "setRecord", nil, "neo.com", int64(0xFF), "1.2.3.4") + testNameServiceInvoke(t, e, nsHash, "setRecord", nil, "neo.com", int64(nns.A), "not.an.ip.address") + }) + t.Run("invalid witness", func(t *testing.T) { + testNameServiceInvokeAux(t, e, nsHash, defaultNameServiceSysfee, acc, "setRecord", nil, + "neo.com", int64(nns.A), "1.2.3.4") + }) + testNameServiceInvoke(t, e, nsHash, "getRecord", stackitem.Null{}, "neo.com", int64(nns.A)) + testNameServiceInvoke(t, e, nsHash, "setRecord", stackitem.Null{}, "neo.com", int64(nns.A), "1.2.3.4") + testNameServiceInvoke(t, e, nsHash, "getRecord", "1.2.3.4", "neo.com", int64(nns.A)) + testNameServiceInvoke(t, e, nsHash, "setRecord", stackitem.Null{}, "neo.com", int64(nns.A), "1.2.3.4") + testNameServiceInvoke(t, e, nsHash, "getRecord", "1.2.3.4", "neo.com", int64(nns.A)) + testNameServiceInvoke(t, e, nsHash, "setRecord", stackitem.Null{}, "neo.com", int64(nns.AAAA), "2001:0201:1f1f:0000:0000:0100:11a0:11df") + testNameServiceInvoke(t, e, nsHash, "setRecord", stackitem.Null{}, "neo.com", int64(nns.CNAME), "nspcc.ru") + testNameServiceInvoke(t, e, nsHash, "setRecord", stackitem.Null{}, "neo.com", int64(nns.TXT), "sometext") + + // Delete record. + t.Run("invalid witness", func(t *testing.T) { + testNameServiceInvokeAux(t, e, nsHash, defaultNameServiceSysfee, acc, "setRecord", nil, + "neo.com", int64(nns.CNAME)) + }) + testNameServiceInvoke(t, e, nsHash, "getRecord", "nspcc.ru", "neo.com", int64(nns.CNAME)) + testNameServiceInvoke(t, e, nsHash, "deleteRecord", stackitem.Null{}, "neo.com", int64(nns.CNAME)) + testNameServiceInvoke(t, e, nsHash, "getRecord", stackitem.Null{}, "neo.com", int64(nns.CNAME)) + testNameServiceInvoke(t, e, nsHash, "getRecord", "1.2.3.4", "neo.com", int64(nns.A)) + + t.Run("SetRecord_compatibility", func(t *testing.T) { + // tests are got from the NNS C# implementation and changed accordingly to non-native implementation behaviour + testCases := []struct { + Type nns.RecordType + Name string + ShouldFail bool + }{ + {Type: nns.A, Name: "0.0.0.0", ShouldFail: true}, + {Type: nns.A, Name: "1.1.0.1"}, + {Type: nns.A, Name: "10.10.10.10", ShouldFail: true}, + {Type: nns.A, Name: "255.255.255.255", ShouldFail: true}, + {Type: nns.A, Name: "192.168.1.1", ShouldFail: true}, + {Type: nns.A, Name: "1a", ShouldFail: true}, + {Type: nns.A, Name: "256.0.0.0", ShouldFail: true}, + {Type: nns.A, Name: "01.01.01.01", ShouldFail: true}, + {Type: nns.A, Name: "00.0.0.0", ShouldFail: true}, + {Type: nns.A, Name: "0.0.0.-1", ShouldFail: true}, + {Type: nns.A, Name: "0.0.0.0.1", ShouldFail: true}, + {Type: nns.A, Name: "11111111.11111111.11111111.11111111", ShouldFail: true}, + {Type: nns.A, Name: "11111111.11111111.11111111.11111111", ShouldFail: true}, + {Type: nns.A, Name: "ff.ff.ff.ff", ShouldFail: true}, + {Type: nns.A, Name: "0.0.256", ShouldFail: true}, + {Type: nns.A, Name: "0.0.0", ShouldFail: true}, + {Type: nns.A, Name: "0.257", ShouldFail: true}, + {Type: nns.A, Name: "1.1", ShouldFail: true}, + {Type: nns.A, Name: "257", ShouldFail: true}, + {Type: nns.A, Name: "1", ShouldFail: true}, + // {2000} & {2001} & ]2002, 3ffe[ & {3fff} are valid values for IPv6 fragment0 + {Type: nns.AAAA, Name: "2002:db8::8:800:200c:417a", ShouldFail: true}, + {Type: nns.AAAA, Name: "3ffd:1b8::8:800:200c:417a"}, + {Type: nns.AAAA, Name: "3ffd::101"}, + {Type: nns.AAAA, Name: "2003::1"}, + {Type: nns.AAAA, Name: "2003::"}, + {Type: nns.AAAA, Name: "2002:db8:0:0:8:800:200c:417a", ShouldFail: true}, + {Type: nns.AAAA, Name: "3ffd:db8:0:0:8:800:200c:417a"}, + {Type: nns.AAAA, Name: "3ffd:0:0:0:0:0:0:101"}, + {Type: nns.AAAA, Name: "2002:0:0:0:0:0:0:101", ShouldFail: true}, + {Type: nns.AAAA, Name: "3ffd:0:0:0:0:0:0:101"}, + {Type: nns.AAAA, Name: "2001:200:0:0:0:0:0:1"}, + {Type: nns.AAAA, Name: "0:0:0:0:0:0:0:1", ShouldFail: true}, + {Type: nns.AAAA, Name: "2002:0:0:0:0:0:0:1", ShouldFail: true}, + {Type: nns.AAAA, Name: "2001:200:0:0:0:0:0:0"}, + {Type: nns.AAAA, Name: "2002:0:0:0:0:0:0:0", ShouldFail: true}, + {Type: nns.AAAA, Name: "2002:DB8::8:800:200C:417A", ShouldFail: true}, + {Type: nns.AAAA, Name: "3FFD:1B8::8:800:200C:417A"}, + {Type: nns.AAAA, Name: "3FFD::101"}, + {Type: nns.AAAA, Name: "3fFD::101"}, + {Type: nns.AAAA, Name: "2002:DB8:0:0:8:800:200C:417A", ShouldFail: true}, + {Type: nns.AAAA, Name: "3FFD:DB8:0:0:8:800:200C:417A"}, + {Type: nns.AAAA, Name: "3FFD:0:0:0:0:0:0:101"}, + {Type: nns.AAAA, Name: "3FFD::ffff:1.01.1.01", ShouldFail: true}, + {Type: nns.AAAA, Name: "2001:DB8:0:0:8:800:200C:4Z", ShouldFail: true}, + {Type: nns.AAAA, Name: "2001::13.1.68.3", ShouldFail: true}, + } + for _, testCase := range testCases { + var expected interface{} + if testCase.ShouldFail { + expected = nil + } else { + expected = stackitem.Null{} + } + t.Run(testCase.Name, func(t *testing.T) { + testNameServiceInvoke(t, e, nsHash, "setRecord", expected, "neo.com", int64(testCase.Type), testCase.Name) + }) + } + }) +} + +func TestSetAdmin(t *testing.T) { + e, nsHash := newExecutorWithNS(t) + + owner := e.NewAccount(t) + admin := e.NewAccount(t) + guest := e.NewAccount(t) + testNameServiceInvoke(t, e, nsHash, "addRoot", stackitem.Null{}, "com") + + testNameServiceInvokeAux(t, e, nsHash, defaultRegisterSysfee, owner, "register", true, + "neo.com", owner.PrivateKey().GetScriptHash()) + testNameServiceInvokeAux(t, e, nsHash, defaultNameServiceSysfee, guest, "setAdmin", nil, + "neo.com", admin.PrivateKey().GetScriptHash()) + + // Must be witnessed by both owner and admin. + testNameServiceInvokeAux(t, e, nsHash, defaultNameServiceSysfee, owner, "setAdmin", nil, + "neo.com", admin.PrivateKey().GetScriptHash()) + testNameServiceInvokeAux(t, e, nsHash, defaultNameServiceSysfee, admin, "setAdmin", nil, + "neo.com", admin.PrivateKey().GetScriptHash()) + testNameServiceInvokeAux(t, e, nsHash, defaultNameServiceSysfee, []*wallet.Account{owner, admin}, + "setAdmin", stackitem.Null{}, + "neo.com", admin.PrivateKey().GetScriptHash()) + + t.Run("set and delete by admin", func(t *testing.T) { + testNameServiceInvokeAux(t, e, nsHash, defaultNameServiceSysfee, admin, "setRecord", stackitem.Null{}, + "neo.com", int64(nns.TXT), "sometext") + testNameServiceInvokeAux(t, e, nsHash, defaultNameServiceSysfee, guest, "deleteRecord", nil, + "neo.com", int64(nns.TXT)) + testNameServiceInvokeAux(t, e, nsHash, defaultNameServiceSysfee, admin, "deleteRecord", stackitem.Null{}, + "neo.com", int64(nns.TXT)) + }) + + t.Run("set admin to null", func(t *testing.T) { + testNameServiceInvokeAux(t, e, nsHash, defaultNameServiceSysfee, admin, "setRecord", stackitem.Null{}, + "neo.com", int64(nns.TXT), "sometext") + testNameServiceInvokeAux(t, e, nsHash, defaultNameServiceSysfee, owner, "setAdmin", stackitem.Null{}, + "neo.com", nil) + testNameServiceInvokeAux(t, e, nsHash, defaultNameServiceSysfee, admin, "deleteRecord", nil, + "neo.com", int64(nns.TXT)) + }) +} + +func TestTransfer(t *testing.T) { + e, nsHash := newExecutorWithNS(t) + + from := e.NewAccount(t) + to := e.NewAccount(t) + + testNameServiceInvoke(t, e, nsHash, "addRoot", stackitem.Null{}, "com") + testNameServiceInvokeAux(t, e, nsHash, defaultRegisterSysfee, from, "register", + true, "neo.com", from.PrivateKey().GetScriptHash()) + testNameServiceInvokeAux(t, e, nsHash, defaultNameServiceSysfee, from, "setRecord", stackitem.Null{}, + "neo.com", int64(nns.A), "1.2.3.4") + testNameServiceInvokeAux(t, e, nsHash, defaultRegisterSysfee, from, "transfer", + nil, to.Contract.ScriptHash().BytesBE(), []byte("not.exists"), nil) + testNameServiceInvokeAux(t, e, nsHash, defaultRegisterSysfee, e.Committee, "transfer", + false, to.Contract.ScriptHash().BytesBE(), []byte("neo.com"), nil) + testNameServiceInvokeAux(t, e, nsHash, defaultRegisterSysfee, from, "transfer", + true, to.Contract.ScriptHash().BytesBE(), []byte("neo.com"), nil) + testNameServiceInvokeAux(t, e, nsHash, defaultNameServiceSysfee, from, "totalSupply", 1) + testNameServiceInvokeAux(t, e, nsHash, defaultNameServiceSysfee, from, "ownerOf", + to.Contract.ScriptHash().BytesBE(), []byte("neo.com")) + + // without onNEP11Transfer + c := neotest.CompileSource(t, e.CommitteeHash, + strings.NewReader(`package foo + func Main() int { return 0 }`), + &compiler.Options{Name: "foo"}) + e.DeployContract(t, c, nil) + testNameServiceInvokeAux(t, e, nsHash, defaultRegisterSysfee, to, "transfer", + nil, c.Hash.BytesBE(), []byte("neo.com"), nil) + + // with onNEP11Transfer + c = neotest.CompileSource(t, e.CommitteeHash, + strings.NewReader(`package foo + import "github.com/nspcc-dev/neo-go/pkg/interop" + func OnNEP11Payment(from interop.Hash160, amount int, token []byte, data interface{}) {}`), + &compiler.Options{Name: "foo"}) + e.DeployContract(t, c, nil) + testNameServiceInvokeAux(t, e, nsHash, defaultRegisterSysfee, to, "transfer", + true, c.Hash.BytesBE(), []byte("neo.com"), nil) + testNameServiceInvokeAux(t, e, nsHash, defaultNameServiceSysfee, from, "totalSupply", 1) + testNameServiceInvokeAux(t, e, nsHash, defaultNameServiceSysfee, from, "ownerOf", + c.Hash.BytesBE(), []byte("neo.com")) +} + +func TestTokensOf(t *testing.T) { + e, nsHash := newExecutorWithNS(t) + + acc1 := e.NewAccount(t) + acc2 := e.NewAccount(t) + + testNameServiceInvoke(t, e, nsHash, "addRoot", stackitem.Null{}, "com") + testNameServiceInvokeAux(t, e, nsHash, defaultRegisterSysfee, acc1, "register", + true, "neo.com", acc1.PrivateKey().GetScriptHash()) + testNameServiceInvokeAux(t, e, nsHash, defaultRegisterSysfee, acc2, "register", + true, "nspcc.com", acc2.PrivateKey().GetScriptHash()) + + testTokensOf(t, e, nsHash, [][]byte{[]byte("neo.com")}, acc1.Contract.ScriptHash().BytesBE()) + testTokensOf(t, e, nsHash, [][]byte{[]byte("nspcc.com")}, acc2.Contract.ScriptHash().BytesBE()) + testTokensOf(t, e, nsHash, [][]byte{[]byte("neo.com"), []byte("nspcc.com")}) + testTokensOf(t, e, nsHash, [][]byte{}, util.Uint160{}.BytesBE()) // empty hash is a valid hash still +} + +func testTokensOf(t *testing.T, e *neotest.Executor, nsHash util.Uint160, result [][]byte, args ...interface{}) { + method := "tokensOf" + if len(args) == 0 { + method = "tokens" + } + w := io.NewBufBinWriter() + emit.AppCall(w.BinWriter, nsHash, method, callflag.All, args...) + for range result { + emit.Opcodes(w.BinWriter, opcode.DUP) + emit.Syscall(w.BinWriter, interopnames.SystemIteratorNext) + emit.Opcodes(w.BinWriter, opcode.ASSERT) + + emit.Opcodes(w.BinWriter, opcode.DUP) + emit.Syscall(w.BinWriter, interopnames.SystemIteratorValue) + emit.Opcodes(w.BinWriter, opcode.SWAP) + } + emit.Opcodes(w.BinWriter, opcode.DROP) + emit.Int(w.BinWriter, int64(len(result))) + emit.Opcodes(w.BinWriter, opcode.PACK) + require.NoError(t, w.Err) + script := w.Bytes() + tx := transaction.New(script, defaultNameServiceSysfee) + tx.ValidUntilBlock = e.Chain.BlockHeight() + 1 + v, err := neotest.TestInvoke(e.Chain, tx) + if result == nil { + require.Error(t, err) + return + } + require.NoError(t, err) + arr := make([]stackitem.Item, 0, len(result)) + for i := len(result) - 1; i >= 0; i-- { + arr = append(arr, stackitem.Make(result[i])) + } + require.Equal(t, stackitem.NewArray(arr), v.Estack().Pop().Item()) +} + +func TestResolve(t *testing.T) { + e, nsHash := newExecutorWithNS(t) + + acc := e.NewAccount(t) + + testNameServiceInvoke(t, e, nsHash, "addRoot", stackitem.Null{}, "com") + testNameServiceInvokeAux(t, e, nsHash, defaultRegisterSysfee, acc, "register", + true, "neo.com", acc.PrivateKey().GetScriptHash()) + testNameServiceInvokeAux(t, e, nsHash, defaultNameServiceSysfee, acc, "setRecord", stackitem.Null{}, + "neo.com", int64(nns.A), "1.2.3.4") + testNameServiceInvokeAux(t, e, nsHash, defaultNameServiceSysfee, acc, "setRecord", stackitem.Null{}, + "neo.com", int64(nns.CNAME), "alias.com") + + testNameServiceInvokeAux(t, e, nsHash, defaultRegisterSysfee, acc, "register", + true, "alias.com", acc.PrivateKey().GetScriptHash()) + testNameServiceInvokeAux(t, e, nsHash, defaultNameServiceSysfee, acc, "setRecord", stackitem.Null{}, + "alias.com", int64(nns.TXT), "sometxt") + + testNameServiceInvoke(t, e, nsHash, "resolve", "1.2.3.4", + "neo.com", int64(nns.A)) + testNameServiceInvoke(t, e, nsHash, "resolve", "alias.com", + "neo.com", int64(nns.CNAME)) + testNameServiceInvoke(t, e, nsHash, "resolve", "sometxt", + "neo.com", int64(nns.TXT)) + testNameServiceInvoke(t, e, nsHash, "resolve", stackitem.Null{}, + "neo.com", int64(nns.AAAA)) +} + +const ( + defaultNameServiceDomainPrice = 10_0000_0000 + defaultNameServiceSysfee = 6000_0000 + defaultRegisterSysfee = 10_0000_0000 + defaultNameServiceDomainPrice +) + +func testNameServiceInvoke(t *testing.T, e *neotest.Executor, nsHash util.Uint160, method string, result interface{}, args ...interface{}) { + testNameServiceInvokeAux(t, e, nsHash, defaultNameServiceSysfee, e.Committee, method, result, args...) +} + +func testNameServiceInvokeAux(t *testing.T, e *neotest.Executor, nsHash util.Uint160, sysfee int64, signer interface{}, method string, result interface{}, args ...interface{}) { + if sysfee < 0 { + sysfee = defaultNameServiceSysfee + } + tx := e.PrepareInvokeNoSign(t, nsHash, method, args...) + e.SignTx(t, tx, sysfee, signer) + e.AddBlock(t, tx) + if result == nil { + e.CheckFault(t, tx.Hash(), "") + } else { + e.CheckHalt(t, tx.Hash(), stackitem.Make(result)) + } +} diff --git a/pkg/core/helper_test.go b/pkg/core/helper_test.go index 182f88544..51b03a157 100644 --- a/pkg/core/helper_test.go +++ b/pkg/core/helper_test.go @@ -52,27 +52,10 @@ var neoOwner = testchain.MultisigScriptHash() // examplesPrefix is a prefix of the example smart-contracts. const examplesPrefix = "../../examples/" -// newTestChainWithNS should be called before newBlock invocation to properly setup -// global state. -func newTestChainWithNS(t *testing.T) (*Blockchain, util.Uint160) { - bc := newTestChainWithCustomCfg(t, nil) - acc := newAccountWithGAS(t, bc) - // Push NameService contract into the chain. - nsPath := examplesPrefix + "nft-nd-nns/" - nsConfigPath := nsPath + "nns.yml" - txDeploy4, _ := newDeployTx(t, bc, acc.PrivateKey().GetScriptHash(), nsPath, nsPath, &nsConfigPath) - txDeploy4.Nonce = 123 - txDeploy4.ValidUntilBlock = bc.BlockHeight() + 1 - require.NoError(t, addNetworkFee(bc, txDeploy4, acc)) - require.NoError(t, acc.SignTx(testchain.Network(), txDeploy4)) - b := bc.newBlock(txDeploy4) - require.NoError(t, bc.AddBlock(b)) - checkTxHalt(t, bc, txDeploy4.Hash()) - - h, err := bc.GetContractScriptHash(1) - require.NoError(t, err) - return bc, h -} +const ( + defaultNameServiceDomainPrice = 10_0000_0000 + defaultNameServiceSysfee = 6000_0000 +) // newTestChain should be called before newBlock invocation to properly setup // global state. diff --git a/pkg/core/native_neo_test.go b/pkg/core/native_neo_test.go index 4d2fbf2f1..a31cf80a5 100644 --- a/pkg/core/native_neo_test.go +++ b/pkg/core/native_neo_test.go @@ -469,3 +469,10 @@ func TestNEO_TransferZeroWithNonZeroBalance(t *testing.T) { check(t, false) }) } + +func newAccountWithGAS(t *testing.T, bc *Blockchain) *wallet.Account { + acc, err := wallet.NewAccount() + require.NoError(t, err) + transferTokenFromMultisigAccount(t, bc, acc.PrivateKey().GetScriptHash(), bc.contracts.GAS.Hash, 1000_00000000) + return acc +} diff --git a/pkg/core/nonnative_name_service_test.go b/pkg/core/nonnative_name_service_test.go deleted file mode 100644 index 0a071001c..000000000 --- a/pkg/core/nonnative_name_service_test.go +++ /dev/null @@ -1,470 +0,0 @@ -package core - -import ( - "testing" - - nns "github.com/nspcc-dev/neo-go/examples/nft-nd-nns" - "github.com/nspcc-dev/neo-go/internal/testchain" - "github.com/nspcc-dev/neo-go/pkg/core/block" - "github.com/nspcc-dev/neo-go/pkg/core/interop/interopnames" - "github.com/nspcc-dev/neo-go/pkg/core/transaction" - "github.com/nspcc-dev/neo-go/pkg/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/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/nspcc-dev/neo-go/pkg/wallet" - "github.com/stretchr/testify/require" -) - -func TestNameService_Price(t *testing.T) { - bc, nsHash := newTestChainWithNS(t) - - testGetSet(t, bc, nsHash, "Price", - defaultNameServiceDomainPrice, 0, 10000_00000000) -} - -func TestNonfungible(t *testing.T) { - bc, nsHash := newTestChainWithNS(t) - - acc := newAccountWithGAS(t, bc) - testNameServiceInvokeAux(t, bc, nsHash, defaultNameServiceSysfee, acc, "symbol", "NNS") - testNameServiceInvokeAux(t, bc, nsHash, defaultNameServiceSysfee, acc, "decimals", 0) - testNameServiceInvokeAux(t, bc, nsHash, defaultNameServiceSysfee, acc, "totalSupply", 0) -} - -func TestAddRoot(t *testing.T) { - bc, nsHash := newTestChainWithNS(t) - - transferFundsToCommittee(t, bc) - - t.Run("invalid format", func(t *testing.T) { - testNameServiceInvoke(t, bc, nsHash, "addRoot", nil, "") - }) - t.Run("not signed by committee", func(t *testing.T) { - aer, err := invokeContractMethod(bc, 1000_0000, nsHash, "addRoot", "some") - require.NoError(t, err) - checkFAULTState(t, aer) - }) - - testNameServiceInvoke(t, bc, nsHash, "addRoot", stackitem.Null{}, "some") - t.Run("already exists", func(t *testing.T) { - testNameServiceInvoke(t, bc, nsHash, "addRoot", nil, "some") - }) -} - -func TestExpiration(t *testing.T) { - bc, nsHash := newTestChainWithNS(t) - - transferFundsToCommittee(t, bc) - acc := newAccountWithGAS(t, bc) - - testNameServiceInvoke(t, bc, nsHash, "addRoot", stackitem.Null{}, "com") - testNameServiceInvokeAux(t, bc, nsHash, defaultRegisterSysfee, acc, "register", - true, "first.com", acc.Contract.ScriptHash()) - - testNameServiceInvokeAux(t, bc, nsHash, defaultNameServiceSysfee, acc, - "setRecord", stackitem.Null{}, "first.com", int64(nns.TXT), "sometext") - b1 := bc.topBlock.Load().(*block.Block) - - tx, err := prepareContractMethodInvokeGeneric(bc, defaultRegisterSysfee, nsHash, - "register", acc, "second.com", acc.Contract.ScriptHash()) - require.NoError(t, err) - b2 := newBlockCustom(bc.GetConfig(), func(b *block.Block) { - b.Index = b1.Index + 1 - b.PrevHash = b1.Hash() - b.Timestamp = b1.Timestamp + 10000 - }, tx) - require.NoError(t, bc.AddBlock(b2)) - checkTxHalt(t, bc, tx.Hash()) - - tx, err = prepareContractMethodInvokeGeneric(bc, defaultNameServiceSysfee, nsHash, - "isAvailable", acc, "first.com") - require.NoError(t, err) - b3 := newBlockCustom(bc.GetConfig(), func(b *block.Block) { - b.Index = b2.Index + 1 - b.PrevHash = b2.Hash() - b.Timestamp = b1.Timestamp + (millisecondsInYear + 1) - }, tx) - require.NoError(t, bc.AddBlock(b3)) - aer, err := bc.GetAppExecResults(tx.Hash(), trigger.Application) - require.NoError(t, err) - checkResult(t, &aer[0], stackitem.NewBool(true)) - - tx, err = prepareContractMethodInvokeGeneric(bc, defaultNameServiceSysfee, nsHash, - "isAvailable", acc, "second.com") - require.NoError(t, err) - b4 := newBlockCustom(bc.GetConfig(), func(b *block.Block) { - b.Index = b3.Index + 1 - b.PrevHash = b3.Hash() - b.Timestamp = b3.Timestamp + 1000 - }, tx) - require.NoError(t, bc.AddBlock(b4)) - aer, err = bc.GetAppExecResults(tx.Hash(), trigger.Application) - require.NoError(t, err) - checkResult(t, &aer[0], stackitem.NewBool(false)) - - tx, err = prepareContractMethodInvokeGeneric(bc, defaultNameServiceSysfee, nsHash, - "getRecord", acc, "first.com", int64(nns.TXT)) - require.NoError(t, err) - b5 := newBlockCustom(bc.GetConfig(), func(b *block.Block) { - b.Index = b4.Index + 1 - b.PrevHash = b4.Hash() - b.Timestamp = b4.Timestamp + 1000 - }, tx) - require.NoError(t, bc.AddBlock(b5)) - aer, err = bc.GetAppExecResults(tx.Hash(), trigger.Application) - require.NoError(t, err) - checkFAULTState(t, &aer[0]) // name has expired (panic) -} - -const millisecondsInYear = 365 * 24 * 3600 * 1000 - -func TestRegisterAndRenew(t *testing.T) { - bc, nsHash := newTestChainWithNS(t) - - transferFundsToCommittee(t, bc) - - testNameServiceInvoke(t, bc, nsHash, "isAvailable", nil, "neo.com") - testNameServiceInvoke(t, bc, nsHash, "addRoot", stackitem.Null{}, "org") - testNameServiceInvoke(t, bc, nsHash, "isAvailable", nil, "neo.com") - testNameServiceInvoke(t, bc, nsHash, "addRoot", stackitem.Null{}, "com") - testNameServiceInvoke(t, bc, nsHash, "isAvailable", true, "neo.com") - testNameServiceInvoke(t, bc, nsHash, "register", nil, "neo.org", testchain.CommitteeScriptHash()) - testNameServiceInvoke(t, bc, nsHash, "register", nil, "docs.neo.org", testchain.CommitteeScriptHash()) - testNameServiceInvoke(t, bc, nsHash, "register", nil, "\nneo.com'", testchain.CommitteeScriptHash()) - testNameServiceInvoke(t, bc, nsHash, "register", nil, "neo.com\n", testchain.CommitteeScriptHash()) - testNameServiceInvoke(t, bc, nsHash, "register", nil, "neo.com", testchain.CommitteeScriptHash()) - testNameServiceInvokeAux(t, bc, nsHash, defaultNameServiceDomainPrice, true, "register", - nil, "neo.com", testchain.CommitteeScriptHash()) - - testNameServiceInvoke(t, bc, nsHash, "isAvailable", true, "neo.com") - testNameServiceInvoke(t, bc, nsHash, "balanceOf", 0, testchain.CommitteeScriptHash()) - testNameServiceInvokeAux(t, bc, nsHash, defaultRegisterSysfee, true, "register", - true, "neo.com", testchain.CommitteeScriptHash()) - topBlock := bc.topBlock.Load().(*block.Block) - expectedExpiration := topBlock.Timestamp + millisecondsInYear - testNameServiceInvokeAux(t, bc, nsHash, defaultRegisterSysfee, true, "register", - false, "neo.com", testchain.CommitteeScriptHash()) - testNameServiceInvoke(t, bc, nsHash, "isAvailable", false, "neo.com") - - props := stackitem.NewMap() - props.Add(stackitem.Make("name"), stackitem.Make("neo.com")) - props.Add(stackitem.Make("expiration"), stackitem.Make(expectedExpiration)) - testNameServiceInvoke(t, bc, nsHash, "properties", props, "neo.com") - testNameServiceInvoke(t, bc, nsHash, "balanceOf", 1, testchain.CommitteeScriptHash()) - testNameServiceInvoke(t, bc, nsHash, "ownerOf", testchain.CommitteeScriptHash().BytesBE(), []byte("neo.com")) - - t.Run("invalid token ID", func(t *testing.T) { - testNameServiceInvoke(t, bc, nsHash, "properties", nil, "not.exists") - testNameServiceInvoke(t, bc, nsHash, "ownerOf", nil, "not.exists") - testNameServiceInvoke(t, bc, nsHash, "properties", nil, []interface{}{}) - testNameServiceInvoke(t, bc, nsHash, "ownerOf", nil, []interface{}{}) - }) - - // Renew - expectedExpiration += millisecondsInYear - testNameServiceInvokeAux(t, bc, nsHash, 100_0000_0000, true, "renew", expectedExpiration, "neo.com") - - props.Add(stackitem.Make("expiration"), stackitem.Make(expectedExpiration)) - testNameServiceInvoke(t, bc, nsHash, "properties", props, "neo.com") -} - -func TestSetGetRecord(t *testing.T) { - bc, nsHash := newTestChainWithNS(t) - - transferFundsToCommittee(t, bc) - acc := newAccountWithGAS(t, bc) - testNameServiceInvoke(t, bc, nsHash, "addRoot", stackitem.Null{}, "com") - - t.Run("set before register", func(t *testing.T) { - testNameServiceInvoke(t, bc, nsHash, "setRecord", nil, "neo.com", int64(nns.TXT), "sometext") - }) - testNameServiceInvokeAux(t, bc, nsHash, defaultRegisterSysfee, true, "register", - true, "neo.com", testchain.CommitteeScriptHash()) - t.Run("invalid parameters", func(t *testing.T) { - testNameServiceInvoke(t, bc, nsHash, "setRecord", nil, "neo.com", int64(0xFF), "1.2.3.4") - testNameServiceInvoke(t, bc, nsHash, "setRecord", nil, "neo.com", int64(nns.A), "not.an.ip.address") - }) - t.Run("invalid witness", func(t *testing.T) { - testNameServiceInvokeAux(t, bc, nsHash, defaultNameServiceSysfee, acc, "setRecord", nil, - "neo.com", int64(nns.A), "1.2.3.4") - }) - testNameServiceInvoke(t, bc, nsHash, "getRecord", stackitem.Null{}, "neo.com", int64(nns.A)) - testNameServiceInvoke(t, bc, nsHash, "setRecord", stackitem.Null{}, "neo.com", int64(nns.A), "1.2.3.4") - testNameServiceInvoke(t, bc, nsHash, "getRecord", "1.2.3.4", "neo.com", int64(nns.A)) - testNameServiceInvoke(t, bc, nsHash, "setRecord", stackitem.Null{}, "neo.com", int64(nns.A), "1.2.3.4") - testNameServiceInvoke(t, bc, nsHash, "getRecord", "1.2.3.4", "neo.com", int64(nns.A)) - testNameServiceInvoke(t, bc, nsHash, "setRecord", stackitem.Null{}, "neo.com", int64(nns.AAAA), "2001:0201:1f1f:0000:0000:0100:11a0:11df") - testNameServiceInvoke(t, bc, nsHash, "setRecord", stackitem.Null{}, "neo.com", int64(nns.CNAME), "nspcc.ru") - testNameServiceInvoke(t, bc, nsHash, "setRecord", stackitem.Null{}, "neo.com", int64(nns.TXT), "sometext") - - // Delete record. - t.Run("invalid witness", func(t *testing.T) { - testNameServiceInvokeAux(t, bc, nsHash, defaultNameServiceSysfee, acc, "setRecord", nil, - "neo.com", int64(nns.CNAME)) - }) - testNameServiceInvoke(t, bc, nsHash, "getRecord", "nspcc.ru", "neo.com", int64(nns.CNAME)) - testNameServiceInvoke(t, bc, nsHash, "deleteRecord", stackitem.Null{}, "neo.com", int64(nns.CNAME)) - testNameServiceInvoke(t, bc, nsHash, "getRecord", stackitem.Null{}, "neo.com", int64(nns.CNAME)) - testNameServiceInvoke(t, bc, nsHash, "getRecord", "1.2.3.4", "neo.com", int64(nns.A)) - - t.Run("SetRecord_compatibility", func(t *testing.T) { - // tests are got from the NNS C# implementation and changed accordingly to non-native implementation behaviour - testCases := []struct { - Type nns.RecordType - Name string - ShouldFail bool - }{ - {Type: nns.A, Name: "0.0.0.0", ShouldFail: true}, - {Type: nns.A, Name: "1.1.0.1"}, - {Type: nns.A, Name: "10.10.10.10", ShouldFail: true}, - {Type: nns.A, Name: "255.255.255.255", ShouldFail: true}, - {Type: nns.A, Name: "192.168.1.1", ShouldFail: true}, - {Type: nns.A, Name: "1a", ShouldFail: true}, - {Type: nns.A, Name: "256.0.0.0", ShouldFail: true}, - {Type: nns.A, Name: "01.01.01.01", ShouldFail: true}, - {Type: nns.A, Name: "00.0.0.0", ShouldFail: true}, - {Type: nns.A, Name: "0.0.0.-1", ShouldFail: true}, - {Type: nns.A, Name: "0.0.0.0.1", ShouldFail: true}, - {Type: nns.A, Name: "11111111.11111111.11111111.11111111", ShouldFail: true}, - {Type: nns.A, Name: "11111111.11111111.11111111.11111111", ShouldFail: true}, - {Type: nns.A, Name: "ff.ff.ff.ff", ShouldFail: true}, - {Type: nns.A, Name: "0.0.256", ShouldFail: true}, - {Type: nns.A, Name: "0.0.0", ShouldFail: true}, - {Type: nns.A, Name: "0.257", ShouldFail: true}, - {Type: nns.A, Name: "1.1", ShouldFail: true}, - {Type: nns.A, Name: "257", ShouldFail: true}, - {Type: nns.A, Name: "1", ShouldFail: true}, - // {2000} & {2001} & ]2002, 3ffe[ & {3fff} are valid values for IPv6 fragment0 - {Type: nns.AAAA, Name: "2002:db8::8:800:200c:417a", ShouldFail: true}, - {Type: nns.AAAA, Name: "3ffd:1b8::8:800:200c:417a"}, - {Type: nns.AAAA, Name: "3ffd::101"}, - {Type: nns.AAAA, Name: "2003::1"}, - {Type: nns.AAAA, Name: "2003::"}, - {Type: nns.AAAA, Name: "2002:db8:0:0:8:800:200c:417a", ShouldFail: true}, - {Type: nns.AAAA, Name: "3ffd:db8:0:0:8:800:200c:417a"}, - {Type: nns.AAAA, Name: "3ffd:0:0:0:0:0:0:101"}, - {Type: nns.AAAA, Name: "2002:0:0:0:0:0:0:101", ShouldFail: true}, - {Type: nns.AAAA, Name: "3ffd:0:0:0:0:0:0:101"}, - {Type: nns.AAAA, Name: "2001:200:0:0:0:0:0:1"}, - {Type: nns.AAAA, Name: "0:0:0:0:0:0:0:1", ShouldFail: true}, - {Type: nns.AAAA, Name: "2002:0:0:0:0:0:0:1", ShouldFail: true}, - {Type: nns.AAAA, Name: "2001:200:0:0:0:0:0:0"}, - {Type: nns.AAAA, Name: "2002:0:0:0:0:0:0:0", ShouldFail: true}, - {Type: nns.AAAA, Name: "2002:DB8::8:800:200C:417A", ShouldFail: true}, - {Type: nns.AAAA, Name: "3FFD:1B8::8:800:200C:417A"}, - {Type: nns.AAAA, Name: "3FFD::101"}, - {Type: nns.AAAA, Name: "3fFD::101"}, - {Type: nns.AAAA, Name: "2002:DB8:0:0:8:800:200C:417A", ShouldFail: true}, - {Type: nns.AAAA, Name: "3FFD:DB8:0:0:8:800:200C:417A"}, - {Type: nns.AAAA, Name: "3FFD:0:0:0:0:0:0:101"}, - {Type: nns.AAAA, Name: "3FFD::ffff:1.01.1.01", ShouldFail: true}, - {Type: nns.AAAA, Name: "2001:DB8:0:0:8:800:200C:4Z", ShouldFail: true}, - {Type: nns.AAAA, Name: "2001::13.1.68.3", ShouldFail: true}, - } - for _, testCase := range testCases { - var expected interface{} - if testCase.ShouldFail { - expected = nil - } else { - expected = stackitem.Null{} - } - t.Run(testCase.Name, func(t *testing.T) { - testNameServiceInvoke(t, bc, nsHash, "setRecord", expected, "neo.com", int64(testCase.Type), testCase.Name) - }) - } - }) -} - -func TestSetAdmin(t *testing.T) { - bc, nsHash := newTestChainWithNS(t) - - transferFundsToCommittee(t, bc) - owner := newAccountWithGAS(t, bc) - admin := newAccountWithGAS(t, bc) - guest := newAccountWithGAS(t, bc) - testNameServiceInvoke(t, bc, nsHash, "addRoot", stackitem.Null{}, "com") - - testNameServiceInvokeAux(t, bc, nsHash, defaultRegisterSysfee, owner, "register", true, - "neo.com", owner.PrivateKey().GetScriptHash()) - testNameServiceInvokeAux(t, bc, nsHash, defaultNameServiceSysfee, guest, "setAdmin", nil, - "neo.com", admin.PrivateKey().GetScriptHash()) - - // Must be witnessed by both owner and admin. - testNameServiceInvokeAux(t, bc, nsHash, defaultNameServiceSysfee, owner, "setAdmin", nil, - "neo.com", admin.PrivateKey().GetScriptHash()) - testNameServiceInvokeAux(t, bc, nsHash, defaultNameServiceSysfee, admin, "setAdmin", nil, - "neo.com", admin.PrivateKey().GetScriptHash()) - testNameServiceInvokeAux(t, bc, nsHash, defaultNameServiceSysfee, []*wallet.Account{owner, admin}, - "setAdmin", stackitem.Null{}, - "neo.com", admin.PrivateKey().GetScriptHash()) - - t.Run("set and delete by admin", func(t *testing.T) { - testNameServiceInvokeAux(t, bc, nsHash, defaultNameServiceSysfee, admin, "setRecord", stackitem.Null{}, - "neo.com", int64(nns.TXT), "sometext") - testNameServiceInvokeAux(t, bc, nsHash, defaultNameServiceSysfee, guest, "deleteRecord", nil, - "neo.com", int64(nns.TXT)) - testNameServiceInvokeAux(t, bc, nsHash, defaultNameServiceSysfee, admin, "deleteRecord", stackitem.Null{}, - "neo.com", int64(nns.TXT)) - }) - - t.Run("set admin to null", func(t *testing.T) { - testNameServiceInvokeAux(t, bc, nsHash, defaultNameServiceSysfee, admin, "setRecord", stackitem.Null{}, - "neo.com", int64(nns.TXT), "sometext") - testNameServiceInvokeAux(t, bc, nsHash, defaultNameServiceSysfee, owner, "setAdmin", stackitem.Null{}, - "neo.com", nil) - testNameServiceInvokeAux(t, bc, nsHash, defaultNameServiceSysfee, admin, "deleteRecord", nil, - "neo.com", int64(nns.TXT)) - }) -} - -func TestTransfer(t *testing.T) { - bc, nsHash := newTestChainWithNS(t) - - transferFundsToCommittee(t, bc) - from := newAccountWithGAS(t, bc) - to := newAccountWithGAS(t, bc) - - testNameServiceInvoke(t, bc, nsHash, "addRoot", stackitem.Null{}, "com") - testNameServiceInvokeAux(t, bc, nsHash, defaultRegisterSysfee, from, "register", - true, "neo.com", from.PrivateKey().GetScriptHash()) - testNameServiceInvokeAux(t, bc, nsHash, defaultNameServiceSysfee, from, "setRecord", stackitem.Null{}, - "neo.com", int64(nns.A), "1.2.3.4") - testNameServiceInvokeAux(t, bc, nsHash, defaultRegisterSysfee, from, "transfer", - nil, to.Contract.ScriptHash().BytesBE(), []byte("not.exists"), nil) - testNameServiceInvokeAux(t, bc, nsHash, defaultRegisterSysfee, true, "transfer", - false, to.Contract.ScriptHash().BytesBE(), []byte("neo.com"), nil) - testNameServiceInvokeAux(t, bc, nsHash, defaultRegisterSysfee, from, "transfer", - true, to.Contract.ScriptHash().BytesBE(), []byte("neo.com"), nil) - testNameServiceInvokeAux(t, bc, nsHash, defaultNameServiceSysfee, from, "totalSupply", 1) - testNameServiceInvokeAux(t, bc, nsHash, defaultNameServiceSysfee, from, "ownerOf", - to.Contract.ScriptHash().BytesBE(), []byte("neo.com")) - cs, cs2 := getTestContractState(bc) // cs2 doesn't have OnNEP11Transfer - require.NoError(t, bc.contracts.Management.PutContractState(bc.dao, cs)) - require.NoError(t, bc.contracts.Management.PutContractState(bc.dao, cs2)) - testNameServiceInvokeAux(t, bc, nsHash, defaultRegisterSysfee, to, "transfer", - nil, cs2.Hash.BytesBE(), []byte("neo.com"), nil) - testNameServiceInvokeAux(t, bc, nsHash, defaultRegisterSysfee, to, "transfer", - true, cs.Hash.BytesBE(), []byte("neo.com"), nil) - testNameServiceInvokeAux(t, bc, nsHash, defaultNameServiceSysfee, from, "totalSupply", 1) - testNameServiceInvokeAux(t, bc, nsHash, defaultNameServiceSysfee, from, "ownerOf", - cs.Hash.BytesBE(), []byte("neo.com")) -} - -func TestTokensOf(t *testing.T) { - bc, nsHash := newTestChainWithNS(t) - - transferFundsToCommittee(t, bc) - acc1 := newAccountWithGAS(t, bc) - acc2 := newAccountWithGAS(t, bc) - - testNameServiceInvoke(t, bc, nsHash, "addRoot", stackitem.Null{}, "com") - testNameServiceInvokeAux(t, bc, nsHash, defaultRegisterSysfee, acc1, "register", - true, "neo.com", acc1.PrivateKey().GetScriptHash()) - testNameServiceInvokeAux(t, bc, nsHash, defaultRegisterSysfee, acc2, "register", - true, "nspcc.com", acc2.PrivateKey().GetScriptHash()) - - testTokensOf(t, bc, nsHash, acc1, [][]byte{[]byte("neo.com")}, acc1.Contract.ScriptHash().BytesBE()) - testTokensOf(t, bc, nsHash, acc1, [][]byte{[]byte("nspcc.com")}, acc2.Contract.ScriptHash().BytesBE()) - testTokensOf(t, bc, nsHash, acc1, [][]byte{[]byte("neo.com"), []byte("nspcc.com")}) - testTokensOf(t, bc, nsHash, acc1, [][]byte{}, util.Uint160{}.BytesBE()) // empty hash is a valid hash still -} - -func testTokensOf(t *testing.T, bc *Blockchain, nsHash util.Uint160, signer *wallet.Account, result [][]byte, args ...interface{}) { - method := "tokensOf" - if len(args) == 0 { - method = "tokens" - } - w := io.NewBufBinWriter() - emit.AppCall(w.BinWriter, nsHash, method, callflag.All, args...) - for range result { - emit.Opcodes(w.BinWriter, opcode.DUP) - emit.Syscall(w.BinWriter, interopnames.SystemIteratorNext) - emit.Opcodes(w.BinWriter, opcode.ASSERT) - - emit.Opcodes(w.BinWriter, opcode.DUP) - emit.Syscall(w.BinWriter, interopnames.SystemIteratorValue) - emit.Opcodes(w.BinWriter, opcode.SWAP) - } - emit.Opcodes(w.BinWriter, opcode.DROP) - emit.Int(w.BinWriter, int64(len(result))) - emit.Opcodes(w.BinWriter, opcode.PACK) - require.NoError(t, w.Err) - script := w.Bytes() - tx := transaction.New(script, defaultNameServiceSysfee) - tx.ValidUntilBlock = bc.BlockHeight() + 1 - signTxWithAccounts(bc, tx, signer) - aers, err := persistBlock(bc, tx) - require.NoError(t, err) - if result == nil { - checkFAULTState(t, aers[0]) - return - } - arr := make([]stackitem.Item, 0, len(result)) - for i := len(result) - 1; i >= 0; i-- { - arr = append(arr, stackitem.Make(result[i])) - } - checkResult(t, aers[0], stackitem.NewArray(arr)) -} - -func TestResolve(t *testing.T) { - bc, nsHash := newTestChainWithNS(t) - - transferFundsToCommittee(t, bc) - acc := newAccountWithGAS(t, bc) - - testNameServiceInvoke(t, bc, nsHash, "addRoot", stackitem.Null{}, "com") - testNameServiceInvokeAux(t, bc, nsHash, defaultRegisterSysfee, acc, "register", - true, "neo.com", acc.PrivateKey().GetScriptHash()) - testNameServiceInvokeAux(t, bc, nsHash, defaultNameServiceSysfee, acc, "setRecord", stackitem.Null{}, - "neo.com", int64(nns.A), "1.2.3.4") - testNameServiceInvokeAux(t, bc, nsHash, defaultNameServiceSysfee, acc, "setRecord", stackitem.Null{}, - "neo.com", int64(nns.CNAME), "alias.com") - - testNameServiceInvokeAux(t, bc, nsHash, defaultRegisterSysfee, acc, "register", - true, "alias.com", acc.PrivateKey().GetScriptHash()) - testNameServiceInvokeAux(t, bc, nsHash, defaultNameServiceSysfee, acc, "setRecord", stackitem.Null{}, - "alias.com", int64(nns.TXT), "sometxt") - - testNameServiceInvoke(t, bc, nsHash, "resolve", "1.2.3.4", - "neo.com", int64(nns.A)) - testNameServiceInvoke(t, bc, nsHash, "resolve", "alias.com", - "neo.com", int64(nns.CNAME)) - testNameServiceInvoke(t, bc, nsHash, "resolve", "sometxt", - "neo.com", int64(nns.TXT)) - testNameServiceInvoke(t, bc, nsHash, "resolve", stackitem.Null{}, - "neo.com", int64(nns.AAAA)) -} - -const ( - defaultNameServiceDomainPrice = 10_0000_0000 - defaultNameServiceSysfee = 6000_0000 - defaultRegisterSysfee = 10_0000_0000 + defaultNameServiceDomainPrice -) - -func testNameServiceInvoke(t *testing.T, bc *Blockchain, nsHash util.Uint160, method string, result interface{}, args ...interface{}) { - testNameServiceInvokeAux(t, bc, nsHash, defaultNameServiceSysfee, true, method, result, args...) -} - -func testNameServiceInvokeAux(t *testing.T, bc *Blockchain, nsHash util.Uint160, sysfee int64, signer interface{}, method string, result interface{}, args ...interface{}) { - if sysfee < 0 { - sysfee = defaultNameServiceSysfee - } - aer, err := invokeContractMethodGeneric(bc, sysfee, nsHash, method, signer, args...) - require.NoError(t, err) - if result == nil { - checkFAULTState(t, aer) - return - } - checkResult(t, aer, stackitem.Make(result)) -} - -func newAccountWithGAS(t *testing.T, bc *Blockchain) *wallet.Account { - acc, err := wallet.NewAccount() - require.NoError(t, err) - transferTokenFromMultisigAccount(t, bc, acc.PrivateKey().GetScriptHash(), bc.contracts.GAS.Hash, 1000_00000000) - return acc -} From 1f9fd4a472451e8673e1532ccc5199cdd7629c11 Mon Sep 17 00:00:00 2001 From: Evgeniy Stratonikov Date: Sat, 23 Oct 2021 15:36:26 +0300 Subject: [PATCH 3/7] neotest: add contract client wrapper Reduces amount of boilerplate code in tests. Signed-off-by: Evgeniy Stratonikov --- .../tests/nonnative_name_service_test.go | 415 ++++++++---------- pkg/neotest/basic.go | 4 +- pkg/neotest/client.go | 83 ++++ 3 files changed, 276 insertions(+), 226 deletions(-) create mode 100644 pkg/neotest/client.go diff --git a/examples/nft-nd-nns/tests/nonnative_name_service_test.go b/examples/nft-nd-nns/tests/nonnative_name_service_test.go index 1b23bb862..3a5d217cb 100644 --- a/examples/nft-nd-nns/tests/nonnative_name_service_test.go +++ b/examples/nft-nd-nns/tests/nonnative_name_service_test.go @@ -6,21 +6,16 @@ import ( nns "github.com/nspcc-dev/neo-go/examples/nft-nd-nns" "github.com/nspcc-dev/neo-go/pkg/compiler" - "github.com/nspcc-dev/neo-go/pkg/core/interop/interopnames" - "github.com/nspcc-dev/neo-go/pkg/core/transaction" - "github.com/nspcc-dev/neo-go/pkg/io" + "github.com/nspcc-dev/neo-go/pkg/core/interop/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/smartcontract/callflag" "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/nspcc-dev/neo-go/pkg/wallet" "github.com/stretchr/testify/require" ) -func newExecutorWithNS(t *testing.T) (*neotest.Executor, util.Uint160) { +func newNSClient(t *testing.T) *neotest.ContractInvoker { bc, acc := chain.NewSingle(t) e := neotest.NewExecutor(t, bc, acc) c := neotest.CompileFile(t, e.CommitteeHash, "..", "../nns.yml") @@ -29,85 +24,113 @@ func newExecutorWithNS(t *testing.T) (*neotest.Executor, util.Uint160) { h, err := e.Chain.GetContractScriptHash(1) require.NoError(t, err) require.Equal(t, c.Hash, h) - return e, c.Hash + return e.CommitteeInvoker(h) } -// -//func TestNameService_Price(t *testing.T) { -// e, nsHash := newExecutorWithNS(t) -// -// testGetSet(t, e.Chain, nsHash, "Price", -// defaultNameServiceDomainPrice, 0, 10000_00000000) -//} +func TestNameService_Price(t *testing.T) { + const ( + minPrice = int64(0) + maxPrice = int64(10000_00000000) + ) + + c := newNSClient(t) + + t.Run("set, not signed by committee", func(t *testing.T) { + acc := c.NewAccount(t) + cAcc := c.WithSigner(acc) + cAcc.InvokeFail(t, "not witnessed by committee", "setPrice", minPrice+1) + }) + + t.Run("get, default value", func(t *testing.T) { + c.Invoke(t, defaultNameServiceDomainPrice, "getPrice") + }) + + t.Run("set, too small value", func(t *testing.T) { + c.InvokeFail(t, "The price is out of range.", "setPrice", minPrice-1) + }) + + t.Run("set, too large value", func(t *testing.T) { + c.InvokeFail(t, "The price is out of range.", "setPrice", maxPrice+1) + }) + + t.Run("set, success", func(t *testing.T) { + txSet := c.PrepareInvoke(t, "setPrice", int64(defaultNameServiceDomainPrice+1)) + txGet := c.PrepareInvoke(t, "getPrice") + c.AddBlockCheckHalt(t, txSet, txGet) + c.CheckHalt(t, txSet.Hash(), stackitem.Null{}) + c.CheckHalt(t, txGet.Hash(), stackitem.Make(defaultNameServiceDomainPrice+1)) + + // Get in the next block. + c.Invoke(t, stackitem.Make(defaultNameServiceDomainPrice+1), "getPrice") + }) +} func TestNonfungible(t *testing.T) { - e, nsHash := newExecutorWithNS(t) + c := newNSClient(t) - acc := e.NewAccount(t) - testNameServiceInvokeAux(t, e, nsHash, defaultNameServiceSysfee, acc, "symbol", "NNS") - testNameServiceInvokeAux(t, e, nsHash, defaultNameServiceSysfee, acc, "decimals", 0) - testNameServiceInvokeAux(t, e, nsHash, defaultNameServiceSysfee, acc, "totalSupply", 0) + c.Signer = c.NewAccount(t) + c.Invoke(t, "NNS", "symbol") + c.Invoke(t, 0, "decimals") + c.Invoke(t, 0, "totalSupply") } func TestAddRoot(t *testing.T) { - e, nsHash := newExecutorWithNS(t) + c := newNSClient(t) t.Run("invalid format", func(t *testing.T) { - testNameServiceInvoke(t, e, nsHash, "addRoot", nil, "") + c.InvokeFail(t, "invalid root format", "addRoot", "") }) t.Run("not signed by committee", func(t *testing.T) { - acc := e.NewAccount(t) - tx := e.PrepareInvoke(t, acc, nsHash, "addRoot", "some") - e.AddBlock(t, tx) - e.CheckFault(t, tx.Hash(), "not witnessed by committee") + acc := c.NewAccount(t) + c := c.WithSigner(acc) + c.InvokeFail(t, "not witnessed by committee", "addRoot", "some") }) - testNameServiceInvoke(t, e, nsHash, "addRoot", stackitem.Null{}, "some") + c.Invoke(t, stackitem.Null{}, "addRoot", "some") t.Run("already exists", func(t *testing.T) { - testNameServiceInvoke(t, e, nsHash, "addRoot", nil, "some") + c.InvokeFail(t, "already exists", "addRoot", "some") }) } func TestExpiration(t *testing.T) { - e, nsHash := newExecutorWithNS(t) + c := newNSClient(t) + e := c.Executor bc := e.Chain acc := e.NewAccount(t) + cAcc := c.WithSigner(acc) - testNameServiceInvoke(t, e, nsHash, "addRoot", stackitem.Null{}, "com") - testNameServiceInvokeAux(t, e, nsHash, defaultRegisterSysfee, acc, "register", - true, "first.com", acc.Contract.ScriptHash()) - - testNameServiceInvokeAux(t, e, nsHash, defaultNameServiceSysfee, acc, - "setRecord", stackitem.Null{}, "first.com", int64(nns.TXT), "sometext") + c.Invoke(t, stackitem.Null{}, "addRoot", "com") + cAcc.Invoke(t, true, "register", "first.com", acc.Contract.ScriptHash()) + cAcc.Invoke(t, stackitem.Null{}, "setRecord", "first.com", int64(nns.TXT), "sometext") b1 := e.TopBlock(t) - tx := e.PrepareInvoke(t, acc, nsHash, "register", "second.com", acc.Contract.ScriptHash()) - b2 := e.NewBlock(t, tx) + tx := cAcc.PrepareInvoke(t, "register", "second.com", acc.Contract.ScriptHash()) + b2 := e.NewUnsignedBlock(t, tx) b2.Index = b1.Index + 1 b2.PrevHash = b1.Hash() b2.Timestamp = b1.Timestamp + 10000 require.NoError(t, bc.AddBlock(e.SignBlock(b2))) e.CheckHalt(t, tx.Hash()) - tx = e.PrepareInvoke(t, acc, nsHash, "isAvailable", "first.com") - b3 := e.NewBlock(t, tx) + tx = cAcc.PrepareInvoke(t, "isAvailable", "first.com") + b3 := e.NewUnsignedBlock(t, tx) b3.Index = b2.Index + 1 b3.PrevHash = b2.Hash() b3.Timestamp = b1.Timestamp + (millisecondsInYear + 1) require.NoError(t, bc.AddBlock(e.SignBlock(b3))) e.CheckHalt(t, tx.Hash(), stackitem.NewBool(true)) - tx = e.PrepareInvoke(t, acc, nsHash, "isAvailable", "second.com") - b4 := e.NewBlock(t, tx) + tx = cAcc.PrepareInvoke(t, "isAvailable", "second.com") + b4 := e.NewUnsignedBlock(t, tx) b4.Index = b3.Index + 1 b4.PrevHash = b3.Hash() b4.Timestamp = b3.Timestamp + 1000 require.NoError(t, bc.AddBlock(e.SignBlock(b4))) e.CheckHalt(t, tx.Hash(), stackitem.NewBool(false)) - tx = e.PrepareInvoke(t, acc, nsHash, "getRecord", "first.com", int64(nns.TXT)) - b5 := e.NewBlock(t, tx) + tx = cAcc.PrepareInvoke(t, "getRecord", "first.com", int64(nns.TXT)) + b5 := e.NewUnsignedBlock(t, tx) b5.Index = b4.Index + 1 b5.PrevHash = b4.Hash() b5.Timestamp = b4.Timestamp + 1000 @@ -118,90 +141,87 @@ func TestExpiration(t *testing.T) { const millisecondsInYear = 365 * 24 * 3600 * 1000 func TestRegisterAndRenew(t *testing.T) { - e, nsHash := newExecutorWithNS(t) + c := newNSClient(t) + e := c.Executor - testNameServiceInvoke(t, e, nsHash, "isAvailable", nil, "neo.com") - testNameServiceInvoke(t, e, nsHash, "addRoot", stackitem.Null{}, "org") - testNameServiceInvoke(t, e, nsHash, "isAvailable", nil, "neo.com") - testNameServiceInvoke(t, e, nsHash, "addRoot", stackitem.Null{}, "com") - testNameServiceInvoke(t, e, nsHash, "isAvailable", true, "neo.com") - testNameServiceInvoke(t, e, nsHash, "register", nil, "neo.org", e.CommitteeHash) - testNameServiceInvoke(t, e, nsHash, "register", nil, "docs.neo.org", e.CommitteeHash) - testNameServiceInvoke(t, e, nsHash, "register", nil, "\nneo.com'", e.CommitteeHash) - testNameServiceInvoke(t, e, nsHash, "register", nil, "neo.com\n", e.CommitteeHash) - testNameServiceInvoke(t, e, nsHash, "register", nil, "neo.com", e.CommitteeHash) - testNameServiceInvokeAux(t, e, nsHash, defaultNameServiceDomainPrice, e.Committee, "register", - nil, "neo.com", e.CommitteeHash) + c.InvokeFail(t, "root not found", "isAvailable", "neo.com") + c.Invoke(t, stackitem.Null{}, "addRoot", "org") + c.InvokeFail(t, "root not found", "isAvailable", "neo.com") + c.Invoke(t, stackitem.Null{}, "addRoot", "com") + c.Invoke(t, true, "isAvailable", "neo.com") + c.InvokeWithFeeFail(t, "GAS limit exceeded", defaultNameServiceSysfee, "register", "neo.org", e.CommitteeHash) + c.InvokeFail(t, "invalid domain name format", "register", "docs.neo.org", e.CommitteeHash) + c.InvokeFail(t, "invalid domain name format", "register", "\nneo.com'", e.CommitteeHash) + c.InvokeFail(t, "invalid domain name format", "register", "neo.com\n", e.CommitteeHash) + c.InvokeWithFeeFail(t, "GAS limit exceeded", defaultNameServiceSysfee, "register", "neo.org", e.CommitteeHash) + c.InvokeWithFeeFail(t, "GAS limit exceeded", defaultNameServiceDomainPrice, "register", "neo.com", e.CommitteeHash) - testNameServiceInvoke(t, e, nsHash, "isAvailable", true, "neo.com") - testNameServiceInvoke(t, e, nsHash, "balanceOf", 0, e.CommitteeHash) - testNameServiceInvokeAux(t, e, nsHash, defaultRegisterSysfee, e.Committee, "register", - true, "neo.com", e.CommitteeHash) + c.Invoke(t, true, "isAvailable", "neo.com") + c.Invoke(t, 0, "balanceOf", e.CommitteeHash) + c.Invoke(t, true, "register", "neo.com", e.CommitteeHash) topBlock := e.TopBlock(t) expectedExpiration := topBlock.Timestamp + millisecondsInYear - testNameServiceInvokeAux(t, e, nsHash, defaultRegisterSysfee, e.Committee, "register", - false, "neo.com", e.CommitteeHash) - testNameServiceInvoke(t, e, nsHash, "isAvailable", false, "neo.com") + c.Invoke(t, false, "register", "neo.com", e.CommitteeHash) + c.Invoke(t, false, "isAvailable", "neo.com") props := stackitem.NewMap() props.Add(stackitem.Make("name"), stackitem.Make("neo.com")) props.Add(stackitem.Make("expiration"), stackitem.Make(expectedExpiration)) - testNameServiceInvoke(t, e, nsHash, "properties", props, "neo.com") - testNameServiceInvoke(t, e, nsHash, "balanceOf", 1, e.CommitteeHash) - testNameServiceInvoke(t, e, nsHash, "ownerOf", e.CommitteeHash.BytesBE(), []byte("neo.com")) + c.Invoke(t, props, "properties", "neo.com") + c.Invoke(t, 1, "balanceOf", e.CommitteeHash) + c.Invoke(t, e.CommitteeHash.BytesBE(), "ownerOf", []byte("neo.com")) t.Run("invalid token ID", func(t *testing.T) { - testNameServiceInvoke(t, e, nsHash, "properties", nil, "not.exists") - testNameServiceInvoke(t, e, nsHash, "ownerOf", nil, "not.exists") - testNameServiceInvoke(t, e, nsHash, "properties", nil, []interface{}{}) - testNameServiceInvoke(t, e, nsHash, "ownerOf", nil, []interface{}{}) + c.InvokeFail(t, "token not found", "properties", "not.exists") + c.InvokeFail(t, "token not found", "ownerOf", "not.exists") + c.InvokeFail(t, "invalid conversion", "properties", []interface{}{}) + c.InvokeFail(t, "invalid conversion", "ownerOf", []interface{}{}) }) // Renew expectedExpiration += millisecondsInYear - testNameServiceInvokeAux(t, e, nsHash, 100_0000_0000, e.Committee, "renew", expectedExpiration, "neo.com") + c.Invoke(t, expectedExpiration, "renew", "neo.com") props.Add(stackitem.Make("expiration"), stackitem.Make(expectedExpiration)) - testNameServiceInvoke(t, e, nsHash, "properties", props, "neo.com") + c.Invoke(t, props, "properties", "neo.com") } func TestSetGetRecord(t *testing.T) { - e, nsHash := newExecutorWithNS(t) + c := newNSClient(t) + e := c.Executor acc := e.NewAccount(t) - testNameServiceInvoke(t, e, nsHash, "addRoot", stackitem.Null{}, "com") + cAcc := c.WithSigner(acc) + c.Invoke(t, stackitem.Null{}, "addRoot", "com") t.Run("set before register", func(t *testing.T) { - testNameServiceInvoke(t, e, nsHash, "setRecord", nil, "neo.com", int64(nns.TXT), "sometext") + c.InvokeFail(t, "token not found", "setRecord", "neo.com", int64(nns.TXT), "sometext") }) - testNameServiceInvokeAux(t, e, nsHash, defaultRegisterSysfee, e.Committee, "register", - true, "neo.com", e.CommitteeHash) + c.Invoke(t, true, "register", "neo.com", e.CommitteeHash) t.Run("invalid parameters", func(t *testing.T) { - testNameServiceInvoke(t, e, nsHash, "setRecord", nil, "neo.com", int64(0xFF), "1.2.3.4") - testNameServiceInvoke(t, e, nsHash, "setRecord", nil, "neo.com", int64(nns.A), "not.an.ip.address") + c.InvokeFail(t, "unsupported record type", "setRecord", "neo.com", int64(0xFF), "1.2.3.4") + c.InvokeFail(t, "invalid record", "setRecord", "neo.com", int64(nns.A), "not.an.ip.address") }) t.Run("invalid witness", func(t *testing.T) { - testNameServiceInvokeAux(t, e, nsHash, defaultNameServiceSysfee, acc, "setRecord", nil, - "neo.com", int64(nns.A), "1.2.3.4") + cAcc.InvokeFail(t, "not witnessed by admin", "setRecord", "neo.com", int64(nns.A), "1.2.3.4") }) - testNameServiceInvoke(t, e, nsHash, "getRecord", stackitem.Null{}, "neo.com", int64(nns.A)) - testNameServiceInvoke(t, e, nsHash, "setRecord", stackitem.Null{}, "neo.com", int64(nns.A), "1.2.3.4") - testNameServiceInvoke(t, e, nsHash, "getRecord", "1.2.3.4", "neo.com", int64(nns.A)) - testNameServiceInvoke(t, e, nsHash, "setRecord", stackitem.Null{}, "neo.com", int64(nns.A), "1.2.3.4") - testNameServiceInvoke(t, e, nsHash, "getRecord", "1.2.3.4", "neo.com", int64(nns.A)) - testNameServiceInvoke(t, e, nsHash, "setRecord", stackitem.Null{}, "neo.com", int64(nns.AAAA), "2001:0201:1f1f:0000:0000:0100:11a0:11df") - testNameServiceInvoke(t, e, nsHash, "setRecord", stackitem.Null{}, "neo.com", int64(nns.CNAME), "nspcc.ru") - testNameServiceInvoke(t, e, nsHash, "setRecord", stackitem.Null{}, "neo.com", int64(nns.TXT), "sometext") + c.Invoke(t, stackitem.Null{}, "getRecord", "neo.com", int64(nns.A)) + c.Invoke(t, stackitem.Null{}, "setRecord", "neo.com", int64(nns.A), "1.2.3.4") + c.Invoke(t, "1.2.3.4", "getRecord", "neo.com", int64(nns.A)) + c.Invoke(t, stackitem.Null{}, "setRecord", "neo.com", int64(nns.A), "1.2.3.4") + c.Invoke(t, "1.2.3.4", "getRecord", "neo.com", int64(nns.A)) + c.Invoke(t, stackitem.Null{}, "setRecord", "neo.com", int64(nns.AAAA), "2001:0201:1f1f:0000:0000:0100:11a0:11df") + c.Invoke(t, stackitem.Null{}, "setRecord", "neo.com", int64(nns.CNAME), "nspcc.ru") + c.Invoke(t, stackitem.Null{}, "setRecord", "neo.com", int64(nns.TXT), "sometext") // Delete record. t.Run("invalid witness", func(t *testing.T) { - testNameServiceInvokeAux(t, e, nsHash, defaultNameServiceSysfee, acc, "setRecord", nil, - "neo.com", int64(nns.CNAME)) + cAcc.InvokeFail(t, "not witnessed by admin", "deleteRecord", "neo.com", int64(nns.CNAME)) }) - testNameServiceInvoke(t, e, nsHash, "getRecord", "nspcc.ru", "neo.com", int64(nns.CNAME)) - testNameServiceInvoke(t, e, nsHash, "deleteRecord", stackitem.Null{}, "neo.com", int64(nns.CNAME)) - testNameServiceInvoke(t, e, nsHash, "getRecord", stackitem.Null{}, "neo.com", int64(nns.CNAME)) - testNameServiceInvoke(t, e, nsHash, "getRecord", "1.2.3.4", "neo.com", int64(nns.A)) + c.Invoke(t, "nspcc.ru", "getRecord", "neo.com", int64(nns.CNAME)) + c.Invoke(t, stackitem.Null{}, "deleteRecord", "neo.com", int64(nns.CNAME)) + c.Invoke(t, stackitem.Null{}, "getRecord", "neo.com", int64(nns.CNAME)) + c.Invoke(t, "1.2.3.4", "getRecord", "neo.com", int64(nns.A)) t.Run("SetRecord_compatibility", func(t *testing.T) { // tests are got from the NNS C# implementation and changed accordingly to non-native implementation behaviour @@ -258,206 +278,153 @@ func TestSetGetRecord(t *testing.T) { {Type: nns.AAAA, Name: "2001::13.1.68.3", ShouldFail: true}, } for _, testCase := range testCases { - var expected interface{} - if testCase.ShouldFail { - expected = nil - } else { - expected = stackitem.Null{} - } + args := []interface{}{"neo.com", int64(testCase.Type), testCase.Name} t.Run(testCase.Name, func(t *testing.T) { - testNameServiceInvoke(t, e, nsHash, "setRecord", expected, "neo.com", int64(testCase.Type), testCase.Name) + if testCase.ShouldFail { + c.InvokeFail(t, "", "setRecord", args...) + } else { + c.Invoke(t, stackitem.Null{}, "setRecord", args...) + } }) } }) } func TestSetAdmin(t *testing.T) { - e, nsHash := newExecutorWithNS(t) + c := newNSClient(t) + e := c.Executor owner := e.NewAccount(t) + cOwner := c.WithSigner(owner) admin := e.NewAccount(t) + cAdmin := c.WithSigner(admin) guest := e.NewAccount(t) - testNameServiceInvoke(t, e, nsHash, "addRoot", stackitem.Null{}, "com") + cGuest := c.WithSigner(guest) - testNameServiceInvokeAux(t, e, nsHash, defaultRegisterSysfee, owner, "register", true, - "neo.com", owner.PrivateKey().GetScriptHash()) - testNameServiceInvokeAux(t, e, nsHash, defaultNameServiceSysfee, guest, "setAdmin", nil, - "neo.com", admin.PrivateKey().GetScriptHash()) + c.Invoke(t, stackitem.Null{}, "addRoot", "com") + + cOwner.Invoke(t, true, "register", "neo.com", owner.PrivateKey().GetScriptHash()) + cGuest.InvokeFail(t, "not witnessed", "setAdmin", "neo.com", admin.PrivateKey().GetScriptHash()) // Must be witnessed by both owner and admin. - testNameServiceInvokeAux(t, e, nsHash, defaultNameServiceSysfee, owner, "setAdmin", nil, - "neo.com", admin.PrivateKey().GetScriptHash()) - testNameServiceInvokeAux(t, e, nsHash, defaultNameServiceSysfee, admin, "setAdmin", nil, - "neo.com", admin.PrivateKey().GetScriptHash()) - testNameServiceInvokeAux(t, e, nsHash, defaultNameServiceSysfee, []*wallet.Account{owner, admin}, - "setAdmin", stackitem.Null{}, - "neo.com", admin.PrivateKey().GetScriptHash()) + cOwner.InvokeFail(t, "not witnessed by admin", "setAdmin", "neo.com", admin.PrivateKey().GetScriptHash()) + cAdmin.InvokeFail(t, "not witnessed by owner", "setAdmin", "neo.com", admin.PrivateKey().GetScriptHash()) + cc := c.WithSigner([]*wallet.Account{owner, admin}) + cc.Invoke(t, stackitem.Null{}, "setAdmin", "neo.com", admin.PrivateKey().GetScriptHash()) t.Run("set and delete by admin", func(t *testing.T) { - testNameServiceInvokeAux(t, e, nsHash, defaultNameServiceSysfee, admin, "setRecord", stackitem.Null{}, - "neo.com", int64(nns.TXT), "sometext") - testNameServiceInvokeAux(t, e, nsHash, defaultNameServiceSysfee, guest, "deleteRecord", nil, - "neo.com", int64(nns.TXT)) - testNameServiceInvokeAux(t, e, nsHash, defaultNameServiceSysfee, admin, "deleteRecord", stackitem.Null{}, - "neo.com", int64(nns.TXT)) + cAdmin.Invoke(t, stackitem.Null{}, "setRecord", "neo.com", int64(nns.TXT), "sometext") + cGuest.InvokeFail(t, "not witnessed by admin", "deleteRecord", "neo.com", int64(nns.TXT)) + cAdmin.Invoke(t, stackitem.Null{}, "deleteRecord", "neo.com", int64(nns.TXT)) }) t.Run("set admin to null", func(t *testing.T) { - testNameServiceInvokeAux(t, e, nsHash, defaultNameServiceSysfee, admin, "setRecord", stackitem.Null{}, - "neo.com", int64(nns.TXT), "sometext") - testNameServiceInvokeAux(t, e, nsHash, defaultNameServiceSysfee, owner, "setAdmin", stackitem.Null{}, - "neo.com", nil) - testNameServiceInvokeAux(t, e, nsHash, defaultNameServiceSysfee, admin, "deleteRecord", nil, - "neo.com", int64(nns.TXT)) + cAdmin.Invoke(t, stackitem.Null{}, "setRecord", "neo.com", int64(nns.TXT), "sometext") + cOwner.Invoke(t, stackitem.Null{}, "setAdmin", "neo.com", nil) + cAdmin.InvokeFail(t, "not witnessed by admin", "deleteRecord", "neo.com", int64(nns.TXT)) }) } func TestTransfer(t *testing.T) { - e, nsHash := newExecutorWithNS(t) + c := newNSClient(t) + e := c.Executor from := e.NewAccount(t) + cFrom := c.WithSigner(from) to := e.NewAccount(t) + cTo := c.WithSigner(to) - testNameServiceInvoke(t, e, nsHash, "addRoot", stackitem.Null{}, "com") - testNameServiceInvokeAux(t, e, nsHash, defaultRegisterSysfee, from, "register", - true, "neo.com", from.PrivateKey().GetScriptHash()) - testNameServiceInvokeAux(t, e, nsHash, defaultNameServiceSysfee, from, "setRecord", stackitem.Null{}, - "neo.com", int64(nns.A), "1.2.3.4") - testNameServiceInvokeAux(t, e, nsHash, defaultRegisterSysfee, from, "transfer", - nil, to.Contract.ScriptHash().BytesBE(), []byte("not.exists"), nil) - testNameServiceInvokeAux(t, e, nsHash, defaultRegisterSysfee, e.Committee, "transfer", - false, to.Contract.ScriptHash().BytesBE(), []byte("neo.com"), nil) - testNameServiceInvokeAux(t, e, nsHash, defaultRegisterSysfee, from, "transfer", - true, to.Contract.ScriptHash().BytesBE(), []byte("neo.com"), nil) - testNameServiceInvokeAux(t, e, nsHash, defaultNameServiceSysfee, from, "totalSupply", 1) - testNameServiceInvokeAux(t, e, nsHash, defaultNameServiceSysfee, from, "ownerOf", - to.Contract.ScriptHash().BytesBE(), []byte("neo.com")) + c.Invoke(t, stackitem.Null{}, "addRoot", "com") + cFrom.Invoke(t, true, "register", "neo.com", from.PrivateKey().GetScriptHash()) + cFrom.Invoke(t, stackitem.Null{}, "setRecord", "neo.com", int64(nns.A), "1.2.3.4") + cFrom.InvokeFail(t, "token not found", "transfer", to.Contract.ScriptHash(), "not.exists", nil) + c.Invoke(t, false, "transfer", to.Contract.ScriptHash(), "neo.com", nil) + cFrom.Invoke(t, true, "transfer", to.Contract.ScriptHash(), "neo.com", nil) + cFrom.Invoke(t, 1, "totalSupply") + cFrom.Invoke(t, to.Contract.ScriptHash().BytesBE(), "ownerOf", "neo.com") // without onNEP11Transfer - c := neotest.CompileSource(t, e.CommitteeHash, + ctr := neotest.CompileSource(t, e.CommitteeHash, strings.NewReader(`package foo func Main() int { return 0 }`), &compiler.Options{Name: "foo"}) - e.DeployContract(t, c, nil) - testNameServiceInvokeAux(t, e, nsHash, defaultRegisterSysfee, to, "transfer", - nil, c.Hash.BytesBE(), []byte("neo.com"), nil) + e.DeployContract(t, ctr, nil) + cTo.InvokeFail(t, "method not found", "transfer", ctr.Hash, []byte("neo.com"), nil) // with onNEP11Transfer - c = neotest.CompileSource(t, e.CommitteeHash, + ctr = neotest.CompileSource(t, e.CommitteeHash, strings.NewReader(`package foo import "github.com/nspcc-dev/neo-go/pkg/interop" func OnNEP11Payment(from interop.Hash160, amount int, token []byte, data interface{}) {}`), &compiler.Options{Name: "foo"}) - e.DeployContract(t, c, nil) - testNameServiceInvokeAux(t, e, nsHash, defaultRegisterSysfee, to, "transfer", - true, c.Hash.BytesBE(), []byte("neo.com"), nil) - testNameServiceInvokeAux(t, e, nsHash, defaultNameServiceSysfee, from, "totalSupply", 1) - testNameServiceInvokeAux(t, e, nsHash, defaultNameServiceSysfee, from, "ownerOf", - c.Hash.BytesBE(), []byte("neo.com")) + e.DeployContract(t, ctr, nil) + cTo.Invoke(t, true, "transfer", ctr.Hash, []byte("neo.com"), nil) + cFrom.Invoke(t, 1, "totalSupply") + cFrom.Invoke(t, ctr.Hash.BytesBE(), "ownerOf", []byte("neo.com")) } func TestTokensOf(t *testing.T) { - e, nsHash := newExecutorWithNS(t) + c := newNSClient(t) + e := c.Executor acc1 := e.NewAccount(t) + cAcc1 := c.WithSigner(acc1) acc2 := e.NewAccount(t) + cAcc2 := c.WithSigner(acc2) - testNameServiceInvoke(t, e, nsHash, "addRoot", stackitem.Null{}, "com") - testNameServiceInvokeAux(t, e, nsHash, defaultRegisterSysfee, acc1, "register", - true, "neo.com", acc1.PrivateKey().GetScriptHash()) - testNameServiceInvokeAux(t, e, nsHash, defaultRegisterSysfee, acc2, "register", - true, "nspcc.com", acc2.PrivateKey().GetScriptHash()) + c.Invoke(t, stackitem.Null{}, "addRoot", "com") + cAcc1.Invoke(t, true, "register", "neo.com", acc1.PrivateKey().GetScriptHash()) + cAcc2.Invoke(t, true, "register", "nspcc.com", acc2.PrivateKey().GetScriptHash()) - testTokensOf(t, e, nsHash, [][]byte{[]byte("neo.com")}, acc1.Contract.ScriptHash().BytesBE()) - testTokensOf(t, e, nsHash, [][]byte{[]byte("nspcc.com")}, acc2.Contract.ScriptHash().BytesBE()) - testTokensOf(t, e, nsHash, [][]byte{[]byte("neo.com"), []byte("nspcc.com")}) - testTokensOf(t, e, nsHash, [][]byte{}, util.Uint160{}.BytesBE()) // empty hash is a valid hash still + testTokensOf(t, c, [][]byte{[]byte("neo.com")}, acc1.Contract.ScriptHash().BytesBE()) + testTokensOf(t, c, [][]byte{[]byte("nspcc.com")}, acc2.Contract.ScriptHash().BytesBE()) + testTokensOf(t, c, [][]byte{[]byte("neo.com"), []byte("nspcc.com")}) + testTokensOf(t, c, [][]byte{}, util.Uint160{}.BytesBE()) // empty hash is a valid hash still } -func testTokensOf(t *testing.T, e *neotest.Executor, nsHash util.Uint160, result [][]byte, args ...interface{}) { +func testTokensOf(t *testing.T, c *neotest.ContractInvoker, result [][]byte, args ...interface{}) { method := "tokensOf" if len(args) == 0 { method = "tokens" } - w := io.NewBufBinWriter() - emit.AppCall(w.BinWriter, nsHash, method, callflag.All, args...) - for range result { - emit.Opcodes(w.BinWriter, opcode.DUP) - emit.Syscall(w.BinWriter, interopnames.SystemIteratorNext) - emit.Opcodes(w.BinWriter, opcode.ASSERT) - - emit.Opcodes(w.BinWriter, opcode.DUP) - emit.Syscall(w.BinWriter, interopnames.SystemIteratorValue) - emit.Opcodes(w.BinWriter, opcode.SWAP) - } - emit.Opcodes(w.BinWriter, opcode.DROP) - emit.Int(w.BinWriter, int64(len(result))) - emit.Opcodes(w.BinWriter, opcode.PACK) - require.NoError(t, w.Err) - script := w.Bytes() - tx := transaction.New(script, defaultNameServiceSysfee) - tx.ValidUntilBlock = e.Chain.BlockHeight() + 1 - v, err := neotest.TestInvoke(e.Chain, tx) + s, err := c.TestInvoke(t, method, args...) if result == nil { require.Error(t, err) return } require.NoError(t, err) + iter := s.Pop().Interop().Value().(*storage.Iterator) arr := make([]stackitem.Item, 0, len(result)) - for i := len(result) - 1; i >= 0; i-- { + for i := range result { + require.True(t, iter.Next()) + require.Equal(t, result[i], iter.Value().Value()) arr = append(arr, stackitem.Make(result[i])) } - require.Equal(t, stackitem.NewArray(arr), v.Estack().Pop().Item()) + require.False(t, iter.Next()) } func TestResolve(t *testing.T) { - e, nsHash := newExecutorWithNS(t) + c := newNSClient(t) + e := c.Executor acc := e.NewAccount(t) + cAcc := c.WithSigner(acc) - testNameServiceInvoke(t, e, nsHash, "addRoot", stackitem.Null{}, "com") - testNameServiceInvokeAux(t, e, nsHash, defaultRegisterSysfee, acc, "register", - true, "neo.com", acc.PrivateKey().GetScriptHash()) - testNameServiceInvokeAux(t, e, nsHash, defaultNameServiceSysfee, acc, "setRecord", stackitem.Null{}, - "neo.com", int64(nns.A), "1.2.3.4") - testNameServiceInvokeAux(t, e, nsHash, defaultNameServiceSysfee, acc, "setRecord", stackitem.Null{}, - "neo.com", int64(nns.CNAME), "alias.com") + c.Invoke(t, stackitem.Null{}, "addRoot", "com") + cAcc.Invoke(t, true, "register", "neo.com", acc.PrivateKey().GetScriptHash()) + cAcc.Invoke(t, stackitem.Null{}, "setRecord", "neo.com", int64(nns.A), "1.2.3.4") + cAcc.Invoke(t, stackitem.Null{}, "setRecord", "neo.com", int64(nns.CNAME), "alias.com") - testNameServiceInvokeAux(t, e, nsHash, defaultRegisterSysfee, acc, "register", - true, "alias.com", acc.PrivateKey().GetScriptHash()) - testNameServiceInvokeAux(t, e, nsHash, defaultNameServiceSysfee, acc, "setRecord", stackitem.Null{}, - "alias.com", int64(nns.TXT), "sometxt") + cAcc.Invoke(t, true, "register", "alias.com", acc.PrivateKey().GetScriptHash()) + cAcc.Invoke(t, stackitem.Null{}, "setRecord", "alias.com", int64(nns.TXT), "sometxt") - testNameServiceInvoke(t, e, nsHash, "resolve", "1.2.3.4", - "neo.com", int64(nns.A)) - testNameServiceInvoke(t, e, nsHash, "resolve", "alias.com", - "neo.com", int64(nns.CNAME)) - testNameServiceInvoke(t, e, nsHash, "resolve", "sometxt", - "neo.com", int64(nns.TXT)) - testNameServiceInvoke(t, e, nsHash, "resolve", stackitem.Null{}, - "neo.com", int64(nns.AAAA)) + c.Invoke(t, "1.2.3.4", "resolve", "neo.com", int64(nns.A)) + c.Invoke(t, "alias.com", "resolve", "neo.com", int64(nns.CNAME)) + c.Invoke(t, "sometxt", "resolve", "neo.com", int64(nns.TXT)) + c.Invoke(t, stackitem.Null{}, "resolve", "neo.com", int64(nns.AAAA)) } const ( defaultNameServiceDomainPrice = 10_0000_0000 defaultNameServiceSysfee = 6000_0000 - defaultRegisterSysfee = 10_0000_0000 + defaultNameServiceDomainPrice ) - -func testNameServiceInvoke(t *testing.T, e *neotest.Executor, nsHash util.Uint160, method string, result interface{}, args ...interface{}) { - testNameServiceInvokeAux(t, e, nsHash, defaultNameServiceSysfee, e.Committee, method, result, args...) -} - -func testNameServiceInvokeAux(t *testing.T, e *neotest.Executor, nsHash util.Uint160, sysfee int64, signer interface{}, method string, result interface{}, args ...interface{}) { - if sysfee < 0 { - sysfee = defaultNameServiceSysfee - } - tx := e.PrepareInvokeNoSign(t, nsHash, method, args...) - e.SignTx(t, tx, sysfee, signer) - e.AddBlock(t, tx) - if result == nil { - e.CheckFault(t, tx.Hash(), "") - } else { - e.CheckHalt(t, tx.Hash(), stackitem.Make(result)) - } -} diff --git a/pkg/neotest/basic.go b/pkg/neotest/basic.go index b374e0d53..0e6e9ac16 100644 --- a/pkg/neotest/basic.go +++ b/pkg/neotest/basic.go @@ -238,8 +238,8 @@ func (e *Executor) SignBlock(b *block.Block) *block.Block { return b } -// AddBlockCheckHalt is a convenient wrapper over AddNewBlock and CheckHalt. -func (e *Executor) AddBlockCheckHalt(t *testing.T, bc blockchainer.Blockchainer, txs ...*transaction.Transaction) *block.Block { +// AddBlockCheckHalt is a convenient wrapper over AddBlock and CheckHalt. +func (e *Executor) AddBlockCheckHalt(t *testing.T, txs ...*transaction.Transaction) *block.Block { b := e.AddNewBlock(t, txs...) for _, tx := range txs { e.CheckHalt(t, tx.Hash()) diff --git a/pkg/neotest/client.go b/pkg/neotest/client.go new file mode 100644 index 000000000..e767ed58b --- /dev/null +++ b/pkg/neotest/client.go @@ -0,0 +1,83 @@ +package neotest + +import ( + "testing" + + "github.com/nspcc-dev/neo-go/pkg/core/transaction" + "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/stackitem" +) + +// ContractInvoker is a client for specific contract. +type ContractInvoker struct { + *Executor + Hash util.Uint160 + Signer interface{} +} + +// CommitteeInvoker creates new ContractInvoker for contract with hash h. +func (e *Executor) CommitteeInvoker(h util.Uint160) *ContractInvoker { + return &ContractInvoker{ + Executor: e, + Hash: h, + Signer: e.Committee, + } +} + +// TestInvoke creates test VM and invokes method with args. +func (c *ContractInvoker) TestInvoke(t *testing.T, method string, args ...interface{}) (*vm.Stack, error) { + tx := c.PrepareInvokeNoSign(t, method, args...) + b := c.NewUnsignedBlock(t, tx) + v, f := c.Chain.GetTestVM(trigger.Application, tx, b) + t.Cleanup(f) + + v.LoadWithFlags(tx.Script, callflag.All) + err := v.Run() + return v.Estack(), err +} + +// WithSigner creates new client with the provided signer. +func (c *ContractInvoker) WithSigner(signer interface{}) *ContractInvoker { + newC := *c + newC.Signer = signer + return &newC +} + +// PrepareInvoke creates new invocation transaction. +func (c *ContractInvoker) PrepareInvoke(t *testing.T, method string, args ...interface{}) *transaction.Transaction { + return c.Executor.NewTx(t, c.Signer, c.Hash, method, args...) +} + +// PrepareInvokeNoSign creates new unsigned invocation transaction. +func (c *ContractInvoker) PrepareInvokeNoSign(t *testing.T, 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 { + tx := c.PrepareInvoke(t, method, args...) + c.AddNewBlock(t, tx) + c.CheckHalt(t, tx.Hash(), stackitem.Make(result)) + return tx.Hash() +} + +// 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 { + tx := c.PrepareInvokeNoSign(t, method, args...) + c.Executor.SignTx(t, tx, sysFee, c.Signer) + c.AddNewBlock(t, tx) + c.CheckFault(t, tx.Hash(), message) + return tx.Hash() +} + +// 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{}) { + tx := c.PrepareInvoke(t, method, args...) + c.AddNewBlock(t, tx) + c.CheckFault(t, tx.Hash(), message) +} From 950adb7b893194309bb66312b309335e7ca8ea17 Mon Sep 17 00:00:00 2001 From: Evgeniy Stratonikov Date: Wed, 3 Nov 2021 13:44:46 +0300 Subject: [PATCH 4/7] neotest: support painless multi-signing Implementing a separate `Signer` interface is beneficial in multiple ways: 1. Support both single and multiple transaction witnesses. 2. It should be easy to add contract signer this way. Tests should use accounts created with `NewAccount` so hiding all details doesn't seem to be an issue. Signed-off-by: Evgeniy Stratonikov --- .../tests/nonnative_name_service_test.go | 65 +++++---- pkg/neotest/basic.go | 74 +++++------ pkg/neotest/chain/chain.go | 5 +- pkg/neotest/client.go | 16 +-- pkg/neotest/signer.go | 124 ++++++++++++++++++ 5 files changed, 197 insertions(+), 87 deletions(-) create mode 100644 pkg/neotest/signer.go diff --git a/examples/nft-nd-nns/tests/nonnative_name_service_test.go b/examples/nft-nd-nns/tests/nonnative_name_service_test.go index 3a5d217cb..c6176e8a2 100644 --- a/examples/nft-nd-nns/tests/nonnative_name_service_test.go +++ b/examples/nft-nd-nns/tests/nonnative_name_service_test.go @@ -11,7 +11,6 @@ import ( "github.com/nspcc-dev/neo-go/pkg/neotest/chain" "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" ) @@ -37,7 +36,7 @@ func TestNameService_Price(t *testing.T) { t.Run("set, not signed by committee", func(t *testing.T) { acc := c.NewAccount(t) - cAcc := c.WithSigner(acc) + cAcc := c.WithSigners(acc) cAcc.InvokeFail(t, "not witnessed by committee", "setPrice", minPrice+1) }) @@ -68,7 +67,7 @@ func TestNameService_Price(t *testing.T) { func TestNonfungible(t *testing.T) { c := newNSClient(t) - c.Signer = c.NewAccount(t) + c.Signers = []neotest.Signer{c.NewAccount(t)} c.Invoke(t, "NNS", "symbol") c.Invoke(t, 0, "decimals") c.Invoke(t, 0, "totalSupply") @@ -82,7 +81,7 @@ func TestAddRoot(t *testing.T) { }) t.Run("not signed by committee", func(t *testing.T) { acc := c.NewAccount(t) - c := c.WithSigner(acc) + c := c.WithSigners(acc) c.InvokeFail(t, "not witnessed by committee", "addRoot", "some") }) @@ -98,14 +97,14 @@ func TestExpiration(t *testing.T) { bc := e.Chain acc := e.NewAccount(t) - cAcc := c.WithSigner(acc) + cAcc := c.WithSigners(acc) c.Invoke(t, stackitem.Null{}, "addRoot", "com") - cAcc.Invoke(t, true, "register", "first.com", acc.Contract.ScriptHash()) + cAcc.Invoke(t, true, "register", "first.com", acc.ScriptHash()) cAcc.Invoke(t, stackitem.Null{}, "setRecord", "first.com", int64(nns.TXT), "sometext") b1 := e.TopBlock(t) - tx := cAcc.PrepareInvoke(t, "register", "second.com", acc.Contract.ScriptHash()) + tx := cAcc.PrepareInvoke(t, "register", "second.com", acc.ScriptHash()) b2 := e.NewUnsignedBlock(t, tx) b2.Index = b1.Index + 1 b2.PrevHash = b1.Hash() @@ -191,7 +190,7 @@ func TestSetGetRecord(t *testing.T) { e := c.Executor acc := e.NewAccount(t) - cAcc := c.WithSigner(acc) + cAcc := c.WithSigners(acc) c.Invoke(t, stackitem.Null{}, "addRoot", "com") t.Run("set before register", func(t *testing.T) { @@ -295,22 +294,22 @@ func TestSetAdmin(t *testing.T) { e := c.Executor owner := e.NewAccount(t) - cOwner := c.WithSigner(owner) + cOwner := c.WithSigners(owner) admin := e.NewAccount(t) - cAdmin := c.WithSigner(admin) + cAdmin := c.WithSigners(admin) guest := e.NewAccount(t) - cGuest := c.WithSigner(guest) + cGuest := c.WithSigners(guest) c.Invoke(t, stackitem.Null{}, "addRoot", "com") - cOwner.Invoke(t, true, "register", "neo.com", owner.PrivateKey().GetScriptHash()) - cGuest.InvokeFail(t, "not witnessed", "setAdmin", "neo.com", admin.PrivateKey().GetScriptHash()) + cOwner.Invoke(t, true, "register", "neo.com", owner.ScriptHash()) + cGuest.InvokeFail(t, "not witnessed", "setAdmin", "neo.com", admin.ScriptHash()) // Must be witnessed by both owner and admin. - cOwner.InvokeFail(t, "not witnessed by admin", "setAdmin", "neo.com", admin.PrivateKey().GetScriptHash()) - cAdmin.InvokeFail(t, "not witnessed by owner", "setAdmin", "neo.com", admin.PrivateKey().GetScriptHash()) - cc := c.WithSigner([]*wallet.Account{owner, admin}) - cc.Invoke(t, stackitem.Null{}, "setAdmin", "neo.com", admin.PrivateKey().GetScriptHash()) + cOwner.InvokeFail(t, "not witnessed by admin", "setAdmin", "neo.com", admin.ScriptHash()) + cAdmin.InvokeFail(t, "not witnessed by owner", "setAdmin", "neo.com", admin.ScriptHash()) + cc := c.WithSigners(owner, admin) + cc.Invoke(t, stackitem.Null{}, "setAdmin", "neo.com", admin.ScriptHash()) t.Run("set and delete by admin", func(t *testing.T) { cAdmin.Invoke(t, stackitem.Null{}, "setRecord", "neo.com", int64(nns.TXT), "sometext") @@ -330,18 +329,18 @@ func TestTransfer(t *testing.T) { e := c.Executor from := e.NewAccount(t) - cFrom := c.WithSigner(from) + cFrom := c.WithSigners(from) to := e.NewAccount(t) - cTo := c.WithSigner(to) + cTo := c.WithSigners(to) c.Invoke(t, stackitem.Null{}, "addRoot", "com") - cFrom.Invoke(t, true, "register", "neo.com", from.PrivateKey().GetScriptHash()) + cFrom.Invoke(t, true, "register", "neo.com", from.ScriptHash()) cFrom.Invoke(t, stackitem.Null{}, "setRecord", "neo.com", int64(nns.A), "1.2.3.4") - cFrom.InvokeFail(t, "token not found", "transfer", to.Contract.ScriptHash(), "not.exists", nil) - c.Invoke(t, false, "transfer", to.Contract.ScriptHash(), "neo.com", nil) - cFrom.Invoke(t, true, "transfer", to.Contract.ScriptHash(), "neo.com", nil) + cFrom.InvokeFail(t, "token not found", "transfer", to.ScriptHash(), "not.exists", nil) + c.Invoke(t, false, "transfer", to.ScriptHash(), "neo.com", nil) + cFrom.Invoke(t, true, "transfer", to.ScriptHash(), "neo.com", nil) cFrom.Invoke(t, 1, "totalSupply") - cFrom.Invoke(t, to.Contract.ScriptHash().BytesBE(), "ownerOf", "neo.com") + cFrom.Invoke(t, to.ScriptHash().BytesBE(), "ownerOf", "neo.com") // without onNEP11Transfer ctr := neotest.CompileSource(t, e.CommitteeHash, @@ -368,16 +367,16 @@ func TestTokensOf(t *testing.T) { e := c.Executor acc1 := e.NewAccount(t) - cAcc1 := c.WithSigner(acc1) + cAcc1 := c.WithSigners(acc1) acc2 := e.NewAccount(t) - cAcc2 := c.WithSigner(acc2) + cAcc2 := c.WithSigners(acc2) c.Invoke(t, stackitem.Null{}, "addRoot", "com") - cAcc1.Invoke(t, true, "register", "neo.com", acc1.PrivateKey().GetScriptHash()) - cAcc2.Invoke(t, true, "register", "nspcc.com", acc2.PrivateKey().GetScriptHash()) + cAcc1.Invoke(t, true, "register", "neo.com", acc1.ScriptHash()) + cAcc2.Invoke(t, true, "register", "nspcc.com", acc2.ScriptHash()) - testTokensOf(t, c, [][]byte{[]byte("neo.com")}, acc1.Contract.ScriptHash().BytesBE()) - testTokensOf(t, c, [][]byte{[]byte("nspcc.com")}, acc2.Contract.ScriptHash().BytesBE()) + testTokensOf(t, c, [][]byte{[]byte("neo.com")}, acc1.ScriptHash().BytesBE()) + testTokensOf(t, c, [][]byte{[]byte("nspcc.com")}, acc2.ScriptHash().BytesBE()) testTokensOf(t, c, [][]byte{[]byte("neo.com"), []byte("nspcc.com")}) testTokensOf(t, c, [][]byte{}, util.Uint160{}.BytesBE()) // empty hash is a valid hash still } @@ -408,14 +407,14 @@ func TestResolve(t *testing.T) { e := c.Executor acc := e.NewAccount(t) - cAcc := c.WithSigner(acc) + cAcc := c.WithSigners(acc) c.Invoke(t, stackitem.Null{}, "addRoot", "com") - cAcc.Invoke(t, true, "register", "neo.com", acc.PrivateKey().GetScriptHash()) + cAcc.Invoke(t, true, "register", "neo.com", acc.ScriptHash()) cAcc.Invoke(t, stackitem.Null{}, "setRecord", "neo.com", int64(nns.A), "1.2.3.4") cAcc.Invoke(t, stackitem.Null{}, "setRecord", "neo.com", int64(nns.CNAME), "alias.com") - cAcc.Invoke(t, true, "register", "alias.com", acc.PrivateKey().GetScriptHash()) + cAcc.Invoke(t, true, "register", "alias.com", acc.ScriptHash()) cAcc.Invoke(t, stackitem.Null{}, "setRecord", "alias.com", int64(nns.TXT), "sometxt") c.Invoke(t, "1.2.3.4", "resolve", "neo.com", int64(nns.A)) diff --git a/pkg/neotest/basic.go b/pkg/neotest/basic.go index 0e6e9ac16..bd33b33ed 100644 --- a/pkg/neotest/basic.go +++ b/pkg/neotest/basic.go @@ -19,7 +19,6 @@ import ( "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" @@ -28,19 +27,20 @@ import ( // Executor is a wrapper over chain state. type Executor struct { Chain blockchainer.Blockchainer - Committee *wallet.Account + Committee Signer CommitteeHash util.Uint160 Contracts map[string]*Contract } // NewExecutor creates new executor instance from provided blockchain and committee. -func NewExecutor(t *testing.T, bc blockchainer.Blockchainer, committee *wallet.Account) *Executor { +func NewExecutor(t *testing.T, bc blockchainer.Blockchainer, committee Signer) *Executor { require.Equal(t, 1, len(bc.GetConfig().StandbyCommittee)) + require.IsType(t, multiSigner{}, committee, "committee must be a multi-signer") return &Executor{ Chain: bc, Committee: committee, - CommitteeHash: committee.Contract.ScriptHash(), + CommitteeHash: committee.ScriptHash(), Contracts: make(map[string]*Contract), } } @@ -74,57 +74,41 @@ 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, signer interface{}, +func (e *Executor) NewTx(t *testing.T, signers []Signer, hash util.Uint160, method string, args ...interface{}) *transaction.Transaction { tx := e.NewUnsignedTx(t, hash, method, args...) - return e.SignTx(t, tx, -1, signer) + return e.SignTx(t, tx, -1, signers...) } // SignTx signs a transaction using provided signers. -// signers can be either *wallet.Account or []*wallet.Account. -func (e *Executor) SignTx(t *testing.T, tx *transaction.Transaction, sysFee int64, signers interface{}) *transaction.Transaction { - switch s := signers.(type) { - case *wallet.Account: +func (e *Executor) SignTx(t *testing.T, tx *transaction.Transaction, sysFee int64, signers ...Signer) *transaction.Transaction { + for _, acc := range signers { tx.Signers = append(tx.Signers, transaction.Signer{ - Account: s.Contract.ScriptHash(), + Account: acc.ScriptHash(), Scopes: transaction.Global, }) - addNetworkFee(e.Chain, tx, s) - addSystemFee(e.Chain, tx, sysFee) - require.NoError(t, s.SignTx(e.Chain.GetConfig().Magic, tx)) - case []*wallet.Account: - for _, acc := range s { - tx.Signers = append(tx.Signers, transaction.Signer{ - Account: acc.Contract.ScriptHash(), - Scopes: transaction.Global, - }) - } - for _, acc := range s { - addNetworkFee(e.Chain, tx, acc) - } - addSystemFee(e.Chain, tx, sysFee) - for _, acc := range s { - require.NoError(t, acc.SignTx(e.Chain.GetConfig().Magic, tx)) - } - default: - panic("invalid signer") } + addNetworkFee(e.Chain, tx, signers...) + addSystemFee(e.Chain, tx, sysFee) + for _, acc := range signers { + require.NoError(t, acc.SignTx(e.Chain.GetConfig().Magic, tx)) + } return tx } -// NewAccount returns new account holding 100.0 GAS. This method advances the chain +// NewAccount returns new signer holding 100.0 GAS. This method advances the chain // by one block with a transfer transaction. -func (e *Executor) NewAccount(t *testing.T) *wallet.Account { +func (e *Executor) NewAccount(t *testing.T) Signer { acc, err := wallet.NewAccount() require.NoError(t, err) - tx := e.NewTx(t, e.Committee, + tx := e.NewTx(t, []Signer{e.Committee}, e.NativeHash(t, nativenames.Gas), "transfer", - e.Committee.Contract.ScriptHash(), acc.Contract.ScriptHash(), int64(100_0000_0000), nil) + e.Committee.ScriptHash(), acc.Contract.ScriptHash(), int64(100_0000_0000), nil) e.AddNewBlock(t, tx) e.CheckHalt(t, tx.Hash()) - return acc + return NewSingleSigner(acc) } // DeployContract compiles and deploys contract to bc. @@ -174,7 +158,7 @@ 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.Committee.Contract.ScriptHash(), + Account: e.Committee.ScriptHash(), Scopes: transaction.Global, }} addNetworkFee(bc, tx, e.Committee) @@ -191,12 +175,14 @@ func addSystemFee(bc blockchainer.Blockchainer, tx *transaction.Transaction, sys tx.SystemFee = v.GasConsumed() } -func addNetworkFee(bc blockchainer.Blockchainer, tx *transaction.Transaction, sender *wallet.Account) { +func addNetworkFee(bc blockchainer.Blockchainer, tx *transaction.Transaction, signers ...Signer) { baseFee := bc.GetPolicer().GetBaseExecFee() size := io.GetVarSize(tx) - netFee, sizeDelta := fee.Calculate(baseFee, sender.Contract.Script) - tx.NetworkFee += netFee - size += sizeDelta + for _, sgr := range signers { + netFee, sizeDelta := fee.Calculate(baseFee, sgr.Script()) + tx.NetworkFee += netFee + size += sizeDelta + } tx.NetworkFee += int64(size) * bc.FeePerByte() } @@ -205,9 +191,9 @@ func (e *Executor) NewUnsignedBlock(t *testing.T, txs ...*transaction.Transactio lastBlock := e.TopBlock(t) b := &block.Block{ Header: block.Header{ - NextConsensus: e.Committee.Contract.ScriptHash(), + NextConsensus: e.Committee.ScriptHash(), Script: transaction.Witness{ - VerificationScript: e.Committee.Contract.Script, + VerificationScript: e.Committee.Script(), }, Timestamp: lastBlock.Timestamp + 1, }, @@ -233,8 +219,8 @@ func (e *Executor) AddNewBlock(t *testing.T, txs ...*transaction.Transaction) *b // SignBlock add validators signature to b. func (e *Executor) SignBlock(b *block.Block) *block.Block { - sign := e.Committee.PrivateKey().SignHashable(uint32(e.Chain.GetConfig().Magic), b) - b.Script.InvocationScript = append([]byte{byte(opcode.PUSHDATA1), 64}, sign...) + invoc := e.Committee.SignHashable(uint32(e.Chain.GetConfig().Magic), b) + b.Script.InvocationScript = invoc return b } diff --git a/pkg/neotest/chain/chain.go b/pkg/neotest/chain/chain.go index 24d79393f..a190f755b 100644 --- a/pkg/neotest/chain/chain.go +++ b/pkg/neotest/chain/chain.go @@ -9,6 +9,7 @@ import ( "github.com/nspcc-dev/neo-go/pkg/core" "github.com/nspcc-dev/neo-go/pkg/core/storage" "github.com/nspcc-dev/neo-go/pkg/crypto/keys" + "github.com/nspcc-dev/neo-go/pkg/neotest" "github.com/nspcc-dev/neo-go/pkg/wallet" "github.com/stretchr/testify/require" "go.uber.org/zap/zaptest" @@ -30,7 +31,7 @@ func init() { // NewSingle creates new blockchain instance with a single validator and // setups cleanup functions. -func NewSingle(t *testing.T) (*core.Blockchain, *wallet.Account) { +func NewSingle(t *testing.T) (*core.Blockchain, neotest.Signer) { protoCfg := config.ProtocolConfiguration{ Magic: netmode.UnitTestNet, SecondsPerBlock: 1, @@ -46,5 +47,5 @@ func NewSingle(t *testing.T) (*core.Blockchain, *wallet.Account) { require.NoError(t, err) go bc.Run() t.Cleanup(bc.Close) - return bc, committeeAcc + return bc, neotest.NewMultiSigner(committeeAcc) } diff --git a/pkg/neotest/client.go b/pkg/neotest/client.go index e767ed58b..646f58143 100644 --- a/pkg/neotest/client.go +++ b/pkg/neotest/client.go @@ -14,8 +14,8 @@ import ( // ContractInvoker is a client for specific contract. type ContractInvoker struct { *Executor - Hash util.Uint160 - Signer interface{} + Hash util.Uint160 + Signers []Signer } // CommitteeInvoker creates new ContractInvoker for contract with hash h. @@ -23,7 +23,7 @@ func (e *Executor) CommitteeInvoker(h util.Uint160) *ContractInvoker { return &ContractInvoker{ Executor: e, Hash: h, - Signer: e.Committee, + Signers: []Signer{e.Committee}, } } @@ -39,16 +39,16 @@ func (c *ContractInvoker) TestInvoke(t *testing.T, method string, args ...interf return v.Estack(), err } -// WithSigner creates new client with the provided signer. -func (c *ContractInvoker) WithSigner(signer interface{}) *ContractInvoker { +// WithSigners creates new client with the provided signer. +func (c *ContractInvoker) WithSigners(signers ...Signer) *ContractInvoker { newC := *c - newC.Signer = signer + newC.Signers = signers return &newC } // PrepareInvoke creates new invocation transaction. func (c *ContractInvoker) PrepareInvoke(t *testing.T, method string, args ...interface{}) *transaction.Transaction { - return c.Executor.NewTx(t, c.Signer, c.Hash, method, args...) + return c.Executor.NewTx(t, c.Signers, c.Hash, method, args...) } // PrepareInvokeNoSign creates new unsigned invocation transaction. @@ -68,7 +68,7 @@ func (c *ContractInvoker) Invoke(t *testing.T, result interface{}, method string // 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 { tx := c.PrepareInvokeNoSign(t, method, args...) - c.Executor.SignTx(t, tx, sysFee, c.Signer) + c.Executor.SignTx(t, tx, sysFee, c.Signers...) c.AddNewBlock(t, tx) c.CheckFault(t, tx.Hash(), message) return tx.Hash() diff --git a/pkg/neotest/signer.go b/pkg/neotest/signer.go new file mode 100644 index 000000000..a1757d428 --- /dev/null +++ b/pkg/neotest/signer.go @@ -0,0 +1,124 @@ +package neotest + +import ( + "bytes" + "fmt" + + "github.com/nspcc-dev/neo-go/pkg/config/netmode" + "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/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/wallet" +) + +// Signer is a generic interface which can be either simple- or multi-signature signer. +type Signer interface { + // ScriptHash returns signer script hash. + Script() []byte + // Script returns signer verification script. + ScriptHash() util.Uint160 + // SignHashable returns invocation script for signing an item. + SignHashable(uint32, hash.Hashable) []byte + // SignTx signs a transaction. + SignTx(netmode.Magic, *transaction.Transaction) error +} + +// signer represents simple-signature signer. +type signer wallet.Account + +// multiSigner represents single multi-signature signer consisting of provided accounts. +type multiSigner []*wallet.Account + +// NewSingleSigner returns multi-signature signer for the provided account. +// It must contain exactly as many accounts as needed to sign the script. +func NewSingleSigner(acc *wallet.Account) Signer { + if !vm.IsSignatureContract(acc.Contract.Script) { + panic("account must have simple-signature verification script") + } + return (*signer)(acc) +} + +// Script implements Signer interface. +func (s *signer) Script() []byte { + return (*wallet.Account)(s).Contract.Script +} + +// ScriptHash implements Signer interface. +func (s *signer) ScriptHash() util.Uint160 { + return (*wallet.Account)(s).Contract.ScriptHash() +} + +// SignHashable implements Signer interface. +func (s *signer) SignHashable(magic uint32, item hash.Hashable) []byte { + return append([]byte{byte(opcode.PUSHDATA1), 64}, + (*wallet.Account)(s).PrivateKey().SignHashable(magic, item)...) +} + +// SignTx implements Signer interface. +func (s *signer) SignTx(magic netmode.Magic, tx *transaction.Transaction) error { + return (*wallet.Account)(s).SignTx(magic, tx) +} + +// NewMultiSigner returns multi-signature signer for the provided account. +// It must contain at least as many accounts as needed to sign the script. +func NewMultiSigner(accs ...*wallet.Account) Signer { + if len(accs) == 0 { + panic("empty account list") + } + script := accs[0].Contract.Script + m, _, ok := vm.ParseMultiSigContract(script) + if !ok { + panic("all accounts must have multi-signature verification script") + } + if len(accs) < m { + panic(fmt.Sprintf("verification script requires %d signatures, "+ + "but only %d accounts were provided", m, len(accs))) + } + for _, acc := range accs { + if !bytes.Equal(script, acc.Contract.Script) { + panic("all accounts must have equal verification script") + } + } + + return multiSigner(accs[:m]) +} + +// ScriptHash implements Signer interface. +func (m multiSigner) ScriptHash() util.Uint160 { + return m[0].Contract.ScriptHash() +} + +// Script implements Signer interface. +func (m multiSigner) Script() []byte { + return m[0].Contract.Script +} + +// SignHashable implements Signer interface. +func (m multiSigner) SignHashable(magic uint32, item hash.Hashable) []byte { + var script []byte + for _, acc := range m { + sign := acc.PrivateKey().SignHashable(magic, item) + script = append(script, byte(opcode.PUSHDATA1), 64) + script = append(script, sign...) + } + return script +} + +// SignTx implements Signer interface. +func (m multiSigner) SignTx(magic netmode.Magic, tx *transaction.Transaction) error { + invoc := m.SignHashable(uint32(magic), tx) + verif := m.Script() + for i := range tx.Scripts { + if bytes.Equal(tx.Scripts[i].VerificationScript, verif) { + tx.Scripts[i].InvocationScript = invoc + return nil + } + } + tx.Scripts = append(tx.Scripts, transaction.Witness{ + InvocationScript: invoc, + VerificationScript: verif, + }) + return nil +} From bef2a6f7ae68f896929eed625bab4e1e90a53ee4 Mon Sep 17 00:00:00 2001 From: Evgeniy Stratonikov Date: Wed, 3 Nov 2021 15:22:08 +0300 Subject: [PATCH 5/7] neotest: provide both validator and committee It will become useful for multi-node committee. Signed-off-by: Evgeniy Stratonikov --- .../nft-nd-nns/tests/nonnative_name_service_test.go | 2 +- pkg/neotest/basic.go | 10 ++++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/examples/nft-nd-nns/tests/nonnative_name_service_test.go b/examples/nft-nd-nns/tests/nonnative_name_service_test.go index c6176e8a2..40840981a 100644 --- a/examples/nft-nd-nns/tests/nonnative_name_service_test.go +++ b/examples/nft-nd-nns/tests/nonnative_name_service_test.go @@ -16,7 +16,7 @@ import ( func newNSClient(t *testing.T) *neotest.ContractInvoker { bc, acc := chain.NewSingle(t) - e := neotest.NewExecutor(t, bc, acc) + e := neotest.NewExecutor(t, bc, acc, acc) c := neotest.CompileFile(t, e.CommitteeHash, "..", "../nns.yml") e.DeployContract(t, c, nil) diff --git a/pkg/neotest/basic.go b/pkg/neotest/basic.go index bd33b33ed..25036d4cc 100644 --- a/pkg/neotest/basic.go +++ b/pkg/neotest/basic.go @@ -27,18 +27,20 @@ import ( // Executor is a wrapper over chain state. type Executor struct { Chain blockchainer.Blockchainer + Validator Signer Committee Signer CommitteeHash util.Uint160 Contracts map[string]*Contract } // NewExecutor creates new executor instance from provided blockchain and committee. -func NewExecutor(t *testing.T, bc blockchainer.Blockchainer, committee Signer) *Executor { +func NewExecutor(t *testing.T, bc blockchainer.Blockchainer, validator, committee Signer) *Executor { require.Equal(t, 1, len(bc.GetConfig().StandbyCommittee)) require.IsType(t, multiSigner{}, committee, "committee must be a multi-signer") return &Executor{ Chain: bc, + Validator: validator, Committee: committee, CommitteeHash: committee.ScriptHash(), Contracts: make(map[string]*Contract), @@ -191,9 +193,9 @@ func (e *Executor) NewUnsignedBlock(t *testing.T, txs ...*transaction.Transactio lastBlock := e.TopBlock(t) b := &block.Block{ Header: block.Header{ - NextConsensus: e.Committee.ScriptHash(), + NextConsensus: e.Validator.ScriptHash(), Script: transaction.Witness{ - VerificationScript: e.Committee.Script(), + VerificationScript: e.Validator.Script(), }, Timestamp: lastBlock.Timestamp + 1, }, @@ -219,7 +221,7 @@ func (e *Executor) AddNewBlock(t *testing.T, txs ...*transaction.Transaction) *b // SignBlock add validators signature to b. func (e *Executor) SignBlock(b *block.Block) *block.Block { - invoc := e.Committee.SignHashable(uint32(e.Chain.GetConfig().Magic), b) + invoc := e.Validator.SignHashable(uint32(e.Chain.GetConfig().Magic), b) b.Script.InvocationScript = invoc return b } From 79a48a7800919277e27e0afb0a239d1987bc20bf Mon Sep 17 00:00:00 2001 From: Evgeniy Stratonikov Date: Wed, 3 Nov 2021 15:28:29 +0300 Subject: [PATCH 6/7] neotest: allow to use 6-node committee Signed-off-by: Evgeniy Stratonikov --- pkg/neotest/basic.go | 4 +- pkg/neotest/chain/chain.go | 109 ++++++++++++++++++++++++++++++-- pkg/neotest/chain/chain_test.go | 21 ++++++ pkg/neotest/signer.go | 17 +++++ 4 files changed, 145 insertions(+), 6 deletions(-) create mode 100644 pkg/neotest/chain/chain_test.go diff --git a/pkg/neotest/basic.go b/pkg/neotest/basic.go index 25036d4cc..67c0ac87b 100644 --- a/pkg/neotest/basic.go +++ b/pkg/neotest/basic.go @@ -35,8 +35,8 @@ 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 { - require.Equal(t, 1, len(bc.GetConfig().StandbyCommittee)) - require.IsType(t, multiSigner{}, committee, "committee must be a multi-signer") + checkMultiSigner(t, validator) + checkMultiSigner(t, committee) return &Executor{ Chain: bc, diff --git a/pkg/neotest/chain/chain.go b/pkg/neotest/chain/chain.go index a190f755b..732bfc2fb 100644 --- a/pkg/neotest/chain/chain.go +++ b/pkg/neotest/chain/chain.go @@ -2,6 +2,7 @@ package chain import ( "encoding/hex" + "sort" "testing" "github.com/nspcc-dev/neo-go/pkg/config" @@ -10,23 +11,102 @@ import ( "github.com/nspcc-dev/neo-go/pkg/core/storage" "github.com/nspcc-dev/neo-go/pkg/crypto/keys" "github.com/nspcc-dev/neo-go/pkg/neotest" + "github.com/nspcc-dev/neo-go/pkg/smartcontract" "github.com/nspcc-dev/neo-go/pkg/wallet" "github.com/stretchr/testify/require" "go.uber.org/zap/zaptest" ) -const validatorWIF = "KxyjQ8eUa4FHt3Gvioyt1Wz29cTUrE4eTqX3yFSk1YFCsPL8uNsY" +const singleValidatorWIF = "KxyjQ8eUa4FHt3Gvioyt1Wz29cTUrE4eTqX3yFSk1YFCsPL8uNsY" -// committeeAcc is an account used to sign tx as a committee. -var committeeAcc *wallet.Account +// committeeWIFs is a list of unencrypted WIFs sorted by public key. +var committeeWIFs = []string{ + "KzfPUYDC9n2yf4fK5ro4C8KMcdeXtFuEnStycbZgX3GomiUsvX6W", + "KzgWE3u3EDp13XPXXuTKZxeJ3Gi8Bsm8f9ijY3ZsCKKRvZUo1Cdn", + singleValidatorWIF, + "L2oEXKRAAMiPEZukwR5ho2S6SMeQLhcK9mF71ZnF7GvT8dU4Kkgz", + + // Provide 2 committee extra members so that committee address differs from + // the validators one. + "L1Tr1iq5oz1jaFaMXP21sHDkJYDDkuLtpvQ4wRf1cjKvJYvnvpAb", + "Kz6XTUrExy78q8f4MjDHnwz8fYYyUE8iPXwPRAkHa3qN2JcHYm7e", +} + +var ( + // committeeAcc is an account used to sign tx as a committee. + committeeAcc *wallet.Account + + // multiCommitteeAcc contains committee accounts used in a multi-node setup. + multiCommitteeAcc []*wallet.Account + + // multiValidatorAcc contains validator accounts used in a multi-node setup. + multiValidatorAcc []*wallet.Account + + // standByCommittee contains list of committee public keys to use in config. + standByCommittee []string +) func init() { - committeeAcc, _ = wallet.NewAccountFromWIF(validatorWIF) + committeeAcc, _ = wallet.NewAccountFromWIF(singleValidatorWIF) pubs := keys.PublicKeys{committeeAcc.PrivateKey().PublicKey()} err := committeeAcc.ConvertMultisig(1, pubs) if err != nil { panic(err) } + + mc := smartcontract.GetMajorityHonestNodeCount(len(committeeWIFs)) + mv := smartcontract.GetDefaultHonestNodeCount(4) + accs := make([]*wallet.Account, len(committeeWIFs)) + pubs = make(keys.PublicKeys, len(accs)) + for i := range committeeWIFs { + accs[i], _ = wallet.NewAccountFromWIF(committeeWIFs[i]) + pubs[i] = accs[i].PrivateKey().PublicKey() + } + + // Config entry must contain validators first in a specific order. + standByCommittee = make([]string, len(pubs)) + standByCommittee[0] = hex.EncodeToString(pubs[2].Bytes()) + standByCommittee[1] = hex.EncodeToString(pubs[0].Bytes()) + standByCommittee[2] = hex.EncodeToString(pubs[3].Bytes()) + standByCommittee[3] = hex.EncodeToString(pubs[1].Bytes()) + standByCommittee[4] = hex.EncodeToString(pubs[4].Bytes()) + standByCommittee[5] = hex.EncodeToString(pubs[5].Bytes()) + + multiValidatorAcc = make([]*wallet.Account, mv) + sort.Sort(pubs[:4]) + +vloop: + for i := 0; i < mv; i++ { + for j := range accs { + if accs[j].PrivateKey().PublicKey().Equal(pubs[i]) { + multiValidatorAcc[i] = wallet.NewAccountFromPrivateKey(accs[j].PrivateKey()) + err := multiValidatorAcc[i].ConvertMultisig(mv, pubs[:4]) + if err != nil { + panic(err) + } + continue vloop + } + } + panic("invalid committee WIFs") + } + + multiCommitteeAcc = make([]*wallet.Account, mc) + sort.Sort(pubs) + +cloop: + for i := 0; i < mc; i++ { + for j := range accs { + if accs[j].PrivateKey().PublicKey().Equal(pubs[i]) { + multiCommitteeAcc[i] = wallet.NewAccountFromPrivateKey(accs[j].PrivateKey()) + err := multiCommitteeAcc[i].ConvertMultisig(mc, pubs) + if err != nil { + panic(err) + } + continue cloop + } + } + panic("invalid committee WIFs") + } } // NewSingle creates new blockchain instance with a single validator and @@ -49,3 +129,24 @@ func NewSingle(t *testing.T) (*core.Blockchain, neotest.Signer) { t.Cleanup(bc.Close) return bc, neotest.NewMultiSigner(committeeAcc) } + +// NewMulti creates new blockchain instance with 4 validators and 6 committee members. +// Second return value is for validator signer, third -- for committee. +func NewMulti(t *testing.T) (*core.Blockchain, neotest.Signer, neotest.Signer) { + protoCfg := config.ProtocolConfiguration{ + Magic: netmode.UnitTestNet, + SecondsPerBlock: 1, + StandbyCommittee: standByCommittee, + ValidatorsCount: 4, + VerifyBlocks: true, + VerifyTransactions: true, + } + + 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...) +} diff --git a/pkg/neotest/chain/chain_test.go b/pkg/neotest/chain/chain_test.go new file mode 100644 index 000000000..d8a85a21c --- /dev/null +++ b/pkg/neotest/chain/chain_test.go @@ -0,0 +1,21 @@ +package chain + +import ( + "testing" + + "github.com/nspcc-dev/neo-go/pkg/neotest" + "github.com/stretchr/testify/require" +) + +// TestNewMulti checks that transaction and block is signed correctly for multi-node setup. +func TestNewMulti(t *testing.T) { + bc, vAcc, cAcc := NewMulti(t) + e := neotest.NewExecutor(t, bc, vAcc, cAcc) + + require.NotEqual(t, vAcc.ScriptHash(), cAcc.ScriptHash()) + + const amount = int64(10_0000_0000) + + c := e.CommitteeInvoker(bc.UtilityTokenHash()).WithSigners(vAcc) + c.Invoke(t, true, "transfer", e.Validator.ScriptHash(), e.Committee.ScriptHash(), amount, nil) +} diff --git a/pkg/neotest/signer.go b/pkg/neotest/signer.go index a1757d428..dc3469e40 100644 --- a/pkg/neotest/signer.go +++ b/pkg/neotest/signer.go @@ -3,6 +3,7 @@ package neotest import ( "bytes" "fmt" + "testing" "github.com/nspcc-dev/neo-go/pkg/config/netmode" "github.com/nspcc-dev/neo-go/pkg/core/transaction" @@ -11,6 +12,7 @@ import ( "github.com/nspcc-dev/neo-go/pkg/vm" "github.com/nspcc-dev/neo-go/pkg/vm/opcode" "github.com/nspcc-dev/neo-go/pkg/wallet" + "github.com/stretchr/testify/require" ) // Signer is a generic interface which can be either simple- or multi-signature signer. @@ -122,3 +124,18 @@ func (m multiSigner) SignTx(magic netmode.Magic, tx *transaction.Transaction) er }) return nil } + +func checkMultiSigner(t *testing.T, s Signer) { + accs, ok := s.(multiSigner) + require.True(t, ok, "expected to be a multi-signer") + require.True(t, len(accs) > 0, "empty multi-signer") + + m := len(accs[0].Contract.Parameters) + require.True(t, m <= len(accs), "honest not count is too big for a multi-signer") + + h := accs[0].Contract.ScriptHash() + for i := 1; i < len(accs); i++ { + require.Equal(t, m, len(accs[i].Contract.Parameters), "inconsistent multi-signer accounts") + require.Equal(t, h, accs[i].Contract.ScriptHash(), "inconsistent multi-signer accounts") + } +} From ce549e4cb22220268b8cabd9cf65e0236209ca7f Mon Sep 17 00:00:00 2001 From: Evgeniy Stratonikov Date: Thu, 11 Nov 2021 10:42:44 +0300 Subject: [PATCH 7/7] core/test: allow to determine system fee automatically Eventually this will be replaced by `pkg/neotest` invocations but for now it allows us to remove NNS constants together with the tests. Signed-off-by: Evgeniy Stratonikov --- pkg/core/helper_test.go | 46 ++++++++++++++++++++++++++++++----------- 1 file changed, 34 insertions(+), 12 deletions(-) diff --git a/pkg/core/helper_test.go b/pkg/core/helper_test.go index 51b03a157..0cef3e47a 100644 --- a/pkg/core/helper_test.go +++ b/pkg/core/helper_test.go @@ -52,11 +52,6 @@ var neoOwner = testchain.MultisigScriptHash() // examplesPrefix is a prefix of the example smart-contracts. const examplesPrefix = "../../examples/" -const ( - defaultNameServiceDomainPrice = 10_0000_0000 - defaultNameServiceSysfee = 6000_0000 -) - // newTestChain should be called before newBlock invocation to properly setup // global state. func newTestChain(t testing.TB) *Blockchain { @@ -471,15 +466,15 @@ func initBasicChain(t *testing.T, bc *Blockchain) { // register `neo.com` with A record type and priv0 owner via NS transferFundsToCommittee(t, bc) // block #12 - res, err := invokeContractMethodGeneric(bc, defaultNameServiceSysfee, + res, err := invokeContractMethodGeneric(bc, -1, nsHash, "addRoot", true, "com") // block #13 require.NoError(t, err) checkResult(t, res, stackitem.Null{}) - res, err = invokeContractMethodGeneric(bc, defaultNameServiceDomainPrice+defaultNameServiceSysfee+1_0000_000, + res, err = invokeContractMethodGeneric(bc, -1, nsHash, "register", acc0, "neo.com", priv0ScriptHash) // block #14 require.NoError(t, err) checkResult(t, res, stackitem.NewBool(true)) - res, err = invokeContractMethodGeneric(bc, defaultNameServiceSysfee, nsHash, + 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{}) @@ -579,22 +574,24 @@ func prepareContractMethodInvokeGeneric(chain *Blockchain, sysfee int64, return nil, w.Err } script := w.Bytes() - tx := transaction.New(script, sysfee) + 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, tx, s) + signTxWithAccounts(chain, sysfee, tx, s) case []*wallet.Account: - signTxWithAccounts(chain, tx, s...) + signTxWithAccounts(chain, sysfee, tx, s...) default: panic("invalid signer") } @@ -604,7 +601,31 @@ func prepareContractMethodInvokeGeneric(chain *Blockchain, sysfee int64, return tx, nil } -func signTxWithAccounts(chain *Blockchain, tx *transaction.Transaction, accs ...*wallet.Account) { +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 + v, f := bc.GetTestVM(trigger.Application, &ttx, b) + defer f() + + v.LoadWithFlags(tx.Script, callflag.All) + _ = v.Run() + tx.SystemFee = v.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) @@ -614,6 +635,7 @@ func signTxWithAccounts(chain *Blockchain, tx *transaction.Transaction, accs ... }) scope = transaction.Global } + setTxSystemFee(chain, sysFee, tx) size := io.GetVarSize(tx) for _, acc := range accs { if acc.Contract.Deployed {