Merge pull request #2668 from nspcc-dev/cli-signing-improvements
CLI signing improvements
This commit is contained in:
commit
20224cb39c
13 changed files with 562 additions and 85 deletions
|
@ -14,6 +14,7 @@ import (
|
||||||
"github.com/nspcc-dev/neo-go/pkg/encoding/address"
|
"github.com/nspcc-dev/neo-go/pkg/encoding/address"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/neorpc/result"
|
"github.com/nspcc-dev/neo-go/pkg/neorpc/result"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/smartcontract"
|
"github.com/nspcc-dev/neo-go/pkg/smartcontract"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/smartcontract/context"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/util"
|
"github.com/nspcc-dev/neo-go/pkg/util"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/vm/vmstate"
|
"github.com/nspcc-dev/neo-go/pkg/vm/vmstate"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
@ -162,13 +163,41 @@ func TestSignMultisigTx(t *testing.T) {
|
||||||
require.Equal(t, vmstate.Halt.String(), res.State, res.FaultException)
|
require.Equal(t, vmstate.Halt.String(), res.State, res.FaultException)
|
||||||
})
|
})
|
||||||
|
|
||||||
e.In.WriteString("pass\r")
|
t.Run("console output", func(t *testing.T) {
|
||||||
e.Run(t, "neo-go", "wallet", "sign",
|
oldIn, err := os.ReadFile(txPath)
|
||||||
"--rpc-endpoint", "http://"+e.RPC.Addr,
|
require.NoError(t, err)
|
||||||
"--wallet", wallet2Path, "--address", multisigAddr,
|
e.In.WriteString("pass\r")
|
||||||
"--in", txPath, "--out", txPath)
|
e.Run(t, "neo-go", "wallet", "sign",
|
||||||
e.checkTxPersisted(t)
|
"--wallet", wallet2Path, "--address", multisigAddr,
|
||||||
|
"--in", txPath)
|
||||||
|
newIn, err := os.ReadFile(txPath)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, oldIn, newIn)
|
||||||
|
|
||||||
|
pcOld := new(context.ParameterContext)
|
||||||
|
require.NoError(t, json.Unmarshal(oldIn, pcOld))
|
||||||
|
|
||||||
|
jOut := e.Out.Bytes()
|
||||||
|
pcNew := new(context.ParameterContext)
|
||||||
|
require.NoError(t, json.Unmarshal(jOut, pcNew))
|
||||||
|
|
||||||
|
require.Equal(t, pcOld.Type, pcNew.Type)
|
||||||
|
require.Equal(t, pcOld.Network, pcNew.Network)
|
||||||
|
require.Equal(t, pcOld.Verifiable, pcNew.Verifiable)
|
||||||
|
require.Equal(t, pcOld.Items[multisigHash].Script, pcNew.Items[multisigHash].Script)
|
||||||
|
// It's completely signed after this, so parameters have signatures now as well.
|
||||||
|
require.NotEqual(t, pcOld.Items[multisigHash].Parameters, pcNew.Items[multisigHash].Parameters)
|
||||||
|
require.NotEqual(t, pcOld.Items[multisigHash].Signatures, pcNew.Items[multisigHash].Signatures)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("sign, save and send", func(t *testing.T) {
|
||||||
|
e.In.WriteString("pass\r")
|
||||||
|
e.Run(t, "neo-go", "wallet", "sign",
|
||||||
|
"--rpc-endpoint", "http://"+e.RPC.Addr,
|
||||||
|
"--wallet", wallet2Path, "--address", multisigAddr,
|
||||||
|
"--in", txPath, "--out", txPath)
|
||||||
|
e.checkTxPersisted(t)
|
||||||
|
})
|
||||||
t.Run("double-sign", func(t *testing.T) {
|
t.Run("double-sign", func(t *testing.T) {
|
||||||
e.In.WriteString("pass\r")
|
e.In.WriteString("pass\r")
|
||||||
e.RunWithError(t, "neo-go", "wallet", "sign",
|
e.RunWithError(t, "neo-go", "wallet", "sign",
|
||||||
|
|
|
@ -13,18 +13,21 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
// InitAndSave creates an incompletely signed transaction which can be used
|
// InitAndSave creates an incompletely signed transaction which can be used
|
||||||
// as an input to `multisig sign`.
|
// as an input to `multisig sign`. If a wallet.Account is given and can sign,
|
||||||
|
// it's signed as well using it.
|
||||||
func InitAndSave(net netmode.Magic, tx *transaction.Transaction, acc *wallet.Account, filename string) error {
|
func InitAndSave(net netmode.Magic, tx *transaction.Transaction, acc *wallet.Account, filename string) error {
|
||||||
priv := acc.PrivateKey()
|
|
||||||
pub := priv.PublicKey()
|
|
||||||
sign := priv.SignHashable(uint32(net), tx)
|
|
||||||
scCtx := context.NewParameterContext("Neo.Network.P2P.Payloads.Transaction", net, tx)
|
scCtx := context.NewParameterContext("Neo.Network.P2P.Payloads.Transaction", net, tx)
|
||||||
h, err := address.StringToUint160(acc.Address)
|
if acc != nil && acc.CanSign() {
|
||||||
if err != nil {
|
priv := acc.PrivateKey()
|
||||||
return fmt.Errorf("invalid address: %s", acc.Address)
|
pub := priv.PublicKey()
|
||||||
}
|
sign := priv.SignHashable(uint32(net), tx)
|
||||||
if err := scCtx.AddSignature(h, acc.Contract, pub, sign); err != nil {
|
h, err := address.StringToUint160(acc.Address)
|
||||||
return fmt.Errorf("can't add signature: %w", err)
|
if err != nil {
|
||||||
|
return fmt.Errorf("invalid address: %s", acc.Address)
|
||||||
|
}
|
||||||
|
if err := scCtx.AddSignature(h, acc.Contract, pub, sign); err != nil {
|
||||||
|
return fmt.Errorf("can't add signature: %w", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return Save(scCtx, filename)
|
return Save(scCtx, filename)
|
||||||
}
|
}
|
||||||
|
|
|
@ -702,14 +702,11 @@ func invokeWithArgs(ctx *cli.Context, acc *wallet.Account, wall *wallet.Wallet,
|
||||||
return sender, cli.NewExitError(errText, 1)
|
return sender, cli.NewExitError(errText, 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
action := "save"
|
action := "send"
|
||||||
process := "Saving"
|
process := "Sending"
|
||||||
if out != "" {
|
if out != "" {
|
||||||
action += "and send"
|
action = "save"
|
||||||
process += "and sending"
|
process = "Saving"
|
||||||
} else {
|
|
||||||
action = "send"
|
|
||||||
process = "Sending"
|
|
||||||
}
|
}
|
||||||
if !ctx.Bool("force") {
|
if !ctx.Bool("force") {
|
||||||
return sender, cli.NewExitError(errText+".\nUse --force flag to "+action+" the transaction anyway.", 1)
|
return sender, cli.NewExitError(errText+".\nUse --force flag to "+action+" the transaction anyway.", 1)
|
||||||
|
|
|
@ -25,6 +25,18 @@ func NewCommands() []cli.Command {
|
||||||
and converted to other formats. Strings are escaped and output in quotes.`,
|
and converted to other formats. Strings are escaped and output in quotes.`,
|
||||||
Action: handleParse,
|
Action: handleParse,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Name: "sendtx",
|
||||||
|
Usage: "Send complete transaction stored in a context file",
|
||||||
|
UsageText: "sendtx [-r <endpoint>] <file.in>",
|
||||||
|
Description: `Sends the transaction from the given context file to the given RPC node if it's
|
||||||
|
completely signed and ready. This command expects a ContractParametersContext
|
||||||
|
JSON file for input, it can't handle binary (or hex- or base64-encoded)
|
||||||
|
transactions.
|
||||||
|
`,
|
||||||
|
Action: sendTx,
|
||||||
|
Flags: txDumpFlags,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
Name: "txdump",
|
Name: "txdump",
|
||||||
Usage: "Dump transaction stored in file",
|
Usage: "Dump transaction stored in file",
|
||||||
|
|
42
cli/util/send.go
Normal file
42
cli/util/send.go
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
package util
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/nspcc-dev/neo-go/cli/options"
|
||||||
|
"github.com/nspcc-dev/neo-go/cli/paramcontext"
|
||||||
|
"github.com/urfave/cli"
|
||||||
|
)
|
||||||
|
|
||||||
|
func sendTx(ctx *cli.Context) error {
|
||||||
|
args := ctx.Args()
|
||||||
|
if len(args) == 0 {
|
||||||
|
return cli.NewExitError("missing input file", 1)
|
||||||
|
} else if len(args) > 1 {
|
||||||
|
return cli.NewExitError("only one input file is accepted", 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
pc, err := paramcontext.Read(args[0])
|
||||||
|
if err != nil {
|
||||||
|
return cli.NewExitError(err, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
tx, err := pc.GetCompleteTransaction()
|
||||||
|
if err != nil {
|
||||||
|
return cli.NewExitError(fmt.Errorf("failed to complete transaction: %w", err), 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
gctx, cancel := options.GetTimeoutContext(ctx)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
c, err := options.GetRPCClient(gctx, ctx)
|
||||||
|
if err != nil {
|
||||||
|
return cli.NewExitError(fmt.Errorf("failed to create RPC client: %w", err), 1)
|
||||||
|
}
|
||||||
|
res, err := c.SendRawTransaction(tx)
|
||||||
|
if err != nil {
|
||||||
|
return cli.NewExitError(fmt.Errorf("failed to submit transaction to RPC node: %w", err), 1)
|
||||||
|
}
|
||||||
|
fmt.Fprintln(ctx.App.Writer, res.StringLE())
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -1,6 +1,7 @@
|
||||||
package wallet
|
package wallet
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/nspcc-dev/neo-go/cli/cmdargs"
|
"github.com/nspcc-dev/neo-go/cli/cmdargs"
|
||||||
|
@ -8,11 +9,15 @@ import (
|
||||||
"github.com/nspcc-dev/neo-go/cli/options"
|
"github.com/nspcc-dev/neo-go/cli/options"
|
||||||
"github.com/nspcc-dev/neo-go/cli/paramcontext"
|
"github.com/nspcc-dev/neo-go/cli/paramcontext"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/core/transaction"
|
"github.com/nspcc-dev/neo-go/pkg/core/transaction"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/encoding/address"
|
|
||||||
"github.com/urfave/cli"
|
"github.com/urfave/cli"
|
||||||
)
|
)
|
||||||
|
|
||||||
func signStoredTransaction(ctx *cli.Context) error {
|
func signStoredTransaction(ctx *cli.Context) error {
|
||||||
|
var (
|
||||||
|
out = ctx.String("out")
|
||||||
|
rpcNode = ctx.String(options.RPCEndpointFlag)
|
||||||
|
addrFlag = ctx.Generic("address").(*flags.Address)
|
||||||
|
)
|
||||||
if err := cmdargs.EnsureNone(ctx); err != nil {
|
if err := cmdargs.EnsureNone(ctx); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -21,28 +26,26 @@ func signStoredTransaction(ctx *cli.Context) error {
|
||||||
return cli.NewExitError(err, 1)
|
return cli.NewExitError(err, 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
c, err := paramcontext.Read(ctx.String("in"))
|
pc, err := paramcontext.Read(ctx.String("in"))
|
||||||
if err != nil {
|
|
||||||
return cli.NewExitError(err, 1)
|
|
||||||
}
|
|
||||||
addrFlag := ctx.Generic("address").(*flags.Address)
|
|
||||||
if !addrFlag.IsSet {
|
|
||||||
return cli.NewExitError("address was not provided", 1)
|
|
||||||
}
|
|
||||||
acc, err := getDecryptedAccount(wall, addrFlag.Uint160(), pass)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return cli.NewExitError(err, 1)
|
return cli.NewExitError(err, 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
tx, ok := c.Verifiable.(*transaction.Transaction)
|
if !addrFlag.IsSet {
|
||||||
|
return cli.NewExitError("address was not provided", 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
var ch = addrFlag.Uint160()
|
||||||
|
acc, err := getDecryptedAccount(wall, ch, pass)
|
||||||
|
if err != nil {
|
||||||
|
return cli.NewExitError(err, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
tx, ok := pc.Verifiable.(*transaction.Transaction)
|
||||||
if !ok {
|
if !ok {
|
||||||
return cli.NewExitError("verifiable item is not a transaction", 1)
|
return cli.NewExitError("verifiable item is not a transaction", 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
ch, err := address.StringToUint160(acc.Address)
|
|
||||||
if err != nil {
|
|
||||||
return cli.NewExitError(fmt.Errorf("wallet contains invalid account: %s", acc.Address), 1)
|
|
||||||
}
|
|
||||||
signerFound := false
|
signerFound := false
|
||||||
for i := range tx.Signers {
|
for i := range tx.Signers {
|
||||||
if tx.Signers[i].Account == ch {
|
if tx.Signers[i].Account == ch {
|
||||||
|
@ -54,23 +57,33 @@ func signStoredTransaction(ctx *cli.Context) error {
|
||||||
return cli.NewExitError("tx signers don't contain provided account", 1)
|
return cli.NewExitError("tx signers don't contain provided account", 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
priv := acc.PrivateKey()
|
if acc.CanSign() {
|
||||||
sign := priv.SignHashable(uint32(c.Network), tx)
|
priv := acc.PrivateKey()
|
||||||
if err := c.AddSignature(ch, acc.Contract, priv.PublicKey(), sign); err != nil {
|
sign := priv.SignHashable(uint32(pc.Network), pc.Verifiable)
|
||||||
return cli.NewExitError(fmt.Errorf("can't add signature: %w", err), 1)
|
if err := pc.AddSignature(ch, acc.Contract, priv.PublicKey(), sign); err != nil {
|
||||||
|
return cli.NewExitError(fmt.Errorf("can't add signature: %w", err), 1)
|
||||||
|
}
|
||||||
|
} else if rpcNode == "" {
|
||||||
|
return cli.NewExitError(fmt.Errorf("can't sign transactions with the given account and no RPC endpoing given to send anything signed"), 1)
|
||||||
}
|
}
|
||||||
if out := ctx.String("out"); out != "" {
|
// Not saving and not sending, print.
|
||||||
if err := paramcontext.Save(c, out); err != nil {
|
if out == "" && rpcNode == "" {
|
||||||
return cli.NewExitError(fmt.Errorf("failed to dump resulting transaction: %w", err), 1)
|
txt, err := json.MarshalIndent(pc, " ", " ")
|
||||||
|
if err != nil {
|
||||||
|
return cli.NewExitError(fmt.Errorf("can't display resulting context: %w", err), 1)
|
||||||
|
}
|
||||||
|
fmt.Fprintln(ctx.App.Writer, string(txt))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if out != "" {
|
||||||
|
if err := paramcontext.Save(pc, out); err != nil {
|
||||||
|
return cli.NewExitError(fmt.Errorf("can't save resulting context: %w", err), 1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if len(ctx.String(options.RPCEndpointFlag)) != 0 {
|
if rpcNode != "" {
|
||||||
for i := range tx.Signers {
|
tx, err = pc.GetCompleteTransaction()
|
||||||
w, err := c.GetWitness(tx.Signers[i].Account)
|
if err != nil {
|
||||||
if err != nil {
|
return cli.NewExitError(fmt.Errorf("failed to complete transaction: %w", err), 1)
|
||||||
return cli.NewExitError(fmt.Errorf("failed to construct witness for signer #%d: %w", i, err), 1)
|
|
||||||
}
|
|
||||||
tx.Scripts = append(tx.Scripts, *w)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
gctx, cancel := options.GetTimeoutContext(ctx)
|
gctx, cancel := options.GetTimeoutContext(ctx)
|
||||||
|
|
|
@ -271,7 +271,7 @@ func getNEPBalance(ctx *cli.Context, standard string, accHandler func(*cli.Conte
|
||||||
// But if we have an exact hash, it must be correct.
|
// But if we have an exact hash, it must be correct.
|
||||||
token, err = getTokenWithStandard(c, h, standard)
|
token, err = getTokenWithStandard(c, h, standard)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return cli.NewExitError(fmt.Errorf("%q is not a valid NEP-17 token: %w", name, err), 1)
|
return cli.NewExitError(fmt.Errorf("%q is not a valid %s token: %w", name, standard, err), 1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -150,13 +150,20 @@ func handleVote(ctx *cli.Context) error {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// getDecryptedAccount tries to unlock the specified account. If password is nil, it will be requested via terminal.
|
// getDecryptedAccount tries to get and unlock the specified account if it has a
|
||||||
|
// key inside (otherwise it's returned as is, without an ability to sign). If
|
||||||
|
// password is nil, it will be requested via terminal.
|
||||||
func getDecryptedAccount(wall *wallet.Wallet, addr util.Uint160, password *string) (*wallet.Account, error) {
|
func getDecryptedAccount(wall *wallet.Wallet, addr util.Uint160, password *string) (*wallet.Account, error) {
|
||||||
acc := wall.GetAccount(addr)
|
acc := wall.GetAccount(addr)
|
||||||
if acc == nil {
|
if acc == nil {
|
||||||
return nil, fmt.Errorf("can't find account for the address: %s", address.Uint160ToString(addr))
|
return nil, fmt.Errorf("can't find account for the address: %s", address.Uint160ToString(addr))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// No private key available, nothing to decrypt, but it's still a useful account for many purposes.
|
||||||
|
if acc.EncryptedWIF == "" {
|
||||||
|
return acc, nil
|
||||||
|
}
|
||||||
|
|
||||||
if password == nil {
|
if password == nil {
|
||||||
pass, err := input.ReadPassword(EnterPasswordPrompt)
|
pass, err := input.ReadPassword(EnterPasswordPrompt)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -281,9 +281,33 @@ func NewCommands() []cli.Command {
|
||||||
{
|
{
|
||||||
Name: "sign",
|
Name: "sign",
|
||||||
Usage: "cosign transaction with multisig/contract/additional account",
|
Usage: "cosign transaction with multisig/contract/additional account",
|
||||||
UsageText: "sign -w wallet [--wallet-config path] --address <address> --in <file.in> --out <file.out> [-r <endpoint>]",
|
UsageText: "sign -w wallet [--wallet-config path] --address <address> --in <file.in> [--out <file.out>] [-r <endpoint>]",
|
||||||
Action: signStoredTransaction,
|
Description: `Signs the given (in file.in) context (which must be a transaction
|
||||||
Flags: signFlags,
|
signing context) for the given address using the given wallet. This command can
|
||||||
|
output the resulting JSON (with additional signature added) right to the console
|
||||||
|
(if no file.out and no RPC endpoint specified) or into a file (which can be the
|
||||||
|
same as input one). If an RPC endpoint is given it'll also try to construct a
|
||||||
|
complete transaction and send it via RPC (printing its hash if everything is OK).
|
||||||
|
`,
|
||||||
|
Action: signStoredTransaction,
|
||||||
|
Flags: signFlags,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "strip-keys",
|
||||||
|
Usage: "remove private keys for all accounts",
|
||||||
|
UsageText: "neo-go wallet strip-keys -w wallet [--wallet-config path] [--force]",
|
||||||
|
Description: `Removes private keys for all accounts from the given wallet. Notice,
|
||||||
|
this is a very dangerous action (you can lose keys if you don't have a wallet
|
||||||
|
backup) that should not be performed unless you know what you're doing. It's
|
||||||
|
mostly useful for creation of special wallets that can be used to create
|
||||||
|
transactions, but can't be used to sign them (offline signing).
|
||||||
|
`,
|
||||||
|
Action: stripKeys,
|
||||||
|
Flags: []cli.Flag{
|
||||||
|
walletPathFlag,
|
||||||
|
walletConfigFlag,
|
||||||
|
forceFlag,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "nep17",
|
Name: "nep17",
|
||||||
|
@ -769,6 +793,29 @@ func dumpKeys(ctx *cli.Context) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func stripKeys(ctx *cli.Context) error {
|
||||||
|
if err := cmdargs.EnsureNone(ctx); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
wall, _, err := readWallet(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return cli.NewExitError(err, 1)
|
||||||
|
}
|
||||||
|
if !ctx.Bool("force") {
|
||||||
|
fmt.Fprintln(ctx.App.Writer, "All private keys for all accounts will be removed from the wallet. This action is irreversible.")
|
||||||
|
if ok := askForConsent(ctx.App.Writer); !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, a := range wall.Accounts {
|
||||||
|
a.EncryptedWIF = ""
|
||||||
|
}
|
||||||
|
if err := wall.Save(); err != nil {
|
||||||
|
return cli.NewExitError(fmt.Errorf("error while saving wallet: %w", err), 1)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func createWallet(ctx *cli.Context) error {
|
func createWallet(ctx *cli.Context) error {
|
||||||
if err := cmdargs.EnsureNone(ctx); err != nil {
|
if err := cmdargs.EnsureNone(ctx); err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
|
@ -630,6 +630,128 @@ func TestWalletImportDeployed(t *testing.T) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestStripKeys(t *testing.T) {
|
||||||
|
e := newExecutor(t, true)
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
walletPath := filepath.Join(tmpDir, "wallet.json")
|
||||||
|
e.In.WriteString("acc1\r")
|
||||||
|
e.In.WriteString("pass\r")
|
||||||
|
e.In.WriteString("pass\r")
|
||||||
|
e.Run(t, "neo-go", "wallet", "init", "--wallet", walletPath, "--account")
|
||||||
|
w1, err := wallet.NewWalletFromFile(walletPath)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
e.RunWithError(t, "neo-go", "wallet", "strip-keys", "--wallet", walletPath, "something")
|
||||||
|
e.RunWithError(t, "neo-go", "wallet", "strip-keys", "--wallet", walletPath+".bad")
|
||||||
|
|
||||||
|
e.In.WriteString("no")
|
||||||
|
e.Run(t, "neo-go", "wallet", "strip-keys", "--wallet", walletPath)
|
||||||
|
w2, err := wallet.NewWalletFromFile(walletPath)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, w1, w2)
|
||||||
|
|
||||||
|
e.In.WriteString("y\r")
|
||||||
|
e.Run(t, "neo-go", "wallet", "strip-keys", "--wallet", walletPath)
|
||||||
|
e.Run(t, "neo-go", "wallet", "strip-keys", "--wallet", walletPath, "--force") // Does nothing effectively, but tests the force flag.
|
||||||
|
w3, err := wallet.NewWalletFromFile(walletPath)
|
||||||
|
require.NoError(t, err)
|
||||||
|
for _, a := range w3.Accounts {
|
||||||
|
require.Equal(t, "", a.EncryptedWIF)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestOfflineSigning(t *testing.T) {
|
||||||
|
e := newExecutor(t, true)
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
walletPath := filepath.Join(tmpDir, "wallet.json")
|
||||||
|
txPath := filepath.Join(tmpDir, "tx.json")
|
||||||
|
|
||||||
|
// Copy wallet.
|
||||||
|
w, err := wallet.NewWalletFromFile(validatorWallet)
|
||||||
|
require.NoError(t, err)
|
||||||
|
jOut, err := w.JSON()
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NoError(t, os.WriteFile(walletPath, jOut, 0644))
|
||||||
|
|
||||||
|
// And remove keys from it.
|
||||||
|
e.Run(t, "neo-go", "wallet", "strip-keys", "--wallet", walletPath, "--force")
|
||||||
|
|
||||||
|
t.Run("1/1 multisig", func(t *testing.T) {
|
||||||
|
args := []string{"neo-go", "wallet", "nep17", "transfer",
|
||||||
|
"--rpc-endpoint", "http://" + e.RPC.Addr,
|
||||||
|
"--wallet", walletPath,
|
||||||
|
"--from", validatorAddr,
|
||||||
|
"--to", w.Accounts[0].Address,
|
||||||
|
"--token", "NEO",
|
||||||
|
"--amount", "1",
|
||||||
|
"--force",
|
||||||
|
}
|
||||||
|
// walletPath has no keys, so this can't be sent.
|
||||||
|
e.RunWithError(t, args...)
|
||||||
|
// But can be saved.
|
||||||
|
e.Run(t, append(args, "--out", txPath)...)
|
||||||
|
// It can't be signed with the original wallet.
|
||||||
|
e.RunWithError(t, "neo-go", "wallet", "sign",
|
||||||
|
"--wallet", walletPath, "--address", validatorAddr,
|
||||||
|
"--in", txPath, "--out", txPath)
|
||||||
|
t.Run("sendtx", func(t *testing.T) {
|
||||||
|
// And it can't be sent.
|
||||||
|
e.RunWithError(t, "neo-go", "util", "sendtx",
|
||||||
|
"--rpc-endpoint", "http://"+e.RPC.Addr,
|
||||||
|
txPath)
|
||||||
|
// Even with too many arguments.
|
||||||
|
e.RunWithError(t, "neo-go", "util", "sendtx",
|
||||||
|
"--rpc-endpoint", "http://"+e.RPC.Addr,
|
||||||
|
txPath, txPath)
|
||||||
|
// Or no arguments at all.
|
||||||
|
e.RunWithError(t, "neo-go", "util", "sendtx",
|
||||||
|
"--rpc-endpoint", "http://"+e.RPC.Addr)
|
||||||
|
})
|
||||||
|
// But it can be signed with a proper wallet.
|
||||||
|
e.In.WriteString("one\r")
|
||||||
|
e.Run(t, "neo-go", "wallet", "sign",
|
||||||
|
"--wallet", validatorWallet, "--address", validatorAddr,
|
||||||
|
"--in", txPath, "--out", txPath)
|
||||||
|
// And then anyone can send (even via wallet sign).
|
||||||
|
e.Run(t, "neo-go", "wallet", "sign",
|
||||||
|
"--rpc-endpoint", "http://"+e.RPC.Addr,
|
||||||
|
"--wallet", walletPath, "--address", validatorAddr,
|
||||||
|
"--in", txPath)
|
||||||
|
})
|
||||||
|
e.checkTxPersisted(t)
|
||||||
|
t.Run("simple signature", func(t *testing.T) {
|
||||||
|
simpleAddr := w.Accounts[0].Address
|
||||||
|
args := []string{"neo-go", "wallet", "nep17", "transfer",
|
||||||
|
"--rpc-endpoint", "http://" + e.RPC.Addr,
|
||||||
|
"--wallet", walletPath,
|
||||||
|
"--from", simpleAddr,
|
||||||
|
"--to", validatorAddr,
|
||||||
|
"--token", "NEO",
|
||||||
|
"--amount", "1",
|
||||||
|
"--force",
|
||||||
|
}
|
||||||
|
// walletPath has no keys, so this can't be sent.
|
||||||
|
e.RunWithError(t, args...)
|
||||||
|
// But can be saved.
|
||||||
|
e.Run(t, append(args, "--out", txPath)...)
|
||||||
|
// It can't be signed with the original wallet.
|
||||||
|
e.RunWithError(t, "neo-go", "wallet", "sign",
|
||||||
|
"--wallet", walletPath, "--address", simpleAddr,
|
||||||
|
"--in", txPath, "--out", txPath)
|
||||||
|
// But can be with a proper one.
|
||||||
|
e.In.WriteString("one\r")
|
||||||
|
e.Run(t, "neo-go", "wallet", "sign",
|
||||||
|
"--wallet", validatorWallet, "--address", simpleAddr,
|
||||||
|
"--in", txPath, "--out", txPath)
|
||||||
|
// Sending without an RPC node is not likely to succeed.
|
||||||
|
e.RunWithError(t, "neo-go", "util", "sendtx", txPath)
|
||||||
|
// But it requires no wallet at all.
|
||||||
|
e.Run(t, "neo-go", "util", "sendtx",
|
||||||
|
"--rpc-endpoint", "http://"+e.RPC.Addr,
|
||||||
|
txPath)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func TestWalletDump(t *testing.T) {
|
func TestWalletDump(t *testing.T) {
|
||||||
e := newExecutor(t, false)
|
e := newExecutor(t, false)
|
||||||
|
|
||||||
|
|
143
docs/cli.md
143
docs/cli.md
|
@ -272,6 +272,11 @@ transactions for this multisignature account with the imported key.
|
||||||
contracts. They also can have WIF keys associated with them (in case your
|
contracts. They also can have WIF keys associated with them (in case your
|
||||||
contract's `verify` method needs some signature).
|
contract's `verify` method needs some signature).
|
||||||
|
|
||||||
|
#### Strip keys from accounts
|
||||||
|
`wallet strip-keys` allows you to remove private keys from the wallet, but let
|
||||||
|
it be used for other purposes (like creating transactions for subsequent
|
||||||
|
offline signing). Use with care, don't lose your keys with it.
|
||||||
|
|
||||||
### Neo voting
|
### Neo voting
|
||||||
`wallet candidate` provides commands to register or unregister a committee
|
`wallet candidate` provides commands to register or unregister a committee
|
||||||
(and therefore validator) candidate key:
|
(and therefore validator) candidate key:
|
||||||
|
@ -377,6 +382,67 @@ $ ./bin/neo-go query voter -r http://localhost:20332 Nj91C8TxQSxW1jCE1ytFre6mg5q
|
||||||
Block: 3970
|
Block: 3970
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Transaction signing
|
||||||
|
|
||||||
|
`wallet sign` command allows to sign arbitary transactions stored in JSON
|
||||||
|
format (also known as ContractParametersContext). Usually it's used in one of
|
||||||
|
the two cases: multisignature signing (when you don't have all keys for an
|
||||||
|
account and need to share the context with others until enough signatures
|
||||||
|
collected) or offline signing (when the node with a key is completely offline
|
||||||
|
and can't interact with the RPC node directly).
|
||||||
|
|
||||||
|
#### Multisignature collection
|
||||||
|
|
||||||
|
For example, you have a four-node default network setup and want to set some
|
||||||
|
key for the oracle role, you create transaction with:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ neo-go contract invokefunction -w .docker/wallets/wallet1.json --out some.part.json -a NVTiAjNgagDkTr5HTzDmQP9kPwPHN5BgVq -r http://localhost:30333 0x49cf4e5378ffcd4dec034fd98a174c5491e395e2 designateAsRole 8 \[ 02b3622bf4017bdfe317c58aed5f4c753f206b7db896046fa7d774bbc4bf7f8dc2 \] -- NVTiAjNgagDkTr5HTzDmQP9kPwPHN5BgVq:CalledByEntry
|
||||||
|
```
|
||||||
|
|
||||||
|
And then sign it with two more keys:
|
||||||
|
```
|
||||||
|
$ neo-go wallet sign -w .docker/wallets/wallet2.json --in some.part.json --out some.part.json -a NVTiAjNgagDkTr5HTzDmQP9kPwPHN5BgVq
|
||||||
|
$ neo-go wallet sign -w .docker/wallets/wallet3.json --in some.part.json -r http://localhost:30333 -a NVTiAjNgagDkTr5HTzDmQP9kPwPHN5BgVq
|
||||||
|
```
|
||||||
|
Notice that the last command sends the transaction (which has a complete set
|
||||||
|
of singatures for 3/4 multisignature account by that time) to the network.
|
||||||
|
|
||||||
|
#### Offline signing
|
||||||
|
|
||||||
|
You want to do a transfer from a single-key account, but the key is on a
|
||||||
|
different (offline) machine. Create a stripped wallet first on the key-holding
|
||||||
|
machine:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ cp wallet.json wallet.stripped.json # don't lose the original wallet
|
||||||
|
$ neo-go wallet strip-keys --wallet wallet.stripped.json
|
||||||
|
```
|
||||||
|
|
||||||
|
This wallet has no keys inside (but has appropriate scripts/addresses), so it
|
||||||
|
can be safely shared with anyone or transferred to network-enabled machine
|
||||||
|
where you then can create a transfer transaction:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ neo-go wallet nep17 transfer --rpc-endpoint http://localhost:20332 \
|
||||||
|
--wallet wallet.stripped.json --from NjEQfanGEXihz85eTnacQuhqhNnA6LxpLp \
|
||||||
|
--to Nj91C8TxQSxW1jCE1ytFre6mg5qxTypg1Y --token NEO --amount 1 --out context.json
|
||||||
|
|
||||||
|
```
|
||||||
|
`context.json` can now be transferred to the machine with the `wallet.json`
|
||||||
|
containing proper keys and signed:
|
||||||
|
```
|
||||||
|
$ neo-go wallet sign --wallet wallet.json \
|
||||||
|
-address NjEQfanGEXihz85eTnacQuhqhNnA6LxpLp --in context.json --out context.json
|
||||||
|
```
|
||||||
|
Now `context.json` contains a transaction with a complete set of signatures
|
||||||
|
(just one in this case, but of course you can do multisignature collection as
|
||||||
|
well). It can be transferred to network-enabled machine again and the
|
||||||
|
transaction can be sent to the network:
|
||||||
|
```
|
||||||
|
$ neo-go util sendtx --rpc-endpoint http://localhost:20332 context.json
|
||||||
|
```
|
||||||
|
|
||||||
### NEP-17 token functions
|
### NEP-17 token functions
|
||||||
|
|
||||||
`wallet nep17` contains a set of commands to use for NEP-17 tokens.
|
`wallet nep17` contains a set of commands to use for NEP-17 tokens.
|
||||||
|
@ -519,7 +585,9 @@ If NEP-11 token supports optional `tokens` method, specify token hash via
|
||||||
./bin/neo-go wallet nep11 tokens -r http://localhost:20332 --token 67ecb7766dba4acf7c877392207984d1b4d15731
|
./bin/neo-go wallet nep11 tokens -r http://localhost:20332 --token 67ecb7766dba4acf7c877392207984d1b4d15731
|
||||||
```
|
```
|
||||||
|
|
||||||
## Conversion utility
|
## Utility commands
|
||||||
|
|
||||||
|
### Value conversion
|
||||||
|
|
||||||
NeoGo provides conversion utility command to reverse data, convert script
|
NeoGo provides conversion utility command to reverse data, convert script
|
||||||
hashes to/from address, convert public keys to hashes/addresses, convert data to/from hexadecimal or base64
|
hashes to/from address, convert public keys to hashes/addresses, convert data to/from hexadecimal or base64
|
||||||
|
@ -538,6 +606,79 @@ String to Hex 6465656537396331383966333030393862306261
|
||||||
String to Base64 ZGVlZTc5YzE4OWYzMDA5OGIwYmE2YTJlYjkwYjNhOTI1OGE2YzdmZg==
|
String to Base64 ZGVlZTc5YzE4OWYzMDA5OGIwYmE2YTJlYjkwYjNhOTI1OGE2YzdmZg==
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Transaction dumps/test invocations
|
||||||
|
|
||||||
|
If you have a transaction signing context saved in a file (and many commands
|
||||||
|
like `wallet nep17 transfer` or `contract invokefunction` can give you one
|
||||||
|
with the `--out` parameter) you may want to check the contents before signing
|
||||||
|
it. This can be done with the `util txdump` command:
|
||||||
|
```
|
||||||
|
$ ./bin/neo-go util txdump -r http://localhost:30333 some.part.json
|
||||||
|
Hash: f143059e0c03546db006608e0a0ad4b621b311a48d7fc62bb7062e405ab8e588
|
||||||
|
OnChain: false
|
||||||
|
ValidUntil: 6004
|
||||||
|
Signer: NVTiAjNgagDkTr5HTzDmQP9kPwPHN5BgVq (CalledByEntry)
|
||||||
|
SystemFee: 0.0208983 GAS
|
||||||
|
NetworkFee: 0.044159 GAS
|
||||||
|
Script: DCECs2Ir9AF73+MXxYrtX0x1PyBrfbiWBG+n13S7xL9/jcIRwBgSwB8MD2Rlc2lnbmF0ZUFzUm9sZQwU4pXjkVRMF4rZTwPsTc3/eFNOz0lBYn1bUg==
|
||||||
|
INDEX OPCODE PARAMETER
|
||||||
|
0 PUSHDATA1 02b3622bf4017bdfe317c58aed5f4c753f206b7db896046fa7d774bbc4bf7f8dc2 <<
|
||||||
|
35 PUSH1
|
||||||
|
36 PACK
|
||||||
|
37 PUSH8
|
||||||
|
38 PUSH2
|
||||||
|
39 PACK
|
||||||
|
40 PUSH15
|
||||||
|
41 PUSHDATA1 64657369676e6174654173526f6c65 ("designateAsRole")
|
||||||
|
58 PUSHDATA1 e295e391544c178ad94f03ec4dcdff78534ecf49
|
||||||
|
80 SYSCALL System.Contract.Call (627d5b52)
|
||||||
|
{
|
||||||
|
"state": "HALT",
|
||||||
|
"gasconsumed": "2089830",
|
||||||
|
"script": "DCECs2Ir9AF73+MXxYrtX0x1PyBrfbiWBG+n13S7xL9/jcIRwBgSwB8MD2Rlc2lnbmF0ZUFzUm9sZQwU4pXjkVRMF4rZTwPsTc3/eFNOz0lBYn1bUg==",
|
||||||
|
"stack": [
|
||||||
|
{
|
||||||
|
"type": "Any"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"exception": null,
|
||||||
|
"notifications": [
|
||||||
|
{
|
||||||
|
"contract": "0x49cf4e5378ffcd4dec034fd98a174c5491e395e2",
|
||||||
|
"eventname": "Designation",
|
||||||
|
"state": {
|
||||||
|
"type": "Array",
|
||||||
|
"value": [
|
||||||
|
{
|
||||||
|
"type": "Integer",
|
||||||
|
"value": "8"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "Integer",
|
||||||
|
"value": "245"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
It always outputs the basic data and also can perform test-invocation if an
|
||||||
|
RPC endpoint is given to it.
|
||||||
|
|
||||||
|
### Sending signed transaction to the network
|
||||||
|
|
||||||
|
If you have a completely finished (with all signatures collected) transaction
|
||||||
|
signing context saved in a file you can send it to the network (without any
|
||||||
|
wallet) using `util sendtx` command:
|
||||||
|
```
|
||||||
|
$ ./bin/neo-go util sendtx -r http://localhost:30333 some.part.json
|
||||||
|
```
|
||||||
|
This is useful in offline signing scenario, where the signing party doesn't
|
||||||
|
have any network access, so you can make a signature there, transfer the file
|
||||||
|
to another machine that has network access and then push the transaction out
|
||||||
|
to the network.
|
||||||
|
|
||||||
## VM CLI
|
## VM CLI
|
||||||
There is a VM CLI that you can use to load/analyze/run/step through some code:
|
There is a VM CLI that you can use to load/analyze/run/step through some code:
|
||||||
|
|
||||||
|
|
|
@ -56,6 +56,24 @@ func NewParameterContext(typ string, network netmode.Magic, verif crypto.Verifia
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *ParameterContext) GetCompleteTransaction() (*transaction.Transaction, error) {
|
||||||
|
tx, ok := c.Verifiable.(*transaction.Transaction)
|
||||||
|
if !ok {
|
||||||
|
return nil, errors.New("verifiable item is not a transaction")
|
||||||
|
}
|
||||||
|
if len(tx.Scripts) > 0 {
|
||||||
|
tx.Scripts = tx.Scripts[:0]
|
||||||
|
}
|
||||||
|
for i := range tx.Signers {
|
||||||
|
w, err := c.GetWitness(tx.Signers[i].Account)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("can't create witness for signer #%d: %w", i, err)
|
||||||
|
}
|
||||||
|
tx.Scripts = append(tx.Scripts, *w)
|
||||||
|
}
|
||||||
|
return tx, nil
|
||||||
|
}
|
||||||
|
|
||||||
// GetWitness returns invocation and verification scripts for the specified contract.
|
// GetWitness returns invocation and verification scripts for the specified contract.
|
||||||
func (c *ParameterContext) GetWitness(h util.Uint160) (*transaction.Witness, error) {
|
func (c *ParameterContext) GetWitness(h util.Uint160) (*transaction.Witness, error) {
|
||||||
item, ok := c.Items[h]
|
item, ok := c.Items[h]
|
||||||
|
@ -65,9 +83,9 @@ func (c *ParameterContext) GetWitness(h util.Uint160) (*transaction.Witness, err
|
||||||
bw := io.NewBufBinWriter()
|
bw := io.NewBufBinWriter()
|
||||||
for i := range item.Parameters {
|
for i := range item.Parameters {
|
||||||
if item.Parameters[i].Type != smartcontract.SignatureType {
|
if item.Parameters[i].Type != smartcontract.SignatureType {
|
||||||
return nil, errors.New("only signature parameters are supported")
|
return nil, fmt.Errorf("unsupported %s parameter #%d", item.Parameters[i].Type.String(), i)
|
||||||
} else if item.Parameters[i].Value == nil {
|
} else if item.Parameters[i].Value == nil {
|
||||||
return nil, errors.New("nil parameter")
|
return nil, fmt.Errorf("no value for parameter #%d (not signed yet?)", i)
|
||||||
}
|
}
|
||||||
emit.Bytes(bw.BinWriter, item.Parameters[i].Value.([]byte))
|
emit.Bytes(bw.BinWriter, item.Parameters[i].Value.([]byte))
|
||||||
}
|
}
|
||||||
|
@ -96,14 +114,19 @@ func (c *ParameterContext) AddSignature(h util.Uint160, ctr *wallet.Contract, pu
|
||||||
return errors.New("public key is not present in script")
|
return errors.New("public key is not present in script")
|
||||||
}
|
}
|
||||||
item.AddSignature(pub, sig)
|
item.AddSignature(pub, sig)
|
||||||
if len(item.Signatures) == len(ctr.Parameters) {
|
if len(item.Signatures) >= len(ctr.Parameters) {
|
||||||
indexMap := map[string]int{}
|
indexMap := map[string]int{}
|
||||||
for i := range pubs {
|
for i := range pubs {
|
||||||
indexMap[hex.EncodeToString(pubs[i])] = i
|
indexMap[hex.EncodeToString(pubs[i])] = i
|
||||||
}
|
}
|
||||||
sigs := make([]sigWithIndex, 0, len(item.Signatures))
|
sigs := make([]sigWithIndex, len(item.Parameters))
|
||||||
|
var i int
|
||||||
for pub, sig := range item.Signatures {
|
for pub, sig := range item.Signatures {
|
||||||
sigs = append(sigs, sigWithIndex{index: indexMap[pub], sig: sig})
|
sigs[i] = sigWithIndex{index: indexMap[pub], sig: sig}
|
||||||
|
i++
|
||||||
|
if i == len(sigs) {
|
||||||
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
sort.Slice(sigs, func(i, j int) bool {
|
sort.Slice(sigs, func(i, j int) bool {
|
||||||
return sigs[i].index < sigs[j].index
|
return sigs[i].index < sigs[j].index
|
||||||
|
|
|
@ -19,11 +19,17 @@ import (
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type verifStub struct{}
|
||||||
|
|
||||||
|
func (v verifStub) Hash() util.Uint256 { return util.Uint256{1, 2, 3} }
|
||||||
|
func (v verifStub) EncodeHashableFields() ([]byte, error) { return []byte{1}, nil }
|
||||||
|
func (v verifStub) DecodeHashableFields([]byte) error { return nil }
|
||||||
|
|
||||||
func TestParameterContext_AddSignatureSimpleContract(t *testing.T) {
|
func TestParameterContext_AddSignatureSimpleContract(t *testing.T) {
|
||||||
tx := getContractTx()
|
|
||||||
priv, err := keys.NewPrivateKey()
|
priv, err := keys.NewPrivateKey()
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
pub := priv.PublicKey()
|
pub := priv.PublicKey()
|
||||||
|
tx := getContractTx(pub.GetScriptHash())
|
||||||
sig := priv.SignHashable(uint32(netmode.UnitTestNet), tx)
|
sig := priv.SignHashable(uint32(netmode.UnitTestNet), tx)
|
||||||
|
|
||||||
t.Run("invalid contract", func(t *testing.T) {
|
t.Run("invalid contract", func(t *testing.T) {
|
||||||
|
@ -75,9 +81,13 @@ func TestParameterContext_AddSignatureSimpleContract(t *testing.T) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestGetCompleteTransactionForNonTx(t *testing.T) {
|
||||||
|
c := NewParameterContext("Neo.Network.P2P.Payloads.Block", netmode.UnitTestNet, verifStub{})
|
||||||
|
_, err := c.GetCompleteTransaction()
|
||||||
|
require.Error(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
func TestParameterContext_AddSignatureMultisig(t *testing.T) {
|
func TestParameterContext_AddSignatureMultisig(t *testing.T) {
|
||||||
tx := getContractTx()
|
|
||||||
c := NewParameterContext("Neo.Network.P2P.Payloads.Transaction", netmode.UnitTestNet, tx)
|
|
||||||
privs, pubs := getPrivateKeys(t, 4)
|
privs, pubs := getPrivateKeys(t, 4)
|
||||||
pubsCopy := keys.PublicKeys(pubs).Copy()
|
pubsCopy := keys.PublicKeys(pubs).Copy()
|
||||||
script, err := smartcontract.CreateMultiSigRedeemScript(3, pubsCopy)
|
script, err := smartcontract.CreateMultiSigRedeemScript(3, pubsCopy)
|
||||||
|
@ -91,29 +101,60 @@ func TestParameterContext_AddSignatureMultisig(t *testing.T) {
|
||||||
newParam(smartcontract.SignatureType, "parameter2"),
|
newParam(smartcontract.SignatureType, "parameter2"),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
tx := getContractTx(ctr.ScriptHash())
|
||||||
|
c := NewParameterContext("Neo.Network.P2P.Payloads.Transaction", netmode.UnitTestNet, tx)
|
||||||
priv, err := keys.NewPrivateKey()
|
priv, err := keys.NewPrivateKey()
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
sig := priv.SignHashable(uint32(c.Network), tx)
|
sig := priv.SignHashable(uint32(c.Network), tx)
|
||||||
require.Error(t, c.AddSignature(ctr.ScriptHash(), ctr, priv.PublicKey(), sig))
|
require.Error(t, c.AddSignature(ctr.ScriptHash(), ctr, priv.PublicKey(), sig))
|
||||||
|
|
||||||
indices := []int{2, 3, 0} // random order
|
indices := []int{2, 3, 0, 1} // random order
|
||||||
for _, i := range indices {
|
testSigWit := func(t *testing.T, num int) {
|
||||||
sig := privs[i].SignHashable(uint32(c.Network), tx)
|
t.Run("GetCompleteTransaction, bad", func(t *testing.T) {
|
||||||
require.NoError(t, c.AddSignature(ctr.ScriptHash(), ctr, pubs[i], sig))
|
_, err := c.GetCompleteTransaction()
|
||||||
require.Error(t, c.AddSignature(ctr.ScriptHash(), ctr, pubs[i], sig))
|
require.Error(t, err)
|
||||||
|
})
|
||||||
|
for _, i := range indices[:num] {
|
||||||
|
sig := privs[i].SignHashable(uint32(c.Network), tx)
|
||||||
|
require.NoError(t, c.AddSignature(ctr.ScriptHash(), ctr, pubs[i], sig))
|
||||||
|
require.Error(t, c.AddSignature(ctr.ScriptHash(), ctr, pubs[i], sig))
|
||||||
|
|
||||||
item := c.Items[ctr.ScriptHash()]
|
item := c.Items[ctr.ScriptHash()]
|
||||||
require.NotNil(t, item)
|
require.NotNil(t, item)
|
||||||
require.Equal(t, sig, item.GetSignature(pubs[i]))
|
require.Equal(t, sig, item.GetSignature(pubs[i]))
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("GetWitness", func(t *testing.T) {
|
||||||
|
w, err := c.GetWitness(ctr.ScriptHash())
|
||||||
|
require.NoError(t, err)
|
||||||
|
v := newTestVM(w, tx)
|
||||||
|
require.NoError(t, v.Run())
|
||||||
|
require.Equal(t, 1, v.Estack().Len())
|
||||||
|
require.Equal(t, true, v.Estack().Pop().Value())
|
||||||
|
})
|
||||||
|
t.Run("GetCompleteTransaction, good", func(t *testing.T) {
|
||||||
|
tx, err := c.GetCompleteTransaction()
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, 1, len(tx.Scripts))
|
||||||
|
scripts1 := make([]transaction.Witness, len(tx.Scripts))
|
||||||
|
copy(scripts1, tx.Scripts)
|
||||||
|
// Doing it twice shouldn't be a problem.
|
||||||
|
tx, err = c.GetCompleteTransaction()
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, scripts1, tx.Scripts)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
t.Run("exact number of sigs", func(t *testing.T) {
|
||||||
t.Run("GetWitness", func(t *testing.T) {
|
testSigWit(t, 3)
|
||||||
w, err := c.GetWitness(ctr.ScriptHash())
|
})
|
||||||
require.NoError(t, err)
|
t.Run("larger number of sigs", func(t *testing.T) {
|
||||||
v := newTestVM(w, tx)
|
// Clean up.
|
||||||
require.NoError(t, v.Run())
|
var itm = c.Items[ctr.ScriptHash()]
|
||||||
require.Equal(t, 1, v.Estack().Len())
|
for i := range itm.Parameters {
|
||||||
require.Equal(t, true, v.Estack().Pop().Value())
|
itm.Parameters[i].Value = nil
|
||||||
|
}
|
||||||
|
itm.Signatures = make(map[string][]byte)
|
||||||
|
testSigWit(t, 4)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -129,7 +170,7 @@ func TestParameterContext_MarshalJSON(t *testing.T) {
|
||||||
priv, err := keys.NewPrivateKey()
|
priv, err := keys.NewPrivateKey()
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
tx := getContractTx()
|
tx := getContractTx(priv.GetScriptHash())
|
||||||
sign := priv.SignHashable(uint32(netmode.UnitTestNet), tx)
|
sign := priv.SignHashable(uint32(netmode.UnitTestNet), tx)
|
||||||
|
|
||||||
expected := &ParameterContext{
|
expected := &ParameterContext{
|
||||||
|
@ -218,11 +259,11 @@ func newParam(typ smartcontract.ParamType, name string) wallet.ContractParam {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func getContractTx() *transaction.Transaction {
|
func getContractTx(signer util.Uint160) *transaction.Transaction {
|
||||||
tx := transaction.New([]byte{byte(opcode.PUSH1)}, 0)
|
tx := transaction.New([]byte{byte(opcode.PUSH1)}, 0)
|
||||||
tx.Attributes = make([]transaction.Attribute, 0)
|
tx.Attributes = make([]transaction.Attribute, 0)
|
||||||
tx.Scripts = make([]transaction.Witness, 0)
|
tx.Scripts = make([]transaction.Witness, 0)
|
||||||
tx.Signers = []transaction.Signer{{Account: util.Uint160{1, 2, 3}}}
|
tx.Signers = []transaction.Signer{{Account: signer}}
|
||||||
tx.Hash()
|
tx.Hash()
|
||||||
return tx
|
return tx
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue