chaintrace 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 chaintrace contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,139 @@
1
+ Metadata-Version: 2.4
2
+ Name: chaintrace
3
+ Version: 0.1.0
4
+ Summary: See what a blockchain node is doing, from the process table and a short kernel trace.
5
+ Author: ehsanhajian
6
+ License-Expression: MIT
7
+ Project-URL: Repository, https://github.com/ehsanhajian/chaintrace
8
+ Project-URL: Issues, https://github.com/ehsanhajian/chaintrace/issues
9
+ Project-URL: Changelog, https://github.com/ehsanhajian/chaintrace/releases
10
+ Keywords: ebpf,bpftrace,ethereum,geth,lighthouse,security,node
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Environment :: Console
13
+ Classifier: Intended Audience :: System Administrators
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Topic :: Security
19
+ Classifier: Topic :: System :: Networking :: Monitoring
20
+ Requires-Python: >=3.11
21
+ Description-Content-Type: text/markdown
22
+ License-File: LICENSE
23
+ Provides-Extra: dev
24
+ Requires-Dist: pytest>=8; extra == "dev"
25
+ Dynamic: license-file
26
+
27
+ # ChainTrace
28
+
29
+ ChainTrace answers one question: **what is my node doing internally?**
30
+
31
+ It reads the process, the files it has open, and the connections it holds. When you ask, it also attaches a short read-only kernel probe. A finding is printed when that behavior does not belong on a node.
32
+
33
+ It does not stop the process, read a file, open a connection, or store a key.
34
+
35
+ ValidatorPulse raises an alert. ChainDiff decides whether an upgrade is safe. chainlog-ai explains a failure from the logs. ChainTrace is the view of the process itself.
36
+
37
+ ## Install
38
+
39
+ ```bash
40
+ pip install chaintrace
41
+ ```
42
+
43
+ Python 3.11 or newer. The package is published from a GitHub release to [PyPI](https://pypi.org/project/chaintrace/).
44
+
45
+ ```bash
46
+ chaintrace --version
47
+ ```
48
+
49
+ ## Which command
50
+
51
+ | Question | Command |
52
+ | --- | --- |
53
+ | What is this process doing? | `status --pid` or `status --name` |
54
+ | Is any of that suspicious? | `check` |
55
+ | What should the next check compare against? | `baseline` |
56
+ | Can I keep the answer? | `report` |
57
+ | What did the kernel see for a few seconds? | `watch --pid` |
58
+
59
+ `status` and `check` read a process table. That is `/proc` on the node. `watch` needs `bpftrace` and root, and it only traces the one pid you name. The probe prints `exec`, `connect`, `open`, and `chmod`. It does not follow a child after the exec, and it prints IPv4 connections. IPv6 peers still show up in `status`.
60
+
61
+ ## Examples
62
+
63
+ A saved process table and a saved trace are in `examples/`. Neither needs root.
64
+
65
+ ```bash
66
+ chaintrace status --proc examples/geth --pid 4242
67
+ chaintrace check --proc examples/geth --pid 4242
68
+ chaintrace check --trace examples/compromise.trace
69
+ ```
70
+
71
+ On the node:
72
+
73
+ ```bash
74
+ chaintrace status --name geth
75
+ chaintrace check --name lighthouse
76
+ chaintrace check --pid 4242 --client teku
77
+ chaintrace baseline --name geth
78
+ chaintrace report --name geth
79
+ chaintrace watch --pid 4242 --seconds 10 > /tmp/node.trace
80
+ chaintrace check --trace /tmp/node.trace
81
+ ```
82
+
83
+ `--client` selects a profile when the process name is generic, such as `java` for Teku. `--json` prints one document. `report` writes `chaintrace-report.md`.
84
+
85
+ A password-style argument is printed as `[redacted]`. A path, including a JWT file path, stays visible. File contents are never read.
86
+
87
+ ## What a connection means
88
+
89
+ | Line | Meaning |
90
+ | --- | --- |
91
+ | `listen` | The process is waiting on this address. |
92
+ | `local` | The other end is on this machine, such as the engine API. |
93
+ | `peer` | The local or remote port is a peer port for this client. |
94
+ | `expected` | The remote port is normal for this client, such as a builder relay on 443. |
95
+ | `remote` | The process has no profile, so the destination is shown and not judged. |
96
+ | `unknown` | This profiled client connected somewhere that is not a peer, a loopback address, or an expected port. |
97
+ | `metadata` | The destination is a cloud metadata address. |
98
+
99
+ Profiles cover the execution, consensus, and builder clients these tools already talk about: Geth, Reth, Nethermind, Erigon, Besu, Lighthouse, Prysm, Teku, Nimbus, Lodestar, MEV-boost, op-node, and op-geth. Any other process is still listed. Its public connections are `remote` rather than `unknown`.
100
+
101
+ ## Findings
102
+
103
+ Exit 0 means there is no warning or critical finding. An `info` line still exits 0. Exit 1 means a warning. Exit 2 means a critical finding. Exit 3 means the process or the trace could not be read.
104
+
105
+ | Finding | Severity | When |
106
+ | --- | --- | --- |
107
+ | `sensitive-file` | info, or warning if another program has it open | A JWT, keystore, node key, or mnemonic path is open. |
108
+ | `unknown-destination` | info | An unknown destination, and no baseline has been saved. |
109
+ | `new-destination` | warning | An unknown destination that was not in the baseline. |
110
+ | `new-listener` | warning | A listen address that was not in the baseline. |
111
+ | `binary-changed` | warning | The executable path changed since the baseline. |
112
+ | `deleted-binary` | warning | The executable path is marked deleted. |
113
+ | `exposed-port` | warning | The HTTP or engine port is listening outside loopback. |
114
+ | `suspicious-port` | warning | An outbound connection to a shell or file-share port. |
115
+ | `traffic-anomaly` | warning | Five or more distinct unknown destinations. |
116
+ | `unexpected-exec` | warning, or critical for a shell or downloader | The process executed something other than itself. |
117
+ | `permission-change` | warning, or critical if the mode is world-writable | `chmod` on a path. |
118
+ | `shell-child` | critical | A child is a shell, downloader, or interpreter. |
119
+ | `metadata-destination` | critical | A connection to a cloud metadata address. |
120
+ | `persistence` | critical | An open path under cron, systemd, sudoers, preload, or authorized keys. |
121
+
122
+ `baseline` stores the executable, the listen addresses, and the unknown destinations in `~/.chaintrace/baseline.json`, or under `CHAINTRACE_HOME`. The file is readable only by its owner. One client name has one baseline. `--no-baseline` ignores it.
123
+
124
+ ## Kernel probe
125
+
126
+ `watch` writes a bpftrace program and runs it. The program is read-only: exec, connect, open, and chmod for that pid, then it exits. A saved probe file is the same text `check --trace` reads. A JSON line per event works too.
127
+
128
+ ```text
129
+ exec 4242 geth /bin/bash
130
+ connect 4242 geth 169.254.169.254 80
131
+ open 4242 geth /etc/ld.so.preload
132
+ chmod 4242 geth /usr/bin/geth 511
133
+ ```
134
+
135
+ Run `watch` only on a host you administer. It does not survive past `--seconds`, which is at most 300.
136
+
137
+ ## What it does not do
138
+
139
+ ChainTrace does not call a node RPC, restart a client, or decide whether a release is safe. It does not read key material. A report can include paths and destination addresses, which is the observation.
@@ -0,0 +1,113 @@
1
+ # ChainTrace
2
+
3
+ ChainTrace answers one question: **what is my node doing internally?**
4
+
5
+ It reads the process, the files it has open, and the connections it holds. When you ask, it also attaches a short read-only kernel probe. A finding is printed when that behavior does not belong on a node.
6
+
7
+ It does not stop the process, read a file, open a connection, or store a key.
8
+
9
+ ValidatorPulse raises an alert. ChainDiff decides whether an upgrade is safe. chainlog-ai explains a failure from the logs. ChainTrace is the view of the process itself.
10
+
11
+ ## Install
12
+
13
+ ```bash
14
+ pip install chaintrace
15
+ ```
16
+
17
+ Python 3.11 or newer. The package is published from a GitHub release to [PyPI](https://pypi.org/project/chaintrace/).
18
+
19
+ ```bash
20
+ chaintrace --version
21
+ ```
22
+
23
+ ## Which command
24
+
25
+ | Question | Command |
26
+ | --- | --- |
27
+ | What is this process doing? | `status --pid` or `status --name` |
28
+ | Is any of that suspicious? | `check` |
29
+ | What should the next check compare against? | `baseline` |
30
+ | Can I keep the answer? | `report` |
31
+ | What did the kernel see for a few seconds? | `watch --pid` |
32
+
33
+ `status` and `check` read a process table. That is `/proc` on the node. `watch` needs `bpftrace` and root, and it only traces the one pid you name. The probe prints `exec`, `connect`, `open`, and `chmod`. It does not follow a child after the exec, and it prints IPv4 connections. IPv6 peers still show up in `status`.
34
+
35
+ ## Examples
36
+
37
+ A saved process table and a saved trace are in `examples/`. Neither needs root.
38
+
39
+ ```bash
40
+ chaintrace status --proc examples/geth --pid 4242
41
+ chaintrace check --proc examples/geth --pid 4242
42
+ chaintrace check --trace examples/compromise.trace
43
+ ```
44
+
45
+ On the node:
46
+
47
+ ```bash
48
+ chaintrace status --name geth
49
+ chaintrace check --name lighthouse
50
+ chaintrace check --pid 4242 --client teku
51
+ chaintrace baseline --name geth
52
+ chaintrace report --name geth
53
+ chaintrace watch --pid 4242 --seconds 10 > /tmp/node.trace
54
+ chaintrace check --trace /tmp/node.trace
55
+ ```
56
+
57
+ `--client` selects a profile when the process name is generic, such as `java` for Teku. `--json` prints one document. `report` writes `chaintrace-report.md`.
58
+
59
+ A password-style argument is printed as `[redacted]`. A path, including a JWT file path, stays visible. File contents are never read.
60
+
61
+ ## What a connection means
62
+
63
+ | Line | Meaning |
64
+ | --- | --- |
65
+ | `listen` | The process is waiting on this address. |
66
+ | `local` | The other end is on this machine, such as the engine API. |
67
+ | `peer` | The local or remote port is a peer port for this client. |
68
+ | `expected` | The remote port is normal for this client, such as a builder relay on 443. |
69
+ | `remote` | The process has no profile, so the destination is shown and not judged. |
70
+ | `unknown` | This profiled client connected somewhere that is not a peer, a loopback address, or an expected port. |
71
+ | `metadata` | The destination is a cloud metadata address. |
72
+
73
+ Profiles cover the execution, consensus, and builder clients these tools already talk about: Geth, Reth, Nethermind, Erigon, Besu, Lighthouse, Prysm, Teku, Nimbus, Lodestar, MEV-boost, op-node, and op-geth. Any other process is still listed. Its public connections are `remote` rather than `unknown`.
74
+
75
+ ## Findings
76
+
77
+ Exit 0 means there is no warning or critical finding. An `info` line still exits 0. Exit 1 means a warning. Exit 2 means a critical finding. Exit 3 means the process or the trace could not be read.
78
+
79
+ | Finding | Severity | When |
80
+ | --- | --- | --- |
81
+ | `sensitive-file` | info, or warning if another program has it open | A JWT, keystore, node key, or mnemonic path is open. |
82
+ | `unknown-destination` | info | An unknown destination, and no baseline has been saved. |
83
+ | `new-destination` | warning | An unknown destination that was not in the baseline. |
84
+ | `new-listener` | warning | A listen address that was not in the baseline. |
85
+ | `binary-changed` | warning | The executable path changed since the baseline. |
86
+ | `deleted-binary` | warning | The executable path is marked deleted. |
87
+ | `exposed-port` | warning | The HTTP or engine port is listening outside loopback. |
88
+ | `suspicious-port` | warning | An outbound connection to a shell or file-share port. |
89
+ | `traffic-anomaly` | warning | Five or more distinct unknown destinations. |
90
+ | `unexpected-exec` | warning, or critical for a shell or downloader | The process executed something other than itself. |
91
+ | `permission-change` | warning, or critical if the mode is world-writable | `chmod` on a path. |
92
+ | `shell-child` | critical | A child is a shell, downloader, or interpreter. |
93
+ | `metadata-destination` | critical | A connection to a cloud metadata address. |
94
+ | `persistence` | critical | An open path under cron, systemd, sudoers, preload, or authorized keys. |
95
+
96
+ `baseline` stores the executable, the listen addresses, and the unknown destinations in `~/.chaintrace/baseline.json`, or under `CHAINTRACE_HOME`. The file is readable only by its owner. One client name has one baseline. `--no-baseline` ignores it.
97
+
98
+ ## Kernel probe
99
+
100
+ `watch` writes a bpftrace program and runs it. The program is read-only: exec, connect, open, and chmod for that pid, then it exits. A saved probe file is the same text `check --trace` reads. A JSON line per event works too.
101
+
102
+ ```text
103
+ exec 4242 geth /bin/bash
104
+ connect 4242 geth 169.254.169.254 80
105
+ open 4242 geth /etc/ld.so.preload
106
+ chmod 4242 geth /usr/bin/geth 511
107
+ ```
108
+
109
+ Run `watch` only on a host you administer. It does not survive past `--seconds`, which is at most 300.
110
+
111
+ ## What it does not do
112
+
113
+ ChainTrace does not call a node RPC, restart a client, or decide whether a release is safe. It does not read key material. A report can include paths and destination addresses, which is the observation.
@@ -0,0 +1,55 @@
1
+ [project]
2
+ name = "chaintrace"
3
+ version = "0.1.0"
4
+ description = "See what a blockchain node is doing, from the process table and a short kernel trace."
5
+ readme = "README.md"
6
+ license = "MIT"
7
+ license-files = ["LICENSE"]
8
+ requires-python = ">=3.11"
9
+ keywords = [
10
+ "ebpf",
11
+ "bpftrace",
12
+ "ethereum",
13
+ "geth",
14
+ "lighthouse",
15
+ "security",
16
+ "node",
17
+ ]
18
+ authors = [{ name = "ehsanhajian" }]
19
+ classifiers = [
20
+ "Development Status :: 3 - Alpha",
21
+ "Environment :: Console",
22
+ "Intended Audience :: System Administrators",
23
+ "Programming Language :: Python :: 3",
24
+ "Programming Language :: Python :: 3.11",
25
+ "Programming Language :: Python :: 3.12",
26
+ "Programming Language :: Python :: 3.13",
27
+ "Topic :: Security",
28
+ "Topic :: System :: Networking :: Monitoring",
29
+ ]
30
+ dependencies = []
31
+
32
+ [project.optional-dependencies]
33
+ dev = ["pytest>=8"]
34
+
35
+ [project.urls]
36
+ Repository = "https://github.com/ehsanhajian/chaintrace"
37
+ Issues = "https://github.com/ehsanhajian/chaintrace/issues"
38
+ Changelog = "https://github.com/ehsanhajian/chaintrace/releases"
39
+
40
+ [project.scripts]
41
+ chaintrace = "chaintrace.cli:main"
42
+
43
+ [build-system]
44
+ requires = ["setuptools>=84"]
45
+ build-backend = "setuptools.build_meta"
46
+
47
+ [tool.setuptools.packages.find]
48
+ where = ["src"]
49
+
50
+ [tool.setuptools.package-data]
51
+ chaintrace = ["clients.json"]
52
+
53
+ [tool.pytest.ini_options]
54
+ testpaths = ["tests"]
55
+ pythonpath = ["src"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,3 @@
1
+ """See what a blockchain node is doing."""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,4 @@
1
+ from chaintrace.cli import main
2
+
3
+ if __name__ == "__main__":
4
+ raise SystemExit(main())
@@ -0,0 +1,81 @@
1
+ """Remember the binary, the listeners, and the unknown destinations."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import os
7
+ from dataclasses import dataclass
8
+ from pathlib import Path
9
+
10
+ from chaintrace.models import Observation
11
+
12
+
13
+ class BaselineError(ValueError):
14
+ """The baseline file could not be read."""
15
+
16
+
17
+ @dataclass(frozen=True)
18
+ class Baseline:
19
+ client: str
20
+ exe: str
21
+ unknown: frozenset[str]
22
+ listen: frozenset[str]
23
+
24
+
25
+ def baseline_key(obs: Observation) -> str:
26
+ return obs.comm if obs.client == "unknown" else obs.client
27
+
28
+
29
+ def home_file() -> Path:
30
+ override = os.environ.get("CHAINTRACE_HOME")
31
+ root = Path(override) if override else Path.home() / ".chaintrace"
32
+ return root / "baseline.json"
33
+
34
+
35
+ def capture(obs: Observation) -> Baseline:
36
+ return Baseline(
37
+ client=baseline_key(obs),
38
+ exe=obs.exe,
39
+ unknown=frozenset(conn.remote for conn in obs.connections if conn.kind == "unknown"),
40
+ listen=frozenset(conn.local for conn in obs.connections if conn.kind == "listen"),
41
+ )
42
+
43
+
44
+ def load(path: Path, client: str) -> Baseline | None:
45
+ if not path.is_file():
46
+ return None
47
+ try:
48
+ raw = json.loads(path.read_text())
49
+ except json.JSONDecodeError as exc:
50
+ raise BaselineError(f"{path} is not a baseline file") from exc
51
+ item = raw.get(client)
52
+ if not isinstance(item, dict):
53
+ return None
54
+ return Baseline(
55
+ client=client,
56
+ exe=str(item.get("exe", "")),
57
+ unknown=frozenset(item.get("unknown", [])),
58
+ listen=frozenset(item.get("listen", [])),
59
+ )
60
+
61
+
62
+ def save(path: Path, baseline: Baseline) -> None:
63
+ path.parent.mkdir(parents=True, exist_ok=True)
64
+ path.parent.chmod(0o700)
65
+ data: dict[str, object] = {}
66
+ if path.is_file():
67
+ try:
68
+ loaded = json.loads(path.read_text())
69
+ except json.JSONDecodeError as exc:
70
+ raise BaselineError(f"{path} is not a baseline file") from exc
71
+ if isinstance(loaded, dict):
72
+ data = loaded
73
+ data[baseline.client] = {
74
+ "exe": baseline.exe,
75
+ "unknown": sorted(baseline.unknown),
76
+ "listen": sorted(baseline.listen),
77
+ }
78
+ temporary = path.with_suffix(".json.tmp")
79
+ temporary.write_text(json.dumps(data, indent=2) + "\n")
80
+ temporary.chmod(0o600)
81
+ temporary.replace(path)
@@ -0,0 +1,159 @@
1
+ """Command line for chaintrace."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import subprocess
7
+ import sys
8
+ import tempfile
9
+ from pathlib import Path
10
+
11
+ from chaintrace import __version__
12
+ from chaintrace.baseline import BaselineError, baseline_key, capture, home_file, load, save
13
+ from chaintrace.clients import ClientError, apply_client
14
+ from chaintrace.models import Observation
15
+ from chaintrace.probe import render_probe, unavailable
16
+ from chaintrace.procfs import ProcError, find_pids, read_pid
17
+ from chaintrace.render import render_check, render_json, render_report, render_status
18
+ from chaintrace.rules import evaluate, exit_code
19
+ from chaintrace.trace import TraceError, read_trace
20
+
21
+
22
+ def main(argv: list[str] | None = None) -> int:
23
+ parser = _parser()
24
+ args = parser.parse_args(argv)
25
+ if args.version:
26
+ print(f"chaintrace {__version__}")
27
+ return 0
28
+ if not args.command:
29
+ parser.print_help(sys.stderr)
30
+ return 3
31
+ try:
32
+ return args.func(args)
33
+ except (ProcError, TraceError, ClientError, BaselineError) as exc:
34
+ print(exc, file=sys.stderr)
35
+ return 3
36
+
37
+
38
+ def _parser() -> argparse.ArgumentParser:
39
+ parser = argparse.ArgumentParser(prog="chaintrace", description="See what a blockchain node is doing.")
40
+ parser.add_argument("--version", action="store_true")
41
+ commands = parser.add_subparsers(dest="command")
42
+
43
+ status = commands.add_parser("status", help="Show the process, files, and connections")
44
+ _process_flags(status)
45
+ status.set_defaults(func=cmd_status, trace=None, no_baseline=True)
46
+
47
+ check = commands.add_parser("check", help="Report behavior that does not belong on a node")
48
+ _process_flags(check)
49
+ check.add_argument("--trace", type=Path)
50
+ check.add_argument("--no-baseline", action="store_true")
51
+ check.set_defaults(func=cmd_check)
52
+
53
+ baseline = commands.add_parser("baseline", help="Remember this binary, its listeners, and unknown destinations")
54
+ _process_flags(baseline)
55
+ baseline.set_defaults(func=cmd_baseline, trace=None, no_baseline=True)
56
+
57
+ report = commands.add_parser("report", help="Write a markdown report")
58
+ _process_flags(report)
59
+ report.add_argument("--trace", type=Path)
60
+ report.add_argument("--no-baseline", action="store_true")
61
+ report.add_argument("--output", type=Path, default=Path("chaintrace-report.md"))
62
+ report.set_defaults(func=cmd_report)
63
+
64
+ watch = commands.add_parser("watch", help="Trace exec, connect, open, and chmod for one pid")
65
+ watch.add_argument("--pid", type=int, required=True)
66
+ watch.add_argument("--seconds", type=int, default=10)
67
+ watch.set_defaults(func=cmd_watch)
68
+ return parser
69
+
70
+
71
+ def _process_flags(parser: argparse.ArgumentParser) -> None:
72
+ parser.add_argument("--proc", type=Path, help="Process table. Defaults to /proc")
73
+ parser.add_argument("--pid", type=int)
74
+ parser.add_argument("--name", help="Process name, such as geth or lighthouse")
75
+ parser.add_argument("--client", help="Client profile when the process name is generic")
76
+ parser.add_argument("--json", action="store_true")
77
+
78
+
79
+ def cmd_status(args: argparse.Namespace) -> int:
80
+ obs = _load(args)
81
+ print(render_json(obs) if args.json else render_status(obs), end="")
82
+ return 0
83
+
84
+
85
+ def cmd_check(args: argparse.Namespace) -> int:
86
+ obs = _load(args)
87
+ findings = evaluate(obs, _stored(args, obs))
88
+ print(render_json(obs, findings) if args.json else render_check(obs, findings), end="")
89
+ return exit_code(findings)
90
+
91
+
92
+ def cmd_baseline(args: argparse.Namespace) -> int:
93
+ obs = _load(args)
94
+ saved = capture(obs)
95
+ path = home_file()
96
+ save(path, saved)
97
+ print(f"baseline {saved.client} {path}")
98
+ return 0
99
+
100
+
101
+ def cmd_report(args: argparse.Namespace) -> int:
102
+ obs = _load(args)
103
+ findings = evaluate(obs, _stored(args, obs))
104
+ args.output.write_text(render_report(obs, findings))
105
+ print(f"report {args.output}")
106
+ return exit_code(findings)
107
+
108
+
109
+ def cmd_watch(args: argparse.Namespace) -> int:
110
+ try:
111
+ script = render_probe(args.pid, args.seconds)
112
+ except ValueError as exc:
113
+ print(exc, file=sys.stderr)
114
+ return 3
115
+ reason = unavailable()
116
+ if reason:
117
+ print(reason, file=sys.stderr)
118
+ return 3
119
+ with tempfile.TemporaryDirectory() as directory:
120
+ path = Path(directory) / "node.bt"
121
+ path.write_text(script)
122
+ try:
123
+ completed = subprocess.run(["bpftrace", str(path)], check=False, timeout=args.seconds + 20)
124
+ except subprocess.TimeoutExpired:
125
+ print("bpftrace did not exit", file=sys.stderr)
126
+ return 3
127
+ return 0 if completed.returncode == 0 else 3
128
+
129
+
130
+ def _stored(args: argparse.Namespace, obs: Observation):
131
+ if args.no_baseline:
132
+ return None
133
+ return load(home_file(), baseline_key(obs))
134
+
135
+
136
+ def _load(args: argparse.Namespace):
137
+ trace = getattr(args, "trace", None)
138
+ if trace and (args.pid or args.name):
139
+ raise TraceError("pass a trace or a process, not both")
140
+ if args.pid and args.name:
141
+ raise ProcError("pass --pid or --name, not both")
142
+ if trace:
143
+ return apply_client(read_trace(trace), args.client)
144
+ proc = args.proc if args.proc is not None else Path("/proc")
145
+ if not proc.is_dir():
146
+ raise ProcError(f"{proc} is not a process table")
147
+ if args.name:
148
+ found = find_pids(proc, args.name)
149
+ if not found:
150
+ raise ProcError(f"no process named {args.name}")
151
+ if len(found) > 1:
152
+ listed = ", ".join(str(item) for item in found)
153
+ raise ProcError(f"more than one {args.name}: {listed}")
154
+ pid = found[0]
155
+ elif args.pid is None:
156
+ raise ProcError("pass --pid or --name")
157
+ else:
158
+ pid = args.pid
159
+ return apply_client(read_pid(proc, pid), args.client)
@@ -0,0 +1,69 @@
1
+ {
2
+ "geth": {
3
+ "comms": ["geth"],
4
+ "p2p_ports": [30303],
5
+ "loopback_ports": [8545, 8551]
6
+ },
7
+ "reth": {
8
+ "comms": ["reth"],
9
+ "p2p_ports": [30303],
10
+ "loopback_ports": [8545, 8551]
11
+ },
12
+ "nethermind": {
13
+ "comms": ["nethermind", "Nethermind"],
14
+ "p2p_ports": [30303],
15
+ "loopback_ports": [8545, 8551]
16
+ },
17
+ "erigon": {
18
+ "comms": ["erigon"],
19
+ "p2p_ports": [30303],
20
+ "loopback_ports": [8545, 8551]
21
+ },
22
+ "besu": {
23
+ "comms": ["besu"],
24
+ "p2p_ports": [30303],
25
+ "loopback_ports": [8545, 8551]
26
+ },
27
+ "lighthouse": {
28
+ "comms": ["lighthouse"],
29
+ "p2p_ports": [9000],
30
+ "loopback_ports": [5052]
31
+ },
32
+ "prysm": {
33
+ "comms": ["prysm", "beacon-chain"],
34
+ "p2p_ports": [13000, 12000],
35
+ "loopback_ports": [3500, 4000]
36
+ },
37
+ "teku": {
38
+ "comms": ["teku"],
39
+ "p2p_ports": [9000],
40
+ "loopback_ports": [5051]
41
+ },
42
+ "nimbus": {
43
+ "comms": ["nimbus", "nimbus_beacon_node", "nimbus_beacon_n"],
44
+ "p2p_ports": [9000],
45
+ "loopback_ports": [5052]
46
+ },
47
+ "lodestar": {
48
+ "comms": ["lodestar"],
49
+ "p2p_ports": [9000],
50
+ "loopback_ports": [9596]
51
+ },
52
+ "mev-boost": {
53
+ "comms": ["mev-boost"],
54
+ "p2p_ports": [],
55
+ "remote_ports": [443, 18550],
56
+ "loopback_ports": [18550]
57
+ },
58
+ "op-node": {
59
+ "comms": ["op-node"],
60
+ "p2p_ports": [9222],
61
+ "remote_ports": [443],
62
+ "loopback_ports": [9545]
63
+ },
64
+ "op-geth": {
65
+ "comms": ["op-geth"],
66
+ "p2p_ports": [30303],
67
+ "loopback_ports": [8545, 8551]
68
+ }
69
+ }