github.com/nspcc-dev/neo-go@v0.105.2-0.20240517133400-6be757af3eba/pkg/neotest/signer.go (about)

     1  package neotest
     2  
     3  import (
     4  	"bytes"
     5  	"fmt"
     6  	"sort"
     7  	"testing"
     8  
     9  	"github.com/nspcc-dev/neo-go/pkg/config/netmode"
    10  	"github.com/nspcc-dev/neo-go/pkg/core/transaction"
    11  	"github.com/nspcc-dev/neo-go/pkg/crypto/hash"
    12  	"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
    13  	"github.com/nspcc-dev/neo-go/pkg/encoding/address"
    14  	"github.com/nspcc-dev/neo-go/pkg/io"
    15  	"github.com/nspcc-dev/neo-go/pkg/util"
    16  	"github.com/nspcc-dev/neo-go/pkg/vm"
    17  	"github.com/nspcc-dev/neo-go/pkg/vm/emit"
    18  	"github.com/nspcc-dev/neo-go/pkg/vm/opcode"
    19  	"github.com/nspcc-dev/neo-go/pkg/wallet"
    20  	"github.com/stretchr/testify/require"
    21  )
    22  
    23  // Signer is a generic interface which can be either a simple- or multi-signature signer.
    24  type Signer interface {
    25  	// Script returns a signer verification script.
    26  	Script() []byte
    27  	// ScriptHash returns a signer script hash.
    28  	ScriptHash() util.Uint160
    29  	// SignHashable returns an invocation script for signing an item.
    30  	SignHashable(uint32, hash.Hashable) []byte
    31  	// SignTx signs a transaction.
    32  	SignTx(netmode.Magic, *transaction.Transaction) error
    33  }
    34  
    35  // SingleSigner is a generic interface for a simple one-signature signer.
    36  type SingleSigner interface {
    37  	Signer
    38  	// Account returns the underlying account which can be used to
    39  	// get a public key and/or sign arbitrary things.
    40  	Account() *wallet.Account
    41  }
    42  
    43  // MultiSigner is an interface for multisignature signing account.
    44  type MultiSigner interface {
    45  	Signer
    46  	// Single returns a simple-signature signer for the n-th account in a list.
    47  	Single(n int) SingleSigner
    48  }
    49  
    50  // signer represents a simple-signature signer.
    51  type signer wallet.Account
    52  
    53  // multiSigner represents a single multi-signature signer consisting of the provided accounts.
    54  type multiSigner struct {
    55  	accounts []*wallet.Account
    56  	m        int
    57  }
    58  
    59  // NewSingleSigner creates a [SingleSigner] from the provided account. It has
    60  // just one key, see [NewMultiSigner] for multisignature accounts.
    61  func NewSingleSigner(acc *wallet.Account) SingleSigner {
    62  	if !vm.IsSignatureContract(acc.Contract.Script) {
    63  		panic("account must have simple-signature verification script")
    64  	}
    65  	return (*signer)(acc)
    66  }
    67  
    68  // Script implements Signer interface.
    69  func (s *signer) Script() []byte {
    70  	return (*wallet.Account)(s).Contract.Script
    71  }
    72  
    73  // ScriptHash implements Signer interface.
    74  func (s *signer) ScriptHash() util.Uint160 {
    75  	return (*wallet.Account)(s).Contract.ScriptHash()
    76  }
    77  
    78  // SignHashable implements Signer interface.
    79  func (s *signer) SignHashable(magic uint32, item hash.Hashable) []byte {
    80  	return append([]byte{byte(opcode.PUSHDATA1), keys.SignatureLen},
    81  		(*wallet.Account)(s).SignHashable(netmode.Magic(magic), item)...)
    82  }
    83  
    84  // SignTx implements Signer interface.
    85  func (s *signer) SignTx(magic netmode.Magic, tx *transaction.Transaction) error {
    86  	return (*wallet.Account)(s).SignTx(magic, tx)
    87  }
    88  
    89  // Account implements SingleSigner interface.
    90  func (s *signer) Account() *wallet.Account {
    91  	return (*wallet.Account)(s)
    92  }
    93  
    94  // NewMultiSigner returns a multi-signature signer for the provided account.
    95  // It must contain at least as many accounts as needed to sign the script.
    96  func NewMultiSigner(accs ...*wallet.Account) MultiSigner {
    97  	if len(accs) == 0 {
    98  		panic("empty account list")
    99  	}
   100  	script := accs[0].Contract.Script
   101  	m, _, ok := vm.ParseMultiSigContract(script)
   102  	if !ok {
   103  		panic("all accounts must have multi-signature verification script")
   104  	}
   105  	if len(accs) < m {
   106  		panic(fmt.Sprintf("verification script requires %d signatures, "+
   107  			"but only %d accounts were provided", m, len(accs)))
   108  	}
   109  	sort.Slice(accs, func(i, j int) bool {
   110  		p1 := accs[i].PublicKey()
   111  		p2 := accs[j].PublicKey()
   112  		return p1.Cmp(p2) == -1
   113  	})
   114  	for _, acc := range accs {
   115  		if !bytes.Equal(script, acc.Contract.Script) {
   116  			panic("all accounts must have equal verification script")
   117  		}
   118  	}
   119  
   120  	return multiSigner{accounts: accs, m: m}
   121  }
   122  
   123  // ScriptHash implements Signer interface.
   124  func (m multiSigner) ScriptHash() util.Uint160 {
   125  	return m.accounts[0].Contract.ScriptHash()
   126  }
   127  
   128  // Script implements Signer interface.
   129  func (m multiSigner) Script() []byte {
   130  	return m.accounts[0].Contract.Script
   131  }
   132  
   133  // SignHashable implements Signer interface.
   134  func (m multiSigner) SignHashable(magic uint32, item hash.Hashable) []byte {
   135  	var script []byte
   136  	for i := 0; i < m.m; i++ {
   137  		sign := m.accounts[i].SignHashable(netmode.Magic(magic), item)
   138  		script = append(script, byte(opcode.PUSHDATA1), keys.SignatureLen)
   139  		script = append(script, sign...)
   140  	}
   141  	return script
   142  }
   143  
   144  // SignTx implements Signer interface.
   145  func (m multiSigner) SignTx(magic netmode.Magic, tx *transaction.Transaction) error {
   146  	invoc := m.SignHashable(uint32(magic), tx)
   147  	verif := m.Script()
   148  	for i := range tx.Scripts {
   149  		if bytes.Equal(tx.Scripts[i].VerificationScript, verif) {
   150  			tx.Scripts[i].InvocationScript = invoc
   151  			return nil
   152  		}
   153  	}
   154  	tx.Scripts = append(tx.Scripts, transaction.Witness{
   155  		InvocationScript:   invoc,
   156  		VerificationScript: verif,
   157  	})
   158  	return nil
   159  }
   160  
   161  // Single implements MultiSigner interface.
   162  func (m multiSigner) Single(n int) SingleSigner {
   163  	if len(m.accounts) <= n {
   164  		panic("invalid index")
   165  	}
   166  	return NewSingleSigner(wallet.NewAccountFromPrivateKey(m.accounts[n].PrivateKey()))
   167  }
   168  
   169  func checkMultiSigner(t testing.TB, s Signer) {
   170  	ms, ok := s.(multiSigner)
   171  	require.True(t, ok, "expected to be a multi-signer")
   172  
   173  	accs := ms.accounts
   174  	require.True(t, len(accs) > 0, "empty multi-signer")
   175  
   176  	m := len(accs[0].Contract.Parameters)
   177  	require.True(t, m <= len(accs), "honest not count is too big for a multi-signer")
   178  
   179  	h := accs[0].Contract.ScriptHash()
   180  	for i := 1; i < len(accs); i++ {
   181  		require.Equal(t, m, len(accs[i].Contract.Parameters), "inconsistent multi-signer accounts")
   182  		require.Equal(t, h, accs[i].Contract.ScriptHash(), "inconsistent multi-signer accounts")
   183  	}
   184  }
   185  
   186  type contractSigner wallet.Account
   187  
   188  // NewContractSigner returns a contract signer for the provided contract hash.
   189  // getInvParams must return params to be used as invocation script for contract-based witness.
   190  func NewContractSigner(h util.Uint160, getInvParams func(tx *transaction.Transaction) []any) SingleSigner {
   191  	return &contractSigner{
   192  		Address: address.Uint160ToString(h),
   193  		Contract: &wallet.Contract{
   194  			Deployed: true,
   195  			InvocationBuilder: func(tx *transaction.Transaction) ([]byte, error) {
   196  				params := getInvParams(tx)
   197  				script := io.NewBufBinWriter()
   198  				for i := range params {
   199  					emit.Any(script.BinWriter, params[i])
   200  				}
   201  				if script.Err != nil {
   202  					return nil, script.Err
   203  				}
   204  				return script.Bytes(), nil
   205  			},
   206  		},
   207  	}
   208  }
   209  
   210  // Script implements ContractSigner.
   211  func (s *contractSigner) Script() []byte {
   212  	return []byte{}
   213  }
   214  
   215  // ScriptHash implements ContractSigner.
   216  func (s *contractSigner) ScriptHash() util.Uint160 {
   217  	return s.Account().ScriptHash()
   218  }
   219  
   220  // ScriptHash implements ContractSigner.
   221  func (s *contractSigner) Account() *wallet.Account {
   222  	return (*wallet.Account)(s)
   223  }
   224  
   225  // SignHashable implements ContractSigner.
   226  func (s *contractSigner) SignHashable(uint32, hash.Hashable) []byte {
   227  	panic("not supported")
   228  }
   229  
   230  // SignTx implements ContractSigner.
   231  func (s *contractSigner) SignTx(magic netmode.Magic, tx *transaction.Transaction) error {
   232  	// Here we rely on `len(s.Contract.Parameters) == 0` being after the `s.Contract.InvocationBuilder != nil` check,
   233  	// because we cannot determine the list of parameters unless we already have tx.
   234  	return s.Account().SignTx(magic, tx)
   235  }