From d4689db47e5ec765773cdf1e21cf12f994b0d367 Mon Sep 17 00:00:00 2001 From: Evgeniy Stratonikov Date: Fri, 3 Dec 2021 16:10:17 +0300 Subject: [PATCH 1/4] neotest: allow to extract account from single signer Signed-off-by: Evgeniy Stratonikov --- pkg/neotest/signer.go | 15 ++++++++++++++- pkg/neotest/signer_test.go | 16 ++++++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) create mode 100644 pkg/neotest/signer_test.go diff --git a/pkg/neotest/signer.go b/pkg/neotest/signer.go index dc3469e40..1f903a619 100644 --- a/pkg/neotest/signer.go +++ b/pkg/neotest/signer.go @@ -27,6 +27,14 @@ type Signer interface { 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 +} + // signer represents simple-signature signer. type signer wallet.Account @@ -35,7 +43,7 @@ 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 { +func NewSingleSigner(acc *wallet.Account) SingleSigner { if !vm.IsSignatureContract(acc.Contract.Script) { panic("account must have simple-signature verification script") } @@ -63,6 +71,11 @@ 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) Signer { diff --git a/pkg/neotest/signer_test.go b/pkg/neotest/signer_test.go new file mode 100644 index 000000000..c0f2d7e35 --- /dev/null +++ b/pkg/neotest/signer_test.go @@ -0,0 +1,16 @@ +package neotest + +import ( + "testing" + + "github.com/nspcc-dev/neo-go/pkg/wallet" + "github.com/stretchr/testify/require" +) + +func TestSingleSigner(t *testing.T) { + a, err := wallet.NewAccount() + require.NoError(t, err) + + s := NewSingleSigner(a) + require.Equal(t, s.ScriptHash(), s.Account().Contract.ScriptHash()) +} From 37ecf51d1320dd9bd1041c9603da921125818572 Mon Sep 17 00:00:00 2001 From: Evgeniy Stratonikov Date: Fri, 3 Dec 2021 16:10:55 +0300 Subject: [PATCH 2/4] 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 --- pkg/neotest/signer.go | 42 ++++++++++++++++++++++++++++++-------- pkg/neotest/signer_test.go | 32 +++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+), 8 deletions(-) diff --git a/pkg/neotest/signer.go b/pkg/neotest/signer.go index 1f903a619..e9580626c 100644 --- a/pkg/neotest/signer.go +++ b/pkg/neotest/signer.go @@ -3,6 +3,7 @@ package neotest import ( "bytes" "fmt" + "sort" "testing" "github.com/nspcc-dev/neo-go/pkg/config/netmode" @@ -35,11 +36,21 @@ type SingleSigner interface { 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 []*wallet.Account +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. @@ -78,7 +89,7 @@ func (s *signer) Account() *wallet.Account { // 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 { +func NewMultiSigner(accs ...*wallet.Account) MultiSigner { if len(accs) == 0 { panic("empty account list") } @@ -91,30 +102,35 @@ func NewMultiSigner(accs ...*wallet.Account) Signer { 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(accs[:m]) + return multiSigner{accounts: accs, m: m} } // ScriptHash implements Signer interface. func (m multiSigner) ScriptHash() util.Uint160 { - return m[0].Contract.ScriptHash() + return m.accounts[0].Contract.ScriptHash() } // Script implements Signer interface. func (m multiSigner) Script() []byte { - return m[0].Contract.Script + return m.accounts[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) + 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...) } @@ -138,9 +154,19 @@ func (m multiSigner) SignTx(magic netmode.Magic, tx *transaction.Transaction) er 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) { - accs, ok := s.(multiSigner) + 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) diff --git a/pkg/neotest/signer_test.go b/pkg/neotest/signer_test.go index c0f2d7e35..3b4f27cc6 100644 --- a/pkg/neotest/signer_test.go +++ b/pkg/neotest/signer_test.go @@ -1,8 +1,11 @@ package neotest import ( + "sort" "testing" + "github.com/nspcc-dev/neo-go/pkg/crypto/keys" + "github.com/nspcc-dev/neo-go/pkg/smartcontract" "github.com/nspcc-dev/neo-go/pkg/wallet" "github.com/stretchr/testify/require" ) @@ -14,3 +17,32 @@ func TestSingleSigner(t *testing.T) { s := NewSingleSigner(a) require.Equal(t, s.ScriptHash(), s.Account().Contract.ScriptHash()) } + +func TestMultiSigner(t *testing.T) { + const size = 4 + + pubs := make(keys.PublicKeys, size) + accs := make([]*wallet.Account, size) + for i := range accs { + a, err := wallet.NewAccount() + require.NoError(t, err) + + accs[i] = a + pubs[i] = a.PrivateKey().PublicKey() + } + + sort.Sort(pubs) + m := smartcontract.GetDefaultHonestNodeCount(size) + for i := range accs { + require.NoError(t, accs[i].ConvertMultisig(m, pubs)) + } + + s := NewMultiSigner(accs...) + for i := range pubs { + for j := range accs { + if pub := accs[j].PrivateKey().PublicKey(); pub.Equal(pubs[i]) { + require.Equal(t, pub, s.Single(i).Account().PrivateKey().PublicKey()) + } + } + } +} From 8886fa8ca9671fdd8125e0acdcfb9952d4640c62 Mon Sep 17 00:00:00 2001 From: Evgeniy Stratonikov Date: Fri, 3 Dec 2021 16:33:34 +0300 Subject: [PATCH 3/4] neotest: simplify committee account construction Simple `sort.Slice` is more readable than multi-level for-loop. Signed-off-by: Evgeniy Stratonikov --- pkg/neotest/chain/chain.go | 44 +++++++++++++++++--------------------- 1 file changed, 20 insertions(+), 24 deletions(-) diff --git a/pkg/neotest/chain/chain.go b/pkg/neotest/chain/chain.go index 732bfc2fb..53ca25f71 100644 --- a/pkg/neotest/chain/chain.go +++ b/pkg/neotest/chain/chain.go @@ -75,37 +75,33 @@ func init() { multiValidatorAcc = make([]*wallet.Account, mv) sort.Sort(pubs[:4]) -vloop: - for i := 0; i < mv; i++ { - for j := range accs { - if accs[j].PrivateKey().PublicKey().Equal(pubs[i]) { - multiValidatorAcc[i] = wallet.NewAccountFromPrivateKey(accs[j].PrivateKey()) - err := multiValidatorAcc[i].ConvertMultisig(mv, pubs[:4]) - if err != nil { - panic(err) - } - continue vloop - } + sort.Slice(accs[:4], func(i, j int) bool { + p1 := accs[i].PrivateKey().PublicKey() + p2 := accs[j].PrivateKey().PublicKey() + return p1.Cmp(p2) == -1 + }) + for i := range multiValidatorAcc { + multiValidatorAcc[i] = wallet.NewAccountFromPrivateKey(accs[i].PrivateKey()) + err := multiValidatorAcc[i].ConvertMultisig(mv, pubs[:4]) + if err != nil { + panic(err) } - panic("invalid committee WIFs") } multiCommitteeAcc = make([]*wallet.Account, mc) sort.Sort(pubs) -cloop: - for i := 0; i < mc; i++ { - for j := range accs { - if accs[j].PrivateKey().PublicKey().Equal(pubs[i]) { - multiCommitteeAcc[i] = wallet.NewAccountFromPrivateKey(accs[j].PrivateKey()) - err := multiCommitteeAcc[i].ConvertMultisig(mc, pubs) - if err != nil { - panic(err) - } - continue cloop - } + sort.Slice(accs, func(i, j int) bool { + p1 := accs[i].PrivateKey().PublicKey() + p2 := accs[j].PrivateKey().PublicKey() + return p1.Cmp(p2) == -1 + }) + for i := range multiCommitteeAcc { + multiCommitteeAcc[i] = wallet.NewAccountFromPrivateKey(accs[i].PrivateKey()) + err := multiCommitteeAcc[i].ConvertMultisig(mc, pubs) + if err != nil { + panic(err) } - panic("invalid committee WIFs") } } From 4ab97094c2c26d9fd9b94720e49892f0055aa357 Mon Sep 17 00:00:00 2001 From: Evgeniy Stratonikov Date: Fri, 3 Dec 2021 16:37:09 +0300 Subject: [PATCH 4/4] neotest: provide full account list for committee Make the behaviour of `committee.Single(n)` more predictable, i.e be able to return every committee member. Signed-off-by: Evgeniy Stratonikov --- pkg/neotest/chain/chain.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/neotest/chain/chain.go b/pkg/neotest/chain/chain.go index 53ca25f71..7eaa5bd9c 100644 --- a/pkg/neotest/chain/chain.go +++ b/pkg/neotest/chain/chain.go @@ -72,7 +72,7 @@ func init() { standByCommittee[4] = hex.EncodeToString(pubs[4].Bytes()) standByCommittee[5] = hex.EncodeToString(pubs[5].Bytes()) - multiValidatorAcc = make([]*wallet.Account, mv) + multiValidatorAcc = make([]*wallet.Account, 4) sort.Sort(pubs[:4]) sort.Slice(accs[:4], func(i, j int) bool { @@ -88,7 +88,7 @@ func init() { } } - multiCommitteeAcc = make([]*wallet.Account, mc) + multiCommitteeAcc = make([]*wallet.Account, len(committeeWIFs)) sort.Sort(pubs) sort.Slice(accs, func(i, j int) bool {