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 <evgeniy@nspcc.ru>
This commit is contained in:
Evgeniy Stratonikov 2021-11-03 13:44:46 +03:00
parent 1f9fd4a472
commit 950adb7b89
5 changed files with 197 additions and 87 deletions

View file

@ -11,7 +11,6 @@ import (
"github.com/nspcc-dev/neo-go/pkg/neotest/chain" "github.com/nspcc-dev/neo-go/pkg/neotest/chain"
"github.com/nspcc-dev/neo-go/pkg/util" "github.com/nspcc-dev/neo-go/pkg/util"
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem" "github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
"github.com/nspcc-dev/neo-go/pkg/wallet"
"github.com/stretchr/testify/require" "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) { t.Run("set, not signed by committee", func(t *testing.T) {
acc := c.NewAccount(t) acc := c.NewAccount(t)
cAcc := c.WithSigner(acc) cAcc := c.WithSigners(acc)
cAcc.InvokeFail(t, "not witnessed by committee", "setPrice", minPrice+1) 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) { func TestNonfungible(t *testing.T) {
c := newNSClient(t) c := newNSClient(t)
c.Signer = c.NewAccount(t) c.Signers = []neotest.Signer{c.NewAccount(t)}
c.Invoke(t, "NNS", "symbol") c.Invoke(t, "NNS", "symbol")
c.Invoke(t, 0, "decimals") c.Invoke(t, 0, "decimals")
c.Invoke(t, 0, "totalSupply") c.Invoke(t, 0, "totalSupply")
@ -82,7 +81,7 @@ func TestAddRoot(t *testing.T) {
}) })
t.Run("not signed by committee", func(t *testing.T) { t.Run("not signed by committee", func(t *testing.T) {
acc := c.NewAccount(t) acc := c.NewAccount(t)
c := c.WithSigner(acc) c := c.WithSigners(acc)
c.InvokeFail(t, "not witnessed by committee", "addRoot", "some") c.InvokeFail(t, "not witnessed by committee", "addRoot", "some")
}) })
@ -98,14 +97,14 @@ func TestExpiration(t *testing.T) {
bc := e.Chain bc := e.Chain
acc := e.NewAccount(t) acc := e.NewAccount(t)
cAcc := c.WithSigner(acc) cAcc := c.WithSigners(acc)
c.Invoke(t, stackitem.Null{}, "addRoot", "com") 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") cAcc.Invoke(t, stackitem.Null{}, "setRecord", "first.com", int64(nns.TXT), "sometext")
b1 := e.TopBlock(t) 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 := e.NewUnsignedBlock(t, tx)
b2.Index = b1.Index + 1 b2.Index = b1.Index + 1
b2.PrevHash = b1.Hash() b2.PrevHash = b1.Hash()
@ -191,7 +190,7 @@ func TestSetGetRecord(t *testing.T) {
e := c.Executor e := c.Executor
acc := e.NewAccount(t) acc := e.NewAccount(t)
cAcc := c.WithSigner(acc) cAcc := c.WithSigners(acc)
c.Invoke(t, stackitem.Null{}, "addRoot", "com") c.Invoke(t, stackitem.Null{}, "addRoot", "com")
t.Run("set before register", func(t *testing.T) { t.Run("set before register", func(t *testing.T) {
@ -295,22 +294,22 @@ func TestSetAdmin(t *testing.T) {
e := c.Executor e := c.Executor
owner := e.NewAccount(t) owner := e.NewAccount(t)
cOwner := c.WithSigner(owner) cOwner := c.WithSigners(owner)
admin := e.NewAccount(t) admin := e.NewAccount(t)
cAdmin := c.WithSigner(admin) cAdmin := c.WithSigners(admin)
guest := e.NewAccount(t) guest := e.NewAccount(t)
cGuest := c.WithSigner(guest) cGuest := c.WithSigners(guest)
c.Invoke(t, stackitem.Null{}, "addRoot", "com") c.Invoke(t, stackitem.Null{}, "addRoot", "com")
cOwner.Invoke(t, true, "register", "neo.com", owner.PrivateKey().GetScriptHash()) cOwner.Invoke(t, true, "register", "neo.com", owner.ScriptHash())
cGuest.InvokeFail(t, "not witnessed", "setAdmin", "neo.com", admin.PrivateKey().GetScriptHash()) cGuest.InvokeFail(t, "not witnessed", "setAdmin", "neo.com", admin.ScriptHash())
// Must be witnessed by both owner and admin. // Must be witnessed by both owner and admin.
cOwner.InvokeFail(t, "not witnessed by admin", "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.PrivateKey().GetScriptHash()) cAdmin.InvokeFail(t, "not witnessed by owner", "setAdmin", "neo.com", admin.ScriptHash())
cc := c.WithSigner([]*wallet.Account{owner, admin}) cc := c.WithSigners(owner, admin)
cc.Invoke(t, stackitem.Null{}, "setAdmin", "neo.com", admin.PrivateKey().GetScriptHash()) cc.Invoke(t, stackitem.Null{}, "setAdmin", "neo.com", admin.ScriptHash())
t.Run("set and delete by admin", func(t *testing.T) { t.Run("set and delete by admin", func(t *testing.T) {
cAdmin.Invoke(t, stackitem.Null{}, "setRecord", "neo.com", int64(nns.TXT), "sometext") cAdmin.Invoke(t, stackitem.Null{}, "setRecord", "neo.com", int64(nns.TXT), "sometext")
@ -330,18 +329,18 @@ func TestTransfer(t *testing.T) {
e := c.Executor e := c.Executor
from := e.NewAccount(t) from := e.NewAccount(t)
cFrom := c.WithSigner(from) cFrom := c.WithSigners(from)
to := e.NewAccount(t) to := e.NewAccount(t)
cTo := c.WithSigner(to) cTo := c.WithSigners(to)
c.Invoke(t, stackitem.Null{}, "addRoot", "com") 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.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) cFrom.InvokeFail(t, "token not found", "transfer", to.ScriptHash(), "not.exists", nil)
c.Invoke(t, false, "transfer", to.Contract.ScriptHash(), "neo.com", nil) c.Invoke(t, false, "transfer", to.ScriptHash(), "neo.com", nil)
cFrom.Invoke(t, true, "transfer", to.Contract.ScriptHash(), "neo.com", nil) cFrom.Invoke(t, true, "transfer", to.ScriptHash(), "neo.com", nil)
cFrom.Invoke(t, 1, "totalSupply") 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 // without onNEP11Transfer
ctr := neotest.CompileSource(t, e.CommitteeHash, ctr := neotest.CompileSource(t, e.CommitteeHash,
@ -368,16 +367,16 @@ func TestTokensOf(t *testing.T) {
e := c.Executor e := c.Executor
acc1 := e.NewAccount(t) acc1 := e.NewAccount(t)
cAcc1 := c.WithSigner(acc1) cAcc1 := c.WithSigners(acc1)
acc2 := e.NewAccount(t) acc2 := e.NewAccount(t)
cAcc2 := c.WithSigner(acc2) cAcc2 := c.WithSigners(acc2)
c.Invoke(t, stackitem.Null{}, "addRoot", "com") c.Invoke(t, stackitem.Null{}, "addRoot", "com")
cAcc1.Invoke(t, true, "register", "neo.com", acc1.PrivateKey().GetScriptHash()) cAcc1.Invoke(t, true, "register", "neo.com", acc1.ScriptHash())
cAcc2.Invoke(t, true, "register", "nspcc.com", acc2.PrivateKey().GetScriptHash()) 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("neo.com")}, acc1.ScriptHash().BytesBE())
testTokensOf(t, c, [][]byte{[]byte("nspcc.com")}, acc2.Contract.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{[]byte("neo.com"), []byte("nspcc.com")})
testTokensOf(t, c, [][]byte{}, util.Uint160{}.BytesBE()) // empty hash is a valid hash still 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 e := c.Executor
acc := e.NewAccount(t) acc := e.NewAccount(t)
cAcc := c.WithSigner(acc) cAcc := c.WithSigners(acc)
c.Invoke(t, stackitem.Null{}, "addRoot", "com") 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.A), "1.2.3.4")
cAcc.Invoke(t, stackitem.Null{}, "setRecord", "neo.com", int64(nns.CNAME), "alias.com") 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") 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)) c.Invoke(t, "1.2.3.4", "resolve", "neo.com", int64(nns.A))

View file

@ -19,7 +19,6 @@ import (
"github.com/nspcc-dev/neo-go/pkg/util" "github.com/nspcc-dev/neo-go/pkg/util"
"github.com/nspcc-dev/neo-go/pkg/vm" "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/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/vm/stackitem"
"github.com/nspcc-dev/neo-go/pkg/wallet" "github.com/nspcc-dev/neo-go/pkg/wallet"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
@ -28,19 +27,20 @@ import (
// Executor is a wrapper over chain state. // Executor is a wrapper over chain state.
type Executor struct { type Executor struct {
Chain blockchainer.Blockchainer Chain blockchainer.Blockchainer
Committee *wallet.Account Committee Signer
CommitteeHash util.Uint160 CommitteeHash util.Uint160
Contracts map[string]*Contract Contracts map[string]*Contract
} }
// NewExecutor creates new executor instance from provided blockchain and committee. // 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.Equal(t, 1, len(bc.GetConfig().StandbyCommittee))
require.IsType(t, multiSigner{}, committee, "committee must be a multi-signer")
return &Executor{ return &Executor{
Chain: bc, Chain: bc,
Committee: committee, Committee: committee,
CommitteeHash: committee.Contract.ScriptHash(), CommitteeHash: committee.ScriptHash(),
Contracts: make(map[string]*Contract), 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. // NewTx creates new transaction which invokes contract method.
// Transaction is signed with signer. // 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 { hash util.Uint160, method string, args ...interface{}) *transaction.Transaction {
tx := e.NewUnsignedTx(t, hash, method, args...) 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. // 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 ...Signer) *transaction.Transaction {
func (e *Executor) SignTx(t *testing.T, tx *transaction.Transaction, sysFee int64, signers interface{}) *transaction.Transaction { for _, acc := range signers {
switch s := signers.(type) {
case *wallet.Account:
tx.Signers = append(tx.Signers, transaction.Signer{ 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, Scopes: transaction.Global,
}) })
} }
for _, acc := range s { addNetworkFee(e.Chain, tx, signers...)
addNetworkFee(e.Chain, tx, acc)
}
addSystemFee(e.Chain, tx, sysFee) addSystemFee(e.Chain, tx, sysFee)
for _, acc := range s {
for _, acc := range signers {
require.NoError(t, acc.SignTx(e.Chain.GetConfig().Magic, tx)) require.NoError(t, acc.SignTx(e.Chain.GetConfig().Magic, tx))
} }
default:
panic("invalid signer")
}
return 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. // 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() acc, err := wallet.NewAccount()
require.NoError(t, err) require.NoError(t, err)
tx := e.NewTx(t, e.Committee, tx := e.NewTx(t, []Signer{e.Committee},
e.NativeHash(t, nativenames.Gas), "transfer", 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.AddNewBlock(t, tx)
e.CheckHalt(t, tx.Hash()) e.CheckHalt(t, tx.Hash())
return acc return NewSingleSigner(acc)
} }
// DeployContract compiles and deploys contract to bc. // 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.Nonce = nonce()
tx.ValidUntilBlock = bc.BlockHeight() + 1 tx.ValidUntilBlock = bc.BlockHeight() + 1
tx.Signers = []transaction.Signer{{ tx.Signers = []transaction.Signer{{
Account: e.Committee.Contract.ScriptHash(), Account: e.Committee.ScriptHash(),
Scopes: transaction.Global, Scopes: transaction.Global,
}} }}
addNetworkFee(bc, tx, e.Committee) addNetworkFee(bc, tx, e.Committee)
@ -191,12 +175,14 @@ func addSystemFee(bc blockchainer.Blockchainer, tx *transaction.Transaction, sys
tx.SystemFee = v.GasConsumed() 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() baseFee := bc.GetPolicer().GetBaseExecFee()
size := io.GetVarSize(tx) size := io.GetVarSize(tx)
netFee, sizeDelta := fee.Calculate(baseFee, sender.Contract.Script) for _, sgr := range signers {
netFee, sizeDelta := fee.Calculate(baseFee, sgr.Script())
tx.NetworkFee += netFee tx.NetworkFee += netFee
size += sizeDelta size += sizeDelta
}
tx.NetworkFee += int64(size) * bc.FeePerByte() tx.NetworkFee += int64(size) * bc.FeePerByte()
} }
@ -205,9 +191,9 @@ func (e *Executor) NewUnsignedBlock(t *testing.T, txs ...*transaction.Transactio
lastBlock := e.TopBlock(t) lastBlock := e.TopBlock(t)
b := &block.Block{ b := &block.Block{
Header: block.Header{ Header: block.Header{
NextConsensus: e.Committee.Contract.ScriptHash(), NextConsensus: e.Committee.ScriptHash(),
Script: transaction.Witness{ Script: transaction.Witness{
VerificationScript: e.Committee.Contract.Script, VerificationScript: e.Committee.Script(),
}, },
Timestamp: lastBlock.Timestamp + 1, 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. // SignBlock add validators signature to b.
func (e *Executor) SignBlock(b *block.Block) *block.Block { func (e *Executor) SignBlock(b *block.Block) *block.Block {
sign := e.Committee.PrivateKey().SignHashable(uint32(e.Chain.GetConfig().Magic), b) invoc := e.Committee.SignHashable(uint32(e.Chain.GetConfig().Magic), b)
b.Script.InvocationScript = append([]byte{byte(opcode.PUSHDATA1), 64}, sign...) b.Script.InvocationScript = invoc
return b return b
} }

View file

@ -9,6 +9,7 @@ import (
"github.com/nspcc-dev/neo-go/pkg/core" "github.com/nspcc-dev/neo-go/pkg/core"
"github.com/nspcc-dev/neo-go/pkg/core/storage" "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/crypto/keys"
"github.com/nspcc-dev/neo-go/pkg/neotest"
"github.com/nspcc-dev/neo-go/pkg/wallet" "github.com/nspcc-dev/neo-go/pkg/wallet"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"go.uber.org/zap/zaptest" "go.uber.org/zap/zaptest"
@ -30,7 +31,7 @@ func init() {
// NewSingle creates new blockchain instance with a single validator and // NewSingle creates new blockchain instance with a single validator and
// setups cleanup functions. // setups cleanup functions.
func NewSingle(t *testing.T) (*core.Blockchain, *wallet.Account) { func NewSingle(t *testing.T) (*core.Blockchain, neotest.Signer) {
protoCfg := config.ProtocolConfiguration{ protoCfg := config.ProtocolConfiguration{
Magic: netmode.UnitTestNet, Magic: netmode.UnitTestNet,
SecondsPerBlock: 1, SecondsPerBlock: 1,
@ -46,5 +47,5 @@ func NewSingle(t *testing.T) (*core.Blockchain, *wallet.Account) {
require.NoError(t, err) require.NoError(t, err)
go bc.Run() go bc.Run()
t.Cleanup(bc.Close) t.Cleanup(bc.Close)
return bc, committeeAcc return bc, neotest.NewMultiSigner(committeeAcc)
} }

View file

@ -15,7 +15,7 @@ import (
type ContractInvoker struct { type ContractInvoker struct {
*Executor *Executor
Hash util.Uint160 Hash util.Uint160
Signer interface{} Signers []Signer
} }
// CommitteeInvoker creates new ContractInvoker for contract with hash h. // CommitteeInvoker creates new ContractInvoker for contract with hash h.
@ -23,7 +23,7 @@ func (e *Executor) CommitteeInvoker(h util.Uint160) *ContractInvoker {
return &ContractInvoker{ return &ContractInvoker{
Executor: e, Executor: e,
Hash: h, 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 return v.Estack(), err
} }
// WithSigner creates new client with the provided signer. // WithSigners creates new client with the provided signer.
func (c *ContractInvoker) WithSigner(signer interface{}) *ContractInvoker { func (c *ContractInvoker) WithSigners(signers ...Signer) *ContractInvoker {
newC := *c newC := *c
newC.Signer = signer newC.Signers = signers
return &newC return &newC
} }
// PrepareInvoke creates new invocation transaction. // PrepareInvoke creates new invocation transaction.
func (c *ContractInvoker) PrepareInvoke(t *testing.T, method string, args ...interface{}) *transaction.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. // 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. // InvokeWithFeeFail is like InvokeFail but sets custom system fee for the transaction.
func (c *ContractInvoker) InvokeWithFeeFail(t *testing.T, message string, sysFee int64, method string, args ...interface{}) util.Uint256 { func (c *ContractInvoker) InvokeWithFeeFail(t *testing.T, message string, sysFee int64, method string, args ...interface{}) util.Uint256 {
tx := c.PrepareInvokeNoSign(t, method, args...) 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.AddNewBlock(t, tx)
c.CheckFault(t, tx.Hash(), message) c.CheckFault(t, tx.Hash(), message)
return tx.Hash() return tx.Hash()

124
pkg/neotest/signer.go Normal file
View file

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