ethspecify 0.2.9__py3-none-any.whl → 0.3.1__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/cli.py +55 -2
- ethspecify/core.py +426 -13
- ethspecify-0.3.1.dist-info/METADATA +237 -0
- ethspecify-0.3.1.dist-info/RECORD +9 -0
- ethspecify-0.2.9.dist-info/METADATA +0 -351
- ethspecify-0.2.9.dist-info/RECORD +0 -9
- {ethspecify-0.2.9.dist-info → ethspecify-0.3.1.dist-info}/WHEEL +0 -0
- {ethspecify-0.2.9.dist-info → ethspecify-0.3.1.dist-info}/entry_points.txt +0 -0
- {ethspecify-0.2.9.dist-info → ethspecify-0.3.1.dist-info}/licenses/LICENSE +0 -0
- {ethspecify-0.2.9.dist-info → ethspecify-0.3.1.dist-info}/top_level.txt +0 -0
ethspecify/cli.py
CHANGED
|
@@ -3,11 +3,11 @@ import json
|
|
|
3
3
|
import os
|
|
4
4
|
import sys
|
|
5
5
|
|
|
6
|
-
from .core import grep, replace_spec_tags, get_pyspec, get_latest_fork, get_spec_item_history, load_config, run_checks
|
|
6
|
+
from .core import grep, replace_spec_tags, get_pyspec, get_latest_fork, get_spec_item_history, load_config, run_checks, sort_specref_yaml, generate_specref_files
|
|
7
7
|
|
|
8
8
|
|
|
9
9
|
def process(args):
|
|
10
|
-
"""Process all spec tags."""
|
|
10
|
+
"""Process all spec tags and sort specref YAML files."""
|
|
11
11
|
project_dir = os.path.abspath(os.path.expanduser(args.path))
|
|
12
12
|
if not os.path.isdir(project_dir):
|
|
13
13
|
print(f"Error: The directory {repr(project_dir)} does not exist.")
|
|
@@ -16,10 +16,26 @@ def process(args):
|
|
|
16
16
|
# Load config once from the project directory
|
|
17
17
|
config = load_config(project_dir)
|
|
18
18
|
|
|
19
|
+
# Process spec tags in files
|
|
19
20
|
for f in grep(project_dir, r"<spec\b.*?>", args.exclude):
|
|
20
21
|
print(f"Processing file: {f}")
|
|
21
22
|
replace_spec_tags(f, config)
|
|
22
23
|
|
|
24
|
+
# Sort specref YAML files if they exist in config
|
|
25
|
+
specrefs_config = config.get('specrefs', {})
|
|
26
|
+
if isinstance(specrefs_config, dict):
|
|
27
|
+
specrefs_files = specrefs_config.get('files', [])
|
|
28
|
+
elif isinstance(specrefs_config, list):
|
|
29
|
+
specrefs_files = specrefs_config
|
|
30
|
+
else:
|
|
31
|
+
specrefs_files = []
|
|
32
|
+
|
|
33
|
+
for yaml_file in specrefs_files:
|
|
34
|
+
yaml_path = os.path.join(project_dir, yaml_file)
|
|
35
|
+
if os.path.exists(yaml_path):
|
|
36
|
+
if not sort_specref_yaml(yaml_path):
|
|
37
|
+
print(f"Error sorting: {yaml_file}")
|
|
38
|
+
|
|
23
39
|
return 0
|
|
24
40
|
|
|
25
41
|
|
|
@@ -166,6 +182,28 @@ def list_forks(args):
|
|
|
166
182
|
return 0
|
|
167
183
|
|
|
168
184
|
|
|
185
|
+
def init(args):
|
|
186
|
+
"""Initialize a specrefs directory with basic configuration and empty source mappings."""
|
|
187
|
+
output_dir = args.path or "specrefs"
|
|
188
|
+
version = args.version
|
|
189
|
+
|
|
190
|
+
# Check if output directory already exists
|
|
191
|
+
if os.path.exists(output_dir):
|
|
192
|
+
print(f"Error: directory {repr(output_dir)} already exists.")
|
|
193
|
+
print("Please specify a different directory or remove the existing one.")
|
|
194
|
+
return 1
|
|
195
|
+
|
|
196
|
+
try:
|
|
197
|
+
# Generate the specref files
|
|
198
|
+
print(f"Initializing specrefs directory: {version}")
|
|
199
|
+
generate_specref_files(output_dir, version, "mainnet")
|
|
200
|
+
print(f"Successfully created specrefs directory at: {output_dir}")
|
|
201
|
+
return 0
|
|
202
|
+
except Exception as e:
|
|
203
|
+
print(f"Error: {e}")
|
|
204
|
+
return 1
|
|
205
|
+
|
|
206
|
+
|
|
169
207
|
def main():
|
|
170
208
|
parser = argparse.ArgumentParser(
|
|
171
209
|
description="Process files containing <spec> tags."
|
|
@@ -240,6 +278,21 @@ def main():
|
|
|
240
278
|
help="Output format (text or json)",
|
|
241
279
|
)
|
|
242
280
|
|
|
281
|
+
# Parser for 'init' command
|
|
282
|
+
init_parser = subparsers.add_parser("init", help="Initialize a specrefs directory")
|
|
283
|
+
init_parser.set_defaults(func=init)
|
|
284
|
+
init_parser.add_argument(
|
|
285
|
+
"version",
|
|
286
|
+
type=str,
|
|
287
|
+
help="Specification version (e.g., 'nightly' or 'v1.6.0-alpha.5')",
|
|
288
|
+
)
|
|
289
|
+
init_parser.add_argument(
|
|
290
|
+
"--path",
|
|
291
|
+
type=str,
|
|
292
|
+
help="Output directory for specrefs (default: specrefs)",
|
|
293
|
+
default="specrefs",
|
|
294
|
+
)
|
|
295
|
+
|
|
243
296
|
# Default to 'process' if no args are provided
|
|
244
297
|
if len(sys.argv) == 1:
|
|
245
298
|
sys.argv.insert(1, "process")
|
ethspecify/core.py
CHANGED
|
@@ -11,6 +11,90 @@ import tokenize
|
|
|
11
11
|
import yaml
|
|
12
12
|
|
|
13
13
|
|
|
14
|
+
def validate_exception_items(exceptions, version):
|
|
15
|
+
"""
|
|
16
|
+
Validate that exception items actually exist in the spec.
|
|
17
|
+
Raises an exception if any item doesn't exist.
|
|
18
|
+
"""
|
|
19
|
+
if not exceptions:
|
|
20
|
+
return
|
|
21
|
+
|
|
22
|
+
# Get the pyspec data
|
|
23
|
+
try:
|
|
24
|
+
pyspec = get_pyspec(version)
|
|
25
|
+
except Exception as e:
|
|
26
|
+
print(f"Warning: Could not validate exceptions - failed to load pyspec: {e}")
|
|
27
|
+
return
|
|
28
|
+
|
|
29
|
+
# Map exception keys to pyspec keys
|
|
30
|
+
exception_to_pyspec_map = {
|
|
31
|
+
'functions': 'functions',
|
|
32
|
+
'fn': 'functions',
|
|
33
|
+
'constants': 'constant_vars',
|
|
34
|
+
'constant_variables': 'constant_vars',
|
|
35
|
+
'constant_var': 'constant_vars',
|
|
36
|
+
'configs': 'config_vars',
|
|
37
|
+
'config_variables': 'config_vars',
|
|
38
|
+
'config_var': 'config_vars',
|
|
39
|
+
'presets': 'preset_vars',
|
|
40
|
+
'preset_variables': 'preset_vars',
|
|
41
|
+
'preset_var': 'preset_vars',
|
|
42
|
+
'ssz_objects': 'ssz_objects',
|
|
43
|
+
'containers': 'ssz_objects',
|
|
44
|
+
'container': 'ssz_objects',
|
|
45
|
+
'dataclasses': 'dataclasses',
|
|
46
|
+
'dataclass': 'dataclasses',
|
|
47
|
+
'custom_types': 'custom_types',
|
|
48
|
+
'custom_type': 'custom_types'
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
errors = []
|
|
52
|
+
|
|
53
|
+
for exception_key, exception_items in exceptions.items():
|
|
54
|
+
# Get the corresponding pyspec key
|
|
55
|
+
pyspec_key = exception_to_pyspec_map.get(exception_key)
|
|
56
|
+
if not pyspec_key:
|
|
57
|
+
errors.append(f"Unknown exception type: '{exception_key}'")
|
|
58
|
+
continue
|
|
59
|
+
|
|
60
|
+
# Ensure exception_items is a list
|
|
61
|
+
if not isinstance(exception_items, list):
|
|
62
|
+
exception_items = [exception_items]
|
|
63
|
+
|
|
64
|
+
for item in exception_items:
|
|
65
|
+
# Parse item#fork format
|
|
66
|
+
if '#' in item:
|
|
67
|
+
item_name, fork = item.split('#', 1)
|
|
68
|
+
else:
|
|
69
|
+
# If no fork specified, we'll check if it exists in any fork
|
|
70
|
+
item_name = item
|
|
71
|
+
fork = None
|
|
72
|
+
|
|
73
|
+
# Check if the item exists
|
|
74
|
+
item_found = False
|
|
75
|
+
if fork:
|
|
76
|
+
# Check specific fork
|
|
77
|
+
if ('mainnet' in pyspec and
|
|
78
|
+
fork in pyspec['mainnet'] and
|
|
79
|
+
pyspec_key in pyspec['mainnet'][fork] and
|
|
80
|
+
item_name in pyspec['mainnet'][fork][pyspec_key]):
|
|
81
|
+
item_found = True
|
|
82
|
+
else:
|
|
83
|
+
# Check if item exists in any fork
|
|
84
|
+
if 'mainnet' in pyspec:
|
|
85
|
+
for check_fork in pyspec['mainnet']:
|
|
86
|
+
if (pyspec_key in pyspec['mainnet'][check_fork] and
|
|
87
|
+
item_name in pyspec['mainnet'][check_fork][pyspec_key]):
|
|
88
|
+
item_found = True
|
|
89
|
+
break
|
|
90
|
+
|
|
91
|
+
if not item_found:
|
|
92
|
+
errors.append(f"invalid key: {exception_key}.{item_name}{"#" + fork if fork else ""}")
|
|
93
|
+
|
|
94
|
+
if errors:
|
|
95
|
+
error_msg = "Invalid exception items in configuration:\n" + "\n".join(f" - {e}" for e in errors)
|
|
96
|
+
raise Exception(error_msg)
|
|
97
|
+
|
|
14
98
|
def load_config(directory=None):
|
|
15
99
|
"""
|
|
16
100
|
Load configuration from .ethspecify.yml file in the specified directory.
|
|
@@ -25,7 +109,22 @@ def load_config(directory=None):
|
|
|
25
109
|
try:
|
|
26
110
|
with open(config_path, 'r') as f:
|
|
27
111
|
config = yaml.safe_load(f)
|
|
28
|
-
|
|
112
|
+
if not config:
|
|
113
|
+
return {}
|
|
114
|
+
|
|
115
|
+
# Get version from config, default to 'nightly'
|
|
116
|
+
version = config.get('version', 'nightly')
|
|
117
|
+
|
|
118
|
+
# Validate exceptions in root config
|
|
119
|
+
if 'exceptions' in config:
|
|
120
|
+
validate_exception_items(config['exceptions'], version)
|
|
121
|
+
|
|
122
|
+
# Also validate exceptions in specrefs section if present
|
|
123
|
+
if 'specrefs' in config and isinstance(config['specrefs'], dict):
|
|
124
|
+
if 'exceptions' in config['specrefs']:
|
|
125
|
+
validate_exception_items(config['specrefs']['exceptions'], version)
|
|
126
|
+
|
|
127
|
+
return config
|
|
29
128
|
except (yaml.YAMLError, IOError) as e:
|
|
30
129
|
print(f"Warning: Error reading .ethspecify.yml file: {e}")
|
|
31
130
|
return {}
|
|
@@ -477,6 +576,140 @@ def extract_attributes(tag):
|
|
|
477
576
|
return dict(attr_pattern.findall(tag))
|
|
478
577
|
|
|
479
578
|
|
|
579
|
+
def sort_specref_yaml(yaml_file):
|
|
580
|
+
"""
|
|
581
|
+
Sort specref entries in a YAML file by their name field.
|
|
582
|
+
Preserves formatting and adds single blank lines between entries.
|
|
583
|
+
"""
|
|
584
|
+
if not os.path.exists(yaml_file):
|
|
585
|
+
return False
|
|
586
|
+
|
|
587
|
+
try:
|
|
588
|
+
with open(yaml_file, 'r') as f:
|
|
589
|
+
content_str = f.read()
|
|
590
|
+
|
|
591
|
+
# Try to fix common YAML issues with unquoted search strings containing colons
|
|
592
|
+
# This is the same fix used in check_source_files
|
|
593
|
+
content_str = re.sub(r'(\s+search:\s+)([^"\n]+:)(\s*$)', r'\1"\2"\3', content_str, flags=re.MULTILINE)
|
|
594
|
+
|
|
595
|
+
try:
|
|
596
|
+
content = yaml.safe_load(content_str)
|
|
597
|
+
except yaml.YAMLError:
|
|
598
|
+
# Fall back to FullLoader if safe_load fails
|
|
599
|
+
content = yaml.load(content_str, Loader=yaml.FullLoader)
|
|
600
|
+
|
|
601
|
+
if not content:
|
|
602
|
+
return False
|
|
603
|
+
|
|
604
|
+
# Handle both array of objects and single object formats
|
|
605
|
+
if isinstance(content, list):
|
|
606
|
+
# Sort the list by 'name' field if it exists
|
|
607
|
+
# Special handling for fork ordering within the same item
|
|
608
|
+
def sort_key(x):
|
|
609
|
+
name = x.get('name', '') if isinstance(x, dict) else str(x)
|
|
610
|
+
|
|
611
|
+
# Known fork names for ordering
|
|
612
|
+
forks = ['phase0', 'altair', 'bellatrix', 'capella', 'deneb', 'electra']
|
|
613
|
+
|
|
614
|
+
# Check if name contains a # separator (like "slash_validator#phase0")
|
|
615
|
+
if '#' in name:
|
|
616
|
+
base_name, fork = name.rsplit('#', 1)
|
|
617
|
+
# Define fork order based on known forks list
|
|
618
|
+
fork_lower = fork.lower()
|
|
619
|
+
if fork_lower in forks:
|
|
620
|
+
fork_order = forks.index(fork_lower)
|
|
621
|
+
else:
|
|
622
|
+
# Unknown forks go after known ones, sorted alphabetically
|
|
623
|
+
fork_order = len(forks)
|
|
624
|
+
return (base_name, fork_order, fork)
|
|
625
|
+
else:
|
|
626
|
+
# Check if name ends with a fork name (like "BeaconStatePhase0")
|
|
627
|
+
name_lower = name.lower()
|
|
628
|
+
for i, fork in enumerate(forks):
|
|
629
|
+
if name_lower.endswith(fork):
|
|
630
|
+
# Extract base name
|
|
631
|
+
base_name = name[:-len(fork)]
|
|
632
|
+
return (base_name, i, name)
|
|
633
|
+
|
|
634
|
+
# No fork pattern found, just sort by name
|
|
635
|
+
return (name, 0, '')
|
|
636
|
+
|
|
637
|
+
sorted_content = sorted(content, key=sort_key)
|
|
638
|
+
|
|
639
|
+
# Custom YAML writing to preserve formatting
|
|
640
|
+
output_lines = []
|
|
641
|
+
for i, item in enumerate(sorted_content):
|
|
642
|
+
if i > 0:
|
|
643
|
+
# Add a single blank line between entries
|
|
644
|
+
output_lines.append("")
|
|
645
|
+
|
|
646
|
+
# Start each entry with a dash
|
|
647
|
+
first_line = True
|
|
648
|
+
for key, value in item.items():
|
|
649
|
+
if first_line:
|
|
650
|
+
prefix = "- "
|
|
651
|
+
first_line = False
|
|
652
|
+
else:
|
|
653
|
+
prefix = " "
|
|
654
|
+
|
|
655
|
+
if key == 'spec':
|
|
656
|
+
# Preserve spec content as-is, using literal style
|
|
657
|
+
output_lines.append(f"{prefix}{key}: |")
|
|
658
|
+
# Indent the spec content - preserve it exactly as-is
|
|
659
|
+
spec_lines = value.rstrip().split('\n') if isinstance(value, str) else str(value).rstrip().split('\n')
|
|
660
|
+
for spec_line in spec_lines:
|
|
661
|
+
output_lines.append(f" {spec_line}")
|
|
662
|
+
elif key == 'sources':
|
|
663
|
+
if isinstance(value, list) and len(value) == 0:
|
|
664
|
+
# Keep empty lists on the same line for clarity
|
|
665
|
+
output_lines.append(f"{prefix}{key}: []")
|
|
666
|
+
else:
|
|
667
|
+
output_lines.append(f"{prefix}{key}:")
|
|
668
|
+
if isinstance(value, list):
|
|
669
|
+
for source in value:
|
|
670
|
+
if isinstance(source, dict):
|
|
671
|
+
output_lines.append(f" - file: {source.get('file', '')}")
|
|
672
|
+
if 'search' in source:
|
|
673
|
+
search_val = source['search']
|
|
674
|
+
# Only quote if:
|
|
675
|
+
# 1. Contains a colon followed by space or end of string (YAML mapping issue)
|
|
676
|
+
# 2. Not already quoted
|
|
677
|
+
# 3. Doesn't contain internal quotes (like regex patterns)
|
|
678
|
+
if ((':' in search_val or search_val.endswith(':')) and
|
|
679
|
+
not (search_val.startswith('"') and search_val.endswith('"')) and
|
|
680
|
+
'"' not in search_val):
|
|
681
|
+
# Only quote simple strings with colons, not complex patterns
|
|
682
|
+
if not any(char in search_val for char in ['\\', '*', '|', '[', ']', '(', ')']):
|
|
683
|
+
search_val = f'"{search_val}"'
|
|
684
|
+
output_lines.append(f" search: {search_val}")
|
|
685
|
+
if 'regex' in source:
|
|
686
|
+
# Keep boolean values lowercase for consistency
|
|
687
|
+
regex_val = str(source['regex']).lower()
|
|
688
|
+
output_lines.append(f" regex: {regex_val}")
|
|
689
|
+
else:
|
|
690
|
+
output_lines.append(f" - {source}")
|
|
691
|
+
else:
|
|
692
|
+
# Handle other fields - don't escape or modify the value
|
|
693
|
+
output_lines.append(f"{prefix}{key}: {value}")
|
|
694
|
+
|
|
695
|
+
# Strip trailing whitespace from all lines
|
|
696
|
+
output_lines = [line.rstrip() for line in output_lines]
|
|
697
|
+
|
|
698
|
+
# Write back the sorted content
|
|
699
|
+
with open(yaml_file, 'w') as f:
|
|
700
|
+
f.write('\n'.join(output_lines))
|
|
701
|
+
f.write('\n') # End file with newline
|
|
702
|
+
|
|
703
|
+
return True
|
|
704
|
+
elif isinstance(content, dict):
|
|
705
|
+
# If it's a single dict, we can't sort it
|
|
706
|
+
return False
|
|
707
|
+
except (yaml.YAMLError, IOError) as e:
|
|
708
|
+
print(f"Error sorting {yaml_file}: {e}")
|
|
709
|
+
return False
|
|
710
|
+
|
|
711
|
+
return False
|
|
712
|
+
|
|
480
713
|
def replace_spec_tags(file_path, config=None):
|
|
481
714
|
with open(file_path, 'r') as file:
|
|
482
715
|
content = file.read()
|
|
@@ -540,7 +773,7 @@ def replace_spec_tags(file_path, config=None):
|
|
|
540
773
|
prefix = content[:match.start()].splitlines()[-1]
|
|
541
774
|
prefixed_spec = "\n".join(
|
|
542
775
|
f"{prefix}{line}" if line.rstrip() else prefix.rstrip()
|
|
543
|
-
for line in spec_content.
|
|
776
|
+
for line in spec_content.split("\n")
|
|
544
777
|
)
|
|
545
778
|
updated_tag = f"{new_opening}\n{prefixed_spec}\n{prefix}</spec>"
|
|
546
779
|
return updated_tag
|
|
@@ -632,19 +865,23 @@ def check_source_files(yaml_file, project_root, exceptions=None):
|
|
|
632
865
|
if not spec_ref and 'name' in item:
|
|
633
866
|
spec_ref = item['name']
|
|
634
867
|
|
|
868
|
+
# Extract item name and fork for exception checking
|
|
869
|
+
item_name_for_exception = None
|
|
870
|
+
fork_for_exception = None
|
|
871
|
+
if spec_ref and '#' in spec_ref and '.' in spec_ref:
|
|
872
|
+
# Format: "functions.item_name#fork"
|
|
873
|
+
_, item_with_fork = spec_ref.split('.', 1)
|
|
874
|
+
if '#' in item_with_fork:
|
|
875
|
+
item_name_for_exception, fork_for_exception = item_with_fork.split('#', 1)
|
|
876
|
+
|
|
635
877
|
# Check if sources list is empty
|
|
636
878
|
if not item['sources']:
|
|
637
879
|
if spec_ref:
|
|
638
|
-
#
|
|
639
|
-
if
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
item_name, fork = item_with_fork.split('#', 1)
|
|
644
|
-
# Check if this item is in exceptions
|
|
645
|
-
if is_excepted(item_name, fork, exceptions):
|
|
646
|
-
total_count += 1
|
|
647
|
-
continue
|
|
880
|
+
# Check if this item is in exceptions
|
|
881
|
+
if item_name_for_exception and fork_for_exception:
|
|
882
|
+
if is_excepted(item_name_for_exception, fork_for_exception, exceptions):
|
|
883
|
+
total_count += 1
|
|
884
|
+
continue
|
|
648
885
|
|
|
649
886
|
errors.append(f"EMPTY SOURCES: {spec_ref}")
|
|
650
887
|
else:
|
|
@@ -654,6 +891,13 @@ def check_source_files(yaml_file, project_root, exceptions=None):
|
|
|
654
891
|
total_count += 1
|
|
655
892
|
continue
|
|
656
893
|
|
|
894
|
+
# Check if item has non-empty sources but is in exceptions
|
|
895
|
+
if item_name_for_exception and fork_for_exception:
|
|
896
|
+
if is_excepted(item_name_for_exception, fork_for_exception, exceptions):
|
|
897
|
+
errors.append(f"EXCEPTION CONFLICT: {spec_ref} has a specref")
|
|
898
|
+
total_count += 1
|
|
899
|
+
continue
|
|
900
|
+
|
|
657
901
|
for source in item['sources']:
|
|
658
902
|
# All sources now use the standardized dict format with file and optional search
|
|
659
903
|
if not isinstance(source, dict) or 'file' not in source:
|
|
@@ -964,7 +1208,7 @@ def process_generated_specrefs(specrefs, exceptions, version):
|
|
|
964
1208
|
# Check if singular form exists when we have plural
|
|
965
1209
|
elif exception_key.endswith('s') and exception_key[:-1] in exceptions:
|
|
966
1210
|
type_exceptions = exceptions[exception_key[:-1]]
|
|
967
|
-
|
|
1211
|
+
|
|
968
1212
|
# Special handling for ssz_objects/containers
|
|
969
1213
|
if spec_type in ['ssz_object', 'container'] and not type_exceptions:
|
|
970
1214
|
# Check for 'containers' as an alternative key
|
|
@@ -1274,3 +1518,172 @@ def run_checks(project_dir, config):
|
|
|
1274
1518
|
overall_success = False
|
|
1275
1519
|
|
|
1276
1520
|
return overall_success, results
|
|
1521
|
+
|
|
1522
|
+
|
|
1523
|
+
def generate_specref_files(output_dir, version="nightly", preset="mainnet"):
|
|
1524
|
+
"""
|
|
1525
|
+
Generate specref YAML files without sources for manual mapping.
|
|
1526
|
+
Creates a basic directory structure with empty sources.
|
|
1527
|
+
"""
|
|
1528
|
+
# Create output directory if it doesn't exist
|
|
1529
|
+
os.makedirs(output_dir, exist_ok=True)
|
|
1530
|
+
|
|
1531
|
+
# Get all spec items
|
|
1532
|
+
pyspec = get_pyspec(version)
|
|
1533
|
+
if preset not in pyspec:
|
|
1534
|
+
raise ValueError(f"Preset '{preset}' not found")
|
|
1535
|
+
|
|
1536
|
+
# Get all forks in chronological order, excluding EIP forks
|
|
1537
|
+
all_forks = sorted(
|
|
1538
|
+
[fork for fork in pyspec[preset].keys() if not fork.startswith("eip")],
|
|
1539
|
+
key=lambda x: (x != "phase0", x)
|
|
1540
|
+
)
|
|
1541
|
+
|
|
1542
|
+
# Map history keys to file names and spec attribute names
|
|
1543
|
+
category_map = {
|
|
1544
|
+
'constant_vars': ('constants.yml', 'constant_var'),
|
|
1545
|
+
'config_vars': ('configs.yml', 'config_var'),
|
|
1546
|
+
'preset_vars': ('presets.yml', 'preset_var'),
|
|
1547
|
+
'functions': ('functions.yml', 'fn'),
|
|
1548
|
+
'ssz_objects': ('containers.yml', 'container'),
|
|
1549
|
+
'dataclasses': ('dataclasses.yml', 'dataclass'),
|
|
1550
|
+
'custom_types': ('types.yml', 'custom_type'),
|
|
1551
|
+
}
|
|
1552
|
+
|
|
1553
|
+
# Collect all items organized by category
|
|
1554
|
+
items_by_category = {cat: {} for cat in category_map.keys()}
|
|
1555
|
+
|
|
1556
|
+
for fork in all_forks:
|
|
1557
|
+
if fork not in pyspec[preset]:
|
|
1558
|
+
continue
|
|
1559
|
+
fork_data = pyspec[preset][fork]
|
|
1560
|
+
|
|
1561
|
+
for category in items_by_category.keys():
|
|
1562
|
+
if category not in fork_data:
|
|
1563
|
+
continue
|
|
1564
|
+
|
|
1565
|
+
for item_name, item_data in fork_data[category].items():
|
|
1566
|
+
# Track which forks have this item
|
|
1567
|
+
if item_name not in items_by_category[category]:
|
|
1568
|
+
items_by_category[category][item_name] = []
|
|
1569
|
+
items_by_category[category][item_name].append((fork, item_data))
|
|
1570
|
+
|
|
1571
|
+
# Generate YAML files for each category
|
|
1572
|
+
for category, (filename, spec_attr) in category_map.items():
|
|
1573
|
+
if not items_by_category[category]:
|
|
1574
|
+
continue
|
|
1575
|
+
|
|
1576
|
+
output_path = os.path.join(output_dir, filename)
|
|
1577
|
+
entries = []
|
|
1578
|
+
|
|
1579
|
+
# Sort items alphabetically
|
|
1580
|
+
for item_name in sorted(items_by_category[category].keys()):
|
|
1581
|
+
forks_data = items_by_category[category][item_name]
|
|
1582
|
+
|
|
1583
|
+
# Find all unique versions of this item (where content differs)
|
|
1584
|
+
versions = [] # List of (fork, item_data, spec_content)
|
|
1585
|
+
prev_content = None
|
|
1586
|
+
|
|
1587
|
+
for fork, item_data in forks_data:
|
|
1588
|
+
# Build the spec content based on category
|
|
1589
|
+
if category == 'functions':
|
|
1590
|
+
spec_content = item_data
|
|
1591
|
+
elif category in ['constant_vars', 'config_vars', 'preset_vars']:
|
|
1592
|
+
# item_data is a list: [type, value, ...]
|
|
1593
|
+
if isinstance(item_data, (list, tuple)) and len(item_data) >= 2:
|
|
1594
|
+
type_info = item_data[0]
|
|
1595
|
+
value = item_data[1]
|
|
1596
|
+
if type_info:
|
|
1597
|
+
spec_content = f"{item_name}: {type_info} = {value}"
|
|
1598
|
+
else:
|
|
1599
|
+
spec_content = f"{item_name} = {value}"
|
|
1600
|
+
else:
|
|
1601
|
+
spec_content = str(item_data)
|
|
1602
|
+
elif category == 'ssz_objects':
|
|
1603
|
+
spec_content = item_data
|
|
1604
|
+
elif category == 'dataclasses':
|
|
1605
|
+
spec_content = item_data.replace("@dataclass\n", "")
|
|
1606
|
+
elif category == 'custom_types':
|
|
1607
|
+
# custom_types are simple type aliases: TypeName = SomeType
|
|
1608
|
+
spec_content = f"{item_name} = {item_data}"
|
|
1609
|
+
else:
|
|
1610
|
+
spec_content = str(item_data)
|
|
1611
|
+
|
|
1612
|
+
# Only add this version if it's different from the previous one
|
|
1613
|
+
if prev_content is None or spec_content != prev_content:
|
|
1614
|
+
versions.append((fork, item_data, spec_content))
|
|
1615
|
+
prev_content = spec_content
|
|
1616
|
+
|
|
1617
|
+
# Create entries based on number of unique versions
|
|
1618
|
+
use_fork_suffix = len(versions) > 1
|
|
1619
|
+
|
|
1620
|
+
for idx, (fork, item_data, spec_content) in enumerate(versions):
|
|
1621
|
+
# Calculate hash of current version
|
|
1622
|
+
hash_value = hashlib.sha256(spec_content.encode('utf-8')).hexdigest()[:8]
|
|
1623
|
+
|
|
1624
|
+
# For multiple versions after the first, use diff style
|
|
1625
|
+
if use_fork_suffix and idx > 0:
|
|
1626
|
+
# Get previous version for diff
|
|
1627
|
+
prev_fork, _, prev_spec_content = versions[idx - 1]
|
|
1628
|
+
|
|
1629
|
+
# Generate diff
|
|
1630
|
+
diff_content = diff(prev_fork, strip_comments(prev_spec_content), fork, strip_comments(spec_content))
|
|
1631
|
+
|
|
1632
|
+
# Build spec tag with style="diff"
|
|
1633
|
+
spec_tag = f'<spec {spec_attr}="{item_name}" fork="{fork}" style="diff" hash="{hash_value}">'
|
|
1634
|
+
content = diff_content
|
|
1635
|
+
else:
|
|
1636
|
+
# Build spec tag without style="diff"
|
|
1637
|
+
spec_tag = f'<spec {spec_attr}="{item_name}" fork="{fork}" hash="{hash_value}">'
|
|
1638
|
+
content = spec_content
|
|
1639
|
+
|
|
1640
|
+
# Create entry
|
|
1641
|
+
entry_name = f'{item_name}#{fork}' if use_fork_suffix else item_name
|
|
1642
|
+
entry = {
|
|
1643
|
+
'name': entry_name,
|
|
1644
|
+
'sources': [],
|
|
1645
|
+
'spec': f'{spec_tag}\n{content}\n</spec>'
|
|
1646
|
+
}
|
|
1647
|
+
entries.append(entry)
|
|
1648
|
+
|
|
1649
|
+
# Write YAML file
|
|
1650
|
+
if entries:
|
|
1651
|
+
with open(output_path, 'w') as f:
|
|
1652
|
+
for i, entry in enumerate(entries):
|
|
1653
|
+
if i > 0:
|
|
1654
|
+
f.write('\n')
|
|
1655
|
+
f.write(f'- name: {entry["name"]}\n')
|
|
1656
|
+
f.write(' sources: []\n')
|
|
1657
|
+
f.write(' spec: |\n')
|
|
1658
|
+
for line in entry['spec'].split('\n'):
|
|
1659
|
+
f.write(f' {line}\n')
|
|
1660
|
+
|
|
1661
|
+
# Create .ethspecify.yml config file
|
|
1662
|
+
config_path = os.path.join(output_dir, '.ethspecify.yml')
|
|
1663
|
+
with open(config_path, 'w') as f:
|
|
1664
|
+
f.write(f'version: {version}\n')
|
|
1665
|
+
f.write('style: full\n')
|
|
1666
|
+
f.write('\n')
|
|
1667
|
+
f.write('specrefs:\n')
|
|
1668
|
+
f.write(' files:\n')
|
|
1669
|
+
for category, (filename, _) in category_map.items():
|
|
1670
|
+
if items_by_category[category]:
|
|
1671
|
+
f.write(f' - {filename}\n')
|
|
1672
|
+
f.write('\n')
|
|
1673
|
+
f.write(' exceptions:\n')
|
|
1674
|
+
f.write(' # Add any exceptions here\n')
|
|
1675
|
+
|
|
1676
|
+
# Strip trailing whitespace from all generated files
|
|
1677
|
+
all_files = [config_path]
|
|
1678
|
+
for category, (filename, _) in category_map.items():
|
|
1679
|
+
if items_by_category[category]:
|
|
1680
|
+
all_files.append(os.path.join(output_dir, filename))
|
|
1681
|
+
|
|
1682
|
+
for file_path in all_files:
|
|
1683
|
+
with open(file_path, 'r') as f:
|
|
1684
|
+
lines = f.readlines()
|
|
1685
|
+
with open(file_path, 'w') as f:
|
|
1686
|
+
for line in lines:
|
|
1687
|
+
f.write(line.rstrip() + '\n')
|
|
1688
|
+
|
|
1689
|
+
return list(category_map.values())
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ethspecify
|
|
3
|
+
Version: 0.3.1
|
|
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
|
+
Requires-Dist: PyYAML>=6.0
|
|
16
|
+
Dynamic: author
|
|
17
|
+
Dynamic: author-email
|
|
18
|
+
Dynamic: classifier
|
|
19
|
+
Dynamic: description
|
|
20
|
+
Dynamic: description-content-type
|
|
21
|
+
Dynamic: home-page
|
|
22
|
+
Dynamic: license-file
|
|
23
|
+
Dynamic: requires-dist
|
|
24
|
+
Dynamic: requires-python
|
|
25
|
+
Dynamic: summary
|
|
26
|
+
|
|
27
|
+
# ethspecify
|
|
28
|
+
|
|
29
|
+
A tool for referencing the Ethereum specifications in clients.
|
|
30
|
+
|
|
31
|
+
The idea is that ethspecify will help developers keep track of when the specification changes. It
|
|
32
|
+
will also help auditors verify that the client implementations match the specifications. Ideally,
|
|
33
|
+
this is configured as a CI check which notifies client developers when the specification changes.
|
|
34
|
+
When that happens, they can update the implementations appropriately.
|
|
35
|
+
|
|
36
|
+
## Getting started
|
|
37
|
+
|
|
38
|
+
### Installation
|
|
39
|
+
|
|
40
|
+
```
|
|
41
|
+
pipx install ethspecify
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### Initialize specrefs
|
|
45
|
+
|
|
46
|
+
From the root of the client source directory, initialize a directory for specrefs:
|
|
47
|
+
|
|
48
|
+
```
|
|
49
|
+
$ ethspecify init v1.6.0-beta.0
|
|
50
|
+
Initializing specrefs directory: v1.6.0-beta.0
|
|
51
|
+
Successfully created specrefs directory at: specrefs
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
This creates a `specrefs` directory with `.ethspecify.yml` and YAML files for each spec category
|
|
55
|
+
(constants, configs, presets, functions, containers, dataclasses, types).
|
|
56
|
+
|
|
57
|
+
### Map sources
|
|
58
|
+
|
|
59
|
+
Edit the YAML files to add sources for where each spec item is implemented.
|
|
60
|
+
|
|
61
|
+
If it's the entire file:
|
|
62
|
+
|
|
63
|
+
```yaml
|
|
64
|
+
- name: BlobParameters
|
|
65
|
+
sources:
|
|
66
|
+
- file: ethereum/spec/src/main/java/tech/pegasys/teku/spec/logic/versions/fulu/helpers/BlobParameters.java
|
|
67
|
+
spec: |
|
|
68
|
+
<spec dataclass="BlobParameters" fork="fulu" hash="a4575aa8">
|
|
69
|
+
class BlobParameters:
|
|
70
|
+
epoch: Epoch
|
|
71
|
+
max_blobs_per_block: uint64
|
|
72
|
+
</spec>
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
If it's multiple entire files:
|
|
76
|
+
|
|
77
|
+
```yaml
|
|
78
|
+
- name: BlobsBundleDeneb
|
|
79
|
+
sources:
|
|
80
|
+
- file: ethereum/spec/src/main/java/tech/pegasys/teku/spec/datastructures/execution/BlobsBundle.java
|
|
81
|
+
- file: ethereum/spec/src/main/java/tech/pegasys/teku/spec/datastructures/builder/BlobsBundleSchema.java
|
|
82
|
+
- file: ethereum/spec/src/main/java/tech/pegasys/teku/spec/datastructures/builder/versions/deneb/BlobsBundleDeneb.java
|
|
83
|
+
- file: ethereum/spec/src/main/java/tech/pegasys/teku/spec/datastructures/builder/versions/deneb/BlobsBundleSchemaDeneb.java
|
|
84
|
+
spec: |
|
|
85
|
+
<spec dataclass="BlobsBundle" fork="deneb" hash="8d6e7be6">
|
|
86
|
+
class BlobsBundle(object):
|
|
87
|
+
commitments: List[KZGCommitment, MAX_BLOB_COMMITMENTS_PER_BLOCK]
|
|
88
|
+
proofs: List[KZGProof, MAX_BLOB_COMMITMENTS_PER_BLOCK]
|
|
89
|
+
blobs: List[Blob, MAX_BLOB_COMMITMENTS_PER_BLOCK]
|
|
90
|
+
</spec>
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
If it's a specific part of a file:
|
|
94
|
+
|
|
95
|
+
```yaml
|
|
96
|
+
- name: EFFECTIVE_BALANCE_INCREMENT
|
|
97
|
+
sources:
|
|
98
|
+
- file: ethereum/spec/src/main/resources/tech/pegasys/teku/spec/config/presets/mainnet/phase0.yaml
|
|
99
|
+
search: "EFFECTIVE_BALANCE_INCREMENT:"
|
|
100
|
+
spec: |
|
|
101
|
+
<spec preset_var="EFFECTIVE_BALANCE_INCREMENT" fork="phase0" hash="23dfe52c">
|
|
102
|
+
EFFECTIVE_BALANCE_INCREMENT: Gwei = 1000000000
|
|
103
|
+
</spec>
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
You can also use regex in the searches if that is necessary:
|
|
107
|
+
|
|
108
|
+
```yaml
|
|
109
|
+
- name: ATTESTATION_DUE_BPS
|
|
110
|
+
sources:
|
|
111
|
+
- file: ethereum/spec/src/main/resources/tech/pegasys/teku/spec/config/configs/mainnet.yaml
|
|
112
|
+
search: "^ATTESTATION_DUE_BPS:"
|
|
113
|
+
regex: true
|
|
114
|
+
spec: |
|
|
115
|
+
<spec config_var="ATTESTATION_DUE_BPS" fork="phase0" hash="929dd1c9">
|
|
116
|
+
ATTESTATION_DUE_BPS: uint64 = 3333
|
|
117
|
+
</spec>
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### Check specrefs
|
|
121
|
+
|
|
122
|
+
Run the check command in CI to verify all spec items are properly mapped:
|
|
123
|
+
|
|
124
|
+
```
|
|
125
|
+
$ ethspecify check --path=specrefs
|
|
126
|
+
MISSING: constants.BLS_MODULUS#deneb
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### Add exceptions
|
|
130
|
+
|
|
131
|
+
Some spec items may not be implemented in your client. Add them to the exceptions list in
|
|
132
|
+
`specrefs/.ethspecify.yml`:
|
|
133
|
+
|
|
134
|
+
```yaml
|
|
135
|
+
specrefs:
|
|
136
|
+
files:
|
|
137
|
+
- containers.yml
|
|
138
|
+
- functions.yml
|
|
139
|
+
# ...
|
|
140
|
+
|
|
141
|
+
exceptions:
|
|
142
|
+
containers:
|
|
143
|
+
# Not defined, unnecessary
|
|
144
|
+
- Eth1Block
|
|
145
|
+
|
|
146
|
+
functions:
|
|
147
|
+
# No light client support
|
|
148
|
+
- is_valid_light_client_header
|
|
149
|
+
- process_light_client_update
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
## Style Options
|
|
153
|
+
|
|
154
|
+
This attribute can be used to change how the specification content is shown.
|
|
155
|
+
|
|
156
|
+
### `hash` (default)
|
|
157
|
+
|
|
158
|
+
This style adds a hash of the specification content to the spec tag, without showing the content.
|
|
159
|
+
|
|
160
|
+
```
|
|
161
|
+
<spec fn="apply_deposit" fork="electra" hash="c723ce7b" />
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
> [!NOTE]
|
|
165
|
+
> The hash is the first 8 characters of the specification content's SHA256 digest.
|
|
166
|
+
|
|
167
|
+
### `full`
|
|
168
|
+
|
|
169
|
+
This style displays the whole content of this specification item, including comments.
|
|
170
|
+
|
|
171
|
+
```
|
|
172
|
+
<spec fn="is_fully_withdrawable_validator" fork="deneb" style="full">
|
|
173
|
+
def is_fully_withdrawable_validator(validator: Validator, balance: Gwei, epoch: Epoch) -> bool:
|
|
174
|
+
"""
|
|
175
|
+
Check if ``validator`` is fully withdrawable.
|
|
176
|
+
"""
|
|
177
|
+
return (
|
|
178
|
+
has_eth1_withdrawal_credential(validator)
|
|
179
|
+
and validator.withdrawable_epoch <= epoch
|
|
180
|
+
and balance > 0
|
|
181
|
+
)
|
|
182
|
+
</spec>
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
### `link`
|
|
186
|
+
|
|
187
|
+
This style displays a GitHub link to the specification item.
|
|
188
|
+
|
|
189
|
+
```
|
|
190
|
+
<spec fn="apply_pending_deposit" fork="electra" style="link" hash="83ee9126">
|
|
191
|
+
https://github.com/ethereum/consensus-specs/blob/dev/specs/electra/beacon-chain.md#new-apply_pending_deposit
|
|
192
|
+
</spec>
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
### `diff`
|
|
196
|
+
|
|
197
|
+
This style displays a diff with the previous fork's version of the specification.
|
|
198
|
+
|
|
199
|
+
```
|
|
200
|
+
<spec ssz_object="BeaconState" fork="electra" style="diff">
|
|
201
|
+
--- deneb
|
|
202
|
+
+++ electra
|
|
203
|
+
@@ -27,3 +27,12 @@
|
|
204
|
+
next_withdrawal_index: WithdrawalIndex
|
|
205
|
+
next_withdrawal_validator_index: ValidatorIndex
|
|
206
|
+
historical_summaries: List[HistoricalSummary, HISTORICAL_ROOTS_LIMIT]
|
|
207
|
+
+ deposit_requests_start_index: uint64
|
|
208
|
+
+ deposit_balance_to_consume: Gwei
|
|
209
|
+
+ exit_balance_to_consume: Gwei
|
|
210
|
+
+ earliest_exit_epoch: Epoch
|
|
211
|
+
+ consolidation_balance_to_consume: Gwei
|
|
212
|
+
+ earliest_consolidation_epoch: Epoch
|
|
213
|
+
+ pending_deposits: List[PendingDeposit, PENDING_DEPOSITS_LIMIT]
|
|
214
|
+
+ pending_partial_withdrawals: List[PendingPartialWithdrawal, PENDING_PARTIAL_WITHDRAWALS_LIMIT]
|
|
215
|
+
+ pending_consolidations: List[PendingConsolidation, PENDING_CONSOLIDATIONS_LIMIT]
|
|
216
|
+
</spec>
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
> [!NOTE]
|
|
220
|
+
> Comments are stripped from the specifications when the `diff` style is used. We do this because
|
|
221
|
+
> these complicate the diff; the "[Modified in Fork]" comments aren't valuable here.
|
|
222
|
+
|
|
223
|
+
This can be used with any specification item, like functions too:
|
|
224
|
+
|
|
225
|
+
```
|
|
226
|
+
<spec fn="is_eligible_for_activation_queue" fork="electra" style="diff">
|
|
227
|
+
--- phase0
|
|
228
|
+
+++ electra
|
|
229
|
+
@@ -4,5 +4,5 @@
|
|
230
|
+
"""
|
|
231
|
+
return (
|
|
232
|
+
validator.activation_eligibility_epoch == FAR_FUTURE_EPOCH
|
|
233
|
+
- and validator.effective_balance == MAX_EFFECTIVE_BALANCE
|
|
234
|
+
+ and validator.effective_balance >= MIN_ACTIVATION_BALANCE
|
|
235
|
+
)
|
|
236
|
+
</spec>
|
|
237
|
+
```
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
ethspecify/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
ethspecify/cli.py,sha256=fbHtny1-VS6IZZlDzPSvkyLr4NDYDYcBXzOiGUGOlyU,10093
|
|
3
|
+
ethspecify/core.py,sha256=gfDBeYzrhNM6wHGtoyy_7SnKUQF_w5iFGljQcdUmaJQ,66694
|
|
4
|
+
ethspecify-0.3.1.dist-info/licenses/LICENSE,sha256=Awxsr73mm9YMBVhBYnzeI7bNdRd-bH6RDtO5ItG0DaM,1071
|
|
5
|
+
ethspecify-0.3.1.dist-info/METADATA,sha256=mI1j8bbLAKBH_dvLHKuP1kgNsdjXnUMcGnS2l2SPh_s,6996
|
|
6
|
+
ethspecify-0.3.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
7
|
+
ethspecify-0.3.1.dist-info/entry_points.txt,sha256=09viGkCg9J3h0c9BFRN-BKaJUEaIc4JyULNgBP5EL_g,51
|
|
8
|
+
ethspecify-0.3.1.dist-info/top_level.txt,sha256=0klaMvlVyOkXW09fwZTijJpdybITEp2c9zQKV5v30VM,11
|
|
9
|
+
ethspecify-0.3.1.dist-info/RECORD,,
|
|
@@ -1,351 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: ethspecify
|
|
3
|
-
Version: 0.2.9
|
|
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
|
-
Requires-Dist: PyYAML>=6.0
|
|
16
|
-
Dynamic: author
|
|
17
|
-
Dynamic: author-email
|
|
18
|
-
Dynamic: classifier
|
|
19
|
-
Dynamic: description
|
|
20
|
-
Dynamic: description-content-type
|
|
21
|
-
Dynamic: home-page
|
|
22
|
-
Dynamic: license-file
|
|
23
|
-
Dynamic: requires-dist
|
|
24
|
-
Dynamic: requires-python
|
|
25
|
-
Dynamic: summary
|
|
26
|
-
|
|
27
|
-
# ethspecify
|
|
28
|
-
|
|
29
|
-
A tool for referencing the Ethereum specifications in clients.
|
|
30
|
-
|
|
31
|
-
The idea is that ethspecify will help developers keep track of when the specification changes. It
|
|
32
|
-
will also help auditors verify that the client implementations match the specifications. Ideally,
|
|
33
|
-
this is configured as a CI check which notifies client developers when the specification changes.
|
|
34
|
-
When that happens, they can update the implementations appropriately.
|
|
35
|
-
|
|
36
|
-
## Getting Started
|
|
37
|
-
|
|
38
|
-
### Installation
|
|
39
|
-
|
|
40
|
-
```
|
|
41
|
-
python3 -mpip install ethspecify
|
|
42
|
-
```
|
|
43
|
-
|
|
44
|
-
### Adding Spec Tags
|
|
45
|
-
|
|
46
|
-
In your client, add HTML tags like this:
|
|
47
|
-
|
|
48
|
-
```
|
|
49
|
-
/*
|
|
50
|
-
* <spec fn="is_fully_withdrawable_validator" fork="deneb" />
|
|
51
|
-
*/
|
|
52
|
-
```
|
|
53
|
-
|
|
54
|
-
```
|
|
55
|
-
/*
|
|
56
|
-
* <spec ssz_object="BeaconState" fork="electra" style="diff" />
|
|
57
|
-
*/
|
|
58
|
-
```
|
|
59
|
-
|
|
60
|
-
### Populating Spec Tags
|
|
61
|
-
|
|
62
|
-
Then, navigate to your codebase and run `ethspecify`:
|
|
63
|
-
|
|
64
|
-
```
|
|
65
|
-
ethspecify
|
|
66
|
-
```
|
|
67
|
-
|
|
68
|
-
## Specification Options
|
|
69
|
-
|
|
70
|
-
### Version
|
|
71
|
-
|
|
72
|
-
This attribute specifies which version of the consensus specifications to use. Default is `nightly`.
|
|
73
|
-
|
|
74
|
-
- `nightly` (default) - Uses the latest nightly build from the master branch
|
|
75
|
-
- `v1.6.0-alpha.2`, `v1.6.0-alpha.3`, etc. - Uses a specific tagged release version
|
|
76
|
-
|
|
77
|
-
Example:
|
|
78
|
-
```
|
|
79
|
-
/*
|
|
80
|
-
* <spec fn="apply_deposit" fork="electra" version="v1.6.0-alpha.3" />
|
|
81
|
-
*/
|
|
82
|
-
```
|
|
83
|
-
|
|
84
|
-
### Fork
|
|
85
|
-
|
|
86
|
-
This attribute can be any of the [executable
|
|
87
|
-
specifications](https://github.com/ethereum/consensus-specs/blob/e6bddd966214a19d2b97199bbe3c02577a22a8b4/Makefile#L3-L15)
|
|
88
|
-
in the consensus-specs. At the time of writing, these are: phase0, altair, bellatrix, capella,
|
|
89
|
-
deneb, electra, fulu, whisk, eip6800, and eip7732.
|
|
90
|
-
|
|
91
|
-
### Style
|
|
92
|
-
|
|
93
|
-
This attribute can be used to change how the specification content is shown.
|
|
94
|
-
|
|
95
|
-
#### `hash` (default)
|
|
96
|
-
|
|
97
|
-
This style adds a hash of the specification content to the spec tag, without showing the content.
|
|
98
|
-
|
|
99
|
-
```
|
|
100
|
-
/*
|
|
101
|
-
* <spec fn="apply_deposit" fork="electra" hash="c723ce7b" />
|
|
102
|
-
*/
|
|
103
|
-
```
|
|
104
|
-
|
|
105
|
-
> [!NOTE]
|
|
106
|
-
> The hash is the first 8 characters of the specification content's SHA256 digest.
|
|
107
|
-
|
|
108
|
-
#### `full`
|
|
109
|
-
|
|
110
|
-
This style displays the whole content of this specification item, including comments.
|
|
111
|
-
|
|
112
|
-
```
|
|
113
|
-
/*
|
|
114
|
-
* <spec fn="is_fully_withdrawable_validator" fork="deneb" style="full">
|
|
115
|
-
* def is_fully_withdrawable_validator(validator: Validator, balance: Gwei, epoch: Epoch) -> bool:
|
|
116
|
-
* """
|
|
117
|
-
* Check if ``validator`` is fully withdrawable.
|
|
118
|
-
* """
|
|
119
|
-
* return (
|
|
120
|
-
* has_eth1_withdrawal_credential(validator)
|
|
121
|
-
* and validator.withdrawable_epoch <= epoch
|
|
122
|
-
* and balance > 0
|
|
123
|
-
* )
|
|
124
|
-
* </spec>
|
|
125
|
-
*/
|
|
126
|
-
```
|
|
127
|
-
|
|
128
|
-
#### `link`
|
|
129
|
-
|
|
130
|
-
This style displays a GitHub link to the specification item.
|
|
131
|
-
|
|
132
|
-
```
|
|
133
|
-
/*
|
|
134
|
-
* <spec fn="apply_pending_deposit" fork="electra" style="link" hash="83ee9126">
|
|
135
|
-
* https://github.com/ethereum/consensus-specs/blob/dev/specs/electra/beacon-chain.md#new-apply_pending_deposit
|
|
136
|
-
* </spec>
|
|
137
|
-
*/
|
|
138
|
-
```
|
|
139
|
-
|
|
140
|
-
#### `diff`
|
|
141
|
-
|
|
142
|
-
This style displays a diff with the previous fork's version of the specification.
|
|
143
|
-
|
|
144
|
-
```
|
|
145
|
-
/*
|
|
146
|
-
* <spec ssz_object="BeaconState" fork="electra" style="diff">
|
|
147
|
-
* --- deneb
|
|
148
|
-
* +++ electra
|
|
149
|
-
* @@ -27,3 +27,12 @@
|
|
150
|
-
* next_withdrawal_index: WithdrawalIndex
|
|
151
|
-
* next_withdrawal_validator_index: ValidatorIndex
|
|
152
|
-
* historical_summaries: List[HistoricalSummary, HISTORICAL_ROOTS_LIMIT]
|
|
153
|
-
* + deposit_requests_start_index: uint64
|
|
154
|
-
* + deposit_balance_to_consume: Gwei
|
|
155
|
-
* + exit_balance_to_consume: Gwei
|
|
156
|
-
* + earliest_exit_epoch: Epoch
|
|
157
|
-
* + consolidation_balance_to_consume: Gwei
|
|
158
|
-
* + earliest_consolidation_epoch: Epoch
|
|
159
|
-
* + pending_deposits: List[PendingDeposit, PENDING_DEPOSITS_LIMIT]
|
|
160
|
-
* + pending_partial_withdrawals: List[PendingPartialWithdrawal, PENDING_PARTIAL_WITHDRAWALS_LIMIT]
|
|
161
|
-
* + pending_consolidations: List[PendingConsolidation, PENDING_CONSOLIDATIONS_LIMIT]
|
|
162
|
-
* </spec>
|
|
163
|
-
*/
|
|
164
|
-
```
|
|
165
|
-
|
|
166
|
-
> [!NOTE]
|
|
167
|
-
> Comments are stripped from the specifications when the `diff` style is used. We do this because
|
|
168
|
-
> these complicate the diff; the "[Modified in Fork]" comments aren't valuable here.
|
|
169
|
-
|
|
170
|
-
This can be used with any specification item, like functions too:
|
|
171
|
-
|
|
172
|
-
```
|
|
173
|
-
/*
|
|
174
|
-
* <spec fn="is_eligible_for_activation_queue" fork="electra" style="diff">
|
|
175
|
-
* --- phase0
|
|
176
|
-
* +++ electra
|
|
177
|
-
* @@ -4,5 +4,5 @@
|
|
178
|
-
* """
|
|
179
|
-
* return (
|
|
180
|
-
* validator.activation_eligibility_epoch == FAR_FUTURE_EPOCH
|
|
181
|
-
* - and validator.effective_balance == MAX_EFFECTIVE_BALANCE
|
|
182
|
-
* + and validator.effective_balance >= MIN_ACTIVATION_BALANCE
|
|
183
|
-
* )
|
|
184
|
-
* </spec>
|
|
185
|
-
*/
|
|
186
|
-
```
|
|
187
|
-
|
|
188
|
-
## Supported Specification Items
|
|
189
|
-
|
|
190
|
-
### Constants
|
|
191
|
-
|
|
192
|
-
These are items found in the `Constants` section of the specifications.
|
|
193
|
-
|
|
194
|
-
```
|
|
195
|
-
/*
|
|
196
|
-
* <spec constant_var="COMPOUNDING_WITHDRAWAL_PREFIX" fork="electra" style="full">
|
|
197
|
-
* COMPOUNDING_WITHDRAWAL_PREFIX: Bytes1 = '0x02'
|
|
198
|
-
* </spec>
|
|
199
|
-
*/
|
|
200
|
-
```
|
|
201
|
-
|
|
202
|
-
### Custom Types
|
|
203
|
-
|
|
204
|
-
These are items found in the `Custom types` section of the specifications.
|
|
205
|
-
|
|
206
|
-
```
|
|
207
|
-
/*
|
|
208
|
-
* <spec custom_type="Blob" fork="electra" style="full">
|
|
209
|
-
* Blob = ByteVector[BYTES_PER_FIELD_ELEMENT * FIELD_ELEMENTS_PER_BLOB]
|
|
210
|
-
* </spec>
|
|
211
|
-
*/
|
|
212
|
-
```
|
|
213
|
-
|
|
214
|
-
### Preset Variables
|
|
215
|
-
|
|
216
|
-
These are items found in the
|
|
217
|
-
[`presets`](https://github.com/ethereum/consensus-specs/tree/dev/presets) directory.
|
|
218
|
-
|
|
219
|
-
For preset variables, in addition to the `preset_var` attribute, you can specify a `preset`
|
|
220
|
-
attribute: minimal or mainnet.
|
|
221
|
-
|
|
222
|
-
```
|
|
223
|
-
/*
|
|
224
|
-
* <spec preset="minimal" preset_var="PENDING_CONSOLIDATIONS_LIMIT" fork="electra" style="full">
|
|
225
|
-
* PENDING_CONSOLIDATIONS_LIMIT: uint64 = 64
|
|
226
|
-
* </spec>
|
|
227
|
-
*
|
|
228
|
-
* <spec preset="mainnet" preset_var="PENDING_CONSOLIDATIONS_LIMIT" fork="electra" style="full">
|
|
229
|
-
* PENDING_CONSOLIDATIONS_LIMIT: uint64 = 262144
|
|
230
|
-
* </spec>
|
|
231
|
-
*/
|
|
232
|
-
```
|
|
233
|
-
|
|
234
|
-
It's not strictly necessary to specify the preset attribute. The default preset is mainnet.
|
|
235
|
-
|
|
236
|
-
```
|
|
237
|
-
/*
|
|
238
|
-
* <spec preset_var="FIELD_ELEMENTS_PER_BLOB" fork="electra" style="full">
|
|
239
|
-
* FIELD_ELEMENTS_PER_BLOB: uint64 = 4096
|
|
240
|
-
* </spec>
|
|
241
|
-
*/
|
|
242
|
-
```
|
|
243
|
-
|
|
244
|
-
### Config Variables
|
|
245
|
-
|
|
246
|
-
These are items found in the
|
|
247
|
-
[`configs`](https://github.com/ethereum/consensus-specs/tree/dev/presets) directory.
|
|
248
|
-
|
|
249
|
-
```
|
|
250
|
-
/*
|
|
251
|
-
* <spec config_var="MAX_REQUEST_BLOB_SIDECARS" fork="electra" style="full">
|
|
252
|
-
* MAX_REQUEST_BLOB_SIDECARS = 768
|
|
253
|
-
* </spec>
|
|
254
|
-
*/
|
|
255
|
-
```
|
|
256
|
-
|
|
257
|
-
### SSZ Objects
|
|
258
|
-
|
|
259
|
-
These are items found in the `Containers` section of the specifications.
|
|
260
|
-
|
|
261
|
-
```
|
|
262
|
-
/*
|
|
263
|
-
* <spec ssz_object="ConsolidationRequest" fork="electra" style="full">
|
|
264
|
-
* class ConsolidationRequest(Container):
|
|
265
|
-
* source_address: ExecutionAddress
|
|
266
|
-
* source_pubkey: BLSPubkey
|
|
267
|
-
* target_pubkey: BLSPubkey
|
|
268
|
-
* </spec>
|
|
269
|
-
*/
|
|
270
|
-
```
|
|
271
|
-
|
|
272
|
-
### Dataclasses
|
|
273
|
-
|
|
274
|
-
These are classes with the `@dataclass` decorator.
|
|
275
|
-
|
|
276
|
-
```
|
|
277
|
-
/*
|
|
278
|
-
* <spec dataclass="PayloadAttributes" fork="electra" style="full">
|
|
279
|
-
* class PayloadAttributes(object):
|
|
280
|
-
* timestamp: uint64
|
|
281
|
-
* prev_randao: Bytes32
|
|
282
|
-
* suggested_fee_recipient: ExecutionAddress
|
|
283
|
-
* withdrawals: Sequence[Withdrawal]
|
|
284
|
-
* parent_beacon_block_root: Root # [New in Deneb:EIP4788]
|
|
285
|
-
* </spec>
|
|
286
|
-
*/
|
|
287
|
-
```
|
|
288
|
-
|
|
289
|
-
### Functions
|
|
290
|
-
|
|
291
|
-
These are all the functions found in the specifications.
|
|
292
|
-
|
|
293
|
-
For example, two versions of the same function:
|
|
294
|
-
|
|
295
|
-
```
|
|
296
|
-
/*
|
|
297
|
-
* <spec fn="is_fully_withdrawable_validator" fork="deneb" style="full">
|
|
298
|
-
* def is_fully_withdrawable_validator(validator: Validator, balance: Gwei, epoch: Epoch) -> bool:
|
|
299
|
-
* """
|
|
300
|
-
* Check if ``validator`` is fully withdrawable.
|
|
301
|
-
* """
|
|
302
|
-
* return (
|
|
303
|
-
* has_eth1_withdrawal_credential(validator)
|
|
304
|
-
* and validator.withdrawable_epoch <= epoch
|
|
305
|
-
* and balance > 0
|
|
306
|
-
* )
|
|
307
|
-
* </spec>
|
|
308
|
-
*/
|
|
309
|
-
```
|
|
310
|
-
|
|
311
|
-
```
|
|
312
|
-
/*
|
|
313
|
-
* <spec fn="is_fully_withdrawable_validator" fork="electra" style="full">
|
|
314
|
-
* def is_fully_withdrawable_validator(validator: Validator, balance: Gwei, epoch: Epoch) -> bool:
|
|
315
|
-
* """
|
|
316
|
-
* Check if ``validator`` is fully withdrawable.
|
|
317
|
-
* """
|
|
318
|
-
* return (
|
|
319
|
-
* has_execution_withdrawal_credential(validator) # [Modified in Electra:EIP7251]
|
|
320
|
-
* and validator.withdrawable_epoch <= epoch
|
|
321
|
-
* and balance > 0
|
|
322
|
-
* )
|
|
323
|
-
* </spec>
|
|
324
|
-
*/
|
|
325
|
-
```
|
|
326
|
-
|
|
327
|
-
With functions, it's possible to specify which line/lines should be displayed. For example:
|
|
328
|
-
|
|
329
|
-
```
|
|
330
|
-
/*
|
|
331
|
-
* <spec fn="is_fully_withdrawable_validator" fork="electra" style="full" lines="5-9">
|
|
332
|
-
* return (
|
|
333
|
-
* has_execution_withdrawal_credential(validator) # [Modified in Electra:EIP7251]
|
|
334
|
-
* and validator.withdrawable_epoch <= epoch
|
|
335
|
-
* and balance > 0
|
|
336
|
-
* )
|
|
337
|
-
* </spec>
|
|
338
|
-
*/
|
|
339
|
-
```
|
|
340
|
-
|
|
341
|
-
Note that the content is automatically dedented.
|
|
342
|
-
|
|
343
|
-
Or, to display just a single line, only specify a single number. For example:
|
|
344
|
-
|
|
345
|
-
```
|
|
346
|
-
/*
|
|
347
|
-
* <spec fn="is_fully_withdrawable_validator" fork="electra" style="full" lines="6">
|
|
348
|
-
* has_execution_withdrawal_credential(validator) # [Modified in Electra:EIP7251]
|
|
349
|
-
* </spec>
|
|
350
|
-
*/
|
|
351
|
-
```
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
ethspecify/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
-
ethspecify/cli.py,sha256=Ida4n7BSjZrkvwINyzh3M61JzFqdLPc93i62JIQ_64g,8183
|
|
3
|
-
ethspecify/core.py,sha256=GwXLoCWKJ-BTyjkVP58eR1Gay2bGegdH6RvAaqLUwas,48832
|
|
4
|
-
ethspecify-0.2.9.dist-info/licenses/LICENSE,sha256=Awxsr73mm9YMBVhBYnzeI7bNdRd-bH6RDtO5ItG0DaM,1071
|
|
5
|
-
ethspecify-0.2.9.dist-info/METADATA,sha256=AhTvvhzxBaGj3gjs1dvsubn0YFTIznMIUWP5cF9-anA,9212
|
|
6
|
-
ethspecify-0.2.9.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
7
|
-
ethspecify-0.2.9.dist-info/entry_points.txt,sha256=09viGkCg9J3h0c9BFRN-BKaJUEaIc4JyULNgBP5EL_g,51
|
|
8
|
-
ethspecify-0.2.9.dist-info/top_level.txt,sha256=0klaMvlVyOkXW09fwZTijJpdybITEp2c9zQKV5v30VM,11
|
|
9
|
-
ethspecify-0.2.9.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|