feat: add pgeth-monitoring plugin

Signed-off-by: Isma <71719097+Doozers@users.noreply.github.com>
This commit is contained in:
Isma 2023-12-21 10:17:07 +01:00 committed by mortimr
parent a86ba8fc0d
commit 9eef30aa38
7 changed files with 992 additions and 0 deletions

1
plugins/pgeth-monitoring/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
plugin.so

View file

@ -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

View file

@ -0,0 +1,4 @@
name: "monitoring"
config:
REDIS_ENDPOINT: "localhost:6379"
BEACON_ENDPOINT: "localhost:5052"

View file

@ -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

View file

@ -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
}

View file

@ -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
}

View file

@ -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)...)
}