Merge pull request #2668 from nspcc-dev/cli-signing-improvements

CLI signing improvements
This commit is contained in:
Roman Khimov 2022-09-01 15:48:39 +03:00 committed by GitHub
commit 20224cb39c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 562 additions and 85 deletions

View file

@ -14,6 +14,7 @@ import (
"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/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/vm/vmstate"
"github.com/stretchr/testify/require"
@ -162,13 +163,41 @@ func TestSignMultisigTx(t *testing.T) {
require.Equal(t, vmstate.Halt.String(), res.State, res.FaultException)
})
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("console output", func(t *testing.T) {
oldIn, err := os.ReadFile(txPath)
require.NoError(t, err)
e.In.WriteString("pass\r")
e.Run(t, "neo-go", "wallet", "sign",
"--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) {
e.In.WriteString("pass\r")
e.RunWithError(t, "neo-go", "wallet", "sign",

View file

@ -13,18 +13,21 @@ import (
)
// 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 {
priv := acc.PrivateKey()
pub := priv.PublicKey()
sign := priv.SignHashable(uint32(net), tx)
scCtx := context.NewParameterContext("Neo.Network.P2P.Payloads.Transaction", net, tx)
h, err := address.StringToUint160(acc.Address)
if err != nil {
return fmt.Errorf("invalid address: %s", acc.Address)
}
if err := scCtx.AddSignature(h, acc.Contract, pub, sign); err != nil {
return fmt.Errorf("can't add signature: %w", err)
if acc != nil && acc.CanSign() {
priv := acc.PrivateKey()
pub := priv.PublicKey()
sign := priv.SignHashable(uint32(net), tx)
h, err := address.StringToUint160(acc.Address)
if err != nil {
return fmt.Errorf("invalid address: %s", acc.Address)
}
if err := scCtx.AddSignature(h, acc.Contract, pub, sign); err != nil {
return fmt.Errorf("can't add signature: %w", err)
}
}
return Save(scCtx, filename)
}

View file

@ -702,14 +702,11 @@ func invokeWithArgs(ctx *cli.Context, acc *wallet.Account, wall *wallet.Wallet,
return sender, cli.NewExitError(errText, 1)
}
action := "save"
process := "Saving"
action := "send"
process := "Sending"
if out != "" {
action += "and send"
process += "and sending"
} else {
action = "send"
process = "Sending"
action = "save"
process = "Saving"
}
if !ctx.Bool("force") {
return sender, cli.NewExitError(errText+".\nUse --force flag to "+action+" the transaction anyway.", 1)

View file

@ -25,6 +25,18 @@ func NewCommands() []cli.Command {
and converted to other formats. Strings are escaped and output in quotes.`,
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",
Usage: "Dump transaction stored in file",

42
cli/util/send.go Normal file
View 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
}

View file

@ -1,6 +1,7 @@
package wallet
import (
"encoding/json"
"fmt"
"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/paramcontext"
"github.com/nspcc-dev/neo-go/pkg/core/transaction"
"github.com/nspcc-dev/neo-go/pkg/encoding/address"
"github.com/urfave/cli"
)
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 {
return err
}
@ -21,28 +26,26 @@ func signStoredTransaction(ctx *cli.Context) error {
return cli.NewExitError(err, 1)
}
c, 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)
pc, err := paramcontext.Read(ctx.String("in"))
if err != nil {
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 {
return cli.NewExitError("verifiable item is not a transaction", 1)
}
ch, err := address.StringToUint160(acc.Address)
if err != nil {
return cli.NewExitError(fmt.Errorf("wallet contains invalid account: %s", acc.Address), 1)
}
signerFound := false
for i := range tx.Signers {
if tx.Signers[i].Account == ch {
@ -54,23 +57,33 @@ func signStoredTransaction(ctx *cli.Context) error {
return cli.NewExitError("tx signers don't contain provided account", 1)
}
priv := acc.PrivateKey()
sign := priv.SignHashable(uint32(c.Network), tx)
if err := c.AddSignature(ch, acc.Contract, priv.PublicKey(), sign); err != nil {
return cli.NewExitError(fmt.Errorf("can't add signature: %w", err), 1)
if acc.CanSign() {
priv := acc.PrivateKey()
sign := priv.SignHashable(uint32(pc.Network), pc.Verifiable)
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 != "" {
if err := paramcontext.Save(c, out); err != nil {
return cli.NewExitError(fmt.Errorf("failed to dump resulting transaction: %w", err), 1)
// Not saving and not sending, print.
if out == "" && rpcNode == "" {
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 {
for i := range tx.Signers {
w, err := c.GetWitness(tx.Signers[i].Account)
if err != nil {
return cli.NewExitError(fmt.Errorf("failed to construct witness for signer #%d: %w", i, err), 1)
}
tx.Scripts = append(tx.Scripts, *w)
if rpcNode != "" {
tx, err = pc.GetCompleteTransaction()
if err != nil {
return cli.NewExitError(fmt.Errorf("failed to complete transaction: %w", err), 1)
}
gctx, cancel := options.GetTimeoutContext(ctx)

View file

@ -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.
token, err = getTokenWithStandard(c, h, standard)
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)
}
}
}

View file

@ -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) {
acc := wall.GetAccount(addr)
if acc == nil {
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 {
pass, err := input.ReadPassword(EnterPasswordPrompt)
if err != nil {

View file

@ -281,9 +281,33 @@ func NewCommands() []cli.Command {
{
Name: "sign",
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>]",
Action: signStoredTransaction,
Flags: signFlags,
UsageText: "sign -w wallet [--wallet-config path] --address <address> --in <file.in> [--out <file.out>] [-r <endpoint>]",
Description: `Signs the given (in file.in) context (which must be a transaction
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",
@ -769,6 +793,29 @@ func dumpKeys(ctx *cli.Context) error {
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 {
if err := cmdargs.EnsureNone(ctx); err != nil {
return err

View file

@ -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) {
e := newExecutor(t, false)

View file

@ -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
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
`wallet candidate` provides commands to register or unregister a committee
(and therefore validator) candidate key:
@ -377,6 +382,67 @@ $ ./bin/neo-go query voter -r http://localhost:20332 Nj91C8TxQSxW1jCE1ytFre6mg5q
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
`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
```
## Conversion utility
## Utility commands
### Value conversion
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
@ -538,6 +606,79 @@ String to Hex 6465656537396331383966333030393862306261
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
There is a VM CLI that you can use to load/analyze/run/step through some code:

View file

@ -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.
func (c *ParameterContext) GetWitness(h util.Uint160) (*transaction.Witness, error) {
item, ok := c.Items[h]
@ -65,9 +83,9 @@ func (c *ParameterContext) GetWitness(h util.Uint160) (*transaction.Witness, err
bw := io.NewBufBinWriter()
for i := range item.Parameters {
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 {
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))
}
@ -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")
}
item.AddSignature(pub, sig)
if len(item.Signatures) == len(ctr.Parameters) {
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))
sigs := make([]sigWithIndex, len(item.Parameters))
var i int
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 {
return sigs[i].index < sigs[j].index

View file

@ -19,11 +19,17 @@ import (
"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) {
tx := getContractTx()
priv, err := keys.NewPrivateKey()
require.NoError(t, err)
pub := priv.PublicKey()
tx := getContractTx(pub.GetScriptHash())
sig := priv.SignHashable(uint32(netmode.UnitTestNet), tx)
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) {
tx := getContractTx()
c := NewParameterContext("Neo.Network.P2P.Payloads.Transaction", netmode.UnitTestNet, tx)
privs, pubs := getPrivateKeys(t, 4)
pubsCopy := keys.PublicKeys(pubs).Copy()
script, err := smartcontract.CreateMultiSigRedeemScript(3, pubsCopy)
@ -91,29 +101,60 @@ func TestParameterContext_AddSignatureMultisig(t *testing.T) {
newParam(smartcontract.SignatureType, "parameter2"),
},
}
tx := getContractTx(ctr.ScriptHash())
c := NewParameterContext("Neo.Network.P2P.Payloads.Transaction", netmode.UnitTestNet, tx)
priv, err := keys.NewPrivateKey()
require.NoError(t, err)
sig := priv.SignHashable(uint32(c.Network), tx)
require.Error(t, c.AddSignature(ctr.ScriptHash(), ctr, priv.PublicKey(), sig))
indices := []int{2, 3, 0} // random order
for _, i := range indices {
sig := privs[i].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))
indices := []int{2, 3, 0, 1} // random order
testSigWit := func(t *testing.T, num int) {
t.Run("GetCompleteTransaction, bad", func(t *testing.T) {
_, err := c.GetCompleteTransaction()
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()]
require.NotNil(t, item)
require.Equal(t, sig, item.GetSignature(pubs[i]))
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.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("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("exact number of sigs", func(t *testing.T) {
testSigWit(t, 3)
})
t.Run("larger number of sigs", func(t *testing.T) {
// Clean up.
var itm = c.Items[ctr.ScriptHash()]
for i := range itm.Parameters {
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()
require.NoError(t, err)
tx := getContractTx()
tx := getContractTx(priv.GetScriptHash())
sign := priv.SignHashable(uint32(netmode.UnitTestNet), tx)
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.Attributes = make([]transaction.Attribute, 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()
return tx
}