chaindiff 0.1.0__py3-none-any.whl

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.
chaindiff/versions.py ADDED
@@ -0,0 +1,131 @@
1
+ """Parse and compare client release tags."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from dataclasses import dataclass, field
7
+ from functools import total_ordering
8
+
9
+ _TAG = re.compile(r"^v?(?P<num>\d+(?:\.\d+)*)(?:-(?P<pre>[0-9A-Za-z.-]+))?$")
10
+ _TOKEN = re.compile(r"^([A-Za-z]+)(\d+)$")
11
+ _PRE_WORD = re.compile(
12
+ r"(?i)(?:^|[^A-Za-z])(alpha|beta|rc|preview|dev|nightly|unstable)(?:[^A-Za-z]|$)"
13
+ )
14
+ _NAMED_RANK = {
15
+ "a": 0,
16
+ "alpha": 0,
17
+ "b": 1,
18
+ "beta": 1,
19
+ "c": 2,
20
+ "rc": 2,
21
+ "preview": 3,
22
+ "dev": 4,
23
+ "nightly": 5,
24
+ "unstable": 6,
25
+ }
26
+
27
+
28
+ def _rank(name: str) -> int:
29
+ return _NAMED_RANK.get(name.lower(), 50)
30
+
31
+
32
+ def _pre_tuple(pre: str) -> tuple[tuple[int, int], ...]:
33
+ parts: list[tuple[int, int]] = []
34
+ for token in pre.split("."):
35
+ if not token or token.lower() == "stable":
36
+ continue
37
+ matched = _TOKEN.fullmatch(token)
38
+ if matched:
39
+ parts.append((_rank(matched.group(1)), int(matched.group(2))))
40
+ elif token.isdigit():
41
+ parts.append((100, int(token)))
42
+ else:
43
+ parts.append((_rank(token), 0))
44
+ return tuple(parts)
45
+
46
+
47
+ def _strip_trailing_zeros(numbers: tuple[int, ...]) -> tuple[int, ...]:
48
+ trimmed = numbers
49
+ while len(trimmed) > 1 and trimmed[-1] == 0:
50
+ trimmed = trimmed[:-1]
51
+ return trimmed
52
+
53
+
54
+ @total_ordering
55
+ @dataclass(frozen=True, eq=False)
56
+ class Version:
57
+ numbers: tuple[int, ...]
58
+ pre: tuple[tuple[int, int], ...] | None
59
+ text: str = field(compare=False, hash=False)
60
+
61
+ def _identity(self) -> tuple[tuple[int, ...], tuple[tuple[int, int], ...] | None]:
62
+ return (_strip_trailing_zeros(self.numbers), self.pre)
63
+
64
+ def __eq__(self, other: object) -> bool:
65
+ if not isinstance(other, Version):
66
+ return NotImplemented
67
+ return self._identity() == other._identity()
68
+
69
+ def __hash__(self) -> int:
70
+ return hash(self._identity())
71
+
72
+ def __lt__(self, other: object) -> bool:
73
+ if not isinstance(other, Version):
74
+ return NotImplemented
75
+ left = _strip_trailing_zeros(self.numbers)
76
+ right = _strip_trailing_zeros(other.numbers)
77
+ size = max(len(left), len(right))
78
+ left_nums = left + (0,) * (size - len(left))
79
+ right_nums = right + (0,) * (size - len(right))
80
+ if left_nums != right_nums:
81
+ return left_nums < right_nums
82
+ if self.pre is None:
83
+ return False
84
+ if other.pre is None:
85
+ return True
86
+ return self.pre < other.pre
87
+
88
+
89
+ def parse_version(tag: str) -> Version | None:
90
+ text = tag.strip()
91
+ if text.lower().endswith("-stable"):
92
+ text = text[: -len("-stable")]
93
+ matched = _TAG.fullmatch(text)
94
+ if not matched:
95
+ return None
96
+ numbers = tuple(int(part) for part in matched.group("num").split("."))
97
+ # Erigon used four-digit date tags (v2022.10.01). They are not semver and
98
+ # must not sort above the 2.x/3.x line. Two-digit calver (26.9.0) stays valid.
99
+ if numbers[0] >= 2000:
100
+ return None
101
+ if len(numbers) < 3:
102
+ numbers = numbers + (0,) * (3 - len(numbers))
103
+ pre_text = matched.group("pre")
104
+ pre = None
105
+ if pre_text and pre_text.lower() != "stable":
106
+ pre = _pre_tuple(pre_text)
107
+ if not pre:
108
+ pre = None
109
+ display_numbers = ".".join(str(part) for part in numbers)
110
+ display = display_numbers if pre is None else f"{display_numbers}-{pre_text}"
111
+ return Version(numbers=numbers, pre=pre, text=display)
112
+
113
+
114
+ def explain_unparsed(tag: str) -> str:
115
+ text = tag.strip()
116
+ if text.lower().endswith("-stable"):
117
+ text = text[: -len("-stable")]
118
+ matched = _TAG.fullmatch(text)
119
+ if matched and int(matched.group("num").split(".")[0]) >= 2000:
120
+ return (
121
+ f"'{tag}' is a date-based release tag. "
122
+ "ChainDiff does not compare those with the current release line."
123
+ )
124
+ return f"Cannot parse version '{tag}'."
125
+
126
+
127
+ def tag_is_prerelease(tag: str) -> bool:
128
+ version = parse_version(tag)
129
+ if version is not None and version.pre is not None:
130
+ return True
131
+ return _PRE_WORD.search(tag) is not None
@@ -0,0 +1,146 @@
1
+ Metadata-Version: 2.4
2
+ Name: chaindiff
3
+ Version: 0.1.0
4
+ Summary: Decide whether an Ethereum node client upgrade is safe.
5
+ Author: ehsanhajian
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/ehsanhajian/chaindiff
8
+ Project-URL: Repository, https://github.com/ehsanhajian/chaindiff
9
+ Project-URL: Issues, https://github.com/ehsanhajian/chaindiff/issues
10
+ Project-URL: Changelog, https://github.com/ehsanhajian/chaindiff/releases
11
+ Keywords: ethereum,blockchain,geth,node,upgrade,cli
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Environment :: Console
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 :: System :: Systems Administration
19
+ Requires-Python: >=3.11
20
+ Description-Content-Type: text/markdown
21
+ License-File: LICENSE
22
+ Provides-Extra: dev
23
+ Requires-Dist: pytest>=8; extra == "dev"
24
+ Dynamic: license-file
25
+
26
+ # ChainDiff
27
+
28
+ Decide whether an Ethereum node client upgrade is safe.
29
+
30
+ ChainDiff compares the client you run with the release you want. It answers **safe** only when every release in between has a sourced review and none of those reviews is breaking. Otherwise the answer is **review required** or **not safe**.
31
+
32
+ It does not restart a node, edit config, or apply an upgrade.
33
+
34
+ ## Scope
35
+
36
+ Supported clients:
37
+
38
+ | Client | Role | Release source |
39
+ | --- | --- | --- |
40
+ | Geth | execution | ethereum/go-ethereum |
41
+ | Nethermind | execution | NethermindEth/nethermind |
42
+ | Erigon | execution | erigontech/erigon |
43
+ | Besu | execution | besu-eth/besu |
44
+ | Reth | execution | paradigmxyz/reth |
45
+ | Lighthouse | consensus | sigp/lighthouse |
46
+ | Prysm | consensus | OffchainLabs/prysm |
47
+ | Teku | consensus | Consensys-Incorporated/teku |
48
+ | Nimbus | consensus | status-im/nimbus-eth2 |
49
+
50
+ Reth and Nimbus are in the catalog because operators run them. Besu and Teku are read from their current GitHub repositories.
51
+
52
+ The shipping catalog is Ethereum mainnet. The tool also covers these networks, one task each. A check has to use the clients that network actually runs, and that chain's upgrade, not only the latest tag of an upstream Ethereum client.
53
+
54
+ - Ethereum mainnet
55
+ - Gnosis
56
+ - BNB Smart Chain
57
+ - Avalanche
58
+ - OP Mainnet
59
+ - Base
60
+ - Arbitrum One
61
+ - Polygon PoS
62
+ - Linea
63
+ - Scroll
64
+ - zkSync Era
65
+ - Starknet
66
+
67
+ Not in this cut: Lodestar, Grandine, applying the upgrade, node metrics, peer or disk checks, and RPC benchmarks. Configuration scanning and execution/consensus pairing are tracked as later work. They need sourced rules, not guesses.
68
+
69
+ Calendar-versioned clients (Besu, Teku, Nimbus) do not treat a new year in the version as a breaking change by itself. Semver clients do: an unreviewed major bump is **not safe**.
70
+
71
+ ## Install
72
+
73
+ ```bash
74
+ pip install chaindiff
75
+ ```
76
+
77
+ That package is published from a GitHub release. Until the first release is on PyPI:
78
+
79
+ ```bash
80
+ pip install git+https://github.com/ehsanhajian/chaindiff.git
81
+ ```
82
+
83
+ ## Commands
84
+
85
+ ```bash
86
+ chaindiff versions
87
+ chaindiff check --client geth --from <installed>
88
+ chaindiff check --client lighthouse --from <installed> --to v8.2.3
89
+ chaindiff plan --client nethermind --from <installed>
90
+ chaindiff refresh
91
+ ```
92
+
93
+ `check` and `plan` use the local catalog. `refresh` is the only command that talks to GitHub. Set `GH_TOKEN` or `GITHUB_TOKEN` if you hit the unauthenticated rate limit.
94
+
95
+ ### Verdicts
96
+
97
+ - **ALREADY CURRENT** (exit 0) — you are on the target release.
98
+ - **SAFE** (exit 0) — every release in the range has a sourced review, and none is breaking.
99
+ - **REVIEW REQUIRED** (exit 1) — ChainDiff will not call it safe. Releases are missing a review, something is deprecated, or the catalog is too old to trust "latest".
100
+ - **NOT SAFE** (exit 2) — a reviewed breaking change, or an unreviewed semver major bump.
101
+ - **PRERELEASE** (exit 2) — the target is a prerelease. Don't run it on a mainnet node.
102
+ - **DOWNGRADE** (exit 2) — the target is older than what you run.
103
+
104
+ A stale catalog (older than 5 days) cannot produce **safe** or **already current** for `latest`. Comparing two explicit versions still works; the report warns that the catalog is old.
105
+
106
+ The checklist always includes the operator steps that are worth doing on any upgrade: read the notes, back up, keep the old binary, upgrade one client at a time, and roll back if the node fails. Client-specific steps appear only when an advisory says what to change.
107
+
108
+ ## Release catalog
109
+
110
+ GitHub Actions refreshes the catalog twice a week, Monday and Thursday at 06:15 UTC, and commits it when the run succeeds. Run `chaindiff refresh` yourself any time.
111
+
112
+ Erigon tags from the old date scheme, such as `v2022.10.01`, are left out. They are not comparable with the 2.x and 3.x line, and treating them as newer versions would hide the current release.
113
+
114
+ The stored catalog is tags, dates, titles, and URLs. It does not copy release-note bodies. A release title that mentions a security fix is reported as a warning. That warning does not by itself make the upgrade safe or unsafe.
115
+
116
+ ## Advisories
117
+
118
+ Safe and not-safe both require a review. Put reviews in `src/chaindiff/data/advisories/<client>.json`:
119
+
120
+ ```json
121
+ {
122
+ "schema_version": 1,
123
+ "advisories": [
124
+ {
125
+ "version": "2.0.0",
126
+ "severity": "breaking",
127
+ "summary": "What breaks.",
128
+ "action": "What the operator changes.",
129
+ "source": "https://github.com/org/repo/releases/tag/2.0.0"
130
+ }
131
+ ]
132
+ }
133
+ ```
134
+
135
+ `severity` is `breaking`, `deprecated`, `note`, or `none`. `none` means the release was reviewed and there is nothing to do. `breaking` and `deprecated` need an `action`. Every advisory needs an `http` or `https` source. Do not mark a release reviewed without reading it.
136
+
137
+ ## Development
138
+
139
+ ```bash
140
+ python3 -m venv .venv
141
+ source .venv/bin/activate
142
+ pip install -e ".[dev]"
143
+ pytest
144
+ ```
145
+
146
+ Tests that need a private catalog can set `CHAINDIFF_DATA` to a directory with the same `clients.json`, `releases/`, and `advisories/` layout.
@@ -0,0 +1,25 @@
1
+ chaindiff/__init__.py,sha256=twwf8sHQjZH6EJ7WujgdeNHTSpsWkY3V4-GDcWxP5W4,85
2
+ chaindiff/__main__.py,sha256=AY-59LlSZTz4M0ez2ut9CMElF7GsSp9REqq9QgDwSHw,88
3
+ chaindiff/catalog.py,sha256=l0UauOrFC9cagrnf8xVmeA3Gw3kA1SOdeOeubAoV7Eg,6059
4
+ chaindiff/cli.py,sha256=MNUTjp4MoBMZqjU9CUewAw5YY24dhe2hvAsKR8Y7-qo,5500
5
+ chaindiff/evaluate.py,sha256=OQrwVNZXLgqIfnHXVRzPK7Xur6plLhFjV5QzKEHssBY,8639
6
+ chaindiff/models.py,sha256=EZTo7pzpKe4aZBE8IM7moBE1I8_vE3xh6FMnkshQNS0,1228
7
+ chaindiff/refresh.py,sha256=vwkub7g9UCpCD_KletOCd8_3sRM5KLFiRowpNWLWJW4,5189
8
+ chaindiff/report.py,sha256=5RHGLelMgQHBv8xdkdUmdIRfb3Jh17bUqTMBbUGh6Tk,5976
9
+ chaindiff/versions.py,sha256=-nBHHorNLAsc8zSQikJpLeXpF2kGsx3Xl7nbnfJbN5A,4140
10
+ chaindiff/data/clients.json,sha256=I1JJ7DyeIrh5inxsi3Wyd2Q0_4jYpqRGjO9JhWyvtD8,1429
11
+ chaindiff/data/releases/besu.json,sha256=pzfXLOl8KL2qCsKMXcbo0zqQPBrq0xujDiv6E9b0ldI,35807
12
+ chaindiff/data/releases/erigon.json,sha256=ku5AhVCpjtNlADdyjoef1xR_RxSJLh7NobgtIHpgxcg,31788
13
+ chaindiff/data/releases/geth.json,sha256=aowjoouikfYbA8AeV2QTiJb9I6SbJSBAfsqvgFQXHb0,52700
14
+ chaindiff/data/releases/lighthouse.json,sha256=bAoIkZMukI2NXMUI2bSYga5hxUvFh0fXgGTuuXaK40Q,27342
15
+ chaindiff/data/releases/nethermind.json,sha256=gTiuDZygGMs-4SQi20KNx4KXbcnrcZTJo7ILIpwujpo,70701
16
+ chaindiff/data/releases/nimbus.json,sha256=LB5sX6sO7QzNM64nBqZCqBqXg0Z2PKbXzuBnDhz2ay4,23915
17
+ chaindiff/data/releases/prysm.json,sha256=QWVvumqlqBaFoQMUuEed0w8Ii7Uf0vqR26fedqY8zfg,38283
18
+ chaindiff/data/releases/reth.json,sha256=OS0wGqsILuGttjv0HlMMqdRCy1OJNnI1uK3og5fypZ8,21889
19
+ chaindiff/data/releases/teku.json,sha256=2sDXBBXgoDxxrUa-hkJymB0iG5Y8b9QwTrL84hV0Zs4,28601
20
+ chaindiff-0.1.0.dist-info/licenses/LICENSE,sha256=lqHxWzKcfMSMJyVvItKPbs0F1i3WwxukYmzkA7z-7A8,1079
21
+ chaindiff-0.1.0.dist-info/METADATA,sha256=VndAcbxD99nm6BGa95F5Aloz_qW4prKYXcs7Qvq95SQ,5970
22
+ chaindiff-0.1.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
23
+ chaindiff-0.1.0.dist-info/entry_points.txt,sha256=UqkB6NcTd9UTnzjhM3YxdEdSjz8fxt2J0wN_Q0kSaFI,49
24
+ chaindiff-0.1.0.dist-info/top_level.txt,sha256=SL6Zy6xpeOGBPd-HUotKgjQcuvmwUFeFw53NG45IbDM,10
25
+ chaindiff-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ chaindiff = chaindiff.cli:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 ChainDiff 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 @@
1
+ chaindiff