Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions protocol/scripts/release_qual/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# Release Qualification Scripts

This directory contains scripts for testing and qualifying new releases on testnet/staging environments.

## Scripts

### submit_upgrade_proposal.py

A Python script to submit software upgrade proposals and automatically vote with test validators on testnet/staging environments.

**Features:**
- Submits upgrade proposals with a specified upgrade name and block height
- Automatically votes "yes" with all test validators
- Restricted to testnet/staging environments only (mainnet explicitly blocked)
- Configurable via command-line arguments or environment variables

**Usage:**
```bash
# Basic usage - upgrade in 300 blocks (default)
./submit_upgrade_proposal.py v5.0.0

# Specify number of blocks to wait
./submit_upgrade_proposal.py v5.0.0 500

# Use custom node and chain-id
./submit_upgrade_proposal.py v5.0.0 --node https://validator.custom.com:443 --chain-id dydxprotocol-testnet

# Using environment variables
export DYDX_NODE=https://validator.custom.com:443
export DYDX_CHAIN_ID=dydxprotocol-testnet
./submit_upgrade_proposal.py v5.0.0
```

**Configuration Priority:**
1. Command-line arguments (highest priority)
2. Environment variables (`DYDX_NODE`, `DYDX_CHAIN_ID`)
3. Default values (v4staging node and testnet chain-id)

**Safety Features:**
- Only works on allowed testnet/staging chain IDs
- Explicitly blocks mainnet chain IDs
- Requires user confirmation before submitting proposal
- Validates chain ID against allowed list

**Test Validators:**
The script automatically votes with these test validators:
- alice, bob, carl, dave, emily, fiona, greg, henry, ian, jeff

## Requirements

- Python 3
- `dydxprotocold` CLI installed and configured
- Test validator keys in the keyring (using `test` backend)
- Access to a testnet/staging node

## Security

These scripts are designed for **testnet/staging environments only**. The `submit_upgrade_proposal.py` script includes multiple safeguards to prevent accidental use on mainnet:
- Hardcoded list of allowed chain IDs (testnet/staging only)
- Explicit blocking of known mainnet chain IDs
- Chain ID validation before execution
242 changes: 242 additions & 0 deletions protocol/scripts/release_qual/submit_upgrade_proposal.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,242 @@
#!/usr/bin/env python3
"""
Submit a software upgrade proposal and auto-vote with test validators.
"""

import argparse
import json
import os
import subprocess
import sys
import time

# Test validators
VALIDATORS = ["alice", "bob", "carl", "dave", "emily", "fiona", "greg", "henry", "ian", "jeff"]

# Allowed chain IDs for testnet/staging only (blocking mainnet)
ALLOWED_CHAIN_IDS = [
"dydxprotocol-testnet", # Standard testnet/staging chain
]

Comment on lines +16 to +20
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Allow safe patterns for testnet/staging instead of a single explicit chain-id

The README says “testnet/staging only,” but the allow-list includes only dydxprotocol-testnet. This will block staging. Replace the strict allow-list with a safe pattern check while retaining the mainnet block-list.

Apply this diff to allow chain-ids containing “testnet” or “staging” (case-insensitive):

-# Allowed chain IDs for testnet/staging only (blocking mainnet)
-ALLOWED_CHAIN_IDS = [
-    "dydxprotocol-testnet",  # Standard testnet/staging chain
-]
+# Allowed chain-id patterns for testnet/staging only (blocking mainnet)
+ALLOWED_CHAIN_ID_PATTERNS = ("testnet", "staging")
@@
-    # Validate chain_id is allowed
-    if chain_id not in ALLOWED_CHAIN_IDS:
-        print(f"Error: Chain ID '{chain_id}' is not in the allowed list.")
-        print(f"Allowed chains: {', '.join(ALLOWED_CHAIN_IDS)}")
-        print("This script is restricted to testnet/staging environments only.")
-        sys.exit(1)
+    # Validate chain_id matches allowed patterns (still block mainnet above)
+    if not any(p in chain_id.lower() for p in ALLOWED_CHAIN_ID_PATTERNS):
+        print(f"Error: Chain ID '{chain_id}' is not allowed.")
+        print(f"Allowed chain-id patterns: {', '.join(ALLOWED_CHAIN_ID_PATTERNS)}")
+        print("This script is restricted to testnet/staging environments only.")
+        sys.exit(1)

