[#2] Print name of invoked method in transaction

Signed-off-by: AndrewDanilin <andnilin@gmail.com>
This commit is contained in:
AndrewDanilin 2023-11-22 00:29:02 +03:00 committed by Alex Vanin
parent 4fc7478e1e
commit aca2229ab7
6 changed files with 401 additions and 0 deletions

View file

@ -12,6 +12,7 @@ import (
"sync" "sync"
"time" "time"
"git.frostfs.info/TrueCloudLab/monza/internal/bytecode"
"git.frostfs.info/TrueCloudLab/monza/internal/chain" "git.frostfs.info/TrueCloudLab/monza/internal/chain"
"github.com/gdamore/tcell/v2" "github.com/gdamore/tcell/v2"
"github.com/nspcc-dev/neo-go/pkg/core/block" "github.com/nspcc-dev/neo-go/pkg/core/block"
@ -248,6 +249,13 @@ func (e *Explorer) Run() error {
} }
res += v res += v
} }
res += fmt.Sprintf("\nContract calls:\n")
calls, err := e.chain.Calls(txHash)
for _, call := range calls {
res += formatCall(call)
}
notifications.SetText(res) notifications.SetText(res)
notifications.ScrollToBeginning() notifications.ScrollToBeginning()
}) })
@ -346,6 +354,7 @@ func (e *Explorer) startWorkers(amount int) {
var err error var err error
if task.txHash != nil { if task.txHash != nil {
_, err = e.chain.ApplicationLog(*task.txHash) _, err = e.chain.ApplicationLog(*task.txHash)
_, err = e.chain.Calls(*task.txHash)
} else { } else {
_, err = e.chain.Block(task.index) _, err = e.chain.Block(task.index)
} }
@ -496,6 +505,10 @@ func formatNotification(event state.NotificationEvent) (string, error) {
return fmt.Sprintf("%s\n---\n%s\n\n", event.Name, formatted.String()), nil return fmt.Sprintf("%s\n---\n%s\n\n", event.Name, formatted.String()), nil
} }
func formatCall(syscallParams bytecode.SyscallParameters) string {
return fmt.Sprintf("Contract: %s; Method: %s\n", syscallParams.Contract, syscallParams.Method)
}
func setFocusColorStyle(target, focus *tview.Box) { func setFocusColorStyle(target, focus *tview.Box) {
focus.SetFocusFunc(func() { focus.SetFocusFunc(func() {
target.SetBorderColor(tcell.ColorGreen) target.SetBorderColor(tcell.ColorGreen)

3
go.mod
View file

@ -7,12 +7,14 @@ require (
github.com/nspcc-dev/neo-go v0.102.0 github.com/nspcc-dev/neo-go v0.102.0
github.com/rivo/tview v0.0.0-20220307222120-9994674d60a8 github.com/rivo/tview v0.0.0-20220307222120-9994674d60a8
github.com/schollz/progressbar/v3 v3.8.3 github.com/schollz/progressbar/v3 v3.8.3
github.com/stretchr/testify v1.8.1
github.com/urfave/cli/v2 v2.3.0 github.com/urfave/cli/v2 v2.3.0
go.etcd.io/bbolt v1.3.7 go.etcd.io/bbolt v1.3.7
) )
require ( require (
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d // indirect github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 // indirect
github.com/gdamore/encoding v1.0.0 // indirect github.com/gdamore/encoding v1.0.0 // indirect
github.com/google/uuid v1.3.0 // indirect github.com/google/uuid v1.3.0 // indirect
@ -25,6 +27,7 @@ require (
github.com/mr-tron/base58 v1.2.0 // indirect github.com/mr-tron/base58 v1.2.0 // indirect
github.com/nspcc-dev/go-ordered-json v0.0.0-20220111165707-25110be27d22 // indirect github.com/nspcc-dev/go-ordered-json v0.0.0-20220111165707-25110be27d22 // indirect
github.com/nspcc-dev/rfc6979 v0.2.0 // indirect github.com/nspcc-dev/rfc6979 v0.2.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rivo/uniseg v0.2.0 // indirect github.com/rivo/uniseg v0.2.0 // indirect
github.com/russross/blackfriday/v2 v2.0.1 // indirect github.com/russross/blackfriday/v2 v2.0.1 // indirect
github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect

5
go.sum
View file

@ -54,7 +54,11 @@ github.com/schollz/progressbar/v3 v3.8.3/go.mod h1:pWnVCjSBZsT2X3nx9HfRdnCDrpbev
github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/syndtr/goleveldb v1.0.1-0.20210305035536-64b5b1c73954 h1:xQdMZ1WLrgkkvOZ/LDQxjVxMLdby7osSh4ZEVa5sIjs= github.com/syndtr/goleveldb v1.0.1-0.20210305035536-64b5b1c73954 h1:xQdMZ1WLrgkkvOZ/LDQxjVxMLdby7osSh4ZEVa5sIjs=
@ -99,5 +103,6 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View file

@ -0,0 +1,146 @@
package bytecode
import (
"encoding/binary"
"encoding/hex"
"errors"
"fmt"
"github.com/nspcc-dev/neo-go/pkg/encoding/address"
"github.com/nspcc-dev/neo-go/pkg/util"
"github.com/nspcc-dev/neo-go/pkg/vm/opcode"
)
type SyscallParameters struct {
Contract string `json:"contract"`
Method string `json:"method"`
}
// ExtractCalls Extract parameters of SYSCALL opcode from bytecode of transaction
func ExtractCalls(script []byte) ([]SyscallParameters, error) {
syscallParams := make([]SyscallParameters, 0)
offsets := make([]int, 0)
for i := 0; i < len(script); {
offsets = append(offsets, i)
if opcode.Opcode(script[i]) == opcode.SYSCALL {
// Current opcode is SYSCALL
// here we parse two previous instruction,
// slice offsets contains positions of these two instructions
//
// PUSHDATA1/2/4 <Len in bytes of name> <Name of method>
// PUSHDATA1 <Len in bytes of address> <Address of contract>
// SYSCALL System.Contract.Call
offset1 := offsets[len(offsets)-3]
offset2 := offsets[len(offsets)-2]
offset1++
var n int
switch opcode.Opcode(script[offset1-1]) {
case opcode.PUSHDATA1:
n = parseInt(offset1, 1, script)
offset1 += 1
case opcode.PUSHDATA2:
n = parseInt(offset1, 2, script)
offset1 += 2
case opcode.PUSHDATA4:
n = parseInt(offset1, 4, script)
offset1 += 4
}
src := script[offset1 : offset1+n]
nameOfMethod, err := hex.DecodeString(hex.EncodeToString(src))
if err != nil {
return nil, fmt.Errorf("failed to convert bytes to UTF-8 string: %w", err)
}
offset2++
var contractAddress string
switch opcode.Opcode(script[offset2-1]) {
case opcode.PUSHDATA1:
n = parseInt(offset2, 1, script)
if n != 20 {
return nil, errors.New("invalid bytecode: address should be 20-byte value")
}
offset2++
u, err := util.Uint160DecodeBytesBE(script[offset2 : offset2+20])
if err != nil {
return nil, fmt.Errorf("failed to decode bytes BE to Uint160: %w", err)
}
contractAddress = address.Uint160ToString(u)
default:
return nil, errors.New("invalid bytecode: address should be 20-byte value")
}
syscallParams = append(syscallParams, SyscallParameters{
Contract: contractAddress,
Method: string(nameOfMethod),
})
}
// Skip n bytes (params of current opcode) to next opcode
err := skipBytes(&i, script)
if err != nil {
return nil, fmt.Errorf("%s%w", "error while parsing bytecode:", err)
}
}
return syscallParams, nil
}
func skipBytes(i *int, script []byte) error {
*i++
switch opcode.Opcode(script[*i-1]) {
case opcode.PUSHINT8:
*i += 1
case opcode.PUSHINT16:
*i += 2
case opcode.PUSHINT32:
*i += 4
case opcode.PUSHINT64:
*i += 8
case opcode.PUSHINT128:
*i += 16
case opcode.PUSHINT256:
*i += 32
case opcode.PUSHDATA1:
n := parseInt(*i, 1, script)
*i += n + 1
case opcode.PUSHDATA2:
n := parseInt(*i, 2, script)
*i += n + 2
case opcode.PUSHDATA4:
n := parseInt(*i, 4, script)
*i += n + 4
case opcode.JMP, opcode.JMPIF, opcode.JMPIFNOT, opcode.JMPEQ, opcode.JMPNE,
opcode.JMPGT, opcode.JMPGE, opcode.JMPLT, opcode.JMPLE,
opcode.CALL, opcode.ISTYPE, opcode.CONVERT, opcode.NEWARRAYT,
opcode.ENDTRY:
*i += 1
case opcode.INITSLOT, opcode.TRY, opcode.CALLT:
*i += 2
case opcode.JMPL, opcode.JMPIFL, opcode.JMPIFNOTL, opcode.JMPEQL, opcode.JMPNEL,
opcode.JMPGTL, opcode.JMPGEL, opcode.JMPLTL, opcode.JMPLEL,
opcode.ENDTRYL,
opcode.CALLL, opcode.SYSCALL, opcode.PUSHA:
*i += 4
case opcode.TRYL:
*i += 8
}
return nil
}
func parseInt(i int, len int, b []byte) int {
switch len {
case 1:
return int(b[i])
case 2:
return int(binary.LittleEndian.Uint16(b[i : i+len]))
case 4:
return int(binary.LittleEndian.Uint32(b[i : i+len]))
default:
return 0
}
}

View file

@ -0,0 +1,168 @@
package bytecode
import (
"encoding/hex"
"testing"
"github.com/stretchr/testify/require"
)
func fromHexadecimalStringToBytes(str string) []byte {
data, _ := hex.DecodeString(str)
return data
}
type ExtractCallTest struct {
name string
script string
bytecodeStr string
expected []SyscallParameters
wantErr bool
}
var tests = []ExtractCallTest{
{
name: "valid bytecode 1",
script: "PUSHINT16 3600\n" +
"PUSHINT32 315360000\n" +
"PUSHINT16 600\n" +
"PUSHINT16 3600\n" +
"PUSHDATA1 6f70734066726f737466732e696e666f\n" +
"PUSHDATA1 0x56c989e76f9a2ca05bb5caa6c96f524d905accd8\n" +
"PUSHDATA1 frostfs\n" +
"PUSH7\n" +
"PACK\n" +
"PUSH15\n" +
"PUSHDATA1 register\n" +
"PUSHDATA1 0x76c10d0295b7e76a5674c55d702f04ce280e745e\n" +
"SYSCALL System.Contract.Call\n" +
"ASSERT",
bytecodeStr: "0c106f70734066726f737466" +
"732e696e666f0c14d8cc5a90" +
"4d526fc9a6cab55ba02c9a6f" +
"e789c9560c0766726f737466" +
"7317c01f0c08726567697374" +
"65720c145e740e28ce042f70" +
"5dc574566ae7b795020dc176" +
"41627d5b5239",
expected: []SyscallParameters{
{
Contract: "NUXPjTBsLY6fgVPfmBf8uLk9QBVqgKKkdB",
Method: "register",
},
},
wantErr: false,
},
{
name: "valid bytecode 2",
script: "PUSHNULL\n" +
"PUSHINT32 100000000\n" +
"PUSHDATA1 0x0945e5d5d2dae45c095a4f66f4d48ccba1e512db\n" +
"PUSHDATA1 0x56c989e76f9a2ca05bb5caa6c96f524d905accd8\n" +
"PUSH4\n" +
"PACK\n" +
"PUSH15\n" +
"PUSHDATA1 transfer\n" +
"PUSHDATA1 0xef4073a0f2b305a38ec4050e4d3d28bc40ea63f5\n" +
"SYSCALL System.Contract.Call\n" +
"ASSERT",
bytecodeStr: "0b0200e1f5050c14db12e5a1" +
"cb8cd4f4664f5a095ce4dad2" +
"d5e545090c14d8cc5a904d52" +
"6fc9a6cab55ba02c9a6fe789" +
"c95614c01f0c087472616e73" +
"6665720c14f563ea40bc283d" +
"4d0e05c48ea305b3f2a07340" +
"ef41627d5b5239",
expected: []SyscallParameters{
{
Contract: "NiHURyS83nX2mpxtA7xq84cGxVbHojj5Wc",
Method: "transfer",
},
},
wantErr: false,
},
{
name: "multiple calls",
script: "PUSH1\n" +
"PUSH1\n" +
"PACK\n" +
"PUSH3\n" +
"PUSHDATA1 setRegisterPrice\n" +
"PUSHDATA1 0xef4073a0f2b305a38ec4050e4d3d28bc40ea63f5\n" +
"SYSCALL System.Contract.Call\n" +
"PUSHDATA1 02b3622bf4017bdfe317c58aed5f4c753f206b7db896046fa7d774bbc4bf7f8dc2\n" +
"PUSH1\n" +
"PACK\n" +
"PUSH3\n" +
"PUSHDATA1 registerCandidate\n" +
"PUSHDATA1 0xef4073a0f2b305a38ec4050e4d3d28bc40ea63f5\n" +
"SYSCALL System.Contract.Call\n" +
"ASSERT\n" +
"PUSHINT64 100000000000\n" +
"PUSH1\n" +
"PACK\n" +
"PUSH3\n" +
"PUSHDATA1 setRegisterPrice\n" +
"PUSHDATA1 0xef4073a0f2b305a38ec4050e4d3d28bc40ea63f5\n" +
"SYSCALL System.Contract.Call",
bytecodeStr: "1111c0130c10736574526567" +
"697374657250726963650c14" +
"f563ea40bc283d4d0e05c48e" +
"a305b3f2a07340ef41627d5b" +
"520c2102b3622bf4017bdfe3" +
"17c58aed5f4c753f206b7db8" +
"96046fa7d774bbc4bf7f8dc2" +
"11c0130c1172656769737465" +
"7243616e6469646174650c14" +
"f563ea40bc283d4d0e05c48e" +
"a305b3f2a07340ef41627d5b" +
"52390300e876481700000011" +
"c0130c107365745265676973" +
"74657250726963650c14f563" +
"ea40bc283d4d0e05c48ea305" +
"b3f2a07340ef41627d5b52",
expected: []SyscallParameters{
{
Contract: "NiHURyS83nX2mpxtA7xq84cGxVbHojj5Wc",
Method: "setRegisterPrice",
},
{
Contract: "NiHURyS83nX2mpxtA7xq84cGxVbHojj5Wc",
Method: "registerCandidate",
},
{
Contract: "NiHURyS83nX2mpxtA7xq84cGxVbHojj5Wc",
Method: "setRegisterPrice",
},
},
},
{
name: "invalid contract address len",
script: "PUSH1\n" +
"PUSH1\n" +
"PACK\n" +
"PUSH3\n" +
"PUSHDATA1 setRegisterPrice\n" +
"PUSHDATA1 a1ef4073a0f2b305a38ec4050e4d3d28bc40ea63f5\n" +
"SYSCALL System.Contract.Call",
bytecodeStr: "1111c0130c1073657452656" +
"7697374657250726963650c" +
"14f563ea40bc283d4d0e05c" +
"48ea305b3f2a07340efa141" +
"627d5b52",
expected: nil,
wantErr: true,
},
}
func TestExtractCalls(t *testing.T) {
for _, tt := range tests {
bytecode := fromHexadecimalStringToBytes(tt.bytecodeStr)
syscallParams, err := ExtractCalls(bytecode)
if tt.wantErr {
require.Error(t, err)
}
require.Equal(t, tt.expected, syscallParams)
}
}

View file

@ -3,11 +3,13 @@ package chain
import ( import (
"context" "context"
"encoding/binary" "encoding/binary"
"encoding/json"
"fmt" "fmt"
"os" "os"
"path" "path"
"strconv" "strconv"
"git.frostfs.info/TrueCloudLab/monza/internal/bytecode"
"github.com/nspcc-dev/neo-go/pkg/core/block" "github.com/nspcc-dev/neo-go/pkg/core/block"
"github.com/nspcc-dev/neo-go/pkg/core/state" "github.com/nspcc-dev/neo-go/pkg/core/state"
"github.com/nspcc-dev/neo-go/pkg/io" "github.com/nspcc-dev/neo-go/pkg/io"
@ -26,6 +28,7 @@ type Chain struct {
var ( var (
blocksBucket = []byte("blocks") blocksBucket = []byte("blocks")
logsBucket = []byte("logs") logsBucket = []byte("logs")
callsBucket = []byte("calls")
) )
func Open(ctx context.Context, dir, endpoint string, rewrite bool) (*Chain, error) { func Open(ctx context.Context, dir, endpoint string, rewrite bool) (*Chain, error) {
@ -156,6 +159,69 @@ func (d *Chain) ApplicationLog(txHash util.Uint256) (*result.ApplicationLog, err
return appLog, d.addApplicationLog(txHash, appLog) return appLog, d.addApplicationLog(txHash, appLog)
} }
func (d *Chain) Calls(txHash util.Uint256) ([]bytecode.SyscallParameters, error) {
cached, err := d.calls(txHash)
if err != nil {
return nil, err
}
if cached != nil {
return cached, nil
}
rawTransaction, err := d.Client.GetRawTransaction(txHash)
if err != nil {
return nil, err
}
script := rawTransaction.Script
extractedCalls, err := bytecode.ExtractCalls(script)
return extractedCalls, d.addCalls(txHash, extractedCalls)
}
func (d *Chain) addCalls(txHash util.Uint256, syscallParams []bytecode.SyscallParameters) error {
err := d.db.Batch(func(tx *bbolt.Tx) error {
val, err := json.Marshal(syscallParams)
if err != nil {
return err
}
bkt, err := tx.CreateBucketIfNotExists(callsBucket)
if err != nil {
return err
}
return bkt.Put(txHash.BytesLE(), val)
})
if err != nil {
return fmt.Errorf("cannot add tx %s to cache: %w", txHash.StringLE(), err)
}
return nil
}
func (d *Chain) calls(txHash util.Uint256) (res []bytecode.SyscallParameters, err error) {
err = d.db.View(func(tx *bbolt.Tx) error {
bkt := tx.Bucket(callsBucket)
if bkt == nil {
return nil
}
data := bkt.Get(txHash.BytesLE())
if len(data) == 0 {
return nil
}
return json.Unmarshal(bkt.Get(txHash.BytesLE()), &res)
})
if err != nil {
return nil, fmt.Errorf("cannot read tx %s from cache: %w", txHash.StringLE(), err)
}
return res, nil
}
func (d *Chain) applicationLog(txHash util.Uint256) (res *result.ApplicationLog, err error) { func (d *Chain) applicationLog(txHash util.Uint256) (res *result.ApplicationLog, err error) {
err = d.db.View(func(tx *bbolt.Tx) error { err = d.db.View(func(tx *bbolt.Tx) error {
bkt := tx.Bucket(logsBucket) bkt := tx.Bucket(logsBucket)