ethspecify 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.
Potentially problematic release.
This version of ethspecify might be problematic. Click here for more details.
- ethspecify/__init__.py +0 -0
- ethspecify/cli.py +34 -0
- ethspecify/core.py +325 -0
- ethspecify-0.1.0.dist-info/LICENSE +21 -0
- ethspecify-0.1.0.dist-info/METADATA +369 -0
- ethspecify-0.1.0.dist-info/RECORD +9 -0
- ethspecify-0.1.0.dist-info/WHEEL +5 -0
- ethspecify-0.1.0.dist-info/entry_points.txt +2 -0
- ethspecify-0.1.0.dist-info/top_level.txt +1 -0
ethspecify/__init__.py
ADDED
|
File without changes
|
ethspecify/cli.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import os
|
|
3
|
+
|
|
4
|
+
from .core import grep, replace_spec_tags # if you split functionality into core.py
|
|
5
|
+
|
|
6
|
+
def main():
|
|
7
|
+
parser = argparse.ArgumentParser(
|
|
8
|
+
description="Process files containing <spec> tags."
|
|
9
|
+
)
|
|
10
|
+
parser.add_argument(
|
|
11
|
+
"--path",
|
|
12
|
+
type=str,
|
|
13
|
+
help="Directory to search for files containing <spec> tags",
|
|
14
|
+
default=".",
|
|
15
|
+
)
|
|
16
|
+
parser.add_argument(
|
|
17
|
+
"--exclude",
|
|
18
|
+
action="append",
|
|
19
|
+
help="Exclude paths matching this regex",
|
|
20
|
+
default=[],
|
|
21
|
+
)
|
|
22
|
+
args = parser.parse_args()
|
|
23
|
+
|
|
24
|
+
project_dir = os.path.abspath(os.path.expanduser(args.path))
|
|
25
|
+
if not os.path.isdir(project_dir):
|
|
26
|
+
print(f"Error: The directory '{project_dir}' does not exist.")
|
|
27
|
+
exit(1)
|
|
28
|
+
|
|
29
|
+
for f in grep(project_dir, r"<spec\b.*?>", args.exclude):
|
|
30
|
+
print(f"Processing file: {f}")
|
|
31
|
+
replace_spec_tags(f)
|
|
32
|
+
|
|
33
|
+
if __name__ == "__main__":
|
|
34
|
+
main()
|
ethspecify/core.py
ADDED
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
import difflib
|
|
2
|
+
import functools
|
|
3
|
+
import hashlib
|
|
4
|
+
import io
|
|
5
|
+
import os
|
|
6
|
+
import re
|
|
7
|
+
import requests
|
|
8
|
+
import textwrap
|
|
9
|
+
import tokenize
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def strip_comments(code):
|
|
13
|
+
# Split the original code into lines so we can decide which to keep or skip
|
|
14
|
+
code_lines = code.splitlines(True) # Keep line endings in each element
|
|
15
|
+
|
|
16
|
+
# Dictionary: line_index -> list of (column, token_string)
|
|
17
|
+
non_comment_tokens = {}
|
|
18
|
+
|
|
19
|
+
# Tokenize the entire code
|
|
20
|
+
tokens = tokenize.generate_tokens(io.StringIO(code).readline)
|
|
21
|
+
for ttype, tstring, (srow, scol), _, _ in tokens:
|
|
22
|
+
# Skip comments and pure newlines
|
|
23
|
+
if ttype == tokenize.COMMENT:
|
|
24
|
+
continue
|
|
25
|
+
if ttype in (tokenize.NEWLINE, tokenize.NL):
|
|
26
|
+
continue
|
|
27
|
+
# Store all other tokens, adjusting line index to be zero-based
|
|
28
|
+
non_comment_tokens.setdefault(srow - 1, []).append((scol, tstring))
|
|
29
|
+
|
|
30
|
+
final_lines = []
|
|
31
|
+
# Reconstruct or skip lines
|
|
32
|
+
for i, original_line in enumerate(code_lines):
|
|
33
|
+
# If the line has no non-comment tokens
|
|
34
|
+
if i not in non_comment_tokens:
|
|
35
|
+
# Check whether the original line is truly blank (just whitespace)
|
|
36
|
+
if original_line.strip():
|
|
37
|
+
# The line wasn't empty => it was a comment-only line, so skip it
|
|
38
|
+
continue
|
|
39
|
+
else:
|
|
40
|
+
# A truly empty/blank line => keep it
|
|
41
|
+
final_lines.append("")
|
|
42
|
+
else:
|
|
43
|
+
# Reconstruct this line from the stored tokens (preserving indentation/spaces)
|
|
44
|
+
tokens_for_line = sorted(non_comment_tokens[i], key=lambda x: x[0])
|
|
45
|
+
line_str = ""
|
|
46
|
+
last_col = 0
|
|
47
|
+
for col, token_str in tokens_for_line:
|
|
48
|
+
# Insert spaces if there's a gap
|
|
49
|
+
if col > last_col:
|
|
50
|
+
line_str += " " * (col - last_col)
|
|
51
|
+
line_str += token_str
|
|
52
|
+
last_col = col + len(token_str)
|
|
53
|
+
# Strip trailing whitespace at the end of the line
|
|
54
|
+
final_lines.append(line_str.rstrip())
|
|
55
|
+
|
|
56
|
+
return "\n".join(final_lines)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def grep(root_directory, search_pattern, excludes=[]):
|
|
60
|
+
matched_files = []
|
|
61
|
+
regex = re.compile(search_pattern)
|
|
62
|
+
exclude_patterns = [re.compile(pattern) for pattern in excludes]
|
|
63
|
+
for dirpath, _, filenames in os.walk(root_directory):
|
|
64
|
+
for filename in filenames:
|
|
65
|
+
file_path = os.path.join(dirpath, filename)
|
|
66
|
+
if any(pattern.search(file_path) for pattern in exclude_patterns):
|
|
67
|
+
continue
|
|
68
|
+
try:
|
|
69
|
+
with open(file_path, "r", encoding="utf-8") as file:
|
|
70
|
+
if any(regex.search(line) for line in file):
|
|
71
|
+
matched_files.append(file_path)
|
|
72
|
+
except (UnicodeDecodeError, IOError):
|
|
73
|
+
continue
|
|
74
|
+
return matched_files
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def diff(a_name, a_content, b_name, b_content):
|
|
78
|
+
diff = difflib.unified_diff(
|
|
79
|
+
a_content.splitlines(), b_content.splitlines(),
|
|
80
|
+
fromfile=a_name, tofile=b_name, lineterm=""
|
|
81
|
+
)
|
|
82
|
+
return "\n".join(diff)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@functools.lru_cache()
|
|
86
|
+
def get_links():
|
|
87
|
+
url = f"https://raw.githubusercontent.com/jtraglia/ethspecify/main/links.json"
|
|
88
|
+
response = requests.get(url)
|
|
89
|
+
response.raise_for_status()
|
|
90
|
+
return response.json()
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
@functools.lru_cache()
|
|
94
|
+
def get_pyspec():
|
|
95
|
+
url = f"https://raw.githubusercontent.com/jtraglia/ethspecify/main/pyspec.json"
|
|
96
|
+
response = requests.get(url)
|
|
97
|
+
response.raise_for_status()
|
|
98
|
+
return response.json()
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def get_previous_forks(fork):
|
|
102
|
+
pyspec = get_pyspec()
|
|
103
|
+
config_vars = pyspec["mainnet"][fork]["config_vars"]
|
|
104
|
+
previous_forks = ["phase0"]
|
|
105
|
+
for key in config_vars.keys():
|
|
106
|
+
if key.endswith("_FORK_VERSION"):
|
|
107
|
+
if key != f"{fork.upper()}_FORK_VERSION":
|
|
108
|
+
if key != "GENESIS_FORK_VERSION":
|
|
109
|
+
f = key.split("_")[0].lower()
|
|
110
|
+
previous_forks.append(f)
|
|
111
|
+
return list(reversed(previous_forks))
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def get_spec(attributes, preset, fork):
|
|
115
|
+
pyspec = get_pyspec()
|
|
116
|
+
spec = None
|
|
117
|
+
if "function" in attributes or "fn" in attributes:
|
|
118
|
+
if "function" in attributes and "fn" in attributes:
|
|
119
|
+
raise Exception(f"cannot contain 'function' and 'fn'")
|
|
120
|
+
if "function" in attributes:
|
|
121
|
+
function_name = attributes["function"]
|
|
122
|
+
else:
|
|
123
|
+
function_name = attributes["fn"]
|
|
124
|
+
|
|
125
|
+
spec = pyspec[preset][fork]["functions"][function_name]
|
|
126
|
+
spec_lines = spec.split("\n")
|
|
127
|
+
start, end = None, None
|
|
128
|
+
|
|
129
|
+
try:
|
|
130
|
+
vars = attributes["lines"].split("-")
|
|
131
|
+
if len(vars) == 1:
|
|
132
|
+
start = min(len(spec_lines), max(1, int(vars[0])))
|
|
133
|
+
end = start
|
|
134
|
+
elif len(vars) == 2:
|
|
135
|
+
start = min(len(spec_lines), max(1, int(vars[0])))
|
|
136
|
+
end = max(1, min(len(spec_lines), int(vars[1])))
|
|
137
|
+
else:
|
|
138
|
+
raise Exception(f"Invalid lines range for {function_name}: {attributes['lines']}")
|
|
139
|
+
except KeyError:
|
|
140
|
+
pass
|
|
141
|
+
|
|
142
|
+
if start or end:
|
|
143
|
+
start = start or 1
|
|
144
|
+
if start > end:
|
|
145
|
+
raise Exception(f"Invalid lines range for {function_name}: ({start}, {end})")
|
|
146
|
+
# Subtract one because line numbers are one-indexed
|
|
147
|
+
spec = "\n".join(spec_lines[start-1:end])
|
|
148
|
+
spec = textwrap.dedent(spec)
|
|
149
|
+
|
|
150
|
+
elif "constant_var" in attributes:
|
|
151
|
+
if spec is not None:
|
|
152
|
+
raise Exception(f"Tag can only specify one spec item")
|
|
153
|
+
info = pyspec[preset][fork]["constant_vars"][attributes["constant_var"]]
|
|
154
|
+
spec = (
|
|
155
|
+
attributes["constant_var"]
|
|
156
|
+
+ (": " + info[0] if info[0] is not None else "")
|
|
157
|
+
+ " = "
|
|
158
|
+
+ info[1]
|
|
159
|
+
)
|
|
160
|
+
elif "preset_var" in attributes:
|
|
161
|
+
if spec is not None:
|
|
162
|
+
raise Exception(f"Tag can only specify one spec item")
|
|
163
|
+
info = pyspec[preset][fork]["preset_vars"][attributes["preset_var"]]
|
|
164
|
+
spec = (
|
|
165
|
+
attributes["preset_var"]
|
|
166
|
+
+ (": " + info[0] if info[0] is not None else "")
|
|
167
|
+
+ " = "
|
|
168
|
+
+ info[1]
|
|
169
|
+
)
|
|
170
|
+
elif "config_var" in attributes:
|
|
171
|
+
if spec is not None:
|
|
172
|
+
raise Exception(f"Tag can only specify one spec item")
|
|
173
|
+
info = pyspec[preset][fork]["config_vars"][attributes["config_var"]]
|
|
174
|
+
spec = (
|
|
175
|
+
attributes["config_var"]
|
|
176
|
+
+ (": " + info[0] if info[0] is not None else "")
|
|
177
|
+
+ " = "
|
|
178
|
+
+ info[1]
|
|
179
|
+
)
|
|
180
|
+
elif "custom_type" in attributes:
|
|
181
|
+
if spec is not None:
|
|
182
|
+
raise Exception(f"Tag can only specify one spec item")
|
|
183
|
+
spec = (
|
|
184
|
+
attributes["custom_type"]
|
|
185
|
+
+ " = "
|
|
186
|
+
+ pyspec[preset][fork]["custom_types"][attributes["custom_type"]]
|
|
187
|
+
)
|
|
188
|
+
elif "ssz_object" in attributes:
|
|
189
|
+
if spec is not None:
|
|
190
|
+
raise Exception(f"Tag can only specify one spec item")
|
|
191
|
+
spec = pyspec[preset][fork]["ssz_objects"][attributes["ssz_object"]]
|
|
192
|
+
elif "dataclass" in attributes:
|
|
193
|
+
if spec is not None:
|
|
194
|
+
raise Exception(f"Tag can only specify one spec item")
|
|
195
|
+
spec = pyspec[preset][fork]["dataclasses"][attributes["dataclass"]].replace("@dataclass\n", "")
|
|
196
|
+
else:
|
|
197
|
+
raise Exception("invalid spec tag")
|
|
198
|
+
return spec
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def parse_common_attributes(attributes):
|
|
202
|
+
try:
|
|
203
|
+
preset = attributes["preset"]
|
|
204
|
+
except KeyError:
|
|
205
|
+
preset = "mainnet"
|
|
206
|
+
|
|
207
|
+
try:
|
|
208
|
+
fork = attributes["fork"]
|
|
209
|
+
except KeyError:
|
|
210
|
+
raise Exception(f"Missing fork attribute")
|
|
211
|
+
|
|
212
|
+
try:
|
|
213
|
+
style = attributes["style"]
|
|
214
|
+
except KeyError:
|
|
215
|
+
style = "hash"
|
|
216
|
+
|
|
217
|
+
return preset, fork, style
|
|
218
|
+
|
|
219
|
+
def get_spec_item(attributes):
|
|
220
|
+
preset, fork, style = parse_common_attributes(attributes)
|
|
221
|
+
spec = get_spec(attributes, preset, fork)
|
|
222
|
+
|
|
223
|
+
if style == "full" or style == "hash":
|
|
224
|
+
return spec
|
|
225
|
+
elif style == "diff":
|
|
226
|
+
previous_forks = get_previous_forks(fork)
|
|
227
|
+
|
|
228
|
+
previous_fork = None
|
|
229
|
+
previous_spec = None
|
|
230
|
+
for i, _ in enumerate(previous_forks):
|
|
231
|
+
previous_fork = previous_forks[i]
|
|
232
|
+
previous_spec = get_spec(attributes, preset, previous_fork)
|
|
233
|
+
if previous_spec != "phase0":
|
|
234
|
+
try:
|
|
235
|
+
previous_previous_fork = previous_forks[i+1]
|
|
236
|
+
previous_previous_spec = get_spec(attributes, preset, previous_previous_fork)
|
|
237
|
+
if previous_previous_spec == previous_spec:
|
|
238
|
+
continue
|
|
239
|
+
except KeyError:
|
|
240
|
+
pass
|
|
241
|
+
except IndexError:
|
|
242
|
+
pass
|
|
243
|
+
if previous_spec != spec:
|
|
244
|
+
break
|
|
245
|
+
if previous_spec == "phase0":
|
|
246
|
+
raise Exception("there is no previous spec for this")
|
|
247
|
+
return diff(previous_fork, strip_comments(previous_spec), fork, strip_comments(spec))
|
|
248
|
+
if style == "link":
|
|
249
|
+
if "function" in attributes or "fn" in attributes:
|
|
250
|
+
if "function" in attributes and "fn" in attributes:
|
|
251
|
+
raise Exception(f"cannot contain 'function' and 'fn'")
|
|
252
|
+
if "function" in attributes:
|
|
253
|
+
function_name = attributes["function"]
|
|
254
|
+
else:
|
|
255
|
+
function_name = attributes["fn"]
|
|
256
|
+
for key, value in get_links().items():
|
|
257
|
+
if fork in key and key.endswith(function_name):
|
|
258
|
+
return value
|
|
259
|
+
return "Could not find link"
|
|
260
|
+
else:
|
|
261
|
+
return "Not available for this type of spec"
|
|
262
|
+
else:
|
|
263
|
+
raise Exception("invalid style type")
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def extract_attributes(tag):
|
|
267
|
+
attr_pattern = re.compile(r'(\w+)="(.*?)"')
|
|
268
|
+
return dict(attr_pattern.findall(tag))
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def replace_spec_tags(file_path):
|
|
272
|
+
with open(file_path, 'r') as file:
|
|
273
|
+
content = file.read()
|
|
274
|
+
|
|
275
|
+
# Define regex to match both long and self-closing <spec> tags
|
|
276
|
+
pattern = re.compile(r'(<spec\b[^>]*)(/?>)(?:([\s\S]*?)(</spec>))?', re.DOTALL)
|
|
277
|
+
|
|
278
|
+
def replacer(match):
|
|
279
|
+
# Extract the tag parts:
|
|
280
|
+
opening_tag_base = match.group(1)
|
|
281
|
+
tag_end = match.group(2) # either ">" or "/>"
|
|
282
|
+
|
|
283
|
+
# Reconstruct the full opening tag for attribute extraction
|
|
284
|
+
opening_tag_full = opening_tag_base + tag_end
|
|
285
|
+
|
|
286
|
+
# Extract attributes from the full opening tag
|
|
287
|
+
attributes = extract_attributes(opening_tag_full)
|
|
288
|
+
print(f"spec tag: {attributes}")
|
|
289
|
+
|
|
290
|
+
# Parse common attributes to get preset, fork, style, etc.
|
|
291
|
+
preset, fork, style = parse_common_attributes(attributes)
|
|
292
|
+
spec = get_spec(attributes, preset, fork)
|
|
293
|
+
|
|
294
|
+
# Compute the first 8 characters of the SHA256 hash of the spec content.
|
|
295
|
+
hash_value = hashlib.sha256(spec.encode('utf-8')).hexdigest()[:8]
|
|
296
|
+
|
|
297
|
+
# Update the full opening tag (opening_tag_full) to include the hash attribute.
|
|
298
|
+
if 'hash="' in opening_tag_full:
|
|
299
|
+
updated_opening = re.sub(
|
|
300
|
+
r'(hash=")[^"]*(")',
|
|
301
|
+
lambda m: f'{m.group(1)}{hash_value}{m.group(2)}',
|
|
302
|
+
opening_tag_full
|
|
303
|
+
)
|
|
304
|
+
else:
|
|
305
|
+
updated_opening = opening_tag_full[:-1] + f' hash="{hash_value}">'
|
|
306
|
+
|
|
307
|
+
if style == "hash":
|
|
308
|
+
# For hash style, output a short self-closing tag with normalized spacing.
|
|
309
|
+
updated_tag = re.sub(r'\s*/?>\s*$', '', updated_opening) + " />"
|
|
310
|
+
else:
|
|
311
|
+
# For full/diff styles, output the long form with content.
|
|
312
|
+
spec_content = get_spec_item(attributes)
|
|
313
|
+
prefix = content[:match.start()].splitlines()[-1]
|
|
314
|
+
prefixed_spec = "\n".join(f"{prefix}{line}" if line.rstrip() else prefix.rstrip() for line in spec_content.rstrip().split("\n"))
|
|
315
|
+
long_opening = updated_opening.rstrip(">/") + ">"
|
|
316
|
+
updated_tag = f"{long_opening}\n{prefixed_spec}\n{prefix}</spec>"
|
|
317
|
+
|
|
318
|
+
return updated_tag
|
|
319
|
+
|
|
320
|
+
# Replace all matches in the content
|
|
321
|
+
updated_content = pattern.sub(replacer, content)
|
|
322
|
+
|
|
323
|
+
# Write the updated content back to the file
|
|
324
|
+
with open(file_path, 'w') as file:
|
|
325
|
+
file.write(updated_content)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Justin Traglia
|
|
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,369 @@
|
|
|
1
|
+
Metadata-Version: 2.2
|
|
2
|
+
Name: ethspecify
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A utility for processing Ethereum specification tags.
|
|
5
|
+
Home-page: https://github.com/jtraglia/ethspecify
|
|
6
|
+
Author: Justin Traglia
|
|
7
|
+
Author-email: jtraglia@pm.me
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Requires-Python: >=3.6
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
License-File: LICENSE
|
|
14
|
+
Requires-Dist: requests==2.32.3
|
|
15
|
+
Dynamic: author
|
|
16
|
+
Dynamic: author-email
|
|
17
|
+
Dynamic: classifier
|
|
18
|
+
Dynamic: description
|
|
19
|
+
Dynamic: description-content-type
|
|
20
|
+
Dynamic: home-page
|
|
21
|
+
Dynamic: requires-dist
|
|
22
|
+
Dynamic: requires-python
|
|
23
|
+
Dynamic: summary
|
|
24
|
+
|
|
25
|
+
# ethspecify
|
|
26
|
+
|
|
27
|
+
A tool for referencing the Ethereum specifications in clients.
|
|
28
|
+
|
|
29
|
+
The idea is that ethspecify will help developers keep track of when the specification changes. It
|
|
30
|
+
will also help auditors verify that the client implementations match the specifications. Ideally,
|
|
31
|
+
this is configured as a CI check which notifies client developers when the specification changes.
|
|
32
|
+
When that happens, they can update the implementations appropriately.
|
|
33
|
+
|
|
34
|
+
## Getting Started
|
|
35
|
+
|
|
36
|
+
### Adding Spec Tags
|
|
37
|
+
|
|
38
|
+
In your client, add an HTML tag like this:
|
|
39
|
+
|
|
40
|
+
```
|
|
41
|
+
/*
|
|
42
|
+
* <spec fn="is_fully_withdrawable_validator" fork="deneb">
|
|
43
|
+
*/
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
This supports all languages and comment styles. It preserves indentation, so something like this
|
|
47
|
+
would also work:
|
|
48
|
+
|
|
49
|
+
```
|
|
50
|
+
/// <spec fn="is_fully_withdrawable_validator" fork="deneb">
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
After the script is finished executing, a new `hash` field will exist in the tag. This tag is used
|
|
54
|
+
to tell if the specification changed, without having to include the specification content.
|
|
55
|
+
|
|
56
|
+
```
|
|
57
|
+
/// <spec fn="is_fully_withdrawable_validator" fork="deneb" hash="e936da25" />
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
> [!NOTE]
|
|
61
|
+
> Closing tags will be added automatically. For `style="hash"` tags, a self-closing tag is used for
|
|
62
|
+
> conciseness. For `style="full"` and `style="diff"` tags, a paired closing tag must be used.
|
|
63
|
+
|
|
64
|
+
### Installation
|
|
65
|
+
|
|
66
|
+
#### Install with Pip
|
|
67
|
+
|
|
68
|
+
```
|
|
69
|
+
python3 -mpip install ethspecify
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
#### Manual Install
|
|
73
|
+
|
|
74
|
+
First, clone the repository. You only need the latest commit.
|
|
75
|
+
|
|
76
|
+
```
|
|
77
|
+
git clone https://github.com/jtraglia/ethspecify.git --depth=1
|
|
78
|
+
cd ethspecify
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Next, build and install the utility.
|
|
82
|
+
|
|
83
|
+
```
|
|
84
|
+
python3 -mpip install .
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Then, change directory to the source source directory and run `ethspecify`.
|
|
88
|
+
|
|
89
|
+
```
|
|
90
|
+
Projects/client$ ethspecify
|
|
91
|
+
Processing file: /Users/user/Projects/client/src/file.ext
|
|
92
|
+
spec tag: {'custom_type': 'Blob', 'fork': 'electra'}
|
|
93
|
+
spec tag: {'dataclass': 'PayloadAttributes', 'fork': 'electra'}
|
|
94
|
+
spec tag: {'ssz_object': 'ConsolidationRequest', 'fork': 'electra'}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### Specification Options
|
|
98
|
+
|
|
99
|
+
#### Fork
|
|
100
|
+
|
|
101
|
+
This attribute can be any of the [executable
|
|
102
|
+
specifications](https://github.com/ethereum/consensus-specs/blob/e6bddd966214a19d2b97199bbe3c02577a22a8b4/Makefile#L3-L15)
|
|
103
|
+
in the consensus-specs. At the time of writing, these are: phase0, altair, bellatrix, capella,
|
|
104
|
+
deneb, electra, fulu, whisk, eip6800, and eip7732.
|
|
105
|
+
|
|
106
|
+
#### Style
|
|
107
|
+
|
|
108
|
+
This attribute can be used to change how the specification content is shown.
|
|
109
|
+
|
|
110
|
+
##### `hash` (default)
|
|
111
|
+
|
|
112
|
+
This style adds a hash of the specification content to the spec tag, without showing the content.
|
|
113
|
+
|
|
114
|
+
```
|
|
115
|
+
/*
|
|
116
|
+
* <spec fn="apply_deposit" fork="electra" hash="c723ce7b" />
|
|
117
|
+
*/
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
> [!NOTE]
|
|
121
|
+
> The hash is the first 8 characters of the specification content's SHA256 digest.
|
|
122
|
+
|
|
123
|
+
##### `full`
|
|
124
|
+
|
|
125
|
+
This style displays the whole content of this specification item, including comments.
|
|
126
|
+
|
|
127
|
+
```
|
|
128
|
+
/*
|
|
129
|
+
* <spec fn="is_fully_withdrawable_validator" fork="deneb" style="full">
|
|
130
|
+
* def is_fully_withdrawable_validator(validator: Validator, balance: Gwei, epoch: Epoch) -> bool:
|
|
131
|
+
* """
|
|
132
|
+
* Check if ``validator`` is fully withdrawable.
|
|
133
|
+
* """
|
|
134
|
+
* return (
|
|
135
|
+
* has_eth1_withdrawal_credential(validator)
|
|
136
|
+
* and validator.withdrawable_epoch <= epoch
|
|
137
|
+
* and balance > 0
|
|
138
|
+
* )
|
|
139
|
+
* </spec>
|
|
140
|
+
*/
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
##### `link`
|
|
144
|
+
|
|
145
|
+
> [!WARNING]
|
|
146
|
+
> This feature is a work-in-progress.
|
|
147
|
+
|
|
148
|
+
This style displays a GitHub link to the specification item.
|
|
149
|
+
|
|
150
|
+
```
|
|
151
|
+
/*
|
|
152
|
+
* <spec fn="apply_pending_deposit" fork="electra" style="link" hash="83ee9126">
|
|
153
|
+
* https://github.com/ethereum/consensus-specs/blob/dev/specs/electra/beacon-chain.md#new-apply_pending_deposit
|
|
154
|
+
* </spec>
|
|
155
|
+
*/
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
##### `diff`
|
|
159
|
+
|
|
160
|
+
This style displays a diff with the previous fork's version of the specification.
|
|
161
|
+
|
|
162
|
+
```
|
|
163
|
+
/*
|
|
164
|
+
* <spec ssz_object="BeaconState" fork="electra" style="diff">
|
|
165
|
+
* --- deneb
|
|
166
|
+
* +++ electra
|
|
167
|
+
* @@ -27,3 +27,12 @@
|
|
168
|
+
* next_withdrawal_index: WithdrawalIndex
|
|
169
|
+
* next_withdrawal_validator_index: ValidatorIndex
|
|
170
|
+
* historical_summaries: List[HistoricalSummary, HISTORICAL_ROOTS_LIMIT]
|
|
171
|
+
* + deposit_requests_start_index: uint64
|
|
172
|
+
* + deposit_balance_to_consume: Gwei
|
|
173
|
+
* + exit_balance_to_consume: Gwei
|
|
174
|
+
* + earliest_exit_epoch: Epoch
|
|
175
|
+
* + consolidation_balance_to_consume: Gwei
|
|
176
|
+
* + earliest_consolidation_epoch: Epoch
|
|
177
|
+
* + pending_deposits: List[PendingDeposit, PENDING_DEPOSITS_LIMIT]
|
|
178
|
+
* + pending_partial_withdrawals: List[PendingPartialWithdrawal, PENDING_PARTIAL_WITHDRAWALS_LIMIT]
|
|
179
|
+
* + pending_consolidations: List[PendingConsolidation, PENDING_CONSOLIDATIONS_LIMIT]
|
|
180
|
+
* </spec>
|
|
181
|
+
*/
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
> [!NOTE]
|
|
185
|
+
> Comments are stripped from the specifications when the `diff` style is used. We do this because
|
|
186
|
+
> these complicate the diff; the "[Modified in Fork]" comments aren't valuable here.
|
|
187
|
+
|
|
188
|
+
This can be used with any specification item, like functions too:
|
|
189
|
+
|
|
190
|
+
```
|
|
191
|
+
/*
|
|
192
|
+
* <spec fn="is_eligible_for_activation_queue" fork="electra" style="diff">
|
|
193
|
+
* --- phase0
|
|
194
|
+
* +++ electra
|
|
195
|
+
* @@ -4,5 +4,5 @@
|
|
196
|
+
* """
|
|
197
|
+
* return (
|
|
198
|
+
* validator.activation_eligibility_epoch == FAR_FUTURE_EPOCH
|
|
199
|
+
* - and validator.effective_balance == MAX_EFFECTIVE_BALANCE
|
|
200
|
+
* + and validator.effective_balance >= MIN_ACTIVATION_BALANCE
|
|
201
|
+
* )
|
|
202
|
+
* </spec>
|
|
203
|
+
*/
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
### Supported Specification Items
|
|
207
|
+
|
|
208
|
+
#### Constants
|
|
209
|
+
|
|
210
|
+
These are items found in the `Constants` section of the specifications.
|
|
211
|
+
|
|
212
|
+
```
|
|
213
|
+
/*
|
|
214
|
+
* <spec constant_var="COMPOUNDING_WITHDRAWAL_PREFIX" fork="electra" style="full">
|
|
215
|
+
* COMPOUNDING_WITHDRAWAL_PREFIX: Bytes1 = '0x02'
|
|
216
|
+
* </spec>
|
|
217
|
+
*/
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
#### Custom Types
|
|
221
|
+
|
|
222
|
+
These are items found in the `Custom types` section of the specifications.
|
|
223
|
+
|
|
224
|
+
```
|
|
225
|
+
/*
|
|
226
|
+
* <spec custom_type="Blob" fork="electra" style="full">
|
|
227
|
+
* Blob = ByteVector[BYTES_PER_FIELD_ELEMENT * FIELD_ELEMENTS_PER_BLOB]
|
|
228
|
+
* </spec>
|
|
229
|
+
*/
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
#### Preset Variables
|
|
233
|
+
|
|
234
|
+
These are items found in the
|
|
235
|
+
[`presets`](https://github.com/ethereum/consensus-specs/tree/dev/presets) directory.
|
|
236
|
+
|
|
237
|
+
For preset variables, in addition to the `preset_var` attribute, you can specify a `preset`
|
|
238
|
+
attribute: minimal or mainnet.
|
|
239
|
+
|
|
240
|
+
```
|
|
241
|
+
/*
|
|
242
|
+
* <spec preset="minimal" preset_var="PENDING_CONSOLIDATIONS_LIMIT" fork="electra" style="full">
|
|
243
|
+
* PENDING_CONSOLIDATIONS_LIMIT: uint64 = 64
|
|
244
|
+
* </spec>
|
|
245
|
+
*
|
|
246
|
+
* <spec preset="mainnet" preset_var="PENDING_CONSOLIDATIONS_LIMIT" fork="electra" style="full">
|
|
247
|
+
* PENDING_CONSOLIDATIONS_LIMIT: uint64 = 262144
|
|
248
|
+
* </spec>
|
|
249
|
+
*/
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
It's not strictly necessary to specify the preset attribute. The default preset is mainnet.
|
|
253
|
+
|
|
254
|
+
```
|
|
255
|
+
/*
|
|
256
|
+
* <spec preset_var="FIELD_ELEMENTS_PER_BLOB" fork="electra" style="full">
|
|
257
|
+
* FIELD_ELEMENTS_PER_BLOB: uint64 = 4096
|
|
258
|
+
* </spec>
|
|
259
|
+
*/
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
#### Config Variables
|
|
263
|
+
|
|
264
|
+
These are items found in the
|
|
265
|
+
[`configs`](https://github.com/ethereum/consensus-specs/tree/dev/presets) directory.
|
|
266
|
+
|
|
267
|
+
```
|
|
268
|
+
/*
|
|
269
|
+
* <spec config_var="MAX_REQUEST_BLOB_SIDECARS" fork="electra" style="full">
|
|
270
|
+
* MAX_REQUEST_BLOB_SIDECARS = 768
|
|
271
|
+
* </spec>
|
|
272
|
+
*/
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
#### SSZ Objects
|
|
276
|
+
|
|
277
|
+
These are items found in the `Containers` section of the specifications.
|
|
278
|
+
|
|
279
|
+
```
|
|
280
|
+
/*
|
|
281
|
+
* <spec ssz_object="ConsolidationRequest" fork="electra" style="full">
|
|
282
|
+
* class ConsolidationRequest(Container):
|
|
283
|
+
* source_address: ExecutionAddress
|
|
284
|
+
* source_pubkey: BLSPubkey
|
|
285
|
+
* target_pubkey: BLSPubkey
|
|
286
|
+
* </spec>
|
|
287
|
+
*/
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
#### Dataclasses
|
|
291
|
+
|
|
292
|
+
These are classes with the `@dataclass` decorator.
|
|
293
|
+
|
|
294
|
+
```
|
|
295
|
+
/*
|
|
296
|
+
* <spec dataclass="PayloadAttributes" fork="electra" style="full">
|
|
297
|
+
* class PayloadAttributes(object):
|
|
298
|
+
* timestamp: uint64
|
|
299
|
+
* prev_randao: Bytes32
|
|
300
|
+
* suggested_fee_recipient: ExecutionAddress
|
|
301
|
+
* withdrawals: Sequence[Withdrawal]
|
|
302
|
+
* parent_beacon_block_root: Root # [New in Deneb:EIP4788]
|
|
303
|
+
* </spec>
|
|
304
|
+
*/
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
#### Functions
|
|
308
|
+
|
|
309
|
+
These are all the functions found in the specifications.
|
|
310
|
+
|
|
311
|
+
For example, two versions of the same function:
|
|
312
|
+
|
|
313
|
+
```
|
|
314
|
+
/*
|
|
315
|
+
* <spec fn="is_fully_withdrawable_validator" fork="deneb" style="full">
|
|
316
|
+
* def is_fully_withdrawable_validator(validator: Validator, balance: Gwei, epoch: Epoch) -> bool:
|
|
317
|
+
* """
|
|
318
|
+
* Check if ``validator`` is fully withdrawable.
|
|
319
|
+
* """
|
|
320
|
+
* return (
|
|
321
|
+
* has_eth1_withdrawal_credential(validator)
|
|
322
|
+
* and validator.withdrawable_epoch <= epoch
|
|
323
|
+
* and balance > 0
|
|
324
|
+
* )
|
|
325
|
+
* </spec>
|
|
326
|
+
*/
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
```
|
|
330
|
+
/*
|
|
331
|
+
* <spec fn="is_fully_withdrawable_validator" fork="electra" style="full">
|
|
332
|
+
* def is_fully_withdrawable_validator(validator: Validator, balance: Gwei, epoch: Epoch) -> bool:
|
|
333
|
+
* """
|
|
334
|
+
* Check if ``validator`` is fully withdrawable.
|
|
335
|
+
* """
|
|
336
|
+
* return (
|
|
337
|
+
* has_execution_withdrawal_credential(validator) # [Modified in Electra:EIP7251]
|
|
338
|
+
* and validator.withdrawable_epoch <= epoch
|
|
339
|
+
* and balance > 0
|
|
340
|
+
* )
|
|
341
|
+
* </spec>
|
|
342
|
+
*/
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
With functions, it's possible to specify which line/lines should be displayed. For example:
|
|
346
|
+
|
|
347
|
+
```
|
|
348
|
+
/*
|
|
349
|
+
* <spec fn="is_fully_withdrawable_validator" fork="electra" style="full" lines="5-9">
|
|
350
|
+
* return (
|
|
351
|
+
* has_execution_withdrawal_credential(validator) # [Modified in Electra:EIP7251]
|
|
352
|
+
* and validator.withdrawable_epoch <= epoch
|
|
353
|
+
* and balance > 0
|
|
354
|
+
* )
|
|
355
|
+
* </spec>
|
|
356
|
+
*/
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
Note that the content is automatically dedented.
|
|
360
|
+
|
|
361
|
+
Or, to display just a single line, only specify a single number. For example:
|
|
362
|
+
|
|
363
|
+
```
|
|
364
|
+
/*
|
|
365
|
+
* <spec fn="is_fully_withdrawable_validator" fork="electra" style="full" lines="6">
|
|
366
|
+
* has_execution_withdrawal_credential(validator) # [Modified in Electra:EIP7251]
|
|
367
|
+
* </spec>
|
|
368
|
+
*/
|
|
369
|
+
```
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
ethspecify/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
ethspecify/cli.py,sha256=EFjH1ioHibDG1U-6XiozQ2aT2j-GyaWhUJJ-QMALrLc,925
|
|
3
|
+
ethspecify/core.py,sha256=sxmTdXCQIpaAjebMG0NE-7mTf4m0NG_J9IjgGlmOTVk,12064
|
|
4
|
+
ethspecify-0.1.0.dist-info/LICENSE,sha256=Awxsr73mm9YMBVhBYnzeI7bNdRd-bH6RDtO5ItG0DaM,1071
|
|
5
|
+
ethspecify-0.1.0.dist-info/METADATA,sha256=-ot8oXPC7HfNHVD6_ZCcwn7oZJ6EHN5bJeQL9ERByDg,9965
|
|
6
|
+
ethspecify-0.1.0.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
|
|
7
|
+
ethspecify-0.1.0.dist-info/entry_points.txt,sha256=09viGkCg9J3h0c9BFRN-BKaJUEaIc4JyULNgBP5EL_g,51
|
|
8
|
+
ethspecify-0.1.0.dist-info/top_level.txt,sha256=0klaMvlVyOkXW09fwZTijJpdybITEp2c9zQKV5v30VM,11
|
|
9
|
+
ethspecify-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
ethspecify
|