diff --git a/plugins/pgeth-monitoring/.gitignore b/plugins/pgeth-monitoring/.gitignore new file mode 100644 index 0000000000..fa6ba895f2 --- /dev/null +++ b/plugins/pgeth-monitoring/.gitignore @@ -0,0 +1 @@ +plugin.so diff --git a/plugins/pgeth-monitoring/README.md b/plugins/pgeth-monitoring/README.md new file mode 100644 index 0000000000..4d4cfc959d --- /dev/null +++ b/plugins/pgeth-monitoring/README.md @@ -0,0 +1,37 @@ +# pgeth-monitoring plugin + +Runs custom tracers while simulating all transactions, encode and feed everything to redis with topics making things easy to subscribe. + +``` +/head/tx/0xb0ba6c81c185bf7652f9339fdd86f35e47aea38a1215aa107f97d26ca5806c62/0x68c4D9E03D7D902053C428Ca2D74b612Db7F583A/0x5954aB967Bc958940b7EB73ee84797Dc8a2AFbb9/C@0x5954aB967Bc958940b7EB73ee84797Dc8a2AFbb9_20a325d0[S@0xBC4CA0EdA7647A8aB7C2061c2E118A18a936f13D_6352211e,C@0x4d224452801ACEd8B2F0aebE155379bb5D594381_a9059cbb] +``` + +The topics follow the following format + +``` +/CHANNEL/tx/TX_HASH/FROM/TO/CALL_TRACES +``` + +Where the example above call traces can be interpreted as: + +``` +C@0x5954aB967Bc958940b7EB73ee84797Dc8a2AFbb9_20a325d0[S@0xBC4CA0EdA7647A8aB7C2061c2E118A18a936f13D_6352211e,C@0x4d224452801ACEd8B2F0aebE155379bb5D594381_a9059cbb] + +C@0x5954aB967Bc958940b7EB73ee84797Dc8a2AFbb9_20a325d0[ + S@0xBC4CA0EdA7647A8aB7C2061c2E118A18a936f13D_6352211e, + C@0x4d224452801ACEd8B2F0aebE155379bb5D594381_a9059cbb +] +``` + +- Initial call (`C`) made to `0x5954aB967Bc958940b7EB73ee84797Dc8a2AFbb9` on function selector `20a325d0` + - Static call (`S`) made to `0xBC4CA0EdA7647A8aB7C2061c2E118A18a936f13D` on function selector `6352211e` + - Call (`C`) made to `0x4d224452801ACEd8B2F0aebE155379bb5D594381` on function selector `a9059cbb` + + +The different call modes are + +- `C`, a regular `call` +- `S`, a `staticcall` +- `D`, a `delegatecall` + +The message payload will provide complete execution details with inputs, outputs, context and code address for every step diff --git a/plugins/pgeth-monitoring/config.yaml b/plugins/pgeth-monitoring/config.yaml new file mode 100644 index 0000000000..c12f98de9f --- /dev/null +++ b/plugins/pgeth-monitoring/config.yaml @@ -0,0 +1,4 @@ +name: "monitoring" +config: + REDIS_ENDPOINT: "localhost:6379" + BEACON_ENDPOINT: "localhost:5052" diff --git a/plugins/pgeth-monitoring/dependencies.sh b/plugins/pgeth-monitoring/dependencies.sh new file mode 100755 index 0000000000..78b2f96ac4 --- /dev/null +++ b/plugins/pgeth-monitoring/dependencies.sh @@ -0,0 +1,7 @@ +#! /bin/bash + +go get github.com/ethereum/go-ethereum/plugins/pgeth-monitoring +go get github.com/redis/go-redis/v9@v9.0.3 +go get github.com/prometheus/client_golang@v1.12.0 +go get github.com/attestantio/go-eth2-client/http@v0.16.0 +go mod tidy diff --git a/plugins/pgeth-monitoring/pgeth_monitoring.go b/plugins/pgeth-monitoring/pgeth_monitoring.go new file mode 100644 index 0000000000..63fc3d9c01 --- /dev/null +++ b/plugins/pgeth-monitoring/pgeth_monitoring.go @@ -0,0 +1,511 @@ +package pgeth_monitoring + +import ( + "context" + "encoding/json" + "fmt" + "math/big" + "strings" + "time" + + eth2client "github.com/attestantio/go-eth2-client" + "github.com/redis/go-redis/v9" + + "github.com/attestantio/go-eth2-client/http" + "github.com/rs/zerolog" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/consensus/misc/eip1559" + "github.com/ethereum/go-ethereum/core" + "github.com/ethereum/go-ethereum/core/state" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/core/vm" + "github.com/ethereum/go-ethereum/eth" + "github.com/ethereum/go-ethereum/params" + "github.com/ethereum/go-ethereum/pgeth/toolkit" + "github.com/ethereum/go-ethereum/plugins/pgeth-monitoring/pkg/tracer" + "github.com/ethereum/go-ethereum/rpc" +) + +func Version() { + fmt.Println("pgeth-monitoring: v0.0.1") +} + +// plugin.so entrypoint +func Start(pt *toolkit.PluginToolkit, cfg map[string]interface{}, ctx context.Context, errChan chan error) { + var redisEndpointRaw interface{} + var redisEndpoint string + var beaconEndpointRaw interface{} + var beaconEndpoint string + var ok bool + + if redisEndpointRaw, ok = cfg["REDIS_ENDPOINT"]; !ok { + pt.Logger.Error("missing REDIS_ENDPOINT config var") + return + } + + if redisEndpoint, ok = redisEndpointRaw.(string); !ok { + pt.Logger.Error("invalid REDIS_ENDPOINT value") + return + } + + if beaconEndpointRaw, ok = cfg["BEACON_ENDPOINT"]; !ok { + pt.Logger.Error("missing BEACON_ENDPOINT config var") + return + } + + if beaconEndpoint, ok = beaconEndpointRaw.(string); !ok { + pt.Logger.Error("invalid BEACON_ENDPOINT value") + return + } + + client, err := http.New(ctx, + http.WithAddress(beaconEndpoint), + http.WithLogLevel(zerolog.WarnLevel), + ) + if err != nil { + errChan <- err + } + + rdb := redis.NewClient(&redis.Options{ + Addr: redisEndpoint, + Password: "", + DB: 0, + }) + me := NewMonitoringEngine(pt, rdb, client, beaconEndpoint, errChan) + + me.Start(ctx) + +} + +// MonitoringEngine is the main plugin struct +// It contains the plugin toolkit, the redis client, and the eth2 client +type MonitoringEngine struct { + ptk *toolkit.PluginToolkit + + backend *eth.EthAPIBackend + chainConfig *params.ChainConfig + coinbase common.Address + state *state.StateDB + header *types.Header + + latestBlock *types.Block + + beaconEndpoint string + + errChan chan error + + rdb *redis.Client + eth2 eth2client.Service +} + +// AnalyzedTransaction is a transaction with its traces +// It is used to encode the transaction and its traces in a redis key +// Then the entire struct is sent to redis +type AnalyzedTransaction struct { + Transaction *types.Transaction `json:"transaction"` + From common.Address `json:"from"` + Receipt *types.Receipt `json:"receipt"` + Traces tracer.Action `json:"traces"` +} + +// CachedBlockSimulation is a block simulation that is cached in memory +// It is used to avoid simulating the same block twice for finalized blocks +// Two hours after the block is cached, it is removed from memory +// When a cache block is found as finalized, it is removed from memory +type CachedBlockSimulation struct { + Time time.Time + AnalyzedTransactions []AnalyzedTransaction +} + +func NewMonitoringEngine(pt *toolkit.PluginToolkit, rdb *redis.Client, eth2 eth2client.Service, beaconEndpoint string, errChan chan error) *MonitoringEngine { + + return &MonitoringEngine{ + ptk: pt, + backend: pt.Backend.(*eth.EthAPIBackend), + chainConfig: pt.Backend.ChainConfig(), + beaconEndpoint: beaconEndpoint, + errChan: errChan, + rdb: rdb, + eth2: eth2, + } +} + +func (me *MonitoringEngine) Start(ctx context.Context) { + me.startHeadListener(ctx) +} + +func (me *MonitoringEngine) update(ctx context.Context, parent *types.Block) { + + state, _, err := me.backend.StateAndHeaderByNumberOrHash(ctx, rpc.BlockNumberOrHashWithHash(parent.Hash(), true)) + if err != nil { + me.errChan <- err + return + } + me.header = &types.Header{ + ParentHash: parent.Hash(), + Number: new(big.Int).Add(parent.Number(), common.Big1), + GasLimit: parent.GasLimit(), + Time: parent.Time() + 12, + Coinbase: parent.Coinbase(), + BaseFee: eip1559.CalcBaseFee(me.chainConfig, parent.Header()), + Difficulty: parent.Difficulty(), + } + me.coinbase = parent.Coinbase() + me.state = state +} + +func (me *MonitoringEngine) encodeAndBroadcastCallTrace(ctx context.Context, at *AnalyzedTransaction, channel string) { + var topic string + if at.Transaction.To() == nil { + topic = fmt.Sprintf("/%s/tx/%s/%s/null/%s", channel, at.Transaction.Hash(), at.From, encodeActionCalls(at.Traces)) + } else { + topic = fmt.Sprintf("/%s/tx/%s/%s/%s/%s", channel, at.Transaction.Hash(), at.From, at.Transaction.To(), encodeActionCalls(at.Traces)) + + } + + jsoned, err := json.Marshal(*at) + if err != nil { + me.errChan <- err + return + } + + err = me.rdb.Publish(ctx, topic, jsoned).Err() + if err != nil { + me.errChan <- err + } + + err = me.rdb.Expire(ctx, topic, 1*time.Hour).Err() + if err != nil { + me.errChan <- err + } +} + +func (me *MonitoringEngine) encodeAndBroadcast(ctx context.Context, ats []AnalyzedTransaction, channel string) { + for _, analyzedTx := range ats { + me.encodeAndBroadcastCallTrace(ctx, &analyzedTx, channel) + } +} + +// analyze is the main function of the plugin +// It takes a block and returns a list of AnalyzedTransaction +// It simulates the block and gets the traces of all the transactions +func (me *MonitoringEngine) analyze(ctx context.Context, block *types.Block, scope string) []AnalyzedTransaction { + // We retrieve the parent block + parentBlk, err := me.backend.BlockByHash(ctx, block.ParentHash()) + if err != nil { + me.errChan <- err + return nil + } + // We retrieve the state of the parent block + state, _, err := me.backend.StateAndHeaderByNumberOrHash(ctx, rpc.BlockNumberOrHashWithHash(parentBlk.Hash(), true)) + if err != nil { + me.errChan <- err + return nil + } + // We configure the vm to use our monitoring tracer + gp := new(core.GasPool).AddGas(block.Header().GasLimit) + mt := tracer.MonitoringTracer{} + var vmConfig vm.Config = vm.Config{ + Tracer: &mt, + NoBaseFee: false, + EnablePreimageRecording: false, + ExtraEips: []int{}, + } + analyzedTransactions := []AnalyzedTransaction{} + // We simulate all the transactions of the block + for idx, tx := range block.Transactions() { + state.SetTxContext(tx.Hash(), idx) + receipt, err := core.ApplyTransaction(me.chainConfig, me.backend.Ethereum().BlockChain(), &block.Header().Coinbase, gp, state, block.Header(), tx, &block.Header().GasUsed, vmConfig) + if err != nil { + me.errChan <- err + return nil + } + receipt.EffectiveGasPrice = getEffectiveGasPrice(tx, parentBlk.BaseFee()) + signer := types.MakeSigner(me.backend.Ethereum().BlockChain().Config(), receipt.BlockNumber, block.Time()) + from, _ := types.Sender(signer, tx) + analyzedTransactions = append(analyzedTransactions, AnalyzedTransaction{ + Transaction: tx, + From: from, + Receipt: receipt, + Traces: mt.Action, + }) + mt.Clear() + } + me.ptk.Logger.Info("Simulated txs", "count", len(block.Transactions()), "scope", scope, "number", block.Number()) + // We encode and broadcast the traces + me.encodeAndBroadcast(ctx, analyzedTransactions, scope) + me.ptk.Logger.Info("Broadcasted txs", "count", len(block.Transactions()), "scope", scope, "number", block.Number()) + me.latestBlock = block + return analyzedTransactions +} + +func (me *MonitoringEngine) analyzePending(ctx context.Context, txs []*types.Transaction) { + gp := new(core.GasPool).AddGas(me.header.GasLimit) + mt := tracer.MonitoringTracer{} + var vmConfig vm.Config = vm.Config{ + Tracer: &mt, + NoBaseFee: false, + EnablePreimageRecording: false, + ExtraEips: []int{}, + } + analyzedTransactions := []AnalyzedTransaction{} + for _, tx := range txs { + // we copy the state at the head + stateClone := me.state.Copy() + stateClone.SetTxContext(tx.Hash(), 0) + // we simulate the pending tx on top of it + receipt, err := core.ApplyTransaction(me.chainConfig, me.backend.Ethereum().BlockChain(), &me.header.Coinbase, gp, stateClone, me.header, tx, &me.header.GasUsed, vmConfig) + if err != nil { + continue + } + receipt.EffectiveGasPrice = getEffectiveGasPrice(tx, me.header.BaseFee) + signer := types.MakeSigner(me.backend.Ethereum().BlockChain().Config(), receipt.BlockNumber, me.header.Time) + from, _ := types.Sender(signer, tx) + analyzedTransactions = append(analyzedTransactions, AnalyzedTransaction{ + Transaction: tx, + From: from, + Receipt: receipt, + Traces: mt.Action, + }) + mt.Clear() + } + if len(analyzedTransactions) > 0 { + // we encode and broadcast the traces under the pending topic + me.encodeAndBroadcast(ctx, analyzedTransactions, "pending") + } +} + +func (me *MonitoringEngine) startHeadListener(ctx context.Context) { + + headChan := make(chan core.ChainHeadEvent) + headSubscription := me.backend.SubscribeChainHeadEvent(headChan) + pendingChan := make(chan core.NewTxsEvent) + pendingSubscription := me.backend.SubscribeNewTxsEvent(pendingChan) + ticker := time.NewTicker(30 * time.Second) + cache := make(map[common.Hash]CachedBlockSimulation) + analyzedPendingTxs := 0 + + highestFinalized := uint64(0) + + defer headSubscription.Unsubscribe() + defer pendingSubscription.Unsubscribe() + defer ticker.Stop() + + for { + select { + case <-ctx.Done(): + return + case err := <-headSubscription.Err(): + if err != nil { + me.errChan <- err + } + return + case err := <-pendingSubscription.Err(): + if err != nil { + me.errChan <- err + } + return + case newHead := <-headChan: + me.update(ctx, newHead.Block) + me.ptk.Logger.Info("Head was updated", "number", newHead.Block.NumberU64(), "hash", newHead.Block.Hash(), "root", newHead.Block.Root()) + + analyzedTransactions := me.analyze(ctx, newHead.Block, "head") + cache[newHead.Block.Hash()] = CachedBlockSimulation{ + Time: time.Now(), + AnalyzedTransactions: analyzedTransactions, + } + case newTxs := <-pendingChan: + if me.header == nil { + me.ptk.Logger.Warn("Skipping pending tx simulation, not ready") + } else { + me.analyzePending(ctx, newTxs.Txs) + analyzedPendingTxs += len(newTxs.Txs) + } + case <-ticker.C: + if analyzedPendingTxs > 0 { + me.ptk.Logger.Info("Analyzed pending txs", "count", analyzedPendingTxs, "rate", fmt.Sprintf("%f/s", float64(analyzedPendingTxs)/float64(30))) + analyzedPendingTxs = 0 + } + + if me.eth2 == nil { + client, err := http.New(ctx, + http.WithAddress(me.beaconEndpoint), + http.WithLogLevel(zerolog.WarnLevel), + ) + if err != nil { + me.errChan <- err + continue + } + me.eth2 = client + } + + res, err := me.eth2.(eth2client.SignedBeaconBlockProvider).SignedBeaconBlock(ctx, "finalized") + if err != nil { + me.errChan <- err + continue + } + + newHighestFinalized := res.Capella.Message.Body.ExecutionPayload.BlockNumber + if highestFinalized == 0 { + highestFinalized = res.Capella.Message.Body.ExecutionPayload.BlockNumber + me.ptk.Logger.Info("Finalized head was updated", "number", highestFinalized) + } else if newHighestFinalized > highestFinalized { + me.ptk.Logger.Info("Updating finalized head", "from", highestFinalized, "to", newHighestFinalized) + breaked := false + for i := highestFinalized + 1; i <= newHighestFinalized; i++ { + blk, err := me.backend.BlockByNumber(ctx, rpc.BlockNumber(i)) + if err != nil { + me.errChan <- err + breaked = true + break + } + if blk == nil { + breaked = true + break + } + if cachedValue, ok := cache[blk.Hash()]; ok { + me.encodeAndBroadcast(ctx, cachedValue.AnalyzedTransactions, "finalized") + me.ptk.Logger.Info("Broadcasted txs", "count", len(cachedValue.AnalyzedTransactions), "scope", "finalized", "number", i) + delete(cache, blk.Hash()) + } + } + if breaked { + continue + } + me.ptk.Logger.Info("Finalized head was updated", "number", newHighestFinalized) + highestFinalized = newHighestFinalized + + } + + for k, v := range cache { + if time.Since(v.Time) > 2*time.Hour { + delete(cache, k) + } + } + if len(cache) > 0 { + me.ptk.Logger.Info("Cached blocks", "count", len(cache)) + } + } + } +} + +func getEffectiveGasPrice(tx *types.Transaction, baseFee *big.Int) *big.Int { + switch tx.Type() { + case types.DynamicFeeTxType: + if baseFee == nil { + return tx.GasFeeCap() + } + tip := new(big.Int).Sub(tx.GasFeeCap(), baseFee) + if tip.Cmp(tx.GasTipCap()) > 0 { + tip.Set(tx.GasTipCap()) + } + return tip.Add(tip, baseFee) + case types.AccessListTxType: + return tx.GasPrice() + case types.LegacyTxType: + return tx.GasPrice() + default: + return big.NewInt(0) + } +} + +func callTypeToPrefix(c *tracer.Call) string { + switch c.Type() { + case "call": + return "C" + case "staticcall": + return "S" + case "delegatecall": + return "D" + case "initial_call": + return "C" + } + return "X" +} + +func eventTypeToPrefix(e *tracer.Event) string { + switch e.LogType { + case "log0": + fallthrough + case "log1": + fallthrough + case "log2": + fallthrough + case "log3": + fallthrough + case "log4": + return "L" + } + return "X" +} + +func revertTypeToPrefix(r *tracer.Revert) string { + switch r.ErrorType { + case "revert": + return "R" + case "panic": + return "P" + } + return "X" +} + +func minInt(a int, b int) int { + if a < b { + return a + } + return b +} + +func encodeSelector(c *tracer.Call) string { + selector := fmt.Sprintf("%x", c.In[0:minInt(4, len(c.In))]) + for len(selector) < 8 { + selector += "X" + } + return selector +} + +func encodeRevertSelector(c *tracer.Revert) string { + selector := fmt.Sprintf("%x", c.Data[0:minInt(4, len(c.Data))]) + for len(selector) < 8 { + selector += "X" + } + return selector +} + +func encodeActionCalls(a tracer.Action) string { + res := "" + if c, ok := a.(*tracer.Call); ok { + prefix := callTypeToPrefix(c) + selector := encodeSelector(c) + res = fmt.Sprintf("%s@%s_%s", prefix, c.To.String(), selector) + if len(a.Children()) > 0 { + chldArr := []string{} + for _, chld := range a.Children() { + chldRes := encodeActionCalls(chld) + if len(chldRes) > 0 { + chldArr = append(chldArr, chldRes) + } + } + if len(chldArr) > 0 { + joinedChldRes := strings.Join(chldArr[:], ",") + res = fmt.Sprintf("%s[%s]", res, joinedChldRes) + } + } + } + if e, ok := a.(*tracer.Event); ok { + concatenatedTopics := "" + for _, topic := range e.Topics { + concatenatedTopics += topic.String()[2:] + } + dataLength := len(e.Data) + res = fmt.Sprintf("%s@%s_%s_%d", eventTypeToPrefix(e), e.ContextValue.String(), concatenatedTopics, dataLength) + } + if r, ok := a.(*tracer.Revert); ok { + prefix := revertTypeToPrefix(r) + selector := encodeRevertSelector(r) + res = fmt.Sprintf("%s@%s_%s", prefix, r.ContextValue.String(), selector) + } + + return res +} diff --git a/plugins/pgeth-monitoring/pkg/tracer/action.go b/plugins/pgeth-monitoring/pkg/tracer/action.go new file mode 100644 index 0000000000..35bc8c39f2 --- /dev/null +++ b/plugins/pgeth-monitoring/pkg/tracer/action.go @@ -0,0 +1,191 @@ +package tracer + +import ( + "fmt" + "strings" + + "github.com/ethereum/go-ethereum/common" +) + +type Action interface { + Type() string + Children() []Action + Parent() Action + Depth() int + Log() + Has(string) bool + Context() common.Address + Code() common.Address + + AddChildren(Action) +} + +type Call struct { + ParentValue Action `json:"-"` + DepthValue int `json:"depth,omitempty"` + + TypeValue string `json:"type,omitempty"` + CallType string `json:"callType,omitempty"` + ChildrenValue []Action `json:"children,omitempty"` + + ContextValue common.Address `json:"context,omitempty"` + CodeValue common.Address `json:"code,omitempty"` + ForwardedContext common.Address `json:"forwardedContext,omitempty"` + ForwardedCode common.Address `json:"forwardedCode,omitempty"` + + From common.Address `json:"from,omitempty"` + To common.Address `json:"to,omitempty"` + Value string `json:"value,omitempty"` + In []byte `json:"-"` + Out []byte `json:"-"` + InHex string `json:"in,omitempty"` + OutHex string `json:"out,omitempty"` +} + +func (c *Call) Type() string { + return c.CallType +} + +func (c *Call) Children() []Action { + return c.ChildrenValue +} + +func (c *Call) Context() common.Address { + return c.ContextValue +} + +func (c *Call) Code() common.Address { + return c.CodeValue +} + +func (c *Call) Depth() int { + return c.DepthValue +} + +func (c *Call) Parent() Action { + return c.ParentValue +} + +func (c *Call) AddChildren(a Action) { + c.ChildrenValue = append(c.ChildrenValue, a) +} + +func (c *Call) Log() { + fmt.Printf("%s- %s %s to %s (%s:%s) (%d,%d) (%d)\n", strings.Repeat(" ", c.DepthValue), c.Type(), c.From.String(), c.To.String(), c.Context().String(), c.Code().String(), len(c.In), len(c.Out), len(c.ChildrenValue)) + for _, subcall := range c.ChildrenValue { + subcall.Log() + } +} + +func (c *Call) Has(typ string) bool { + if c.Type() == typ { + return true + } + for _, chld := range c.ChildrenValue { + if chld.Has(typ) { + return true + } + } + return false +} + +type Event struct { + ParentValue Action `json:"-"` + DepthValue int `json:"depth,omitempty"` + + TypeValue string `json:"type,omitempty"` + LogType string `json:"logType,omitempty"` + + ContextValue common.Address `json:"context,omitempty"` + CodeValue common.Address `json:"code,omitempty"` + + Data []byte `json:"-"` + DataHex string `json:"data,omitempty"` + Topics []common.Hash `json:"topics,omitempty"` + From common.Address `json:"from,omitempty"` +} + +func (c *Event) Type() string { + return c.LogType +} + +func (c *Event) Children() []Action { + return []Action{} +} + +func (c *Event) Context() common.Address { + return c.ContextValue +} + +func (c *Event) Code() common.Address { + return c.CodeValue +} + +func (c *Event) Depth() int { + return c.DepthValue +} + +func (c *Event) Parent() Action { + return c.ParentValue +} + +func (c *Event) AddChildren(a Action) { +} + +func (c *Event) Log() { + fmt.Printf("%s- %s (%s:%s) \n", strings.Repeat(" ", c.DepthValue), c.Type(), c.Context().String(), c.Code().String()) +} + +func (c *Event) Has(typ string) bool { + return c.Type() == typ +} + +type Revert struct { + ParentValue Action `json:"-"` + DepthValue int `json:"depth,omitempty"` + + TypeValue string `json:"type,omitempty"` + ErrorType string `json:"errorType,omitempty"` + + ContextValue common.Address `json:"context,omitempty"` + CodeValue common.Address `json:"code,omitempty"` + + Data []byte `json:"-"` + DataHex string `json:"data,omitempty"` + From common.Address `json:"from,omitempty"` +} + +func (r *Revert) Type() string { + return r.ErrorType +} + +func (r *Revert) Children() []Action { + return []Action{} +} + +func (r *Revert) Context() common.Address { + return r.ContextValue +} + +func (r *Revert) Code() common.Address { + return r.CodeValue +} + +func (r *Revert) Depth() int { + return r.DepthValue +} + +func (r *Revert) Parent() Action { + return r.ParentValue +} + +func (r *Revert) AddChildren(a Action) { +} + +func (r *Revert) Log() { + fmt.Printf("%s- %s (%s:%s) %x\n", strings.Repeat(" ", r.DepthValue), r.Type(), r.Context().String(), r.Code().String(), r.Data) +} + +func (r *Revert) Has(typ string) bool { + return r.Type() == typ +} diff --git a/plugins/pgeth-monitoring/pkg/tracer/tracer.go b/plugins/pgeth-monitoring/pkg/tracer/tracer.go new file mode 100644 index 0000000000..ab763eabff --- /dev/null +++ b/plugins/pgeth-monitoring/pkg/tracer/tracer.go @@ -0,0 +1,241 @@ +package tracer + +import ( + "encoding/hex" + "fmt" + "math/big" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/vm" +) + +type MonitoringTracer struct { + Action Action + Cursor Action +} + +func (m *MonitoringTracer) Clear() { + m.Action = nil + m.Cursor = nil +} + +func (m *MonitoringTracer) CaptureTxStart(gasLimit uint64) { + +} + +func (m *MonitoringTracer) CaptureTxEnd(restGas uint64) { + +} + +func (m *MonitoringTracer) CaptureStart(env *vm.EVM, from common.Address, to common.Address, create bool, input []byte, gas uint64, value *big.Int) { + copyInput := make([]byte, len(input)) + usedValue := big.NewInt(0) + if value != nil { + usedValue.Set(value) + } + copy(copyInput, input) + m.Action = &Call{ + CallType: "initial_call", + TypeValue: "call", + ChildrenValue: []Action{}, + ParentValue: nil, + DepthValue: 0, + + ContextValue: common.Address{}, + CodeValue: common.Address{}, + + ForwardedContext: to, + ForwardedCode: to, + + From: from, + To: to, + In: copyInput, + InHex: "0x" + hex.EncodeToString(copyInput), + Value: "0x" + usedValue.Text(16), + } + m.Cursor = m.Action +} + +func (m *MonitoringTracer) CaptureEnd(output []byte, gasUsed uint64, err error) { + copyOutput := make([]byte, len(output)) + copy(copyOutput, output) + m.Cursor.(*Call).Out = copyOutput + m.Cursor.(*Call).OutHex = "0x" + hex.EncodeToString(copyOutput) +} + +func (m *MonitoringTracer) CaptureEnter(typ vm.OpCode, from common.Address, to common.Address, input []byte, gas uint64, value *big.Int) { + callType := callOpcodeToString(typ) + ctx, code := parentContextAndCode(m.Cursor) + forwardedCode := to + forwardedContext := to + if callType == "delegatecall" { + forwardedContext = from + } + usedValue := big.NewInt(0) + if value != nil { + usedValue.Set(value) + } + copyInput := make([]byte, len(input)) + copy(copyInput, input) + call := &Call{ + CallType: callType, + TypeValue: "call", + ChildrenValue: []Action{}, + ParentValue: m.Cursor, + DepthValue: m.Cursor.Depth() + 1, + + ForwardedContext: forwardedContext, + ForwardedCode: forwardedCode, + ContextValue: ctx, + CodeValue: code, + From: from, + To: to, + In: copyInput, + InHex: "0x" + hex.EncodeToString(copyInput), + Value: "0x" + usedValue.Text(16), + } + m.Cursor.AddChildren(call) + m.Cursor = call +} + +func (m *MonitoringTracer) CaptureExit(output []byte, gasUsed uint64, err error) { + copyOutput := make([]byte, len(output)) + copy(copyOutput, output) + m.Cursor.(*Call).Out = copyOutput + m.Cursor.(*Call).OutHex = "0x" + hex.EncodeToString(copyOutput) + m.Cursor = m.Cursor.Parent() +} + +func (m *MonitoringTracer) CaptureState(pc uint64, op vm.OpCode, gas, cost uint64, scope *vm.ScopeContext, rData []byte, depth int, err error) { + if op >= 160 && op <= 164 { + stack := scope.Stack.Data() + stackLen := len(stack) + var offset int64 = 0 + var size int64 = 0 + if stackLen >= 2 { + offset = stack[stackLen-1].ToBig().Int64() + size = stack[stackLen-2].ToBig().Int64() + } + fetchSize := size + var data []byte = []byte{} + if int64(scope.Memory.Len()) < offset { + fetchSize = 0 + // generate zero array + } else if int64(scope.Memory.Len()) < offset+size { + fetchSize -= (offset + size) - int64(scope.Memory.Len()) + } + + if fetchSize > 0 { + data = scope.Memory.GetCopy(offset, fetchSize) + } + + if fetchSize < size { + data = addZeros(data, size-fetchSize) + } + + topics := []common.Hash{} + for idx := 0; idx < int(op-160); idx++ { + if stackLen-3-idx >= 0 { + topics = append(topics, stack[stackLen-3-idx].Bytes32()) + } + } + + ctx, code := parentContextAndCode(m.Cursor) + + m.Cursor.AddChildren(&Event{ + LogType: fmt.Sprintf("log%d", op-160), + TypeValue: "event", + Data: data, + DataHex: "0x" + hex.EncodeToString(data), + Topics: topics, + From: scope.Contract.Address(), + + ContextValue: ctx, + CodeValue: code, + ParentValue: m.Cursor, + DepthValue: m.Cursor.Depth() + 1, + }) + } + if op == 253 { + errorType := "revert" + data := []byte{} + stack := scope.Stack.Data() + stackLen := len(stack) + var offset int64 = 0 + var size int64 = 0 + if stackLen >= 2 { + offset = stack[stackLen-1].ToBig().Int64() + size = stack[stackLen-2].ToBig().Int64() + } + fetchSize := size + if int64(scope.Memory.Len()) < offset { + fetchSize = 0 + // generate zero array + } else if int64(scope.Memory.Len()) < offset+size { + fetchSize -= (offset + size) - int64(scope.Memory.Len()) + } + + if fetchSize > 0 { + data = scope.Memory.GetCopy(offset, fetchSize) + } + + if fetchSize < size { + data = addZeros(data, size-fetchSize) + } + + ctx, code := parentContextAndCode(m.Cursor) + + m.Cursor.AddChildren(&Revert{ + ErrorType: errorType, + TypeValue: "revert", + Data: data, + DataHex: "0x" + hex.EncodeToString(data), + From: scope.Contract.Address(), + ContextValue: ctx, + CodeValue: code, + ParentValue: m.Cursor, + DepthValue: m.Cursor.Depth() + 1, + }) + } +} + +func (m *MonitoringTracer) CaptureFault(pc uint64, op vm.OpCode, gas, cost uint64, scope *vm.ScopeContext, depth int, err error) { + if op != 253 { + ctx, code := parentContextAndCode(m.Cursor) + m.Cursor.AddChildren(&Revert{ + ErrorType: "panic", + TypeValue: "revert", + Data: []byte{}, + DataHex: "0x" + hex.EncodeToString([]byte{}), + From: scope.Contract.Address(), + ContextValue: ctx, + CodeValue: code, + ParentValue: m.Cursor, + DepthValue: m.Cursor.Depth() + 1, + }) + } +} + +func callOpcodeToString(c vm.OpCode) string { + switch c { + case 241: + return "call" + case 244: + return "delegatecall" + case 250: + return "staticcall" + default: + return fmt.Sprintf("unknown %d", c) + } +} + +func parentContextAndCode(p Action) (common.Address, common.Address) { + if p != nil { + return p.(*Call).ForwardedContext, p.(*Call).ForwardedCode + } + return common.Address{}, common.Address{} +} + +func addZeros(arr []byte, zeros int64) []byte { + return append(arr, make([]byte, zeros)...) +}