From 33f99104e8b993ed73d2a87246a3dc5cd410b5e9 Mon Sep 17 00:00:00 2001 From: Evgenii Stratonikov Date: Tue, 3 Mar 2020 11:25:00 +0300 Subject: [PATCH 01/13] transaction: unmarshal Output properly Address is marshaled in base58 and needs to be decoded accordingly. --- pkg/core/transaction/output.go | 24 ++++++++++++++++++++++++ pkg/core/transaction/transaction_test.go | 17 +++++++++++++++++ 2 files changed, 41 insertions(+) diff --git a/pkg/core/transaction/output.go b/pkg/core/transaction/output.go index 1ee3d3252..11ad85b2e 100644 --- a/pkg/core/transaction/output.go +++ b/pkg/core/transaction/output.go @@ -24,6 +24,13 @@ type Output struct { Position int `json:"n"` } +type outputAux struct { + AssetID util.Uint256 `json:"asset"` + Amount util.Fixed8 `json:"value"` + ScriptHash string `json:"address"` + Position int `json:"n"` +} + // NewOutput returns a new transaction output. func NewOutput(assetID util.Uint256, amount util.Fixed8, scriptHash util.Uint160) *Output { return &Output{ @@ -56,3 +63,20 @@ func (out *Output) MarshalJSON() ([]byte, error) { "n": out.Position, }) } + +// UnmarshalJSON implements json.Unmarshaler interface. +func (out *Output) UnmarshalJSON(data []byte) error { + var outAux outputAux + err := json.Unmarshal(data, &outAux) + if err != nil { + return err + } + out.ScriptHash, err = address.StringToUint160(outAux.ScriptHash) + if err != nil { + return err + } + out.Amount = outAux.Amount + out.AssetID = outAux.AssetID + out.Position = outAux.Position + return nil +} diff --git a/pkg/core/transaction/transaction_test.go b/pkg/core/transaction/transaction_test.go index ec410e5aa..8af560d66 100644 --- a/pkg/core/transaction/transaction_test.go +++ b/pkg/core/transaction/transaction_test.go @@ -2,6 +2,7 @@ package transaction import ( "encoding/hex" + "encoding/json" "testing" "github.com/nspcc-dev/neo-go/pkg/encoding/address" @@ -175,3 +176,19 @@ func TestEncodingTXWithNoData(t *testing.T) { tx.EncodeBinary(buf.BinWriter) require.Error(t, buf.Err) } + +func TestMarshalUnmarshalJSON(t *testing.T) { + tx := NewContractTX() + tx.Outputs = []Output{{ + AssetID: util.Uint256{1, 2, 3, 4}, + Amount: 567, + ScriptHash: util.Uint160{7, 8, 9, 10}, + Position: 13, + }} + data, err := json.Marshal(tx) + require.NoError(t, err) + + txNew := new(Transaction) + require.NoError(t, json.Unmarshal(data, txNew)) + require.Equal(t, tx, txNew) +} From 634e9483d31c949a67e3d279ee4a2495acb8c55c Mon Sep 17 00:00:00 2001 From: Evgenii Stratonikov Date: Tue, 3 Mar 2020 11:29:07 +0300 Subject: [PATCH 02/13] transaction: unmarshal Witness properly Both verification and invocation scripts need to be unmarshaled from hex. Also fix failing RPC tests: block contains non-pointer `transaction.Witness` field and (*Witness).MarshalJSON method is not called. --- pkg/core/transaction/transaction_test.go | 4 ++++ pkg/core/transaction/witness.go | 16 +++++++++++++++- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/pkg/core/transaction/transaction_test.go b/pkg/core/transaction/transaction_test.go index 8af560d66..b98d07ab0 100644 --- a/pkg/core/transaction/transaction_test.go +++ b/pkg/core/transaction/transaction_test.go @@ -185,6 +185,10 @@ func TestMarshalUnmarshalJSON(t *testing.T) { ScriptHash: util.Uint160{7, 8, 9, 10}, Position: 13, }} + tx.Scripts = []Witness{{ + InvocationScript: []byte{5, 3, 1}, + VerificationScript: []byte{2, 4, 6}, + }} data, err := json.Marshal(tx) require.NoError(t, err) diff --git a/pkg/core/transaction/witness.go b/pkg/core/transaction/witness.go index 501663cd8..2c90fa8a7 100644 --- a/pkg/core/transaction/witness.go +++ b/pkg/core/transaction/witness.go @@ -28,7 +28,7 @@ func (w *Witness) EncodeBinary(bw *io.BinWriter) { } // MarshalJSON implements the json marshaller interface. -func (w *Witness) MarshalJSON() ([]byte, error) { +func (w Witness) MarshalJSON() ([]byte, error) { data := map[string]string{ "invocation": hex.EncodeToString(w.InvocationScript), "verification": hex.EncodeToString(w.VerificationScript), @@ -37,6 +37,20 @@ func (w *Witness) MarshalJSON() ([]byte, error) { return json.Marshal(data) } +// UnmarshalJSON implements json.Unmarshaler interface. +func (w *Witness) UnmarshalJSON(data []byte) error { + m := map[string]string{} + err := json.Unmarshal(data, &m) + if err != nil { + return err + } + if w.InvocationScript, err = hex.DecodeString(m["invocation"]); err != nil { + return err + } + w.VerificationScript, err = hex.DecodeString(m["verification"]) + return err +} + // ScriptHash returns the hash of the VerificationScript. func (w Witness) ScriptHash() util.Uint160 { return hash.Hash160(w.VerificationScript) From 7eaeb18f18689ab1438c7484f67de474e2305fef Mon Sep 17 00:00:00 2001 From: Evgenii Stratonikov Date: Tue, 3 Mar 2020 21:20:58 +0300 Subject: [PATCH 03/13] transaction: marshal ContractTX even if Data is nil It contains no information so it doesn't matter anyway. --- cli/wallet/wallet.go | 1 - pkg/core/transaction/transaction.go | 7 +++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/cli/wallet/wallet.go b/cli/wallet/wallet.go index 41c043d90..2928192ac 100644 --- a/cli/wallet/wallet.go +++ b/cli/wallet/wallet.go @@ -416,7 +416,6 @@ func transferAsset(ctx *cli.Context) error { } tx := transaction.NewContractTX() - tx.Data = new(transaction.ContractTX) if err := request.AddInputsAndUnspentsToTx(tx, from, asset, amount, c); err != nil { return cli.NewExitError(err, 1) } diff --git a/pkg/core/transaction/transaction.go b/pkg/core/transaction/transaction.go index adfa9e2dc..da6704e8e 100644 --- a/pkg/core/transaction/transaction.go +++ b/pkg/core/transaction/transaction.go @@ -158,7 +158,8 @@ func (t *Transaction) EncodeBinary(bw *io.BinWriter) { // encodeHashableFields encodes the fields that are not used for // signing the transaction, which are all fields except the scripts. func (t *Transaction) encodeHashableFields(bw *io.BinWriter) { - if t.Data == nil { + noData := t.Type == ContractType + if t.Data == nil && !noData { bw.Err = errors.New("transaction has no data") return } @@ -166,7 +167,9 @@ func (t *Transaction) encodeHashableFields(bw *io.BinWriter) { bw.WriteB(byte(t.Version)) // Underlying TXer. - t.Data.EncodeBinary(bw) + if !noData { + t.Data.EncodeBinary(bw) + } // Attributes bw.WriteArray(t.Attributes) From 924d920423c582f5b8e73df3a9b7bd28d2e553ae Mon Sep 17 00:00:00 2001 From: Evgenii Stratonikov Date: Wed, 4 Mar 2020 12:30:48 +0300 Subject: [PATCH 04/13] smartcontract: support JSON unmarshaling of Signature param --- pkg/smartcontract/param_context.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/smartcontract/param_context.go b/pkg/smartcontract/param_context.go index 14f953f6b..498e4dc8c 100644 --- a/pkg/smartcontract/param_context.go +++ b/pkg/smartcontract/param_context.go @@ -137,7 +137,7 @@ func (p *Parameter) UnmarshalJSON(data []byte) (err error) { return } p.Value = boolean - case ByteArrayType, PublicKeyType: + case ByteArrayType, PublicKeyType, SignatureType: if err = json.Unmarshal(r.Value, &s); err != nil { return } From 46db4e9d9d3cbdf949b140e0cbafcdb57235aeb3 Mon Sep 17 00:00:00 2001 From: Evgenii Stratonikov Date: Wed, 4 Mar 2020 12:41:40 +0300 Subject: [PATCH 05/13] smartcontract: rename param_context.go to parameter.go --- pkg/smartcontract/{param_context.go => parameter.go} | 0 pkg/smartcontract/{param_context_test.go => parameter_test.go} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename pkg/smartcontract/{param_context.go => parameter.go} (100%) rename pkg/smartcontract/{param_context_test.go => parameter_test.go} (100%) diff --git a/pkg/smartcontract/param_context.go b/pkg/smartcontract/parameter.go similarity index 100% rename from pkg/smartcontract/param_context.go rename to pkg/smartcontract/parameter.go diff --git a/pkg/smartcontract/param_context_test.go b/pkg/smartcontract/parameter_test.go similarity index 100% rename from pkg/smartcontract/param_context_test.go rename to pkg/smartcontract/parameter_test.go From acea3867b202b72f2fc42417c11733b6c7cfc915 Mon Sep 17 00:00:00 2001 From: Evgenii Stratonikov Date: Wed, 4 Mar 2020 12:40:01 +0300 Subject: [PATCH 06/13] smartcontract: implement ContextItem --- pkg/smartcontract/context/item.go | 75 ++++++++++++++++++++++++++ pkg/smartcontract/context/item_test.go | 74 +++++++++++++++++++++++++ 2 files changed, 149 insertions(+) create mode 100644 pkg/smartcontract/context/item.go create mode 100644 pkg/smartcontract/context/item_test.go diff --git a/pkg/smartcontract/context/item.go b/pkg/smartcontract/context/item.go new file mode 100644 index 000000000..fcbb3e9fc --- /dev/null +++ b/pkg/smartcontract/context/item.go @@ -0,0 +1,75 @@ +package context + +import ( + "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 + Parameters []smartcontract.Parameter + Signatures map[string][]byte +} + +type itemAux struct { + Script util.Uint160 `json:"script"` + Parameters []smartcontract.Parameter `json:"parameters"` + Signatures map[string]string `json:"signatures"` +} + +// GetSignature returns signature for pub if present. +func (it *Item) GetSignature(pub *keys.PublicKey) []byte { + return it.Signatures[hex.EncodeToString(pub.Bytes())] +} + +// AddSignature adds a signature for pub. +func (it *Item) AddSignature(pub *keys.PublicKey, sig []byte) { + pubHex := hex.EncodeToString(pub.Bytes()) + it.Signatures[pubHex] = sig +} + +// MarshalJSON implements json.Marshaler interface. +func (it Item) MarshalJSON() ([]byte, error) { + ci := itemAux{ + Script: it.Script, + Parameters: it.Parameters, + Signatures: make(map[string]string, len(it.Signatures)), + } + + for key, sig := range it.Signatures { + ci.Signatures[key] = hex.EncodeToString(sig) + } + + return json.Marshal(ci) +} + +// UnmarshalJSON implements json.Unmarshaler interface. +func (it *Item) UnmarshalJSON(data []byte) error { + ci := new(itemAux) + if err := json.Unmarshal(data, ci); err != nil { + return err + } + + sigs := make(map[string][]byte, len(ci.Signatures)) + for keyHex, sigHex := range ci.Signatures { + _, err := keys.NewPublicKeyFromString(keyHex) + if err != nil { + return err + } + sig, err := hex.DecodeString(sigHex) + if err != nil { + return err + } + sigs[keyHex] = sig + } + + it.Signatures = sigs + it.Script = ci.Script + it.Parameters = ci.Parameters + return nil +} diff --git a/pkg/smartcontract/context/item_test.go b/pkg/smartcontract/context/item_test.go new file mode 100644 index 000000000..fd7fd8be1 --- /dev/null +++ b/pkg/smartcontract/context/item_test.go @@ -0,0 +1,74 @@ +package context + +import ( + "encoding/hex" + "encoding/json" + "io" + "math/rand" + "testing" + "time" + + "github.com/nspcc-dev/neo-go/pkg/smartcontract" + + "github.com/nspcc-dev/neo-go/pkg/crypto/keys" + "github.com/nspcc-dev/neo-go/pkg/util" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestContextItem_AddSignature(t *testing.T) { + item := &Item{Signatures: make(map[string][]byte)} + + priv1, err := keys.NewPrivateKey() + require.NoError(t, err) + + pub1 := priv1.PublicKey() + sig1 := []byte{1, 2, 3} + item.AddSignature(pub1, sig1) + require.Equal(t, sig1, item.GetSignature(pub1)) + + priv2, err := keys.NewPrivateKey() + require.NoError(t, err) + + pub2 := priv2.PublicKey() + sig2 := []byte{5, 6, 7} + item.AddSignature(pub2, sig2) + require.Equal(t, sig2, item.GetSignature(pub2)) + require.Equal(t, sig1, item.GetSignature(pub1)) +} + +func TestContextItem_MarshalJSON(t *testing.T) { + priv1, err := keys.NewPrivateKey() + require.NoError(t, err) + + priv2, err := keys.NewPrivateKey() + require.NoError(t, err) + + expected := &Item{ + Script: util.Uint160{1, 2, 3}, + Parameters: []smartcontract.Parameter{{ + Type: smartcontract.SignatureType, + Value: getRandomSlice(t, 64), + }}, + Signatures: map[string][]byte{ + hex.EncodeToString(priv1.PublicKey().Bytes()): getRandomSlice(t, 64), + hex.EncodeToString(priv2.PublicKey().Bytes()): getRandomSlice(t, 64), + }, + } + + data, err := json.Marshal(expected) + require.NoError(t, err) + + actual := new(Item) + require.NoError(t, json.Unmarshal(data, actual)) + assert.Equal(t, expected, actual) +} + +func getRandomSlice(t *testing.T, n int) []byte { + src := rand.NewSource(time.Now().UnixNano()) + r := rand.New(src) + data := make([]byte, n) + _, err := io.ReadFull(r, data) + require.NoError(t, err) + return data +} From 8819d4f9707685ca9050d669fcdc8db000a5f38f Mon Sep 17 00:00:00 2001 From: Evgenii Stratonikov Date: Thu, 5 Mar 2020 09:41:35 +0300 Subject: [PATCH 07/13] vm: implement ParseMultisigContract() When creating witness for a multisig contract, it is needed to extract the order of public keys from it. --- pkg/vm/contract_checks.go | 29 +++++++++++++++++++---------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/pkg/vm/contract_checks.go b/pkg/vm/contract_checks.go index ecb9dfb31..ac23b6c66 100644 --- a/pkg/vm/contract_checks.go +++ b/pkg/vm/contract_checks.go @@ -31,49 +31,58 @@ func getNumOfThingsFromInstr(instr opcode.Opcode, param []byte) (int, bool) { // IsMultiSigContract checks whether the passed script is a multi-signature // contract. func IsMultiSigContract(script []byte) bool { + _, ok := ParseMultiSigContract(script) + return ok +} + +// ParseMultiSigContract returns list of public keys from the verification +// script of the contract. +func ParseMultiSigContract(script []byte) ([][]byte, bool) { var nsigs, nkeys int ctx := NewContext(script) instr, param, err := ctx.Next() if err != nil { - return false + return nil, false } nsigs, ok := getNumOfThingsFromInstr(instr, param) if !ok { - return false + return nil, false } + var pubs [][]byte for { instr, param, err = ctx.Next() if err != nil { - return false + return nil, false } if instr != opcode.PUSHBYTES33 { break } + pubs = append(pubs, param) nkeys++ if nkeys > MaxArraySize { - return false + return nil, false } } if nkeys < nsigs { - return false + return nil, false } nkeys2, ok := getNumOfThingsFromInstr(instr, param) if !ok { - return false + return nil, false } if nkeys2 != nkeys { - return false + return nil, false } instr, _, err = ctx.Next() if err != nil || instr != opcode.CHECKMULTISIG { - return false + return nil, false } instr, _, err = ctx.Next() if err != nil || instr != opcode.RET || ctx.ip != len(script) { - return false + return nil, false } - return true + return pubs, true } // IsSignatureContract checks whether the passed script is a signature check From 0d419d3899293919aa5bc1f9ab4b374a81640020 Mon Sep 17 00:00:00 2001 From: Evgenii Stratonikov Date: Wed, 4 Mar 2020 13:18:49 +0300 Subject: [PATCH 08/13] smartcontract: implement ParameterContext --- pkg/smartcontract/context/context.go | 92 +++++++++++++++++++++++ pkg/smartcontract/context/context_test.go | 64 ++++++++++++++++ 2 files changed, 156 insertions(+) create mode 100644 pkg/smartcontract/context/context.go create mode 100644 pkg/smartcontract/context/context_test.go diff --git a/pkg/smartcontract/context/context.go b/pkg/smartcontract/context/context.go new file mode 100644 index 000000000..3dd73ca5a --- /dev/null +++ b/pkg/smartcontract/context/context.go @@ -0,0 +1,92 @@ +package context + +import ( + "encoding/hex" + "encoding/json" + "fmt" + "strings" + + "github.com/nspcc-dev/neo-go/pkg/core/transaction" + "github.com/nspcc-dev/neo-go/pkg/io" + "github.com/nspcc-dev/neo-go/pkg/util" +) + +// ParameterContext represents smartcontract parameter's context. +type ParameterContext struct { + // Type is a type of a verifiable item. + Type string + // Verifiable is an object which can be (de-)serialized. + Verifiable io.Serializable + // Items is a map from script hashes to context items. + Items map[util.Uint160]*Item +} + +type paramContext struct { + Type string `json:"type"` + Hex string `json:"hex"` + Items map[string]json.RawMessage `json:"items"` +} + +// MarshalJSON implements json.Marshaler interface. +func (c ParameterContext) MarshalJSON() ([]byte, error) { + bw := io.NewBufBinWriter() + c.Verifiable.EncodeBinary(bw.BinWriter) + if bw.Err != nil { + return nil, bw.Err + } + items := make(map[string]json.RawMessage, len(c.Items)) + for u := range c.Items { + data, err := json.Marshal(c.Items[u]) + if err != nil { + return nil, err + } + items["0x"+u.StringBE()] = data + } + pc := ¶mContext{ + Type: c.Type, + Hex: hex.EncodeToString(bw.Bytes()), + Items: items, + } + return json.Marshal(pc) +} + +// UnmarshalJSON implements json.Unmarshaler interface. +func (c *ParameterContext) UnmarshalJSON(data []byte) error { + pc := new(paramContext) + if err := json.Unmarshal(data, pc); err != nil { + return err + } + data, err := hex.DecodeString(pc.Hex) + if err != nil { + return err + } + + var verif io.Serializable + switch pc.Type { + case "Neo.Core.ContractTransaction": + verif = new(transaction.Transaction) + default: + return fmt.Errorf("unsupported type: %s", c.Type) + } + br := io.NewBinReaderFromBuf(data) + verif.DecodeBinary(br) + if br.Err != nil { + return br.Err + } + items := make(map[util.Uint160]*Item, len(pc.Items)) + for h := range pc.Items { + u, err := util.Uint160DecodeStringBE(strings.TrimPrefix(h, "0x")) + if err != nil { + return err + } + item := new(Item) + if err := json.Unmarshal(pc.Items[h], item); err != nil { + return err + } + items[u] = item + } + c.Type = pc.Type + c.Verifiable = verif + c.Items = items + return nil +} diff --git a/pkg/smartcontract/context/context_test.go b/pkg/smartcontract/context/context_test.go new file mode 100644 index 000000000..4cf7d967f --- /dev/null +++ b/pkg/smartcontract/context/context_test.go @@ -0,0 +1,64 @@ +package context + +import ( + "encoding/hex" + "encoding/json" + "testing" + + "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/smartcontract" + "github.com/nspcc-dev/neo-go/pkg/util" + "github.com/stretchr/testify/require" +) + +func TestParameterContext_MarshalJSON(t *testing.T) { + priv, err := keys.NewPrivateKey() + require.NoError(t, err) + + tx := getContractTx() + data := tx.GetSignedPart() + sign := priv.Sign(data) + + expected := &ParameterContext{ + Type: "Neo.Core.ContractTransaction", + Verifiable: tx, + Items: map[util.Uint160]*Item{ + priv.GetScriptHash(): { + Script: priv.GetScriptHash(), + Parameters: []smartcontract.Parameter{{ + Type: smartcontract.SignatureType, + Value: sign, + }}, + Signatures: map[string][]byte{ + hex.EncodeToString(priv.PublicKey().Bytes()): sign, + }, + }, + }, + } + + data, err = json.Marshal(expected) + require.NoError(t, err) + + actual := new(ParameterContext) + require.NoError(t, json.Unmarshal(data, actual)) + require.Equal(t, expected, actual) +} + +func getContractTx() *transaction.Transaction { + tx := transaction.NewContractTX() + tx.AddInput(&transaction.Input{ + PrevHash: util.Uint256{1, 2, 3, 4}, + PrevIndex: 5, + }) + tx.AddOutput(&transaction.Output{ + AssetID: util.Uint256{7, 8, 9}, + Amount: 10, + ScriptHash: util.Uint160{11, 12}, + }) + tx.Data = new(transaction.ContractTX) + tx.Attributes = make([]transaction.Attribute, 0) + tx.Scripts = make([]transaction.Witness, 0) + tx.Hash() + return tx +} From 44901ca867a541dd6d4f7eb62e029c3a4c047a25 Mon Sep 17 00:00:00 2001 From: Evgenii Stratonikov Date: Wed, 4 Mar 2020 13:58:33 +0300 Subject: [PATCH 09/13] wallet: export contractParam It is needed to be able to create Contracts from the outside. --- pkg/wallet/account.go | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/pkg/wallet/account.go b/pkg/wallet/account.go index 46e016c23..273584259 100644 --- a/pkg/wallet/account.go +++ b/pkg/wallet/account.go @@ -56,7 +56,7 @@ type Contract struct { Script []byte `json:"script"` // A list of parameters used deploying this contract. - Parameters []contractParam `json:"parameters"` + Parameters []ContractParam `json:"parameters"` // Indicates whether the contract has been deployed to the blockchain. Deployed bool `json:"deployed"` @@ -68,13 +68,15 @@ type contract struct { Script string `json:"script"` // A list of parameters used deploying this contract. - Parameters []contractParam `json:"parameters"` + Parameters []ContractParam `json:"parameters"` // Indicates whether the contract has been deployed to the blockchain. Deployed bool `json:"deployed"` } -type contractParam struct { +// ContractParam is a descriptor of a contract parameter +// containing type and optional name. +type ContractParam struct { Name string `json:"name"` Type smartcontract.ParamType `json:"type"` } @@ -252,8 +254,8 @@ func newAccountFromPrivateKey(p *keys.PrivateKey) *Account { return a } -func getContractParams(n int) []contractParam { - params := make([]contractParam, n) +func getContractParams(n int) []ContractParam { + params := make([]ContractParam, n) for i := range params { params[i].Name = fmt.Sprintf("parameter%d", i) params[i].Type = smartcontract.SignatureType From cd487e3ad40ea53ed34827a4306c52839629c1cd Mon Sep 17 00:00:00 2001 From: Evgenii Stratonikov Date: Wed, 4 Mar 2020 14:59:58 +0300 Subject: [PATCH 10/13] 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{ From 0d4ad9f76c30becd3113f23172c8614120a7998a Mon Sep 17 00:00:00 2001 From: Evgenii Stratonikov Date: Tue, 3 Mar 2020 10:35:21 +0300 Subject: [PATCH 11/13] cli: add --out flag to the `wallet transfer` command When transferring assets from multisig accounts, it is useful to export tx into a file, so that other participants can sign it too. --- cli/wallet/wallet.go | 30 ++++++++++++++++++++++++++---- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/cli/wallet/wallet.go b/cli/wallet/wallet.go index 2928192ac..98b36f68a 100644 --- a/cli/wallet/wallet.go +++ b/cli/wallet/wallet.go @@ -3,8 +3,10 @@ package wallet import ( "bufio" "context" + "encoding/json" "errors" "fmt" + "io/ioutil" "os" "strings" "syscall" @@ -15,6 +17,7 @@ import ( "github.com/nspcc-dev/neo-go/pkg/encoding/address" "github.com/nspcc-dev/neo-go/pkg/rpc/client" "github.com/nspcc-dev/neo-go/pkg/rpc/request" + context2 "github.com/nspcc-dev/neo-go/pkg/smartcontract/context" "github.com/nspcc-dev/neo-go/pkg/util" "github.com/nspcc-dev/neo-go/pkg/wallet" "github.com/urfave/cli" @@ -47,6 +50,10 @@ var ( Name: "timeout, t", Usage: "Timeout for the operation", } + outFlag = cli.StringFlag{ + Name: "out", + Usage: "file to put JSON transaction to", + } ) // NewCommands returns 'wallet' command. @@ -144,12 +151,13 @@ func NewCommands() []cli.Command { Name: "transfer", Usage: "transfer NEO/GAS", UsageText: "transfer --path --from --to " + - " --amount --asset [NEO|GAS|]", + " --amount --asset [NEO|GAS|] [--out ]", Action: transferAsset, Flags: []cli.Flag{ walletPathFlag, rpcFlag, timeoutFlag, + outFlag, cli.StringFlag{ Name: "from", Usage: "Address to send an asset from", @@ -431,9 +439,23 @@ func transferAsset(ctx *cli.Context) error { Position: 1, }) - _ = acc.SignTx(tx) - if err := c.SendRawTransaction(tx); err != nil { - return cli.NewExitError(err, 1) + if outFile := ctx.String("out"); outFile != "" { + priv := acc.PrivateKey() + pub := priv.PublicKey() + sign := priv.Sign(tx.GetSignedPart()) + c := context2.NewParameterContext("Neo.Core.ContractTransaction", tx) + if err := c.AddSignature(acc.Contract, pub, sign); err != nil { + return cli.NewExitError(fmt.Errorf("can't add signature: %v", err), 1) + } else if data, err := json.Marshal(c); err != nil { + return cli.NewExitError(fmt.Errorf("can't marshal tx to JSON: %v", err), 1) + } else if err := ioutil.WriteFile(outFile, data, 0644); err != nil { + return cli.NewExitError(fmt.Errorf("can't write tx to file: %v", err), 1) + } + } else { + _ = acc.SignTx(tx) + if err := c.SendRawTransaction(tx); err != nil { + return cli.NewExitError(err, 1) + } } fmt.Println(tx.Hash().StringLE()) From 85755a4628c481fce8cb9ea0da3a6ec458ea105b Mon Sep 17 00:00:00 2001 From: Evgenii Stratonikov Date: Wed, 4 Mar 2020 16:30:07 +0300 Subject: [PATCH 12/13] smartcontract: implement (*ParameterContext).GetWitness() After all signatures were accumulated it should be possible to extract Witness for the verifiable item. --- pkg/smartcontract/context/context.go | 19 +++++++++++++++ pkg/smartcontract/context/context_test.go | 28 +++++++++++++++++++---- 2 files changed, 43 insertions(+), 4 deletions(-) diff --git a/pkg/smartcontract/context/context.go b/pkg/smartcontract/context/context.go index 7348989b7..b9dc12966 100644 --- a/pkg/smartcontract/context/context.go +++ b/pkg/smartcontract/context/context.go @@ -15,6 +15,7 @@ import ( "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/vm/emit" "github.com/nspcc-dev/neo-go/pkg/wallet" ) @@ -48,6 +49,24 @@ func NewParameterContext(typ string, verif io.Serializable) *ParameterContext { } } +// GetWitness returns invocation and verification scripts for the specified contract. +func (c *ParameterContext) GetWitness(ctr *wallet.Contract) (*transaction.Witness, error) { + item := c.getItemForContract(ctr) + bw := io.NewBufBinWriter() + for i := range item.Parameters { + if item.Parameters[i].Type != smartcontract.SignatureType { + return nil, errors.New("only signature parameters are supported") + } else if item.Parameters[i].Value == nil { + return nil, errors.New("nil parameter") + } + emit.Bytes(bw.BinWriter, item.Parameters[i].Value.([]byte)) + } + return &transaction.Witness{ + InvocationScript: bw.Bytes(), + VerificationScript: ctr.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) diff --git a/pkg/smartcontract/context/context_test.go b/pkg/smartcontract/context/context_test.go index 276a72c1b..f366fdc10 100644 --- a/pkg/smartcontract/context/context_test.go +++ b/pkg/smartcontract/context/context_test.go @@ -9,6 +9,7 @@ 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/vm" "github.com/nspcc-dev/neo-go/pkg/wallet" "github.com/stretchr/testify/require" ) @@ -50,6 +51,18 @@ func TestParameterContext_AddSignatureSimpleContract(t *testing.T) { 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) + require.NoError(t, err) + v := vm.New() + v.SetCheckedHash(tx.VerificationHash().BytesBE()) + v.LoadScript(w.VerificationScript) + v.LoadScript(w.InvocationScript) + require.NoError(t, v.Run()) + require.Equal(t, 1, v.Estack().Len()) + require.Equal(t, true, v.Estack().Pop().Value()) + }) } func TestParameterContext_AddSignatureMultisig(t *testing.T) { @@ -86,10 +99,17 @@ func TestParameterContext_AddSignatureMultisig(t *testing.T) { 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) - } + t.Run("GetWitness", func(t *testing.T) { + w, err := c.GetWitness(ctr) + require.NoError(t, err) + v := vm.New() + v.SetCheckedHash(tx.VerificationHash().BytesBE()) + v.LoadScript(w.VerificationScript) + v.LoadScript(w.InvocationScript) + require.NoError(t, v.Run()) + require.Equal(t, 1, v.Estack().Len()) + require.Equal(t, true, v.Estack().Pop().Value()) + }) } func TestParameterContext_MarshalJSON(t *testing.T) { From a9783d05f526693e325cbeb423ca3d8c22ad2f62 Mon Sep 17 00:00:00 2001 From: Evgenii Stratonikov Date: Mon, 2 Mar 2020 15:39:02 +0300 Subject: [PATCH 13/13] cli: implement `wallet multisig sign` command Implement ability to sign transactions with multisig address. This should be done in several steps: 1. Create TX with `wallet transfer --out ` 2. Sign TX with `wallet multisign sign --in --out `. 3. Repeat 2 for every party. Input file contains transaction with possibly incomplete set of the signatures. Output file will contain the same tx with updated signature set. When --rpc flag is provided, result transaction is sent via `sendrawtransaction`. --- cli/wallet/multisig.go | 135 +++++++++++++++++++++++++++++++++++++++++ cli/wallet/wallet.go | 9 +++ 2 files changed, 144 insertions(+) create mode 100644 cli/wallet/multisig.go diff --git a/cli/wallet/multisig.go b/cli/wallet/multisig.go new file mode 100644 index 000000000..883e3149e --- /dev/null +++ b/cli/wallet/multisig.go @@ -0,0 +1,135 @@ +package wallet + +import ( + "encoding/json" + "fmt" + "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/rpc/client" + "github.com/nspcc-dev/neo-go/pkg/smartcontract/context" + "github.com/urfave/cli" +) + +func newMultisigCommands() []cli.Command { + return []cli.Command{ + { + Name: "sign", + Usage: "sign a transaction", + UsageText: "multisig sign --path --addr --in --out ", + Action: signMultisig, + Flags: []cli.Flag{ + walletPathFlag, + rpcFlag, + timeoutFlag, + outFlag, + inFlag, + cli.StringFlag{ + Name: "addr", + Usage: "Address to use", + }, + }, + }, + } +} + +func signMultisig(ctx *cli.Context) error { + wall, err := openWallet(ctx.String("path")) + if err != nil { + return cli.NewExitError(err, 1) + } + defer wall.Close() + + c, err := readParameterContext(ctx.String("in")) + if err != nil { + return cli.NewExitError(err, 1) + } + addr := ctx.String("addr") + sh, err := address.StringToUint160(addr) + if err != nil { + return cli.NewExitError(fmt.Errorf("invalid address: %v", err), 1) + } + acc := wall.GetAccount(sh) + if acc == nil { + return cli.NewExitError(fmt.Errorf("can't find account for the address: %s", addr), 1) + } + + tx, ok := c.Verifiable.(*transaction.Transaction) + if !ok { + return cli.NewExitError("verifiable item is not a transaction", 1) + } + printTxInfo(tx) + fmt.Println("Enter password to unlock wallet and sign the transaction") + pass, err := readPassword("Password > ") + if err != nil { + return cli.NewExitError(err, 1) + } else if err := acc.Decrypt(pass); err != nil { + return cli.NewExitError(fmt.Errorf("can't unlock an account: %v", err), 1) + } + + priv := acc.PrivateKey() + sign := priv.Sign(tx.GetSignedPart()) + if err := c.AddSignature(acc.Contract, priv.PublicKey(), sign); err != nil { + return cli.NewExitError(fmt.Errorf("can't add signature: %v", err), 1) + } else if err := writeParameterContext(c, ctx.String("out")); err != nil { + return cli.NewExitError(err, 1) + } + if endpoint := ctx.String("rpc"); endpoint != "" { + w, err := c.GetWitness(acc.Contract) + if err != nil { + return cli.NewExitError(err, 1) + } + tx.Scripts = append(tx.Scripts, *w) + + gctx, cancel := getGoContext(ctx) + defer cancel() + + c, err := client.New(gctx, ctx.String("rpc"), client.Options{}) + if err != nil { + return cli.NewExitError(err, 1) + } else if err := c.SendRawTransaction(tx); err != nil { + return cli.NewExitError(err, 1) + } + } + + fmt.Println(tx.Hash().StringLE()) + return nil +} + +func readParameterContext(filename string) (*context.ParameterContext, error) { + data, err := ioutil.ReadFile(filename) + if err != nil { + return nil, fmt.Errorf("can't read input file: %v", err) + } + + c := new(context.ParameterContext) + if err := json.Unmarshal(data, c); err != nil { + return nil, fmt.Errorf("can't parse transaction: %v", err) + } + return c, nil +} + +func writeParameterContext(c *context.ParameterContext, filename string) error { + if data, err := json.Marshal(c); err != nil { + return fmt.Errorf("can't marshal transaction: %v", err) + } else if err := ioutil.WriteFile(filename, data, 0644); err != nil { + return fmt.Errorf("can't write transaction to file: %v", err) + } + return nil +} + +func printTxInfo(t *transaction.Transaction) { + fmt.Printf("Hash: %s\n", t.Hash().StringLE()) + for i := range t.Inputs { + fmt.Printf("Input%02d: [%2d] %s\n", i, t.Inputs[i].PrevIndex, t.Inputs[i].PrevHash.StringLE()) + } + for i := range t.Outputs { + fmt.Printf("Output%02d:\n", i) + fmt.Printf("\tAssetID : %s\n", t.Outputs[i].AssetID.StringLE()) + fmt.Printf("\tAmount : %s\n", t.Outputs[i].Amount.String()) + h := t.Outputs[i].ScriptHash + fmt.Printf("\tScriptHash: %s\n", t.Outputs[i].ScriptHash.StringLE()) + fmt.Printf("\tToAddr : %s\n", address.Uint160ToString(h)) + } +} diff --git a/cli/wallet/wallet.go b/cli/wallet/wallet.go index 98b36f68a..d816d2afa 100644 --- a/cli/wallet/wallet.go +++ b/cli/wallet/wallet.go @@ -54,6 +54,10 @@ var ( Name: "out", Usage: "file to put JSON transaction to", } + inFlag = cli.StringFlag{ + Name: "in", + Usage: "file with JSON transaction", + } ) // NewCommands returns 'wallet' command. @@ -176,6 +180,11 @@ func NewCommands() []cli.Command { }, }, }, + { + Name: "multisig", + Usage: "work with multisig address", + Subcommands: newMultisigCommands(), + }, }, }} }