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 }