Also applies to: 57-63

🤖 Prompt for AI Agents
In protocol/scripts/release_qual/submit_upgrade_proposal.py around lines 16-20
(and also update lines 57-63), the current allow-list hardcodes
"dydxprotocol-testnet" which blocks other testnet/staging chain-ids; replace the
strict list with a case-insensitive pattern check that permits any chain-id
containing "testnet" or "staging" and keep the explicit mainnet block (e.g.,
check that chain_id.lower() contains "testnet" or "staging" and otherwise
reject, while still preventing mainnet values).

# Explicitly blocked mainnet chain IDs
BLOCKED_CHAIN_IDS = [
"dydx-mainnet-1",
"dydxprotocol-mainnet",
"mainnet"
]

def load_config(args):
"""Load configuration from command-line args or environment variables.
Priority: command-line args > environment variables > defaults
"""
# Default values
default_node = "https://validator.v4staging.dydx.exchange:443"
default_chain_id = "dydxprotocol-testnet"

# First priority: command-line arguments
if args.node:
node = args.node
print(f"Using node from command-line: {node}")
else:
# Second priority: environment variables, fallback to defaults
node = os.environ.get("DYDX_NODE", default_node)

if args.chain_id:
chain_id = args.chain_id
print(f"Using chain-id from command-line: {chain_id}")
else:
# Second priority: environment variables, fallback to defaults
chain_id = os.environ.get("DYDX_CHAIN_ID", default_chain_id)

# Check if explicitly blocked (mainnet)
if chain_id in BLOCKED_CHAIN_IDS or "mainnet" in chain_id.lower():
print(f"Error: This script cannot run on mainnet (chain_id: {chain_id})")
print("This script is only for testnet/staging environments.")
sys.exit(1)

# Validate chain_id is allowed
if chain_id not in ALLOWED_CHAIN_IDS:
print(f"Error: Chain ID '{chain_id}' is not in the allowed list.")
print(f"Allowed chains: {', '.join(ALLOWED_CHAIN_IDS)}")
print("This script is restricted to testnet/staging environments only.")
sys.exit(1)

print(f"Using node: {node}")
print(f"Using chain-id: {chain_id}")

return node, chain_id

def run_cmd(cmd, node=None):
"""Run command and return stdout."""
# Add node flag if provided
if node and "--node" not in cmd:
cmd.extend(["--node", node])
try:
result = subprocess.run(
cmd, capture_output=True, text=True, check=True
)
return result.stdout
except subprocess.CalledProcessError as e:
print(f"Error: {e.stderr}")
return None

def main():
"""Submit a software upgrade proposal and auto-vote with test validators."""
parser = argparse.ArgumentParser(
description='Submit a software upgrade proposal and auto-vote with test validators.'
)
parser.add_argument('upgrade_name', help='Name of the upgrade (e.g., v5.0.0)')
parser.add_argument(
'blocks_to_wait', nargs='?', type=int, default=300,
help='Number of blocks to wait for an upgrade and voting period (default: 300)'
)
parser.add_argument(
'--node',
help='Node RPC endpoint (e.g., http://validator.v4staging.dydx.exchange:26657)'
)
parser.add_argument(
'--chain-id', dest='chain_id', help='Chain ID (e.g., dydxprotocol-testnet)'
)

args = parser.parse_args()

upgrade_name = args.upgrade_name
wait_blocks = args.blocks_to_wait

# Load configuration
node, chain_id = load_config(args)

# Display configuration and ask for confirmation
print("\n" + "="*60)
print("UPGRADE PROPOSAL CONFIGURATION")
print("="*60)
print(f"Chain ID: {chain_id}")
print(f"Node: {node}")
print(f"Upgrade Name: {upgrade_name}")
print(f"Block Wait: {wait_blocks} blocks")
print("="*60)

response = input("\nDo you want to proceed with this upgrade proposal? (yes/no): ")
if response.lower() not in ['yes', 'y']:
print("Upgrade proposal cancelled.")
sys.exit(0)

print("\nProceeding with upgrade proposal...")

