Skip to content

Commit dd8da25

Browse files
jusbar23pthmas
authored andcommitted
Add release qual script for performing a chain upgrade (dydxprotocol#3044)
1 parent d724c14 commit dd8da25

File tree

2 files changed

+303
-0
lines changed

2 files changed

+303
-0
lines changed
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
# Release Qualification Scripts
2+
3+
This directory contains scripts for testing and qualifying new releases on testnet/staging environments.
4+
5+
## Scripts
6+
7+
### submit_upgrade_proposal.py
8+
9+
A Python script to submit software upgrade proposals and automatically vote with test validators on testnet/staging environments.
10+
11+
**Features:**
12+
- Submits upgrade proposals with a specified upgrade name and block height
13+
- Automatically votes "yes" with all test validators
14+
- Restricted to testnet/staging environments only (mainnet explicitly blocked)
15+
- Configurable via command-line arguments or environment variables
16+
17+
**Usage:**
18+
```bash
19+
# Basic usage - upgrade in 300 blocks (default)
20+
./submit_upgrade_proposal.py v5.0.0
21+
22+
# Specify number of blocks to wait
23+
./submit_upgrade_proposal.py v5.0.0 500
24+
25+
# Use custom node and chain-id
26+
./submit_upgrade_proposal.py v5.0.0 --node https://validator.custom.com:443 --chain-id dydxprotocol-testnet
27+
28+
# Using environment variables
29+
export DYDX_NODE=https://validator.custom.com:443
30+
export DYDX_CHAIN_ID=dydxprotocol-testnet
31+
./submit_upgrade_proposal.py v5.0.0
32+
```
33+
34+
**Configuration Priority:**
35+
1. Command-line arguments (highest priority)
36+
2. Environment variables (`DYDX_NODE`, `DYDX_CHAIN_ID`)
37+
3. Default values (v4staging node and testnet chain-id)
38+
39+
**Safety Features:**
40+
- Only works on allowed testnet/staging chain IDs
41+
- Explicitly blocks mainnet chain IDs
42+
- Requires user confirmation before submitting proposal
43+
- Validates chain ID against allowed list
44+
45+
**Test Validators:**
46+
The script automatically votes with these test validators:
47+
- alice, bob, carl, dave, emily, fiona, greg, henry, ian, jeff
48+
49+
## Requirements
50+
51+
- Python 3
52+
- `dydxprotocold` CLI installed and configured
53+
- Test validator keys in the keyring (using `test` backend)
54+
- Access to a testnet/staging node
55+
56+
## Security
57+
58+
These scripts are designed for **testnet/staging environments only**. The `submit_upgrade_proposal.py` script includes multiple safeguards to prevent accidental use on mainnet:
59+
- Hardcoded list of allowed chain IDs (testnet/staging only)
60+
- Explicit blocking of known mainnet chain IDs
61+
- Chain ID validation before execution
Lines changed: 242 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,242 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Submit a software upgrade proposal and auto-vote with test validators.
4+
"""
5+
6+
import argparse
7+
import json
8+
import os
9+
import subprocess
10+
import sys
11+
import time
12+
13+
# Test validators
14+
VALIDATORS = ["alice", "bob", "carl", "dave", "emily", "fiona", "greg", "henry", "ian", "jeff"]
15+
16+
# Allowed chain IDs for testnet/staging only (blocking mainnet)
17+
ALLOWED_CHAIN_IDS = [
18+
"dydxprotocol-testnet", # Standard testnet/staging chain
19+
]
20+
21+
# Explicitly blocked mainnet chain IDs
22+
BLOCKED_CHAIN_IDS = [
23+
"dydx-mainnet-1",
24+
"dydxprotocol-mainnet",
25+
"mainnet"
26+
]
27+
28+
def load_config(args):
29+
"""Load configuration from command-line args or environment variables.
30+
Priority: command-line args > environment variables > defaults
31+
"""
32+
# Default values
33+
default_node = "https://validator.v4staging.dydx.exchange:443"
34+
default_chain_id = "dydxprotocol-testnet"
35+
36+
# First priority: command-line arguments
37+
if args.node:
38+
node = args.node
39+
print(f"Using node from command-line: {node}")
40+
else:
41+
# Second priority: environment variables, fallback to defaults
42+
node = os.environ.get("DYDX_NODE", default_node)
43+
44+
if args.chain_id:
45+
chain_id = args.chain_id
46+
print(f"Using chain-id from command-line: {chain_id}")
47+
else:
48+
# Second priority: environment variables, fallback to defaults
49+
chain_id = os.environ.get("DYDX_CHAIN_ID", default_chain_id)
50+
51+
# Check if explicitly blocked (mainnet)
52+
if chain_id in BLOCKED_CHAIN_IDS or "mainnet" in chain_id.lower():
53+
print(f"Error: This script cannot run on mainnet (chain_id: {chain_id})")
54+
print("This script is only for testnet/staging environments.")
55+
sys.exit(1)
56+
57+
# Validate chain_id is allowed
58+
if chain_id not in ALLOWED_CHAIN_IDS:
59+
print(f"Error: Chain ID '{chain_id}' is not in the allowed list.")
60+
print(f"Allowed chains: {', '.join(ALLOWED_CHAIN_IDS)}")
61+
print("This script is restricted to testnet/staging environments only.")
62+
sys.exit(1)
63+
64+
print(f"Using node: {node}")
65+
print(f"Using chain-id: {chain_id}")
66+
67+
return node, chain_id
68+
69+
def run_cmd(cmd, node=None):
70+
"""Run command and return stdout."""
71+
# Add node flag if provided
72+
if node and "--node" not in cmd:
73+
cmd.extend(["--node", node])
74+
try:
75+
result = subprocess.run(
76+
cmd, capture_output=True, text=True, check=True
77+
)
78+
return result.stdout
79+
except subprocess.CalledProcessError as e:
80+
print(f"Error: {e.stderr}")
81+
return None
82+
83+
def main():
84+
"""Submit a software upgrade proposal and auto-vote with test validators."""
85+
parser = argparse.ArgumentParser(
86+
description='Submit a software upgrade proposal and auto-vote with test validators.'
87+
)
88+
parser.add_argument('upgrade_name', help='Name of the upgrade (e.g., v5.0.0)')
89+
parser.add_argument(
90+
'blocks_to_wait', nargs='?', type=int, default=300,
91+
help='Number of blocks to wait for an upgrade and voting period (default: 300)'
92+
)
93+
parser.add_argument(
94+
'--node',
95+
help='Node RPC endpoint (e.g., http://validator.v4staging.dydx.exchange:26657)'
96+
)
97+
parser.add_argument(
98+
'--chain-id', dest='chain_id', help='Chain ID (e.g., dydxprotocol-testnet)'
99+
)
100+
101+
args = parser.parse_args()
102+
103+
upgrade_name = args.upgrade_name
104+
wait_blocks = args.blocks_to_wait
105+
106+
# Load configuration
107+
node, chain_id = load_config(args)
108+
109+
# Display configuration and ask for confirmation
110+
print("\n" + "="*60)
111+
print("UPGRADE PROPOSAL CONFIGURATION")
112+
print("="*60)
113+
print(f"Chain ID: {chain_id}")
114+
print(f"Node: {node}")
115+
print(f"Upgrade Name: {upgrade_name}")
116+
print(f"Block Wait: {wait_blocks} blocks")
117+
print("="*60)
118+
119+
response = input("\nDo you want to proceed with this upgrade proposal? (yes/no): ")
120+
if response.lower() not in ['yes', 'y']:
121+
print("Upgrade proposal cancelled.")
122+
sys.exit(0)
123+
124+
print("\nProceeding with upgrade proposal...")
125+
126+
# Get current block height
127+
result = run_cmd(["dydxprotocold", "status"], node=node)
128+
if result:
129+
try:
130+
status = json.loads(result)
131+
current_height = int(status['sync_info']['latest_block_height'])
132+
upgrade_height = current_height + wait_blocks
133+
print(f"Current height: {current_height}, upgrade at: {upgrade_height}")
134+
except (json.JSONDecodeError, KeyError) as e:
135+
# Fallback if we can't get current height
136+
print(f"Could not parse block height, using default. Error: {e}")
137+
upgrade_height = 1000000
138+
print(f"Using default upgrade height: {upgrade_height}")
139+
else:
140+
upgrade_height = 1000000
141+
142+
# Create proposal.json
143+
proposal = {
144+
"messages": [{
145+
"@type": "/cosmos.upgrade.v1beta1.MsgSoftwareUpgrade",
146+
"authority": "dydx10d07y265gmmuvt4z0w9aw880jnsr700jnmapky",
147+
"plan": {
148+
"name": upgrade_name,
149+
"height": str(upgrade_height),
150+
"info": f"Upgrade to {upgrade_name}"
151+
}
152+
}],
153+
"metadata": "",
154+
"deposit": "20000000adv4tnt",
155+
"title": f"Software Upgrade to {upgrade_name}",
156+
"summary": f"Upgrade the chain to {upgrade_name}"
157+
}
158+
159+
with open("proposal.json", "w", encoding="utf-8") as f:
160+
json.dump(proposal, f, indent=2)
161+
162+
print(f"Submitting upgrade proposal for {upgrade_name} at height {upgrade_height}...")
163+
164+
# Submit proposal
165+
cmd = [
166+
"dydxprotocold", "tx", "gov", "submit-proposal", "proposal.json",
167+
"--from", "alice",
168+
"--chain-id", chain_id,
169+
"--yes",
170+
"--broadcast-mode", "sync",
171+
"--gas", "auto",
172+
"--fees", "5000000000000000adv4tnt",
173+
"--keyring-backend", "test"
174+
]
175+
176+
result = run_cmd(cmd, node=node)
177+
if not result:
178+
os.remove("proposal.json")
179+
sys.exit(1)
180+
181+
# Extract txhash
182+
for line in result.split('\n'):
183+
if 'txhash:' in line:
184+
print(f"Submitted: {line.split('txhash:')[1].strip()}")
185+
186+
time.sleep(5)
187+
188+
# Get proposal ID
189+
result = run_cmd(["dydxprotocold", "query", "gov", "proposals", "--output", "json"], node=node)
190+
if not result:
191+
os.remove("proposal.json")
192+
sys.exit(1)
193+
194+
proposals = json.loads(result)
195+
proposal_id = proposals['proposals'][-1]['id']
196+
print(f"Proposal ID: {proposal_id}")
197+
198+
# Vote
199+
print(f"Voting with {len(VALIDATORS)} validators...")
200+
for voter in VALIDATORS:
201+
cmd = [
202+
"dydxprotocold", "tx", "gov", "vote", str(proposal_id), "yes",
203+
"--from", voter,
204+
"--chain-id", chain_id,
205+
"--yes",
206+
"--gas", "auto",
207+
"--fees", "5000000000000000adv4tnt",
208+
"--keyring-backend", "test"
209+
]
210+
result = run_cmd(cmd, node=node)
211+
if result and 'txhash:' in result:
212+
print(f" {voter}: ✓")
213+
else:
214+
print(f" {voter}: ✗")
215+
time.sleep(1)
216+
217+
# Clean up
218+
os.remove("proposal.json")
219+
220+
# Wait for voting period (same as blocks to upgrade)
221+
print(f"\nWaiting {wait_blocks} seconds (~{wait_blocks} blocks) for voting period...")
222+
for i in range(0, wait_blocks, 30):
223+
remaining = wait_blocks - i
224+
if remaining > 0:
225+
print(f" {remaining}s remaining...")
226+
time.sleep(min(30, remaining))
227+
228+
# Check status
229+
cmd = [
230+
"dydxprotocold", "query", "gov", "proposal",
231+
str(proposal_id), "--output", "json"
232+
]
233+
result = run_cmd(cmd, node=node)
234+
if result:
235+
data = json.loads(result)
236+
status = data['proposal']['status']
237+
print(f"\nFinal status: {status}")
238+
if "PASSED" in status:
239+
print(f"✅ Upgrade to {upgrade_name} approved for height {upgrade_height}")
240+
241+
if __name__ == "__main__":
242+
main()

0 commit comments

Comments
 (0)