From cd487e3ad40ea53ed34827a4306c52839629c1cd Mon Sep 17 00:00:00 2001 From: Evgenii Stratonikov Date: Wed, 4 Mar 2020 14:59:58 +0300 Subject: [PATCH] smartcontract: implement (*ParameterContext).AddSignature() --- pkg/smartcontract/context/context.go | 96 ++++++++++++++++++++++ pkg/smartcontract/context/context_test.go | 99 +++++++++++++++++++++++ 2 files changed, 195 insertions(+) diff --git a/pkg/smartcontract/context/context.go b/pkg/smartcontract/context/context.go index 3dd73ca5a..7348989b7 100644 --- a/pkg/smartcontract/context/context.go +++ b/pkg/smartcontract/context/context.go @@ -1,14 +1,21 @@ package context import ( + "bytes" "encoding/hex" "encoding/json" + "errors" "fmt" + "sort" "strings" "github.com/nspcc-dev/neo-go/pkg/core/transaction" + "github.com/nspcc-dev/neo-go/pkg/crypto/keys" "github.com/nspcc-dev/neo-go/pkg/io" + "github.com/nspcc-dev/neo-go/pkg/smartcontract" "github.com/nspcc-dev/neo-go/pkg/util" + "github.com/nspcc-dev/neo-go/pkg/vm" + "github.com/nspcc-dev/neo-go/pkg/wallet" ) // ParameterContext represents smartcontract parameter's context. @@ -27,6 +34,95 @@ type paramContext struct { Items map[string]json.RawMessage `json:"items"` } +type sigWithIndex struct { + index int + sig []byte +} + +// NewParameterContext returns ParameterContext with the specified type and item to sign. +func NewParameterContext(typ string, verif io.Serializable) *ParameterContext { + return &ParameterContext{ + Type: typ, + Verifiable: verif, + Items: make(map[util.Uint160]*Item), + } +} + +// AddSignature adds a signature for the specified contract and public key. +func (c *ParameterContext) AddSignature(ctr *wallet.Contract, pub *keys.PublicKey, sig []byte) error { + item := c.getItemForContract(ctr) + if pubs, ok := vm.ParseMultiSigContract(ctr.Script); ok { + if item.GetSignature(pub) != nil { + return errors.New("signature is already added") + } + pubBytes := pub.Bytes() + var contained bool + for i := range pubs { + if bytes.Equal(pubBytes, pubs[i]) { + contained = true + break + } + } + if !contained { + return errors.New("public key is not present in script") + } + item.AddSignature(pub, sig) + if len(item.Signatures) == len(ctr.Parameters) { + indexMap := map[string]int{} + for i := range pubs { + indexMap[hex.EncodeToString(pubs[i])] = i + } + sigs := make([]sigWithIndex, 0, len(item.Signatures)) + for pub, sig := range item.Signatures { + sigs = append(sigs, sigWithIndex{index: indexMap[pub], sig: sig}) + } + sort.Slice(sigs, func(i, j int) bool { + return sigs[i].index < sigs[j].index + }) + for i := range sigs { + item.Parameters[i] = smartcontract.Parameter{ + Type: smartcontract.SignatureType, + Value: sigs[i].sig, + } + } + } + return nil + } + + index := -1 + for i := range ctr.Parameters { + if ctr.Parameters[i].Type == smartcontract.SignatureType { + if index >= 0 { + return errors.New("multiple signature parameters in non-multisig contract") + } + index = i + } + } + if index == -1 { + return errors.New("missing signature parameter") + } + item.Parameters[index].Value = sig + return nil +} + +func (c *ParameterContext) getItemForContract(ctr *wallet.Contract) *Item { + h := ctr.ScriptHash() + if item, ok := c.Items[h]; ok { + return item + } + params := make([]smartcontract.Parameter, len(ctr.Parameters)) + for i := range params { + params[i].Type = ctr.Parameters[i].Type + } + item := &Item{ + Script: h, + Parameters: params, + Signatures: make(map[string][]byte), + } + c.Items[h] = item + return item +} + // MarshalJSON implements json.Marshaler interface. func (c ParameterContext) MarshalJSON() ([]byte, error) { bw := io.NewBufBinWriter() diff --git a/pkg/smartcontract/context/context_test.go b/pkg/smartcontract/context/context_test.go index 4cf7d967f..276a72c1b 100644 --- a/pkg/smartcontract/context/context_test.go +++ b/pkg/smartcontract/context/context_test.go @@ -9,9 +9,89 @@ import ( "github.com/nspcc-dev/neo-go/pkg/crypto/keys" "github.com/nspcc-dev/neo-go/pkg/smartcontract" "github.com/nspcc-dev/neo-go/pkg/util" + "github.com/nspcc-dev/neo-go/pkg/wallet" "github.com/stretchr/testify/require" ) +func TestParameterContext_AddSignatureSimpleContract(t *testing.T) { + tx := getContractTx() + priv, err := keys.NewPrivateKey() + require.NoError(t, err) + pub := priv.PublicKey() + sig := priv.Sign(tx.GetSignedPart()) + + t.Run("invalid contract", func(t *testing.T) { + c := NewParameterContext("Neo.Core.ContractTransaction", tx) + ctr := &wallet.Contract{ + Script: pub.GetVerificationScript(), + Parameters: []wallet.ContractParam{ + newParam(smartcontract.SignatureType, "parameter0"), + newParam(smartcontract.SignatureType, "parameter1"), + }, + } + require.Error(t, c.AddSignature(ctr, pub, sig)) + if item := c.Items[ctr.ScriptHash()]; item != nil { + require.Nil(t, item.Parameters[0].Value) + } + + ctr.Parameters = ctr.Parameters[:0] + require.Error(t, c.AddSignature(ctr, pub, sig)) + if item := c.Items[ctr.ScriptHash()]; item != nil { + require.Nil(t, item.Parameters[0].Value) + } + }) + + c := NewParameterContext("Neo.Core.ContractTransaction", tx) + ctr := &wallet.Contract{ + Script: pub.GetVerificationScript(), + Parameters: []wallet.ContractParam{newParam(smartcontract.SignatureType, "parameter0")}, + } + require.NoError(t, c.AddSignature(ctr, pub, sig)) + item := c.Items[ctr.ScriptHash()] + require.NotNil(t, item) + require.Equal(t, sig, item.Parameters[0].Value) +} + +func TestParameterContext_AddSignatureMultisig(t *testing.T) { + tx := getContractTx() + c := NewParameterContext("Neo.Core.ContractTransaction", tx) + privs, pubs := getPrivateKeys(t, 4) + pubsCopy := make(keys.PublicKeys, len(pubs)) + copy(pubsCopy, pubs) + script, err := smartcontract.CreateMultiSigRedeemScript(3, pubsCopy) + require.NoError(t, err) + + ctr := &wallet.Contract{ + Script: script, + Parameters: []wallet.ContractParam{ + newParam(smartcontract.SignatureType, "parameter0"), + newParam(smartcontract.SignatureType, "parameter1"), + newParam(smartcontract.SignatureType, "parameter2"), + }, + } + data := tx.GetSignedPart() + priv, err := keys.NewPrivateKey() + require.NoError(t, err) + sig := priv.Sign(data) + require.Error(t, c.AddSignature(ctr, priv.PublicKey(), sig)) + + indices := []int{2, 3, 0} // random order + for _, i := range indices { + sig := privs[i].Sign(data) + require.NoError(t, c.AddSignature(ctr, pubs[i], sig)) + require.Error(t, c.AddSignature(ctr, pubs[i], sig)) + + item := c.Items[ctr.ScriptHash()] + require.NotNil(t, item) + require.Equal(t, sig, item.GetSignature(pubs[i])) + } + + item := c.Items[ctr.ScriptHash()] + for i := range item.Parameters { + require.NotNil(t, item.Parameters[i].Value) + } +} + func TestParameterContext_MarshalJSON(t *testing.T) { priv, err := keys.NewPrivateKey() require.NoError(t, err) @@ -45,6 +125,25 @@ func TestParameterContext_MarshalJSON(t *testing.T) { require.Equal(t, expected, actual) } +func getPrivateKeys(t *testing.T, n int) ([]*keys.PrivateKey, []*keys.PublicKey) { + privs := make([]*keys.PrivateKey, n) + pubs := make([]*keys.PublicKey, n) + for i := range privs { + var err error + privs[i], err = keys.NewPrivateKey() + require.NoError(t, err) + pubs[i] = privs[i].PublicKey() + } + return privs, pubs +} + +func newParam(typ smartcontract.ParamType, name string) wallet.ContractParam { + return wallet.ContractParam{ + Name: name, + Type: typ, + } +} + func getContractTx() *transaction.Transaction { tx := transaction.NewContractTX() tx.AddInput(&transaction.Input{