diff --git a/cli/multisig_test.go b/cli/multisig_test.go index 337f4d81e..b3d57ac68 100644 --- a/cli/multisig_test.go +++ b/cli/multisig_test.go @@ -12,6 +12,7 @@ import ( "github.com/nspcc-dev/neo-go/pkg/crypto/keys" "github.com/nspcc-dev/neo-go/pkg/encoding/address" "github.com/nspcc-dev/neo-go/pkg/smartcontract" + "github.com/nspcc-dev/neo-go/pkg/util" "github.com/stretchr/testify/require" ) @@ -75,8 +76,23 @@ func TestSignMultisigTx(t *testing.T) { "--to", priv.Address(), "--token", "NEO", "--amount", "1", "--out", txPath) + simplePriv, err := keys.NewPrivateKey() + require.NoError(t, err) + { // Simple signer, not in signers. + e.In.WriteString("acc\rpass\rpass\r") + e.Run(t, "neo-go", "wallet", "import", + "--wallet", wallet1Path, + "--wif", simplePriv.WIF()) + t.Run("sign with missing signer", func(t *testing.T) { + e.In.WriteString("pass\r") + e.RunWithError(t, "neo-go", "wallet", "sign", + "--wallet", wallet1Path, "--address", simplePriv.Address(), + "--in", txPath, "--out", txPath) + }) + } + e.In.WriteString("pass\r") - e.Run(t, "neo-go", "wallet", "multisig", "sign", + e.Run(t, "neo-go", "wallet", "sign", "--rpc-endpoint", "http://"+e.RPC.Addr, "--wallet", wallet2Path, "--address", multisigAddr, "--in", txPath, "--out", txPath) @@ -88,6 +104,26 @@ func TestSignMultisigTx(t *testing.T) { require.Equal(t, big.NewInt(3), b) t.Run("via invokefunction", func(t *testing.T) { + e.In.WriteString("one\r") + e.Run(t, "neo-go", "contract", "deploy", + "--rpc-endpoint", "http://"+e.RPC.Addr, + "--wallet", validatorWallet, "--address", validatorAddr, + "--in", "testdata/verify.nef", "--manifest", "testdata/verify.manifest.json") + + line, err := e.Out.ReadString('\n') + require.NoError(t, err) + line = strings.TrimSpace(strings.TrimPrefix(line, "Contract: ")) + h, err := util.Uint160DecodeStringLE(line) + require.NoError(t, err) + + e.checkTxPersisted(t) + + e.In.WriteString("acc\rpass\rpass\r") + e.Run(t, "neo-go", "wallet", "import-deployed", + "--rpc-endpoint", "http://"+e.RPC.Addr, + "--wallet", wallet1Path, "--wif", simplePriv.WIF(), + "--contract", h.StringLE()) + e.In.WriteString("pass\r") e.Run(t, "neo-go", "contract", "invokefunction", "--rpc-endpoint", "http://"+e.RPC.Addr, @@ -97,14 +133,30 @@ func TestSignMultisigTx(t *testing.T) { "bytes:"+multisigHash.StringBE(), "bytes:"+priv.GetScriptHash().StringBE(), "int:1", "bytes:", - "--", strings.Join([]string{multisigHash.StringLE(), ":", "Global"}, "")) + "--", multisigHash.StringLE()+":"+"Global", + h.StringLE(), + simplePriv.GetScriptHash().StringLE(), + ) e.In.WriteString("pass\r") - e.Run(t, "neo-go", "wallet", "multisig", "sign", - "--rpc-endpoint", "http://"+e.RPC.Addr, + e.Run(t, "neo-go", "wallet", "sign", "--wallet", wallet2Path, "--address", multisigAddr, "--in", txPath, "--out", txPath) - e.checkTxPersisted(t) + + // Simple signer, not in signers. + e.In.WriteString("pass\r") + e.Run(t, "neo-go", "wallet", "sign", + "--wallet", wallet1Path, "--address", simplePriv.Address(), + "--in", txPath, "--out", txPath) + + // Contract. + e.In.WriteString("pass\r") + e.Run(t, "neo-go", "wallet", "sign", + "--rpc-endpoint", "http://"+e.RPC.Addr, + "--wallet", wallet1Path, "--address", address.Uint160ToString(h), + "--in", txPath, "--out", txPath) + tx, _ := e.checkTxPersisted(t) + require.Equal(t, 3, len(tx.Signers)) b, _ := e.Chain.GetGoverningTokenBalance(priv.GetScriptHash()) require.Equal(t, big.NewInt(2), b) diff --git a/cli/paramcontext/context.go b/cli/paramcontext/context.go index f1bc55e9c..5b3da2daa 100644 --- a/cli/paramcontext/context.go +++ b/cli/paramcontext/context.go @@ -6,6 +6,7 @@ import ( "io/ioutil" "github.com/nspcc-dev/neo-go/pkg/core/transaction" + "github.com/nspcc-dev/neo-go/pkg/encoding/address" "github.com/nspcc-dev/neo-go/pkg/smartcontract/context" "github.com/nspcc-dev/neo-go/pkg/wallet" ) @@ -22,7 +23,11 @@ func InitAndSave(tx *transaction.Transaction, acc *wallet.Account, filename stri pub := priv.PublicKey() sign := priv.Sign(tx.GetSignedPart()) scCtx := context.NewParameterContext("Neo.Core.ContractTransaction", tx) - if err := scCtx.AddSignature(acc.Contract, pub, sign); err != nil { + h, err := address.StringToUint160(acc.Address) + if err != nil { + return fmt.Errorf("invalid address: %s", acc.Address) + } + if err := scCtx.AddSignature(h, acc.Contract, pub, sign); err != nil { return fmt.Errorf("can't add signature: %w", err) } return Save(scCtx, filename) diff --git a/cli/wallet/multisig.go b/cli/wallet/multisig.go index 4c730a33b..2a7955d28 100644 --- a/cli/wallet/multisig.go +++ b/cli/wallet/multisig.go @@ -10,29 +10,7 @@ import ( "github.com/urfave/cli" ) -func newMultisigCommands() []cli.Command { - signFlags := []cli.Flag{ - walletPathFlag, - outFlag, - inFlag, - cli.StringFlag{ - Name: "address, a", - Usage: "Address to use", - }, - } - signFlags = append(signFlags, options.RPC...) - return []cli.Command{ - { - Name: "sign", - Usage: "sign a transaction", - UsageText: "multisig sign --wallet --address
--in --out ", - Action: signMultisig, - Flags: signFlags, - }, - } -} - -func signMultisig(ctx *cli.Context) error { +func signStoredTransaction(ctx *cli.Context) error { wall, err := openWallet(ctx.String("wallet")) if err != nil { return cli.NewExitError(err, 1) @@ -58,9 +36,24 @@ func signMultisig(ctx *cli.Context) error { return cli.NewExitError("verifiable item is not a transaction", 1) } + ch, err := address.StringToUint160(acc.Address) + if err != nil { + return cli.NewExitError(fmt.Errorf("wallet contains invalid account: %s", acc.Address), 1) + } + signerFound := false + for i := range tx.Signers { + if tx.Signers[i].Account == ch { + signerFound = true + break + } + } + if !signerFound { + return cli.NewExitError("tx signers don't contain provided account", 1) + } + priv := acc.PrivateKey() sign := priv.Sign(tx.GetSignedPart()) - if err := c.AddSignature(acc.Contract, priv.PublicKey(), sign); err != nil { + if err := c.AddSignature(ch, acc.Contract, priv.PublicKey(), sign); err != nil { return cli.NewExitError(fmt.Errorf("can't add signature: %w", err), 1) } if out := ctx.String("out"); out != "" { @@ -69,15 +62,18 @@ func signMultisig(ctx *cli.Context) error { } } if len(ctx.String(options.RPCEndpointFlag)) != 0 { - w, err := c.GetWitness(acc.Contract) - if err != nil { - return cli.NewExitError(err, 1) + for i := range tx.Signers { + w, err := c.GetWitness(tx.Signers[i].Account) + if err != nil { + return cli.NewExitError(err, 1) + } + tx.Scripts = append(tx.Scripts, *w) } - tx.Scripts = append(tx.Scripts, *w) gctx, cancel := options.GetTimeoutContext(ctx) defer cancel() + var err error // `GetRPCClient` returns specialized type. c, err := options.GetRPCClient(gctx, ctx) if err != nil { return cli.NewExitError(err, 1) diff --git a/cli/wallet/wallet.go b/cli/wallet/wallet.go index 0cf11483c..7081e4937 100644 --- a/cli/wallet/wallet.go +++ b/cli/wallet/wallet.go @@ -70,6 +70,16 @@ func NewCommands() []cli.Command { }, } claimFlags = append(claimFlags, options.RPC...) + signFlags := []cli.Flag{ + walletPathFlag, + outFlag, + inFlag, + cli.StringFlag{ + Name: "address, a", + Usage: "Address to use", + }, + } + signFlags = append(signFlags, options.RPC...) return []cli.Command{{ Name: "wallet", Usage: "create, open and manage a NEO wallet", @@ -204,9 +214,11 @@ func NewCommands() []cli.Command { }, }, { - Name: "multisig", - Usage: "work with multisig address", - Subcommands: newMultisigCommands(), + Name: "sign", + Usage: "cosign transaction with multisig/contract/additional account", + UsageText: "sign --wallet --address
--in --out ", + Action: signStoredTransaction, + Flags: signFlags, }, { Name: "nep17", diff --git a/pkg/smartcontract/context/context.go b/pkg/smartcontract/context/context.go index 16fc0b424..842273a15 100644 --- a/pkg/smartcontract/context/context.go +++ b/pkg/smartcontract/context/context.go @@ -51,8 +51,11 @@ func NewParameterContext(typ string, verif crypto.VerifiableDecodable) *Paramete } // GetWitness returns invocation and verification scripts for the specified contract. -func (c *ParameterContext) GetWitness(ctr *wallet.Contract) (*transaction.Witness, error) { - item := c.getItemForContract(ctr) +func (c *ParameterContext) GetWitness(h util.Uint160) (*transaction.Witness, error) { + item, ok := c.Items[h] + if !ok { + return nil, errors.New("witness not found") + } bw := io.NewBufBinWriter() for i := range item.Parameters { if item.Parameters[i].Type != smartcontract.SignatureType { @@ -64,13 +67,13 @@ func (c *ParameterContext) GetWitness(ctr *wallet.Contract) (*transaction.Witnes } return &transaction.Witness{ InvocationScript: bw.Bytes(), - VerificationScript: ctr.Script, + VerificationScript: item.Script, }, nil } // 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) +func (c *ParameterContext) AddSignature(h util.Uint160, ctr *wallet.Contract, pub *keys.PublicKey, sig []byte) error { + item := c.getItemForContract(h, ctr) if _, pubs, ok := vm.ParseMultiSigContract(ctr.Script); ok { if item.GetSignature(pub) != nil { return errors.New("signature is already added") @@ -118,24 +121,29 @@ func (c *ParameterContext) AddSignature(ctr *wallet.Contract, pub *keys.PublicKe index = i } } - if index == -1 { + if index != -1 { + item.Parameters[index].Value = sig + } else if !ctr.Deployed { 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 { +func (c *ParameterContext) getItemForContract(h util.Uint160, ctr *wallet.Contract) *Item { + item, ok := c.Items[ctr.ScriptHash()] + if 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, + script := ctr.Script + if ctr.Deployed { + script = nil + } + item = &Item{ + Script: script, Parameters: params, Signatures: make(map[string][]byte), } diff --git a/pkg/smartcontract/context/context_test.go b/pkg/smartcontract/context/context_test.go index 8c389e97c..1a47e05fa 100644 --- a/pkg/smartcontract/context/context_test.go +++ b/pkg/smartcontract/context/context_test.go @@ -2,6 +2,7 @@ package context import ( "encoding/hex" + "encoding/json" "testing" "github.com/nspcc-dev/neo-go/internal/testserdes" @@ -34,13 +35,13 @@ func TestParameterContext_AddSignatureSimpleContract(t *testing.T) { newParam(smartcontract.SignatureType, "parameter1"), }, } - require.Error(t, c.AddSignature(ctr, pub, sig)) + require.Error(t, c.AddSignature(ctr.ScriptHash(), 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)) + require.Error(t, c.AddSignature(ctr.ScriptHash(), ctr, pub, sig)) if item := c.Items[ctr.ScriptHash()]; item != nil { require.Nil(t, item.Parameters[0].Value) } @@ -51,19 +52,27 @@ func TestParameterContext_AddSignatureSimpleContract(t *testing.T) { Script: pub.GetVerificationScript(), Parameters: []wallet.ContractParam{newParam(smartcontract.SignatureType, "parameter0")}, } - require.NoError(t, c.AddSignature(ctr, pub, sig)) + require.NoError(t, c.AddSignature(ctr.ScriptHash(), ctr, pub, sig)) item := c.Items[ctr.ScriptHash()] require.NotNil(t, item) require.Equal(t, sig, item.Parameters[0].Value) t.Run("GetWitness", func(t *testing.T) { - w, err := c.GetWitness(ctr) + w, err := c.GetWitness(ctr.ScriptHash()) require.NoError(t, err) v := newTestVM(w, tx) require.NoError(t, v.Run()) require.Equal(t, 1, v.Estack().Len()) require.Equal(t, true, v.Estack().Pop().Value()) }) + t.Run("not found", func(t *testing.T) { + ctr := &wallet.Contract{ + Script: []byte{byte(opcode.DROP), byte(opcode.PUSHT)}, + Parameters: []wallet.ContractParam{newParam(smartcontract.SignatureType, "parameter0")}, + } + _, err := c.GetWitness(ctr.ScriptHash()) + require.Error(t, err) + }) } func TestParameterContext_AddSignatureMultisig(t *testing.T) { @@ -86,13 +95,13 @@ func TestParameterContext_AddSignatureMultisig(t *testing.T) { priv, err := keys.NewPrivateKey() require.NoError(t, err) sig := priv.Sign(data) - require.Error(t, c.AddSignature(ctr, priv.PublicKey(), sig)) + require.Error(t, c.AddSignature(ctr.ScriptHash(), 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)) + require.NoError(t, c.AddSignature(ctr.ScriptHash(), ctr, pubs[i], sig)) + require.Error(t, c.AddSignature(ctr.ScriptHash(), ctr, pubs[i], sig)) item := c.Items[ctr.ScriptHash()] require.NotNil(t, item) @@ -100,7 +109,7 @@ func TestParameterContext_AddSignatureMultisig(t *testing.T) { } t.Run("GetWitness", func(t *testing.T) { - w, err := c.GetWitness(ctr) + w, err := c.GetWitness(ctr.ScriptHash()) require.NoError(t, err) v := newTestVM(w, tx) require.NoError(t, v.Run()) @@ -131,7 +140,7 @@ func TestParameterContext_MarshalJSON(t *testing.T) { Verifiable: tx, Items: map[util.Uint160]*Item{ priv.GetScriptHash(): { - Script: priv.GetScriptHash(), + Script: priv.PublicKey().GetVerificationScript(), Parameters: []smartcontract.Parameter{{ Type: smartcontract.SignatureType, Value: sign, @@ -144,6 +153,22 @@ func TestParameterContext_MarshalJSON(t *testing.T) { } testserdes.MarshalUnmarshalJSON(t, expected, new(ParameterContext)) + + t.Run("invalid script", func(t *testing.T) { + js := `{ + "script": "AQID", + "parameters": [ + { + "type": "Signature", + "value": "QfOZLLqjMyPWMzRxMAKw7fcd8leLcpwiiTV2pUyC0pth/y7Iw7o7WzNpxeAJm5bmExmlF7g5pMhXz1xVT6KK3g==" + } + ], + "signatures": { + "025c210bde738e0e646929ee04ec2ccb42a700356083f55386b5347b9b725c10b9": "a6c6d8a2334791888df559419f07209ee39e2f20688af8cc38010854b98abf77194e37f173bbc86b77dce4afa8ce3ae5170dd346b5265bcb9b723d83299a6f0f", + "035d4da640b3a39f19ed88855aeddd97725422b4230ccae56bd5544419d0056ea9": "058e577f23395f382194eebb83f66bb8903c8f3c5b6afd759c20f2518466124dcd9cbccfc029a42e9a7d5a3a060b091edc73dcac949fd894d7a9d10678296ac6" + }` + require.Error(t, json.Unmarshal([]byte(js), new(ParameterContext))) + }) } func getPrivateKeys(t *testing.T, n int) ([]*keys.PrivateKey, []*keys.PublicKey) { diff --git a/pkg/smartcontract/context/item.go b/pkg/smartcontract/context/item.go index fcbb3e9fc..2b8a3dc57 100644 --- a/pkg/smartcontract/context/item.go +++ b/pkg/smartcontract/context/item.go @@ -1,23 +1,23 @@ package context import ( + "encoding/base64" "encoding/hex" "encoding/json" "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" ) // Item represents a transaction context item. type Item struct { - Script util.Uint160 + Script []byte Parameters []smartcontract.Parameter Signatures map[string][]byte } type itemAux struct { - Script util.Uint160 `json:"script"` + Script string `json:"script"` Parameters []smartcontract.Parameter `json:"parameters"` Signatures map[string]string `json:"signatures"` } @@ -36,7 +36,7 @@ func (it *Item) AddSignature(pub *keys.PublicKey, sig []byte) { // MarshalJSON implements json.Marshaler interface. func (it Item) MarshalJSON() ([]byte, error) { ci := itemAux{ - Script: it.Script, + Script: base64.StdEncoding.EncodeToString(it.Script), Parameters: it.Parameters, Signatures: make(map[string]string, len(it.Signatures)), } @@ -55,6 +55,11 @@ func (it *Item) UnmarshalJSON(data []byte) error { return err } + script, err := base64.StdEncoding.DecodeString(ci.Script) + if err != nil { + return err + } + sigs := make(map[string][]byte, len(ci.Signatures)) for keyHex, sigHex := range ci.Signatures { _, err := keys.NewPublicKeyFromString(keyHex) @@ -69,7 +74,7 @@ func (it *Item) UnmarshalJSON(data []byte) error { } it.Signatures = sigs - it.Script = ci.Script + it.Script = script it.Parameters = ci.Parameters return nil } diff --git a/pkg/smartcontract/context/item_test.go b/pkg/smartcontract/context/item_test.go index ca29fc897..21108aa4d 100644 --- a/pkg/smartcontract/context/item_test.go +++ b/pkg/smartcontract/context/item_test.go @@ -8,7 +8,6 @@ import ( "github.com/nspcc-dev/neo-go/internal/testserdes" "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/stretchr/testify/require" ) @@ -41,7 +40,7 @@ func TestContextItem_MarshalJSON(t *testing.T) { require.NoError(t, err) expected := &Item{ - Script: util.Uint160{1, 2, 3}, + Script: []byte{1, 2, 3}, Parameters: []smartcontract.Parameter{{ Type: smartcontract.SignatureType, Value: random.Bytes(64),