Merge pull request #706 from nspcc-dev/feature/transfer

cli: implement transfer from multisig accounts
This commit is contained in:
Roman Khimov 2020-03-05 12:28:01 +03:00 committed by GitHub
commit d03b2ef4a1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 801 additions and 24 deletions

135
cli/wallet/multisig.go Normal file
View file

@ -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 <path> --addr <addr> --in <file.in> --out <file.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))
}
}

View file

@ -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,14 @@ var (
Name: "timeout, t",
Usage: "Timeout for the operation",
}
outFlag = cli.StringFlag{
Name: "out",
Usage: "file to put JSON transaction to",
}
inFlag = cli.StringFlag{
Name: "in",
Usage: "file with JSON transaction",
}
)
// NewCommands returns 'wallet' command.
@ -144,12 +155,13 @@ func NewCommands() []cli.Command {
Name: "transfer",
Usage: "transfer NEO/GAS",
UsageText: "transfer --path <path> --from <addr> --to <addr>" +
" --amount <amount> --asset [NEO|GAS|<hex-id>]",
" --amount <amount> --asset [NEO|GAS|<hex-id>] [--out <path>]",
Action: transferAsset,
Flags: []cli.Flag{
walletPathFlag,
rpcFlag,
timeoutFlag,
outFlag,
cli.StringFlag{
Name: "from",
Usage: "Address to send an asset from",
@ -168,6 +180,11 @@ func NewCommands() []cli.Command {
},
},
},
{
Name: "multisig",
Usage: "work with multisig address",
Subcommands: newMultisigCommands(),
},
},
}}
}
@ -416,7 +433,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)
}
@ -432,9 +448,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())

View file

@ -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
}

View file

@ -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)

View file

@ -2,6 +2,7 @@ package transaction
import (
"encoding/hex"
"encoding/json"
"testing"
"github.com/nspcc-dev/neo-go/pkg/encoding/address"
@ -175,3 +176,23 @@ 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,
}}
tx.Scripts = []Witness{{
InvocationScript: []byte{5, 3, 1},
VerificationScript: []byte{2, 4, 6},
}}
data, err := json.Marshal(tx)
require.NoError(t, err)
txNew := new(Transaction)
require.NoError(t, json.Unmarshal(data, txNew))
require.Equal(t, tx, txNew)
}

View file

@ -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)

View file

@ -0,0 +1,207 @@
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/vm/emit"
"github.com/nspcc-dev/neo-go/pkg/wallet"
)
// 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"`
}
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),
}
}
// 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)
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()
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 := &paramContext{
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
}

View file

@ -0,0 +1,183 @@
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/nspcc-dev/neo-go/pkg/vm"
"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)
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) {
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]))
}
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) {
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 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{
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
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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

View file

@ -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