diff --git a/cli/multisig_test.go b/cli/multisig_test.go
index e189f1518..4919f1e96 100644
--- a/cli/multisig_test.go
+++ b/cli/multisig_test.go
@@ -2,6 +2,8 @@ package main
 
 import (
 	"encoding/hex"
+	"encoding/json"
+	"fmt"
 	"math/big"
 	"os"
 	"path"
@@ -10,7 +12,9 @@ import (
 	"github.com/nspcc-dev/neo-go/pkg/crypto/hash"
 	"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
 	"github.com/nspcc-dev/neo-go/pkg/encoding/address"
+	"github.com/nspcc-dev/neo-go/pkg/rpc/response/result"
 	"github.com/nspcc-dev/neo-go/pkg/smartcontract"
+	"github.com/nspcc-dev/neo-go/pkg/vm"
 	"github.com/stretchr/testify/require"
 )
 
@@ -92,6 +96,27 @@ func TestSignMultisigTx(t *testing.T) {
 		"--wallet", wallet2Path,
 		"--in", txPath, "--out", txPath)
 
+	t.Run("test invoke", func(t *testing.T) {
+		t.Run("missing file", func(t *testing.T) {
+			e.RunWithError(t, "neo-go", "util", "txdump")
+			fmt.Println(e.Out.String())
+		})
+
+		t.Run("no invoke", func(t *testing.T) {
+			e.Run(t, "neo-go", "util", "txdump", txPath)
+			e.checkTxTestInvokeOutput(t, 11)
+			e.checkEOF(t)
+		})
+
+		e.Run(t, "neo-go", "util", "txdump",
+			"--rpc-endpoint", "http://"+e.RPC.Addr,
+			txPath)
+		e.checkTxTestInvokeOutput(t, 11)
+		res := new(result.Invoke)
+		require.NoError(t, json.Unmarshal(e.Out.Bytes(), res))
+		require.Equal(t, vm.HaltState.String(), res.State, res.FaultException)
+	})
+
 	e.In.WriteString("pass\r")
 	e.Run(t, "neo-go", "wallet", "sign",
 		"--rpc-endpoint", "http://"+e.RPC.Addr,
@@ -153,3 +178,21 @@ func TestSignMultisigTx(t *testing.T) {
 		require.Equal(t, big.NewInt(2), b)
 	})
 }
+
+func (e *executor) checkTxTestInvokeOutput(t *testing.T, scriptSize int) {
+	e.checkNextLine(t, `Hash:\s+`)
+	e.checkNextLine(t, `OnChain:\s+false`)
+	e.checkNextLine(t, `ValidUntil:\s+\d+`)
+	e.checkNextLine(t, `Signer:\s+\w+`)
+	e.checkNextLine(t, `SystemFee:\s+(\d|\.)+`)
+	e.checkNextLine(t, `NetworkFee:\s+(\d|\.)+`)
+	e.checkNextLine(t, `Script:\s+\w+`)
+	e.checkScriptDump(t, scriptSize)
+}
+
+func (e *executor) checkScriptDump(t *testing.T, scriptSize int) {
+	e.checkNextLine(t, `INDEX\s+`)
+	for i := 0; i < scriptSize; i++ {
+		e.checkNextLine(t, `\d+\s+\w+`)
+	}
+}
diff --git a/cli/query/query.go b/cli/query/query.go
index 8d15e121e..8561c2f17 100644
--- a/cli/query/query.go
+++ b/cli/query/query.go
@@ -14,6 +14,7 @@ import (
 	"github.com/nspcc-dev/neo-go/cli/options"
 	"github.com/nspcc-dev/neo-go/pkg/core/native/nativenames"
 	"github.com/nspcc-dev/neo-go/pkg/core/state"
+	"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/encoding/fixedn"
 	"github.com/nspcc-dev/neo-go/pkg/rpc/response/result"
@@ -102,12 +103,16 @@ func queryTx(ctx *cli.Context) error {
 		}
 	}
 
-	dumpApplicationLog(ctx, res, txOut)
+	DumpApplicationLog(ctx, res, &txOut.Transaction, &txOut.TransactionMetadata, ctx.Bool("verbose"))
 	return nil
 }
 
