-
Notifications
You must be signed in to change notification settings - Fork 5
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
CI: enhance CI to check storage layout backwards compatibility #111
Changes from 7 commits
e040289
6e41085
4843715
c62aa21
1a10b62
044fb76
7dfc103
ebf142c
ae4247e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||||||
---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,87 @@ | ||||||||||
#!/usr/bin/env python | ||||||||||
|
||||||||||
import json | ||||||||||
import subprocess | ||||||||||
import pandas as pd | ||||||||||
import os | ||||||||||
from compare_storage_layout import parse_output, compare_layouts, get_current_layout | ||||||||||
|
||||||||||
def get_deployed_addresses(): | ||||||||||
with open('script/deployedContracts.json', 'r') as f: | ||||||||||
data = json.load(f) | ||||||||||
return { | ||||||||||
'Bootstrap': data['clientChain'].get('bootstrapLogic'), | ||||||||||
'ClientChainGateway': data['clientChain'].get('clientGatewayLogic'), | ||||||||||
'Vault': data['clientChain'].get('vaultImplementation'), | ||||||||||
'RewardVault': data['clientChain'].get('rewardVaultImplementation'), | ||||||||||
'ExoCapsule': data['clientChain'].get('capsuleImplementation') | ||||||||||
} | ||||||||||
|
||||||||||
def get_storage_layout(contract_name, address, rpc_url, etherscan_api_key): | ||||||||||
if not address: | ||||||||||
print(f"Skipping {contract_name} as it's not deployed.") | ||||||||||
return pd.DataFrame() | ||||||||||
|
||||||||||
result = subprocess.run(['cast', 'storage', address, '--rpc-url', rpc_url, '--etherscan-api-key', etherscan_api_key], capture_output=True, text=True) | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Check for the 'cast' command before execution If the Add this check before line 25: import shutil
if not shutil.which('cast'):
raise EnvironmentError("'cast' command not found. Please install Foundry to proceed.") |
||||||||||
print(f"finish executing: cast storage {address} --rpc-url ...") | ||||||||||
|
||||||||||
if result.returncode != 0: | ||||||||||
raise Exception(f"Error getting current layout for {contract_name}: {result.stderr}") | ||||||||||
|
||||||||||
return parse_output(contract_name, result.stdout.split('\n')) | ||||||||||
|
||||||||||
def load_and_parse_layout(contract_name, path): | ||||||||||
with open(path, 'r') as f: | ||||||||||
lines = f.readlines() | ||||||||||
return parse_output(contract_name, lines) | ||||||||||
|
||||||||||
if __name__ == "__main__": | ||||||||||
try: | ||||||||||
api_key = os.environ.get('ALCHEMY_API_KEY') | ||||||||||
if not api_key: | ||||||||||
raise ValueError("ALCHEMY_API_KEY environment variable is not set") | ||||||||||
etherscan_api_key = os.environ.get('ETHERSCAN_API_KEY') | ||||||||||
if not etherscan_api_key: | ||||||||||
raise ValueError("ETHERSCAN_API_KEY environment variable is not set") | ||||||||||
|
||||||||||
# Construct the RPC URL for Sepolia | ||||||||||
rpc_url = f"https://eth-sepolia.g.alchemy.com/v2/{api_key}" | ||||||||||
|
||||||||||
addresses = get_deployed_addresses() | ||||||||||
all_mismatches = {} | ||||||||||
|
||||||||||
for contract_name, address in addresses.items(): | ||||||||||
print(f"Checking {contract_name}...") | ||||||||||
deployed_layout = get_storage_layout(contract_name, address, rpc_url, etherscan_api_key) | ||||||||||
if deployed_layout.empty: | ||||||||||
print(f"No deployed layout found for {contract_name}.") | ||||||||||
continue | ||||||||||
|
||||||||||
current_layout = get_current_layout(contract_name) | ||||||||||
if current_layout.empty: | ||||||||||
raise ValueError(f"Error: No valid entries of current layout found for {contract_name}.") | ||||||||||
|
||||||||||
mismatches = compare_layouts(deployed_layout, current_layout) | ||||||||||
if mismatches: | ||||||||||
all_mismatches[contract_name] = mismatches | ||||||||||
|
||||||||||
# then we load the layout file of ExocoreGateway on target branch and compare it with the current layout | ||||||||||
print("Checking ExocoreGateway...") | ||||||||||
target_branch_layout = load_and_parse_layout('ExocoreGateway', 'ExocoreGateway_target.txt') | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ensure correct file path for 'ExocoreGateway_target.txt' The script expects Apply this change: + script_dir = os.path.dirname(os.path.abspath(__file__))
+ layout_path = os.path.join(script_dir, 'ExocoreGateway_target.txt')
- target_branch_layout = load_and_parse_layout('ExocoreGateway', 'ExocoreGateway_target.txt')
+ target_branch_layout = load_and_parse_layout('ExocoreGateway', layout_path) 📝 Committable suggestion
Suggested change
|
||||||||||
current_layout = get_current_layout('ExocoreGateway') | ||||||||||
mismatches = compare_layouts(target_branch_layout, current_layout) | ||||||||||
if mismatches: | ||||||||||
all_mismatches['ExocoreGateway'] = mismatches | ||||||||||
|
||||||||||
if all_mismatches: | ||||||||||
print("Mismatches found for current contracts:") | ||||||||||
for contract, mismatches in all_mismatches.items(): | ||||||||||
print(f"{contract}:") | ||||||||||
for mismatch in mismatches: | ||||||||||
print(f" {mismatch}") | ||||||||||
exit(1) | ||||||||||
else: | ||||||||||
print("Storage layout is compatible with all deployed contracts.") | ||||||||||
except Exception as e: | ||||||||||
print(f"Error: {e}") | ||||||||||
exit(1) | ||||||||||
Comment on lines
+82
to
+87
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Use The built-in Apply this change: +import sys
print(f" {mismatch}")
- exit(1)
+ sys.exit(1)
else:
print("Storage layout is compatible with all deployed contracts.")
except Exception as e:
print(f"Error: {e}")
- exit(1)
+ sys.exit(1)
|
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -1,68 +1,81 @@ | ||||||||||||||||||||||||||||
#!/usr/bin/env python | ||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||
import pandas as pd | ||||||||||||||||||||||||||||
import os | ||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||
def parse_layout(file_path): | ||||||||||||||||||||||||||||
expected_headers = ['Unnamed: 0', 'Name', 'Type', 'Slot', 'Offset', 'Bytes', 'Contract', 'Unnamed: 7'] | ||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||
if not os.path.isfile(file_path): | ||||||||||||||||||||||||||||
raise FileNotFoundError(f"Error: File {file_path} does not exist.") | ||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||
# Read the file using pandas, with '|' as the delimiter | ||||||||||||||||||||||||||||
df = pd.read_csv(file_path, delimiter='|', engine='python', header=0) | ||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||
# Trim leading/trailing whitespace from all columns | ||||||||||||||||||||||||||||
df.columns = [col.strip() for col in df.columns] | ||||||||||||||||||||||||||||
df = df.apply(lambda x: x.strip() if isinstance(x, str) else x) | ||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||
# Check headers | ||||||||||||||||||||||||||||
if not all([df.columns[i] == expected_headers[i] for i in range(len(expected_headers))]): | ||||||||||||||||||||||||||||
raise ValueError(f"Error: Headers in {file_path} do not match expected headers.") | ||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||
# Drop the second row (assuming it's a separator row) | ||||||||||||||||||||||||||||
df = df.drop(df.index[1]) | ||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||
# Combine relevant columns into a single string for comparison | ||||||||||||||||||||||||||||
df['Combined'] = df[['Name', 'Type', 'Slot', 'Offset', 'Bytes']].apply(lambda row: '|'.join(row.values), axis=1) | ||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||
return df['Combined'].tolist() | ||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||
def compare_layouts(clientChainGateway_entries, bootstrap_entries): | ||||||||||||||||||||||||||||
import subprocess | ||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||
def parse_output(contract_name, lines): | ||||||||||||||||||||||||||||
# Clean up the output and create a dataframe | ||||||||||||||||||||||||||||
data = [] | ||||||||||||||||||||||||||||
separator_line = len(lines); | ||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Remove unnecessary semicolon There's an unnecessary semicolon at the end of the line. In Python, semicolons are not required at the end of statements. Apply this diff to remove the semicolon: - separator_line = len(lines);
+ separator_line = len(lines) 📝 Committable suggestion
Suggested change
🧰 Tools🪛 Ruff
|
||||||||||||||||||||||||||||
for i, line in enumerate(lines): # start from the line next to the separator | ||||||||||||||||||||||||||||
if i > separator_line and line.startswith('|'): | ||||||||||||||||||||||||||||
parts = [part.strip() for part in line.split('|')[1:-1]] # Remove empty first and last elements | ||||||||||||||||||||||||||||
data.append(parts[:6]) # Keep Name, Type, Slot, Offset, Bytes, Contract | ||||||||||||||||||||||||||||
elif line.startswith('|') and 'Name' in line: | ||||||||||||||||||||||||||||
separator_line = i + 1 | ||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||
Comment on lines
+11
to
+16
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fix the loop condition to correctly parse the output The current loop condition may prevent the parsing logic from working as intended. Since Consider initializing Apply this diff to adjust the initialization: - separator_line = len(lines)
+ separator_line = -1 Alternatively, adjust the condition in the loop to correctly process the lines after the header. 📝 Committable suggestion
Suggested change
|
||||||||||||||||||||||||||||
if not data: | ||||||||||||||||||||||||||||
raise Exception(f"No valid storage layout data found for {contract_name}") | ||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||
df = pd.DataFrame(data, columns=['Name', 'Type', 'Slot', 'Offset', 'Bytes', 'Contract']) | ||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||
# Convert numeric columns | ||||||||||||||||||||||||||||
for col in ['Slot', 'Offset', 'Bytes']: | ||||||||||||||||||||||||||||
df[col] = pd.to_numeric(df[col]) | ||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||
return df | ||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||
def get_current_layout(contract_name): | ||||||||||||||||||||||||||||
result = subprocess.run(['forge', 'inspect', f'src/core/{contract_name}.sol:{contract_name}', 'storage-layout', '--pretty'], capture_output=True, text=True) | ||||||||||||||||||||||||||||
print(f"finished executing forge inspect for {contract_name}") | ||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||
if result.returncode != 0: | ||||||||||||||||||||||||||||
raise Exception(f"Error getting current layout for {contract_name}: {result.stderr}") | ||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||
return parse_output(contract_name, result.stdout.split('\n')) | ||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||
def compare_layouts(old_layout, new_layout): | ||||||||||||||||||||||||||||
mismatches = [] | ||||||||||||||||||||||||||||
length = len(bootstrap_entries) | ||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||
if length > len(clientChainGateway_entries): | ||||||||||||||||||||||||||||
mismatches.append("Error: Bootstrap entries are more than ClientChainGateway entries.") | ||||||||||||||||||||||||||||
return mismatches | ||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||
for i in range(length): | ||||||||||||||||||||||||||||
if bootstrap_entries[i] != clientChainGateway_entries[i]: | ||||||||||||||||||||||||||||
mismatches.append(f"Mismatch at position {i + 1}: {bootstrap_entries[i]} != {clientChainGateway_entries[i]}") | ||||||||||||||||||||||||||||
# Ensure both dataframes have the same columns | ||||||||||||||||||||||||||||
columns = ['Name', 'Type', 'Slot', 'Offset', 'Bytes'] | ||||||||||||||||||||||||||||
old_layout = old_layout[columns].copy() | ||||||||||||||||||||||||||||
new_layout = new_layout[columns].copy() | ||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||
# Compare non-gap variables | ||||||||||||||||||||||||||||
for index, row in old_layout.iterrows(): | ||||||||||||||||||||||||||||
if row['Name'] != '__gap': | ||||||||||||||||||||||||||||
current_row = new_layout.loc[new_layout['Name'] == row['Name']] | ||||||||||||||||||||||||||||
if current_row.empty: | ||||||||||||||||||||||||||||
mismatches.append(f"Variable {row['Name']} is missing in the current layout") | ||||||||||||||||||||||||||||
elif not current_row.iloc[0].equals(row): | ||||||||||||||||||||||||||||
mismatches.append(f"Variable {row['Name']} has changed") | ||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||
if not mismatches: | ||||||||||||||||||||||||||||
print(f"No mismatches found") | ||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||
return mismatches | ||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||
if __name__ == "__main__": | ||||||||||||||||||||||||||||
try: | ||||||||||||||||||||||||||||
clientChainGateway_entries = parse_layout("ClientChainGateway.md") | ||||||||||||||||||||||||||||
bootstrap_entries = parse_layout("Bootstrap.md") | ||||||||||||||||||||||||||||
clientChainGateway_layout = get_current_layout("ClientChainGateway") | ||||||||||||||||||||||||||||
bootstrap_layout = get_current_layout("Bootstrap") | ||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||
if not clientChainGateway_entries: | ||||||||||||||||||||||||||||
raise ValueError("Error: No valid entries found in ClientChainGateway.md.") | ||||||||||||||||||||||||||||
if clientChainGateway_layout.empty: | ||||||||||||||||||||||||||||
raise ValueError("Error: No valid entries found for ClientChainGateway.") | ||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||
if not bootstrap_entries: | ||||||||||||||||||||||||||||
raise ValueError("Error: No valid entries found in Bootstrap.md.") | ||||||||||||||||||||||||||||
if bootstrap_layout.empty: | ||||||||||||||||||||||||||||
raise ValueError("Error: No valid entries found for Bootstrap.") | ||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||
mismatches = compare_layouts(clientChainGateway_entries, bootstrap_entries) | ||||||||||||||||||||||||||||
mismatches = compare_layouts(bootstrap_layout, clientChainGateway_layout) | ||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||
if mismatches: | ||||||||||||||||||||||||||||
print(f"Mismatches found: {len(mismatches)}") | ||||||||||||||||||||||||||||
for mismatch in mismatches: | ||||||||||||||||||||||||||||
print(mismatch) | ||||||||||||||||||||||||||||
exit(1) | ||||||||||||||||||||||||||||
else: | ||||||||||||||||||||||||||||
print("All entries in Bootstrap are present in ClientChainGateway at the correct positions.") | ||||||||||||||||||||||||||||
print("All entries in Bootstrap match ClientChainGateway at the correct positions.") | ||||||||||||||||||||||||||||
except Exception as e: | ||||||||||||||||||||||||||||
print(e) | ||||||||||||||||||||||||||||
print(f"Error: {e}") | ||||||||||||||||||||||||||||
exit(1) | ||||||||||||||||||||||||||||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -23,3 +23,6 @@ node_modules | |
.idea | ||
|
||
.gas-snapshot | ||
|
||
## secret | ||
.secrets |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use absolute paths to ensure file accessibility
The script assumes that
script/deployedContracts.json
is in the current working directory. This can lead to issues if the script is executed from a different directory. To make the script more robust, use an absolute path relative to the script's location.Apply this change: