diff --git a/explorer.go b/explorer.go index 2706142..d831fb8 100644 --- a/explorer.go +++ b/explorer.go @@ -12,6 +12,7 @@ import ( "sync" "time" + "git.frostfs.info/TrueCloudLab/monza/internal/bytecode" "git.frostfs.info/TrueCloudLab/monza/internal/chain" "github.com/gdamore/tcell/v2" "github.com/nspcc-dev/neo-go/pkg/core/block" @@ -248,6 +249,13 @@ func (e *Explorer) Run() error { } 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.ScrollToBeginning() }) @@ -346,6 +354,7 @@ func (e *Explorer) startWorkers(amount int) { var err error if task.txHash != nil { _, err = e.chain.ApplicationLog(*task.txHash) + _, err = e.chain.Calls(*task.txHash) } else { _, 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 } +func formatCall(syscallParams bytecode.SyscallParameters) string { + return fmt.Sprintf("Contract: %s; Method: %s\n", syscallParams.Contract, syscallParams.Method) +} + func setFocusColorStyle(target, focus *tview.Box) { focus.SetFocusFunc(func() { target.SetBorderColor(tcell.ColorGreen) diff --git a/go.mod b/go.mod index e9a1d22..6786330 100644 --- a/go.mod +++ b/go.mod @@ -7,12 +7,14 @@ require ( github.com/nspcc-dev/neo-go v0.102.0 github.com/rivo/tview v0.0.0-20220307222120-9994674d60a8 github.com/schollz/progressbar/v3 v3.8.3 + github.com/stretchr/testify v1.8.1 github.com/urfave/cli/v2 v2.3.0 go.etcd.io/bbolt v1.3.7 ) require ( 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/gdamore/encoding v1.0.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/nspcc-dev/go-ordered-json v0.0.0-20220111165707-25110be27d22 // 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/russross/blackfriday/v2 v2.0.1 // indirect github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect diff --git a/go.sum b/go.sum index 0a31118..7512bd2 100644 --- a/go.sum +++ b/go.sum @@ -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/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= 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.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/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 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/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 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/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/bytecode/extract.go b/internal/bytecode/extract.go new file mode 100644 index 0000000..4b715d8 --- /dev/null +++ b/internal/bytecode/extract.go @@ -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 + // PUSHDATA1
+ // 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 + } +} diff --git a/internal/bytecode/extract_test.go b/internal/bytecode/extract_test.go new file mode 100644 index 0000000..332353f --- /dev/null +++ b/internal/bytecode/extract_test.go @@ -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) + } +} diff --git a/internal/chain/chain.go b/internal/chain/chain.go index b48f7f5..8294b03 100644 --- a/internal/chain/chain.go +++ b/internal/chain/chain.go @@ -3,11 +3,13 @@ package chain import ( "context" "encoding/binary" + "encoding/json" "fmt" "os" "path" "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/state" "github.com/nspcc-dev/neo-go/pkg/io" @@ -26,6 +28,7 @@ type Chain struct { var ( blocksBucket = []byte("blocks") logsBucket = []byte("logs") + callsBucket = []byte("calls") ) 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) } +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) { err = d.db.View(func(tx *bbolt.Tx) error { bkt := tx.Bucket(logsBucket)