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/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))

View file

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

View file

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

View file

@ -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()

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
}