mirror of
https://github.com/nspcc-dev/neo-go.git
synced 2024-11-27 03:58:06 +00:00
Merge pull request #706 from nspcc-dev/feature/transfer
cli: implement transfer from multisig accounts
This commit is contained in:
commit
d03b2ef4a1
14 changed files with 801 additions and 24 deletions
135
cli/wallet/multisig.go
Normal file
135
cli/wallet/multisig.go
Normal 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))
|
||||
}
|
||||
}
|
|
@ -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())
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
207
pkg/smartcontract/context/context.go
Normal file
207
pkg/smartcontract/context/context.go
Normal 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 := ¶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
|
||||
}
|
183
pkg/smartcontract/context/context_test.go
Normal file
183
pkg/smartcontract/context/context_test.go
Normal 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
|
||||
}
|
75
pkg/smartcontract/context/item.go
Normal file
75
pkg/smartcontract/context/item.go
Normal 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
|
||||
}
|
74
pkg/smartcontract/context/item_test.go
Normal file
74
pkg/smartcontract/context/item_test.go
Normal 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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue