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 +}