neo-go/pkg/neotest/signer.go
Evgeniy Stratonikov 37ecf51d13 neotest: allow to extract simple signers from multi-signer
There is a quirk related to ordering: we store accounts in such an order that
is expected by multi-signature verification script. This was done to
speed up transaction/block signing which is done quite frequently in
tests. This commit allows to provide accounts in any order and to
extract a single signer from multi-signer based on this order.

Signed-off-by: Evgeniy Stratonikov <evgeniy@nspcc.ru>
2021-12-09 21:59:02 +03:00

180 lines
5.4 KiB
Go

package neotest
import (
"bytes"
"fmt"
"sort"
"testing"
"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"
"github.com/stretchr/testify/require"
)
// 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
}
// SingleSigner is a generic interface for simple one-signature signer.
type SingleSigner interface {
Signer
// Account returns underlying account which can be used to
// get public key and/or sign arbitrary things.
Account() *wallet.Account
}
// MultiSigner is the interface for multisignature signing account.
type MultiSigner interface {
Signer
// Single returns simple-signature signer for n-th account in list.
Single(n int) SingleSigner
}
// signer represents simple-signature signer.
type signer wallet.Account
// multiSigner represents single multi-signature signer consisting of provided accounts.
type multiSigner struct {
accounts []*wallet.Account
m int
}
// 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) SingleSigner {
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)
}
// Account implements SingleSigner interface.
func (s *signer) Account() *wallet.Account {
return (*wallet.Account)(s)
}
// 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) MultiSigner {
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)))
}
sort.Slice(accs, func(i, j int) bool {
p1 := accs[i].PrivateKey().PublicKey()
p2 := accs[j].PrivateKey().PublicKey()
return p1.Cmp(p2) == -1
})
for _, acc := range accs {
if !bytes.Equal(script, acc.Contract.Script) {
panic("all accounts must have equal verification script")
}
}
return multiSigner{accounts: accs, m: m}
}
// ScriptHash implements Signer interface.
func (m multiSigner) ScriptHash() util.Uint160 {
return m.accounts[0].Contract.ScriptHash()
}
// Script implements Signer interface.
func (m multiSigner) Script() []byte {
return m.accounts[0].Contract.Script
}
// SignHashable implements Signer interface.
func (m multiSigner) SignHashable(magic uint32, item hash.Hashable) []byte {
var script []byte
for i := 0; i < m.m; i++ {
sign := m.accounts[i].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
}
// Single implements MultiSigner interface.
func (m multiSigner) Single(n int) SingleSigner {
if len(m.accounts) <= n {
panic("invalid index")
}
return NewSingleSigner(wallet.NewAccountFromPrivateKey(m.accounts[n].PrivateKey()))
}
func checkMultiSigner(t *testing.T, s Signer) {
ms, ok := s.(multiSigner)
require.True(t, ok, "expected to be a multi-signer")
accs := ms.accounts
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")
}
}