Skip to content

Commit

Permalink
Monitorism Global_events is module a that listen events onchain. (#20)
Browse files Browse the repository at this point in the history
* feat: global_events listen on the logs of each blocks.

* feat: Yaml template + logs fitering

* chore: add current block infos + ChainId to ensure we are on the correct chain

* chore: move the rules into a dedicated folders

* chore: rename the param

* feat: add logging and infos to ensure no mistake when running

* feat: add the possibilities or reading of the rules at once.

* feat: add a new general structure that hold the addresses and events.

* feat: add the feature to monitore the empty addresses (all the addresses) + add the fact we don't suport the `sep:` inside the yaml files.

* feat: Insert the `common.Address` type

* feat: Monitore all the events when the addresses are empty.

* feat: add the `common.hash()` to have a more robust check.

* chore: add the `globalconfig.yml` this is file is NOT NECESSARY but just to share with the team if necessary.

* feat: add the types tests

* feat: changes the current struct to a better `GlobalConfiguration`.

* feat: adds metrics + clean the codes

* readme: add a short readme during the creation

* chore: update the test

* Rename README.md  to README.md

* chore: fix bugs + added the comments for the events 1 signer

* feat: small git clone to fetch from github embeeded into the binary

* chore: fix the name of the env

* bump: version git

* readme fix the `-`

* chore: errors handling + rpc message error logs.

* chore: fix comments + add the rules

* Update README.md

* Update README.md

* Update README.md

* chore: remove the useless comments + comment the `cloneRepo()` if needed in the future.

* chore: remove the `SignerCanBeRemove()`

* chore: comment the dependencies for `cloneRepo()` if we need to use it the future.

* chore: remove not necessary function.

* chore: remove the `CloneRepo()` function.
  • Loading branch information
Ethnical authored May 28, 2024
1 parent 987adb7 commit 47f8916
Show file tree
Hide file tree
Showing 24 changed files with 839 additions and 6 deletions.
23 changes: 23 additions & 0 deletions op-monitorism/cmd/monitorism/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
monitorism "github.com/ethereum-optimism/monitorism/op-monitorism"
"github.com/ethereum-optimism/monitorism/op-monitorism/balances"
"github.com/ethereum-optimism/monitorism/op-monitorism/fault"
"github.com/ethereum-optimism/monitorism/op-monitorism/global_events"
"github.com/ethereum-optimism/monitorism/op-monitorism/multisig"
"github.com/ethereum-optimism/monitorism/op-monitorism/withdrawals"

Expand Down Expand Up @@ -60,6 +61,13 @@ func newCli(GitCommit string, GitDate string) *cli.App {
Flags: append(balances.CLIFlags("BALANCE_MON"), defaultFlags...),
Action: cliapp.LifecycleCmd(BalanceMain),
},
{
Name: "global_events",
Usage: "Monitors global events with YAML configuration",
Description: "Monitors global events with YAML configuration",
Flags: append(global_events.CLIFlags("GLOBAL_EVENT_MON"), defaultFlags...),
Action: cliapp.LifecycleCmd(global_eventsMain),
},
{
Name: "version",
Usage: "Show version",
Expand All @@ -73,6 +81,21 @@ func newCli(GitCommit string, GitDate string) *cli.App {
}
}

func global_eventsMain(ctx *cli.Context, closeApp context.CancelCauseFunc) (cliapp.Lifecycle, error) {
log := oplog.NewLogger(oplog.AppOut(ctx), oplog.ReadCLIConfig(ctx))
cfg, err := global_events.ReadCLIFlags(ctx)
if err != nil {
return nil, fmt.Errorf("failed to parse global_events config from flags: %w", err)
}

metricsRegistry := opmetrics.NewRegistry()
monitor, err := global_events.NewMonitor(ctx.Context, log, opmetrics.With(metricsRegistry), cfg)
if err != nil {
return nil, fmt.Errorf("failed to create global_events monitor: %w", err)
}

return monitorism.NewCliApp(ctx, log, metricsRegistry, monitor)
}
func MultisigMain(ctx *cli.Context, closeApp context.CancelCauseFunc) (cliapp.Lifecycle, error) {
log := oplog.NewLogger(oplog.AppOut(ctx), oplog.ReadCLIConfig(ctx))
cfg, err := multisig.ReadCLIFlags(ctx)
Expand Down
17 changes: 17 additions & 0 deletions op-monitorism/cmd/monitorism/globalconfig.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
configuration:
- version: "1.0"
name: Safe Watcher
priority: P0
addresses: []
events:
- keccak256_signature: 0x23428b18acfb3ea64b08dc0c1d296ea9c09702c09083ca5272e64d115b687d23
signature: ExecutionFailure(bytes32,uint256)
- keccak256_signature: 0x442e715f626346e8c54381002da614f62bee8d27386535b2521ec8540898556e
signature: ExecutionSuccess(bytes32,uint256)
- version: "1.0"
name: SystemConfig Config Updates
priority: P2
addresses: []
events:
- keccak256_signature: 0x82f8cc4439cd78202f3081cd05a23d895e011595628770738fc5ba8ecba469ed
signature: ConfigUpdate(uint256 indexed version, UpdateType indexed updateType, bytes data)
10 changes: 10 additions & 0 deletions op-monitorism/cmd/rules/SafeExecution.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# This watches all contacts for OP, Mode, and Base mainnets for two logs.
# These logs are emitted by Safes, so this effectively watches for all
# transactions from any Safe on these chains.
version: 1.0
name: Safe Watcher
priority: P0
addresses: # /!\ We are not supporting EIP 3770 yet, if the address is not starting by `0x`, this will panic by safety measure.
events:
- signature: ExecutionFailure(bytes32,uint256)
- signature: ExecutionSuccess(bytes32,uint256)
11 changes: 11 additions & 0 deletions op-monitorism/cmd/rules/SystemConfigUpdate.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# This watches the L1 SystemConfig for OP Mainnet for config updates.
# - eth/op/SystemConfig
version: 1.0
name: SystemConfig Config Updates
priority: P2
addresses: # /!\ We are not supporting EIP 3770 yet, if the address is not starting by 0x, this will panic by safety measure.
events:
- signature: ConfigUpdate(uint256 indexed version, UpdateType indexed updateType, bytes data)
# Notice that the above signature is not normalized. The normalized version is:
# ConfigUpdate(uint256,uint8,bytes)
# But for UX
52 changes: 52 additions & 0 deletions op-monitorism/global_events/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
## Global Events monitoring

This monitoring modules for the yaml rules added with the format ⚠️ This readme will be move into the global readme in the future.

CLI and Docs:
```bash
NAME:
Monitorism global_events - Monitors global events with YAML configuration

USAGE:
Monitorism global_events [command options] [arguments...]

DESCRIPTION:
Monitors global events with YAML configuration

OPTIONS:
--l1.node.url value Node URL of L1 peer (default: "http://127.0.0.1:8545") [$GLOBAL_EVENT_MON_L1_NODE_URL]
--nickname value Nickname of chain being monitored [$GLOBAL_EVENT_MON_NICKNAME]
--PathYamlRules value Path to the yaml file containing the events to monitor [$GLOBAL_EVENT_MON_PATH_YAML]
--log.level value The lowest log level that will be output (default: INFO) [$MONITORISM_LOG_LEVEL]
--log.format value Format the log output. Supported formats: 'text', 'terminal', 'logfmt', 'json', 'json-pretty', (default: text) [$MONITORISM_LOG_FORMAT]
--log.color Color the log output if in terminal mode (default: false) [$MONITORISM_LOG_COLOR]
--metrics.enabled Enable the metrics server (default: false) [$MONITORISM_METRICS_ENABLED]
--metrics.addr value Metrics listening address (default: "0.0.0.0") [$MONITORISM_METRICS_ADDR]
--metrics.port value Metrics listening port (default: 7300) [$MONITORISM_METRICS_PORT]
--loop.interval.msec value Loop interval of the monitor in milliseconds (default: 60000) [$MONITORISM_LOOP_INTERVAL_MSEC]
--help, -h show help

```
The rules are located here: `op-monitorism/global_events/rules/` then we have multiples folders depending the networks you want to monitore (`mainnet` or `sepolia`) for now.
```yaml
# This is a TEMPLATE file please copy this one
# This watches all contacts for OP, Mode, and Base mainnets for two logs.
version: 1.0
name: Template SafeExecution Events (Success/Failure) L1 # Please put the L1 or L2 at the end of the name.
priority: P5 # This is a test, so it is a P5
#If addresses is empty like below it will watch all addresses otherwise you can address specific addresses.
addresses:
# - 0xbEb5Fc579115071764c7423A4f12eDde41f106Ed # Specific Addresses /!\ We are not supporting EIP 3770 yet, if the address is not starting by 0x, this will panic by safety measure.
events:
- signature: ExecutionFailure(bytes32,uint256) # List of the events to watch for the addresses.
- signature: ExecutionSuccess(bytes32,uint256) # List of the events to watch for the addresses.
```
To run it:
```bash

go run ../cmd/monitorism global_events --nickname MySuperNickName --l1.node.url https://localhost:8545 --PathYamlRules /tmp/Monitorism/op-monitorism/global_events/rules/rules_mainnet_L1 --loop.interval.msec 12000

```
58 changes: 58 additions & 0 deletions op-monitorism/global_events/cli.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package global_events

import (
// "fmt"

opservice "github.com/ethereum-optimism/optimism/op-service"

// "github.com/ethereum/go-ethereum/common"

"github.com/urfave/cli/v2"
)

// args in CLI have to be standardized and clean.
const (
L1NodeURLFlagName = "l1.node.url"
NicknameFlagName = "nickname"
PathYamlRulesFlagName = "PathYamlRules"
)

type CLIConfig struct {
L1NodeURL string
Nickname string
PathYamlRules string
// Optional
}

func ReadCLIFlags(ctx *cli.Context) (CLIConfig, error) {
cfg := CLIConfig{
L1NodeURL: ctx.String(L1NodeURLFlagName),
Nickname: ctx.String(NicknameFlagName),
PathYamlRules: ctx.String(PathYamlRulesFlagName),
}

return cfg, nil
}

func CLIFlags(envVar string) []cli.Flag {
return []cli.Flag{
&cli.StringFlag{
Name: L1NodeURLFlagName,
Usage: "Node URL of L1 peer",
Value: "http://127.0.0.1:8545",
EnvVars: opservice.PrefixEnvVar(envVar, "L1_NODE_URL"),
},
&cli.StringFlag{
Name: NicknameFlagName,
Usage: "Nickname of chain being monitored",
EnvVars: opservice.PrefixEnvVar(envVar, "NICKNAME"), //need to change the name to BLOCKCHAIN_NAME
Required: true,
},
&cli.StringFlag{
Name: PathYamlRulesFlagName,
Usage: "Path to the yaml file containing the events to monitor",
EnvVars: opservice.PrefixEnvVar(envVar, "PATH_YAML"), //need to change the name to BLOCKCHAIN_NAME
Required: true,
},
}
}
191 changes: 191 additions & 0 deletions op-monitorism/global_events/monitor.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
package global_events

import (
"context"
"fmt"
"github.com/ethereum-optimism/optimism/op-bindings/bindings"
"github.com/ethereum-optimism/optimism/op-service/metrics"
"github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/ethereum/go-ethereum/log"
"github.com/prometheus/client_golang/prometheus"
"regexp"
"strings"
"time"
)

const (
MetricsNamespace = "global_events_mon"
)

// Monitor is the main struct of the monitor.
type Monitor struct {
log log.Logger

l1Client *ethclient.Client
globalconfig GlobalConfiguration
// nickname is the nickname of the monitor (we need to change the name this is not an ideal one here).
nickname string
safeAddress *bindings.OptimismPortalCaller

LiveAddress *common.Address

filename string //filename of the yaml rules
yamlconfig Configuration

// Prometheus metrics
eventEmitted *prometheus.GaugeVec
unexpectedRpcErrors *prometheus.CounterVec
}

// ChainIDToName() allows to convert the chainID to a human readable name.
// For now only ethereum + Sepolia are supported.
func ChainIDToName(chainID int64) string {
switch chainID {
case 1:
return "Ethereum [Mainnet]"
case 11155111:
return "Sepolia [Testnet]"
}
return "The `ChainID` is Not defined into the `chaindIDToName` function, this is probably a custom chain otherwise something is going wrong!"
}

// NewMonitor creates a new Monitor instance.
func NewMonitor(ctx context.Context, log log.Logger, m metrics.Factory, cfg CLIConfig) (*Monitor, error) {
l1Client, err := ethclient.Dial(cfg.L1NodeURL)
if err != nil {
return nil, fmt.Errorf("failed to dial l1 rpc: %w", err)
}
fmt.Printf("--------------------------------------- Global_events_mon (Infos) -----------------------------\n")
ChainID, err := l1Client.ChainID(context.Background())
if err != nil {
log.Crit("Failed to retrieve chain ID: %v", err)
}
header, err := l1Client.HeaderByNumber(context.Background(), nil)
if err != nil {
log.Crit("Failed to fetch the latest block header", "error", err)
}
// display the infos at the start to ensure everything is correct.
fmt.Printf("latestBlockNumber: %s\n", header.Number)
fmt.Printf("chainId: %+v\n", ChainIDToName(ChainID.Int64()))
fmt.Printf("PathYaml: %v\n", cfg.PathYamlRules)
fmt.Printf("Nickname: %v\n", cfg.Nickname)
fmt.Printf("L1NodeURL: %v\n", cfg.L1NodeURL)
globalConfig, err := ReadAllYamlRules(cfg.PathYamlRules)
if err != nil {
log.Crit("Failed to read the yaml rules", "error", err.Error())
}
// create a globalconfig empty
fmt.Printf("GlobalConfig: %#v\n", globalConfig.Configuration)
globalConfig.DisplayMonitorAddresses()
fmt.Printf("--------------------------------------- End of Infos -----------------------------\n")
time.Sleep(10 * time.Second) // sleep for 10 seconds usefull to read the information before the prod.
return &Monitor{
log: log,
l1Client: l1Client,
globalconfig: globalConfig,

nickname: cfg.Nickname,
eventEmitted: m.NewGaugeVec(prometheus.GaugeOpts{
Namespace: MetricsNamespace,
Name: "eventEmitted",
Help: "Event monitored emitted an log",
}, []string{"nickname", "rulename", "priority", "functionName", "address"}),
unexpectedRpcErrors: m.NewCounterVec(prometheus.CounterOpts{
Namespace: MetricsNamespace,
Name: "unexpectedRpcErrors",
Help: "number of unexpcted rpc errors",
}, []string{"section", "name"}),
}, nil
}

// formatSignature allows to format the signature of a function to be able to hash it.
// e.g: "transfer(address owner, uint256 amount)" -> "transfer(address,uint256)"
func formatSignature(signature string) string {
// Regex to extract function name and parameters
r := regexp.MustCompile(`(\w+)\s*\(([^)]*)\)`)
matches := r.FindStringSubmatch(signature)
if len(matches) != 3 {
return ""
}
// Function name
funcName := matches[1]
// Parameters, split by commas
params := matches[2]
// Clean parameters to keep only types
cleanParams := make([]string, 0)
for _, param := range strings.Split(params, ",") {
parts := strings.Fields(param)
if len(parts) > 0 {
cleanParams = append(cleanParams, parts[0])
}
}
// Return formatted function signature
return fmt.Sprintf("%s(%s)", funcName, strings.Join(cleanParams, ","))
}

// FormatAndHash allow to Format the signature (e.g: "transfer(address,uint256)") to create the keccak256 hash associated with it.
// Formatting allows use to use "transfer(address owner, uint256 amount)" instead of "transfer(address,uint256)"
func FormatAndHash(signature string) common.Hash {
formattedSignature := formatSignature(signature)
if formattedSignature == "" {
panic("Invalid signature")
}
hash := crypto.Keccak256([]byte(formattedSignature))
return common.BytesToHash(hash)

}

// Run the monitor functions declared as a monitor method.
func (m *Monitor) Run(ctx context.Context) {
m.checkEvents(ctx)
}

// checkEvents function to check the events. If an events is emitted onchain and match the rules defined in the yaml file, then we will display the event.
func (m *Monitor) checkEvents(ctx context.Context) { //TODO: Ensure the logs crit are not causing panic in runtime!
header, err := m.l1Client.HeaderByNumber(context.Background(), nil)
if err != nil {
m.unexpectedRpcErrors.WithLabelValues("L1", "HeaderByNumber").Inc()
m.log.Warn("Failed to retrieve latest block header", "error", err.Error()) //TODO:need to wait 12 and retry here!
return
}

latestBlockNumber := header.Number
query := ethereum.FilterQuery{
FromBlock: latestBlockNumber,
ToBlock: latestBlockNumber,
// Addresses: []common.Address{}, //if empty means that all addresses are monitored should be this value for optimisation and avoiding to take every logs every time -> m.globalconfig.GetUniqueMonitoredAddresses
}

logs, err := m.l1Client.FilterLogs(context.Background(), query)
if err != nil { //TODO:need to wait 12 and retry here!
m.unexpectedRpcErrors.WithLabelValues("L1", "FilterLogs").Inc()
m.log.Warn("Failed to retrieve logs:", "error", err.Error())
return
}

for _, vLog := range logs {
if len(vLog.Topics) > 0 { //Ensure no anonymous event is here.
if len(m.globalconfig.SearchIfATopicIsInsideAnAlert(vLog.Topics[0]).Events) > 0 { // We matched an alert!
config := m.globalconfig.SearchIfATopicIsInsideAnAlert(vLog.Topics[0])
fmt.Printf("-------------------------- Event Detected ------------------------\n")
fmt.Printf("TxHash: h%s\nAddress:%s\nTopics: %s\n", vLog.TxHash, vLog.Address, vLog.Topics)
fmt.Printf("The current config that matched this function: %v\n", config)
fmt.Printf("----------------------------------------------------------------\n")
m.eventEmitted.WithLabelValues(m.nickname, config.Name, config.Priority, config.Events[0].Signature, vLog.Address.String()).Set(float64(1))

}
}

}
m.log.Info("Checking events..", "CurrentBlock", latestBlockNumber)

}

// Close closes the monitor.
func (m *Monitor) Close(_ context.Context) error {
m.l1Client.Close()
return nil
}
Loading

0 comments on commit 47f8916

Please sign in to comment.