# Get current block height
result = run_cmd(["dydxprotocold", "status"], node=node)
if result:
try:
status = json.loads(result)
current_height = int(status['sync_info']['latest_block_height'])
upgrade_height = current_height + wait_blocks
print(f"Current height: {current_height}, upgrade at: {upgrade_height}")
except (json.JSONDecodeError, KeyError) as e:
# Fallback if we can't get current height
print(f"Could not parse block height, using default. Error: {e}")
upgrade_height = 1000000
print(f"Using default upgrade height: {upgrade_height}")
else:
upgrade_height = 1000000

# Create proposal.json
proposal = {
"messages": [{
"@type": "/cosmos.upgrade.v1beta1.MsgSoftwareUpgrade",
"authority": "dydx10d07y265gmmuvt4z0w9aw880jnsr700jnmapky",
"plan": {
"name": upgrade_name,
"height": str(upgrade_height),
"info": f"Upgrade to {upgrade_name}"
}
}],
"metadata": "",
"deposit": "10000000000000000000000adv4tnt",
"title": f"Software Upgrade to {upgrade_name}",
"summary": f"Upgrade the chain to {upgrade_name}"
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Don’t hardcode the upgrade authority; fetch it from the chain

Using a hardcoded authority risks unauthorized proposals if the address is wrong. Cosmos SDK exposes the upgrade module authority via a query. Use it.

Apply this diff to retrieve authority dynamically and fail fast if it can’t be retrieved:

-    # Create proposal.json
-    proposal = {
+    # Resolve upgrade module authority dynamically
+    authority_resp = run_cmd(
+        ["dydxprotocold", "query", "upgrade", "authority", "--output", "json"],
+        node=node
+    )
+    if not authority_resp:
+        print("Error: Could not fetch upgrade module authority from chain.")
+        sys.exit(1)
+    try:
+        authority = json.loads(authority_resp)["authority"]
+    except (json.JSONDecodeError, KeyError) as e:
+        print(f"Error parsing authority from response: {e}")
+        sys.exit(1)
+
+    # Create proposal.json
+    proposal = {
         "messages": [{
             "@type": "/cosmos.upgrade.v1beta1.MsgSoftwareUpgrade",
-            "authority": "dydx10d07y265gmmuvt4z0w9aw880jnsr700jnmapky",
+            "authority": authority,
             "plan": {
                 "name": upgrade_name,
                 "height": str(upgrade_height),
                 "info": f"Upgrade to {upgrade_name}"
             }
         }],
         "metadata": "",
         "deposit": "10000000000000000000000adv4tnt",
         "title": f"Software Upgrade to {upgrade_name}",
         "summary": f"Upgrade the chain to {upgrade_name}"
     }

Please confirm dydxprotocold query upgrade authority is supported in your chain version. If not, I can fall back to computing the gov module address or a configurable env var.


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Showing file if present ==="
if [ -f protocol/scripts/release_qual/submit_upgrade_proposal.py ]; then
  sed -n '1,240p' protocol/scripts/release_qual/submit_upgrade_proposal.py || true
else
  echo "File not found: protocol/scripts/release_qual/submit_upgrade_proposal.py"
fi

echo
echo "=== Search for run_cmd usage and definition ==="
rg --line-number "run_cmd\(" || true
rg --line-number "def run_cmd" || true

echo
echo "=== Search for dydxprotocold occurrences ==="
rg --line-number "dydxprotocold" || true

echo
echo "=== Search for 'query upgrade authority' usage ==="
rg --line-number "query.*upgrade.*authority" || true

echo
echo "=== Search for literal \"authority\" occurrences in repo ==="
rg --line-number "\"authority\"" || true

Length of output: 44625


Don’t hardcode the upgrade authority — fetch it from the chain

Found a hardcoded authority in protocol/scripts/release_qual/submit_upgrade_proposal.py (the proposal JSON uses "dydx10d07..."). Hardcoding risks proposing with an incorrect/unauthorized address. Use the upgrade module's authority query and fail-fast if it can't be retrieved.

Files to update:

  • protocol/scripts/release_qual/submit_upgrade_proposal.py
  • protocol/scripts/markets/launch_markets.py
  • protocol/scripts/revshare/add_order_router_rev_share.py
  • protocol/scripts/governance/create_delisting_proposal.py

Apply this change to resolve the issue (uses existing run_cmd):

-    # Create proposal.json
-    proposal = {
+    # Resolve upgrade module authority dynamically
+    authority_resp = run_cmd(
+        ["dydxprotocold", "query", "upgrade", "authority", "--output", "json"],
+        node=node
+    )
+    if not authority_resp:
+        print("Error: Could not fetch upgrade module authority from chain.")
+        sys.exit(1)
+    try:
+        authority = json.loads(authority_resp)["authority"]
+    except (json.JSONDecodeError, KeyError) as e:
+        print(f"Error parsing authority from response: {e}")
+        sys.exit(1)
+
+    # Create proposal.json
+    proposal = {
         "messages": [{
             "@type": "/cosmos.upgrade.v1beta1.MsgSoftwareUpgrade",
-            "authority": "dydx10d07y265gmmuvt4z0w9aw880jnsr700jnmapky",
+            "authority": authority,
             "plan": {
                 "name": upgrade_name,
                 "height": str(upgrade_height),
                 "info": f"Upgrade to {upgrade_name}"
             }
         }],
         "metadata": "",
         "deposit": "10000000000000000000000adv4tnt",
         "title": f"Software Upgrade to {upgrade_name}",
         "summary": f"Upgrade the chain to {upgrade_name}"
     }

Please confirm that dydxprotocold query upgrade authority --output json is supported by the deployed CLI/binary in your environment; if it isn’t, fallback options are: compute the gov/module address programmatically or read the authority from a configurable env var.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
# Create proposal.json
proposal = {
"messages": [{
"@type": "/cosmos.upgrade.v1beta1.MsgSoftwareUpgrade",
"authority": "dydx10d07y265gmmuvt4z0w9aw880jnsr700jnmapky",
"plan": {
"name": upgrade_name,
"height": str(upgrade_height),
"info": f"Upgrade to {upgrade_name}"
}
}],
"metadata": "",
"deposit": "10000000000000000000000adv4tnt",
"title": f"Software Upgrade to {upgrade_name}",
"summary": f"Upgrade the chain to {upgrade_name}"
}
# Resolve upgrade module authority dynamically
authority_resp = run_cmd(
["dydxprotocold", "query", "upgrade", "authority", "--output", "json"],
node=node
)
if not authority_resp:
print("Error: Could not fetch upgrade module authority from chain.")
sys.exit(1)
try:
authority = json.loads(authority_resp)["authority"]
except (json.JSONDecodeError, KeyError) as e:
print(f"Error parsing authority from response: {e}")
sys.exit(1)
# Create proposal.json
proposal = {
"messages": [{
"@type": "/cosmos.upgrade.v1beta1.MsgSoftwareUpgrade",
"authority": authority,
"plan": {
"name": upgrade_name,
"height": str(upgrade_height),
"info": f"Upgrade to {upgrade_name}"
}
}],
"metadata": "",
"deposit": "10000000000000000000000adv4tnt",
"title": f"Software Upgrade to {upgrade_name}",
"summary": f"Upgrade the chain to {upgrade_name}"
}
🤖 Prompt for AI Agents
In protocol/scripts/release_qual/submit_upgrade_proposal.py around lines
142-158, the proposal JSON hardcodes the upgrade authority address; instead run
the chain query (e.g. use the existing run_cmd helper to execute `dydxprotocold
query upgrade authority --output json`), parse the returned authority, fail-fast
with an explicit error if the query or parsing fails, and replace the hardcoded
string with the queried authority; apply the same change pattern to
protocol/scripts/markets/launch_markets.py,
protocol/scripts/revshare/add_order_router_rev_share.py, and
protocol/scripts/governance/create_delisting_proposal.py, and if the CLI query
is not available fall back to either a configured env var or computing the
module/gov address programmatically, ensuring all scripts error out early when
no valid authority is obtained.

with open("proposal.json", "w", encoding="utf-8") as f:
json.dump(proposal, f, indent=2)

print(f"Submitting upgrade proposal for {upgrade_name} at height {upgrade_height}...")

# Submit proposal
cmd = [
"dydxprotocold", "tx", "gov", "submit-proposal", "proposal.json",
"--from", "alice",
"--chain-id", chain_id,
"--yes",
"--broadcast-mode", "sync",
"--gas", "auto",
"--fees", "5000000000000000adv4tnt",
"--keyring-backend", "test"
]

Comment on lines +165 to +175
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Make proposal ID retrieval deterministic (parse tx JSON; block-broadcast)

Relying on “last proposal” can race if multiple proposals exist. Broadcast the tx in “block” mode, request JSON output, and parse the emitted proposal_id. Retain a fallback to the proposals query if parsing fails.

Apply this diff:

     # Submit proposal
     cmd = [
         "dydxprotocold", "tx", "gov", "submit-proposal", "proposal.json",
         "--from", "alice",
         "--chain-id", chain_id,
-        "--yes",
-        "--broadcast-mode", "sync",
+        "--yes",
+        "--broadcast-mode", "block",
         "--gas", "auto",
         "--fees", "5000000000000000adv4tnt",
-        "--keyring-backend", "test"
+        "--keyring-backend", "test",
+        "--output", "json",
     ]
 
     result = run_cmd(cmd, node=node)
     if not result:
         os.remove("proposal.json")
         sys.exit(1)
 
-    # Extract txhash
-    for line in result.split('\n'):
-        if 'txhash:' in line:
-            print(f"Submitted: {line.split('txhash:')[1].strip()}")
-
-    time.sleep(5)
-
-    # Get proposal ID
-    result = run_cmd(["dydxprotocold", "query", "gov", "proposals", "--output", "json"], node=node)
-    if not result:
-        os.remove("proposal.json")
-        sys.exit(1)
-
-    proposals = json.loads(result)
-    proposal_id = proposals['proposals'][-1]['id']
-    print(f"Proposal ID: {proposal_id}")
+    # Extract txhash and proposal_id from tx result
+    proposal_id = None
+    try:
+        tx = json.loads(result)
+        print(f"Submitted: {tx.get('txhash', '')}")
+        # Try to find proposal_id in events
+        for log in tx.get("logs", []):
+            for ev in log.get("events", []):
+                if ev.get("type") in ("submit_proposal", "proposal_submit"):
+                    for attr in ev.get("attributes", []):
+                        if attr.get("key") in ("proposal_id", "proposalId"):
+                            proposal_id = attr.get("value")
+                            break
+                if proposal_id:
+                    break
+            if proposal_id:
+                break
+    except (json.JSONDecodeError, KeyError) as e:
+        print(f"Warning: could not parse tx json for proposal_id: {e}")
+
+    # Fallback: query proposals if not found
+    if not proposal_id:
+        time.sleep(5)
+        result = run_cmd(
+            ["dydxprotocold", "query", "gov", "proposals", "--output", "json"],
+            node=node
+        )
+        if not result:
+            os.remove("proposal.json")
+            sys.exit(1)
+        proposals = json.loads(result).get("proposals", [])
+        if not proposals:
+            print("Error: no proposals returned from chain")
+            os.remove("proposal.json")
+            sys.exit(1)
+        proposal_id = proposals[-1]["id"]
+
+    print(f"Proposal ID: {proposal_id}")

Also applies to: 181-197

🤖 Prompt for AI Agents
In protocol/scripts/release_qual/submit_upgrade_proposal.py around lines 165-175
(and similarly adjust lines 181-197), the script currently relies on querying
the "last proposal" which can race; modify the tx submit command to use
"--broadcast-mode", "block" and add "--output", "json" so the node returns
transaction result JSON, then parse that JSON to extract the emitted
"proposal_id" deterministically from the tx response (events/logs) and use it;
keep the existing proposals query as a fallback if parsing fails, and ensure
errors are handled and logged if neither method yields a proposal_id.

result = run_cmd(cmd, node=node)
if not result:
os.remove("proposal.json")
sys.exit(1)

# Extract txhash
for line in result.split('\n'):
if 'txhash:' in line:
print(f"Submitted: {line.split('txhash:')[1].strip()}")

time.sleep(5)

# Get proposal ID
result = run_cmd(["dydxprotocold", "query", "gov", "proposals", "--output", "json"], node=node)
if not result:
os.remove("proposal.json")
sys.exit(1)

proposals = json.loads(result)
proposal_id = proposals['proposals'][-1]['id']
print(f"Proposal ID: {proposal_id}")

# Vote
print(f"Voting with {len(VALIDATORS)} validators...")
for voter in VALIDATORS:
cmd = [
"dydxprotocold", "tx", "gov", "vote", str(proposal_id), "yes",
"--from", voter,
"--chain-id", chain_id,
"--yes",
"--gas", "auto",
"--fees", "5000000000000000adv4tnt",
"--keyring-backend", "test"
]
result = run_cmd(cmd, node=node)
if result and 'txhash:' in result:
print(f" {voter}: ✓")
else:
print(f" {voter}: ✗")
time.sleep(1)

Comment on lines +198 to +216
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Vote reliably: use block broadcast + JSON and check tx code instead of scanning for ‘txhash:’

String scanning is brittle. Broadcast in block mode, request JSON, and verify code==0. This yields deterministic success/failure per vote.

Apply this diff:

     for voter in VALIDATORS:
         cmd = [
             "dydxprotocold", "tx", "gov", "vote", str(proposal_id), "yes",
             "--from", voter,
             "--chain-id", chain_id,
             "--yes",
+            "--broadcast-mode", "block",
             "--gas", "auto",
             "--fees", "5000000000000000adv4tnt",
-            "--keyring-backend", "test"
+            "--keyring-backend", "test",
+            "--output", "json",
         ]
         result = run_cmd(cmd, node=node)
-        if result and 'txhash:' in result:
-            print(f"  {voter}: ✓")
-        else:
-            print(f"  {voter}: ✗")
+        ok = False
+        if result:
+            try:
+                vote_tx = json.loads(result)
+                ok = vote_tx.get("code", 0) == 0
+            except json.JSONDecodeError:
+                ok = 'txhash' in result  # last-resort fallback
+        print(f"  {voter}: {'✓' if ok else '✗'}")
         time.sleep(1)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
# Vote
print(f"Voting with {len(VALIDATORS)} validators...")
for voter in VALIDATORS:
cmd = [
"dydxprotocold", "tx", "gov", "vote", str(proposal_id), "yes",
"--from", voter,
"--chain-id", chain_id,
"--yes",
"--gas", "auto",
"--fees", "5000000000000000adv4tnt",
"--keyring-backend", "test"
]
result = run_cmd(cmd, node=node)
if result and 'txhash:' in result:
print(f" {voter}: ✓")
else:
print(f" {voter}: ✗")
time.sleep(1)
# Vote
print(f"Voting with {len(VALIDATORS)} validators...")
for voter in VALIDATORS:
cmd = [
"dydxprotocold", "tx", "gov", "vote", str(proposal_id), "yes",
"--from", voter,
"--chain-id", chain_id,
"--yes",
"--broadcast-mode", "block",
"--gas", "auto",
"--fees", "5000000000000000adv4tnt",
"--keyring-backend", "test",
"--output", "json",
]
result = run_cmd(cmd, node=node)
ok = False
if result:
try:
vote_tx = json.loads(result)
ok = vote_tx.get("code", 0) == 0
except json.JSONDecodeError:
ok = 'txhash' in result # last-resort fallback
print(f" {voter}: {'✓' if ok else '✗'}")
time.sleep(1)
🤖 Prompt for AI Agents
In protocol/scripts/release_qual/submit_upgrade_proposal.py around lines 198 to
216, the vote loop currently scans the command output for the string "txhash:"
which is brittle; change the vote invocation to broadcast in block mode and
return JSON (add "--broadcast-mode", "block", "--output", "json"), then parse
the JSON response and check the tx response code (tx_response.code == 0) to
determine success or failure, printing a success mark when code == 0 and a
failure mark otherwise; ensure you handle and log JSON parse errors and non-zero
codes as failures and keep the 1s sleep between votes.

# Clean up
os.remove("proposal.json")

# Wait for voting period (same as blocks to upgrade)
print(f"\nWaiting {wait_blocks} seconds (~{wait_blocks} blocks) for voting period...")
for i in range(0, wait_blocks, 30):
remaining = wait_blocks - i
if remaining > 0:
print(f" {remaining}s remaining...")
time.sleep(min(30, remaining))

# Check status
cmd = [
"dydxprotocold", "query", "gov", "proposal",
str(proposal_id), "--output", "json"
]
result = run_cmd(cmd, node=node)
if result:
data = json.loads(result)
status = data['proposal']['status']
print(f"\nFinal status: {status}")
if "PASSED" in status:
print(f"✅ Upgrade to {upgrade_name} approved for height {upgrade_height}")

if __name__ == "__main__":
main()
Loading