-func dumpApplicationLog(ctx *cli.Context, res *result.ApplicationLog, tx *result.TransactionOutputRaw) {
-	verbose := ctx.Bool("verbose")
+func DumpApplicationLog(
+	ctx *cli.Context,
+	res *result.ApplicationLog,
+	tx *transaction.Transaction,
+	txMeta *result.TransactionMetadata,
+	verbose bool) {
 	buf := bytes.NewBuffer(nil)
 
 	// Ignore the errors below because `Write` to buffer doesn't return error.
@@ -117,7 +122,9 @@ func dumpApplicationLog(ctx *cli.Context, res *result.ApplicationLog, tx *result
 	if res == nil {
 		_, _ = tw.Write([]byte("ValidUntil:\t" + strconv.FormatUint(uint64(tx.ValidUntilBlock), 10) + "\n"))
 	} else {
-		_, _ = tw.Write([]byte("BlockHash:\t" + tx.Blockhash.StringLE() + "\n"))
+		if txMeta != nil {
+			_, _ = tw.Write([]byte("BlockHash:\t" + txMeta.Blockhash.StringLE() + "\n"))
+		}
 		if len(res.Executions) != 1 {
 			_, _ = tw.Write([]byte("Success:\tunknown (no execution data)\n"))
 		} else {
@@ -133,6 +140,9 @@ func dumpApplicationLog(ctx *cli.Context, res *result.ApplicationLog, tx *result
 		_, _ = tw.Write([]byte("SystemFee:\t" + fixedn.Fixed8(tx.SystemFee).String() + " GAS\n"))
 		_, _ = tw.Write([]byte("NetworkFee:\t" + fixedn.Fixed8(tx.NetworkFee).String() + " GAS\n"))
 		_, _ = tw.Write([]byte("Script:\t" + base64.StdEncoding.EncodeToString(tx.Script) + "\n"))
+		v := vm.New()
+		v.Load(tx.Script)
+		v.PrintOps(tw)
 		if res != nil {
 			for _, e := range res.Executions {
 				if e.VMState != vm.HaltState {
diff --git a/cli/query_test.go b/cli/query_test.go
index f0fc761fb..32c658939 100644
--- a/cli/query_test.go
+++ b/cli/query_test.go
@@ -125,6 +125,13 @@ func (e *executor) compareQueryTxVerbose(t *testing.T, tx *transaction.Transacti
 	e.checkNextLine(t, `SystemFee:\s+`+fixedn.Fixed8(tx.SystemFee).String()+" GAS$")
 	e.checkNextLine(t, `NetworkFee:\s+`+fixedn.Fixed8(tx.NetworkFee).String()+" GAS$")
 	e.checkNextLine(t, `Script:\s+`+regexp.QuoteMeta(base64.StdEncoding.EncodeToString(tx.Script)))
+	c := vm.NewContext(tx.Script)
+	n := 0
+	for ; c.NextIP() < c.LenInstr(); _, _, err = c.Next() {
+		require.NoError(t, err)
+		n++
+	}
+	e.checkScriptDump(t, n)
 
 	if res[0].Execution.VMState != vm.HaltState {
 		e.checkNextLine(t, `Exception:\s+`+regexp.QuoteMeta(res[0].Execution.FaultException))
diff --git a/cli/util/convert.go b/cli/util/convert.go
index aecc98b43..7fbd7a5bc 100644
--- a/cli/util/convert.go
+++ b/cli/util/convert.go
@@ -3,12 +3,14 @@ package util
 import (
 	"fmt"
 
+	"github.com/nspcc-dev/neo-go/cli/options"
 	vmcli "github.com/nspcc-dev/neo-go/pkg/vm/cli"
 	"github.com/urfave/cli"
 )
 
 // NewCommands returns util commands for neo-go CLI.
 func NewCommands() []cli.Command {
+	txDumpFlags := append([]cli.Flag{}, options.RPC...)
 	return []cli.Command{
 		{
 			Name:  "util",
@@ -23,6 +25,13 @@ func NewCommands() []cli.Command {
         and converted to other formats. Strings are escaped and output in quotes.`,
 					Action: handleParse,
 				},
+				{
+					Name:      "txdump",
+					Usage:     "Dump transaction stored in file",
+					UsageText: "txdump [-r <endpoint>] <file.in>",
+					Action:    txDump,
+					Flags:     txDumpFlags,
+				},
 			},
 		},
 	}
diff --git a/cli/util/dump.go b/cli/util/dump.go
new file mode 100644
index 000000000..427bfb732
--- /dev/null
+++ b/cli/util/dump.go
@@ -0,0 +1,51 @@
+package util
+
+import (
+	"encoding/json"
+	"fmt"
+
+	"github.com/nspcc-dev/neo-go/cli/options"
+	"github.com/nspcc-dev/neo-go/cli/paramcontext"
+	"github.com/nspcc-dev/neo-go/cli/query"
+	"github.com/nspcc-dev/neo-go/pkg/core/transaction"
+	"github.com/urfave/cli"
+)
+
+func txDump(ctx *cli.Context) error {
+	if len(ctx.Args()) == 0 {
+		return cli.NewExitError("missing input file", 1)
+	}
+
+	c, err := paramcontext.Read(ctx.Args()[0])
+	if err != nil {
+		return cli.NewExitError(err, 1)
+	}
+
+	tx, ok := c.Verifiable.(*transaction.Transaction)
+	if !ok {
+		return cli.NewExitError("verifiable item is not a transaction", 1)
+	}
+
+	query.DumpApplicationLog(ctx, nil, tx, nil, true)
+
+	if ctx.String(options.RPCEndpointFlag) != "" {
+		gctx, cancel := options.GetTimeoutContext(ctx)
+		defer cancel()
+
+		var err error
+		cl, err := options.GetRPCClient(gctx, ctx)
+		if err != nil {
+			return cli.NewExitError(err, 1)
+		}
+		res, err := cl.InvokeScript(tx.Script, tx.Signers)
+		if err != nil {
+			return cli.NewExitError(err, 1)
+		}
+		resS, err := json.MarshalIndent(res, "", " ")
+		if err != nil {
+			return cli.NewExitError(err, 1)
+		}
+		fmt.Fprintln(ctx.App.Writer, string(resS))
+	}
+	return nil
+}