Skip to content

Conversation

@jusbar23
Copy link
Contributor

@jusbar23 jusbar23 commented Aug 13, 2025

Changelist

  • Add a script for upgrading staging for release qual

Test Plan

  • Tested with v9.0 release

Author/Reviewer Checklist

  • If this PR has changes that result in a different app state given the same prior state and transaction list, manually add the state-breaking label.
  • If the PR has breaking postgres changes to the indexer add the indexer-postgres-breaking label.
  • If this PR isn't state-breaking but has changes that modify behavior in PrepareProposal or ProcessProposal, manually add the label proposal-breaking.
  • If this PR is one of many that implement a specific feature, manually label them all feature:[feature-name].
  • If you wish to for mergify-bot to automatically create a PR to backport your change to a release branch, manually add the label backport/[branch-name].
  • Manually add any of the following labels: refactor, chore, bug.

Summary by CodeRabbit

  • New Features

    • Introduces a release qualification tool to submit software upgrade proposals on test networks. Supports CLI and environment configuration, calculates upgrade height, requires confirmation, enforces safety checks to block mainnet, auto-submits proposals, casts “yes” votes with test validators, and reports final status.
  • Documentation

    • Adds a README with setup requirements, usage examples, configuration precedence, supported chain/node settings, validator list, and safety guidance for testnet/staging-only use.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Aug 13, 2025

Walkthrough

Adds a new release qualification utility under protocol/scripts/release_qual/: a README documenting usage and safeguards, and a Python script that submits a software upgrade proposal on testnet/staging and auto-votes with predefined test validators, enforcing strict chain-id restrictions and interactive confirmation.

Changes

Cohort / File(s) Summary of changes
Release Qual Docs
protocol/scripts/release_qual/README.md
New README describing the release qualification script, configuration, usage examples, allowed environments, validator set, and safety constraints.
Governance Automation Script
protocol/scripts/release_qual/submit_upgrade_proposal.py
New Python script to submit a software upgrade proposal and cast “yes” votes using test validators; includes config loading (CLI/env/defaults), chain-id allow/block lists, node status query, proposal file generation, submission, voting, and result query.

Sequence Diagram(s)

sequenceDiagram
  participant U as User
  participant S as submit_upgrade_proposal.py
  participant C as dydxprotocold CLI
  participant N as Node (RPC)
  participant G as Gov Module

  U->>S: Run with args/env (upgrade_name, blocks_to_wait, node, chain-id)
  S->>S: load_config() (validate allow/block lists)
  S-->>U: Prompt for confirmation
  U-->>S: Confirm

  S->>C: status --node <node>
  C->>N: Query status
  N-->>C: Current block height
  C-->>S: Height
  S->>S: Compute upgrade_height

  S->>S: Write proposal.json (MsgSoftwareUpgrade)
  S->>C: tx gov submit-proposal (from alice) --node <node>
  C->>N: Broadcast tx
  N->>G: Create proposal
  N-->>C: txhash
  C-->>S: txhash

  S->>C: query gov proposals --node <node>
  C->>N: List proposals
  N-->>C: proposal_id
  C-->>S: proposal_id

  loop For each test validator
    S->>C: tx gov vote <proposal_id> yes --from <validator> --node <node>
    C->>N: Broadcast vote
    N->>G: Record vote
  end

  S->>C: query gov proposal <proposal_id> --node <node>
  C->>N: Get proposal status
  N-->>C: Status (e.g., PASSED)
  C-->>S: Status
  S-->>U: Print outcome
Loading

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~8 minutes

Possibly related PRs

Poem

I thump the ground: “Upgrade time!” I cheer,
With testnet carrots, validators near.
We hop through blocks, proposals in a row,
A chorus of “yes” to make it go.
Mainnet’s fence we do not pass—
Safe burrow paths, approvals fast!
Ship it, squeak the ears at last.

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch add_release_qual_script

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (6)
protocol/scripts/release_qual/README.md (1)

17-32: Usage: add executable bit guidance and alternative invocation

To avoid friction, add a note to mark the script executable or run it via python. This helps on systems where the file isn’t yet chmod’d.

Apply this diff to add a brief note:

 **Usage:**
 ```bash
 # Basic usage - upgrade in 300 blocks (default)
-./submit_upgrade_proposal.py v5.0.0
+chmod +x submit_upgrade_proposal.py
+./submit_upgrade_proposal.py v5.0.0
+
+# Or run via Python
+python3 submit_upgrade_proposal.py v5.0.0

</blockquote></details>
<details>
<summary>protocol/scripts/release_qual/submit_upgrade_proposal.py (5)</summary><blockquote>

`69-82`: **Harden command execution: add timeouts and better error context**

Long-running RPCs can hang. Add a timeout and improve error messages to include the command executed.


Apply this diff:

```diff
-def run_cmd(cmd, node=None):
-    """Run command and return stdout."""
+def run_cmd(cmd, node=None, timeout=60):
+    """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
+            cmd, capture_output=True, text=True, check=True, timeout=timeout
         )
         return result.stdout
-    except subprocess.CalledProcessError as e:
-        print(f"Error: {e.stderr}")
+    except subprocess.TimeoutExpired:
+        print(f"Error: command timed out: {' '.join(cmd)}")
+        return None
+    except subprocess.CalledProcessError as e:
+        print(f"Error running: {' '.join(cmd)}")
+        print(f"stderr: {e.stderr}")
         return None

126-141: Height parsing: include ValueError in exception handling

int(status['sync_info']['latest_block_height']) can raise ValueError. It’s currently unhandled.

Apply this diff:

-        except (json.JSONDecodeError, KeyError) as e:
+        except (json.JSONDecodeError, KeyError, ValueError) 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}")

199-216: Vote broadcasts: consider block mode for deterministic inclusion

To reduce flakiness and get immediate confirmation, broadcast votes in “block” mode as well, mirroring the proposal submission. This is especially helpful when loops send multiple txs quickly.

Apply this diff:

         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"
         ]

217-219: Ensure proposal.json is always cleaned up; prefer a temp file

If the process exits early (e.g., KeyboardInterrupt), proposal.json may be left behind. Use a temp file and ensure cleanup in a finally block.

Apply this diff to make cleanup resilient:

-    # Clean up
-    os.remove("proposal.json")
+    # Clean up
+    try:
+        os.remove("proposal.json")
+    except FileNotFoundError:
+        pass

Additionally, consider switching to a NamedTemporaryFile so you don’t clash with existing files:

# At top-level imports:
import tempfile

# Replace the hardcoded filename usage:
with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False) as tf:
    json.dump(proposal, tf, indent=2)
    proposal_path = tf.name

# Use proposal_path instead of "proposal.json"
# And in cleanup:
try:
    os.remove(proposal_path)
except FileNotFoundError:
    pass

221-227: Avoid assuming 1s block time; poll voting_end_time or height

Sleeping N seconds as a proxy for N blocks is brittle. Poll the proposal’s voting_end_time or current block height to decide when to check final status.

Example approach:

  • Parse voting_end_time from query gov proposal <id> --output json and sleep until that time plus a small buffer.
  • Alternatively, poll status every few seconds until it transitions out of VOTING_PERIOD or a max timeout elapses.

I can wire either approach if you prefer.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7894730 and 9562b5a.

📒 Files selected for processing (2)
  • protocol/scripts/release_qual/README.md (1 hunks)
  • protocol/scripts/release_qual/submit_upgrade_proposal.py (1 hunks)
🧰 Additional context used
🪛 LanguageTool
protocol/scripts/release_qual/README.md

[grammar] ~11-~11: There might be a mistake here.
Context: ...stnet/staging environments. Features: - Submits upgrade proposals with a specifi...

(QB_NEW_EN)


[grammar] ~12-~12: There might be a mistake here.
Context: ... specified upgrade name and block height - Automatically votes "yes" with all test ...

(QB_NEW_EN)


[grammar] ~13-~13: There might be a mistake here.
Context: ...lly votes "yes" with all test validators - Restricted to testnet/staging environmen...

(QB_NEW_EN)


[grammar] ~14-~14: There might be a mistake here.
Context: ...nments only (mainnet explicitly blocked) - Configurable via command-line arguments ...

(QB_NEW_EN)


[grammar] ~34-~34: There might be a mistake here.
Context: ...py v5.0.0 ``` Configuration Priority: 1. Command-line arguments (highest priority...

(QB_NEW_EN)


[grammar] ~35-~35: There might be a mistake here.
Context: ...ommand-line arguments (highest priority) 2. Environment variables (DYDX_NODE, `DYD...

(QB_NEW_EN)


[grammar] ~36-~36: There might be a mistake here.
Context: ...variables (DYDX_NODE, DYDX_CHAIN_ID) 3. Default values (v4staging node and testn...

(QB_NEW_EN)


[grammar] ~39-~39: There might be a mistake here.
Context: ...nd testnet chain-id) Safety Features: - Only works on allowed testnet/staging ch...

(QB_NEW_EN)


[grammar] ~45-~45: There might be a mistake here.
Context: ...ainst allowed list Test Validators: The script automatically votes with thes...

(QB_NEW_EN)


[grammar] ~46-~46: There might be a mistake here.
Context: ...ically votes with these test validators: - alice, bob, carl, dave, emily, fiona, gr...

(QB_NEW_EN)


[grammar] ~52-~52: There might be a mistake here.
Context: ...xprotocold` CLI installed and configured - Test validator keys in the keyring (usin...

(QB_NEW_EN)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (28)
  • GitHub Check: benchmark
  • GitHub Check: container-tests
  • GitHub Check: build
  • GitHub Check: liveness-test
  • GitHub Check: test-coverage-upload
  • GitHub Check: unit-end-to-end-and-integration
  • GitHub Check: test-race
  • GitHub Check: check-sample-pregenesis-up-to-date
  • GitHub Check: golangci-lint
  • GitHub Check: (Mainnet) Build and Push ECS Services / call-build-and-push-vulcan / (vulcan) Build and Push
  • GitHub Check: (Mainnet) Build and Push ECS Services / call-build-and-push-ecs-service-roundtable / (roundtable) Build and Push
  • GitHub Check: (Mainnet) Build and Push ECS Services / call-build-and-push-ecs-service-socks / (socks) Build and Push
  • GitHub Check: (Mainnet) Build and Push ECS Services / call-build-and-push-auxo-lambda / (auxo) Build and Push Lambda
  • GitHub Check: (Mainnet) Build and Push ECS Services / call-build-and-push-ecs-service-ender / (ender) Build and Push
  • GitHub Check: (Mainnet) Build and Push ECS Services / call-build-and-push-ecs-service-comlink / (comlink) Build and Push
  • GitHub Check: (Mainnet) Build and Push ECS Services / call-build-and-push-bazooka-lambda / (bazooka) Build and Push Lambda
  • GitHub Check: (Public Testnet) Build and Push ECS Services / call-build-and-push-auxo-lambda / (auxo) Build and Push Lambda
  • GitHub Check: (Public Testnet) Build and Push ECS Services / call-build-and-push-vulcan / (vulcan) Build and Push
  • GitHub Check: (Public Testnet) Build and Push ECS Services / call-build-and-push-bazooka-lambda / (bazooka) Build and Push Lambda
  • GitHub Check: (Public Testnet) Build and Push ECS Services / call-build-and-push-ecs-service-roundtable / (roundtable) Build and Push
  • GitHub Check: (Public Testnet) Build and Push ECS Services / call-build-and-push-ecs-service-comlink / (comlink) Build and Push
  • GitHub Check: (Public Testnet) Build and Push ECS Services / call-build-and-push-ecs-service-ender / (ender) Build and Push
  • GitHub Check: (Public Testnet) Build and Push ECS Services / call-build-and-push-ecs-service-socks / (socks) Build and Push
  • GitHub Check: build-and-push-testnet
  • GitHub Check: build-and-push-mainnet
  • GitHub Check: Analyze (javascript-typescript)
  • GitHub Check: Analyze (go)
  • GitHub Check: Summary
🔇 Additional comments (2)
protocol/scripts/release_qual/README.md (1)

39-44: Docs/code mismatch — not an issue: staging uses "dydxprotocol-testnet"

Verified the repo: the script's allow-list and defaults already target the staging node/chain-id used in this repo, so the README's "testnet/staging" wording is accurate and the original comment can be ignored.

  • Evidence:
    • protocol/scripts/release_qual/submit_upgrade_proposal.py
      • ALLOWED_CHAIN_IDS = ["dydxprotocol-testnet"]
      • default_node = "https://validator.v4staging.dydx.exchange:443"
      • default_chain_id = "dydxprotocol-testnet"
      • Explicit mainnet blocking: BLOCKED_CHAIN_IDS and check for "mainnet" in chain_id.lower()
    • protocol/scripts/release_qual/README.md
      • Usage and examples reference --chain-id dydxprotocol-testnet and export DYDX_CHAIN_ID=dydxprotocol-testnet; describes "testnet/staging" and safety features
    • Other files also set CHAIN_ID="dydxprotocol-testnet" (protocol/testing/testnet-staging/staging.sh, protocol/testing/testnet-dev/dev.sh, protocol/testing/snapshotting/snapshot.sh)

Recommendation (optional): if you expect distinct staging chain-id names in the future, either expand ALLOWED_CHAIN_IDS or switch to a safe pattern match (allow "testnet"/"staging" substrings while blocking "mainnet").

Likely an incorrect or invalid review comment.

protocol/scripts/release_qual/submit_upgrade_proposal.py (1)

33-35: Default node/chain-id pairing looks inconsistent

Default node points to v4staging, but default chain-id is testnet. If that staging endpoint doesn’t actually serve the testnet chain-id, the script will fail early on chain-id validation or during queries.

  • Confirm that https://validator.v4staging.dydx.exchange:443 is indeed a node for dydxprotocol-testnet.
  • If not, consider changing either the default chain-id or default node so they align.

Comment on lines +16 to +20
# Allowed chain IDs for testnet/staging only (blocking mainnet)
ALLOWED_CHAIN_IDS = [
"dydxprotocol-testnet", # Standard testnet/staging chain
]

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).

Comment on lines 142 to 158
# 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.

Comment on lines +165 to +175
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"
]

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.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (3)
protocol/scripts/release_qual/submit_upgrade_proposal.py (3)

16-19: Allow safe testnet/staging patterns instead of a single chain-id; keep explicit mainnet block

Current allow-list only includes dydxprotocol-testnet, which blocks staging. Switch to a case-insensitive pattern check for "testnet" or "staging" while retaining the explicit mainnet block you already have.

Apply this diff:

-# 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 blocked by mainnet check 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


142-158: Don’t hardcode the upgrade authority — query it from the chain and fail fast if unavailable

Hardcoding the authority risks submitting an invalid/unauthorized proposal on different environments. Resolve the authority via the upgrade module query before building the proposal JSON.

Apply this diff:

-    # 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": "20000000adv4tnt",
         "title": f"Software Upgrade to {upgrade_name}",
         "summary": f"Upgrade the chain to {upgrade_name}"
     }

If your chain/binary doesn’t support this query, let’s fall back to a configurable env var (e.g., DYDX_UPGRADE_AUTHORITY) as a temporary workaround.


165-175: Make proposal ID retrieval deterministic: block broadcast + parse JSON instead of “last proposal”

Querying “last proposal” can race when multiple proposals are in-flight. Broadcast in block mode with JSON output and parse the emitted proposal_id from tx logs, with a safe fallback.

Apply this diff:

     cmd = [
         "dydxprotocold", "tx", "gov", "submit-proposal", "proposal.json",
         "--from", "alice",
         "--chain-id", chain_id,
         "--yes",
-        "--broadcast-mode", "sync",
+        "--broadcast-mode", "block",
         "--gas", "auto",
         "--fees", "5000000000000000adv4tnt",
-        "--keyring-backend", "test"
+        "--keyring-backend", "test",
+        "--output", "json",
     ]
@@
-    # 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', '')}")
+        # Optionally surface error code
+        if tx.get("code", 0) != 0:
+            print(f"Error: proposal submit failed with code={tx.get('code')} log={tx.get('raw_log')}")
+            os.remove("proposal.json")
+            sys.exit(1)
+        # 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

🧹 Nitpick comments (4)
protocol/scripts/release_qual/submit_upgrade_proposal.py (4)

69-82: Avoid mutating caller’s cmd list in run_cmd; make --node detection robust

Mutating the caller-provided list can cause subtle bugs if reused. Copy the list and detect existing --node even when passed as “--node=…”.

Apply this diff:

 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])
+    # Work on a copy and add node flag if provided (idempotent)
+    cmd = list(cmd)
+    if node and not any(arg.startswith("--node") for arg 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

126-141: Be more robust when deriving current height: add a fallback to “query block” before defaulting

Status parsing can vary; try querying the latest block height if status JSON parsing fails or status isn’t available, before falling back to a magic number.

Apply this diff:

-    # 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
+    # Get current block height (with fallback)
+    result = run_cmd(["dydxprotocold", "status"], node=node)
+    parsed_height = None
+    if result:
+        try:
+            status = json.loads(result)
+            parsed_height = int(status["sync_info"]["latest_block_height"])
+        except (json.JSONDecodeError, KeyError, ValueError) as e:
+            print(f"Warning: could not parse height from status: {e}")
+    if parsed_height is None:
+        block_res = run_cmd(["dydxprotocold", "query", "block", "--output", "json"], node=node)
+        if block_res:
+            try:
+                block = json.loads(block_res)
+                parsed_height = int(block["block"]["header"]["height"])
+            except (json.JSONDecodeError, KeyError, ValueError) as e:
+                print(f"Warning: could not parse height from query block: {e}")
+    if parsed_height is None:
+        print("Could not determine current height reliably; using default upgrade height.")
+        upgrade_height = 1000000
+        print(f"Using default upgrade height: {upgrade_height}")
+    else:
+        current_height = parsed_height
+        upgrade_height = current_height + wait_blocks
+        print(f"Current height: {current_height}, upgrade at: {upgrade_height}")

220-240: Replace fixed sleep with status polling up to a timeout; exit early on terminal states

Rather than sleeping a fixed number of seconds, poll the proposal status at a short interval up to a max duration. Exit as soon as PASSED/REJECTED/CANCELLED is observed.

Apply this diff:

-    # 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}")
+    # Poll proposal status until terminal state or timeout
+    print(f"\nPolling proposal status for up to {wait_blocks}s (~{wait_blocks} blocks)...")
+    start = time.time()
+    terminal = ("PASSED", "REJECTED", "FAILED", "CANCELLED")
+    last_status = None
+    while time.time() - start < wait_blocks:
+        cmd = ["dydxprotocold", "query", "gov", "proposal", str(proposal_id), "--output", "json"]
+        result = run_cmd(cmd, node=node)
+        if result:
+            try:
+                data = json.loads(result)
+                status = data["proposal"]["status"]
+                if status != last_status:
+                    print(f"  Status: {status}")
+                    last_status = status
+                if any(t in status for t in terminal):
+                    print(f"\nFinal status: {status}")
+                    if "PASSED" in status:
+                        print(f"✅ Upgrade to {upgrade_name} approved for height {upgrade_height}")
+                    break
+            except (json.JSONDecodeError, KeyError):
+                pass
+        time.sleep(10)
+    else:
+        # Timeout
+        print("\nTimed out waiting for final status. Please check the proposal manually.")

94-99: Nit: Align --node help example with the default value to reduce confusion

Default uses HTTPS on 443; the help example shows HTTP on 26657. Aligning these avoids mixed signals.

Apply this diff:

     parser.add_argument(
         '--node',
-        help='Node RPC endpoint (e.g., http://validator.v4staging.dydx.exchange:26657)'
+        help='Node RPC endpoint (e.g., https://validator.v4staging.dydx.exchange:443)'
     )
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9562b5a and 9bee12a.

📒 Files selected for processing (1)
  • protocol/scripts/release_qual/submit_upgrade_proposal.py (1 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (9)
  • GitHub Check: test-sim-multi-seed-short
  • GitHub Check: test-sim-import-export
  • GitHub Check: container-tests
  • GitHub Check: test-coverage-upload
  • GitHub Check: liveness-test
  • GitHub Check: test-race
  • GitHub Check: unit-end-to-end-and-integration
  • GitHub Check: benchmark
  • GitHub Check: Summary
🔇 Additional comments (2)
protocol/scripts/release_qual/submit_upgrade_proposal.py (2)

154-154: Confirm deposit denom/amount against chain params (min_deposit) and fee policy

The deposit and fee amounts/denom are hardcoded. Ensure they satisfy min_deposit and any staging fee requirements to avoid proposal rejection or non-determinism across environments.

Please verify on your target chain:

  • dydxprotocold query gov params --output json
  • Confirm that deposit >= params.deposit_params.min_deposit and denom matches.
  • Confirm fee denom is accepted and gas/fee settings are sane for your test nodes.

If you want, I can refactor the script to read these from environment variables or query chain params and compute a compliant deposit automatically.


13-15: LGTM on validator list for staging/test environments

The validator aliases align with the documented test keyring and look correct for auto-voting in staging.

Comment on lines +198 to +216
# 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)

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.

@jusbar23
Copy link
Contributor Author

this doesn't have a protocol change, only a release qual script

@jusbar23 jusbar23 merged commit fb16503 into main Aug 13, 2025
36 of 38 checks passed
@jusbar23 jusbar23 deleted the add_release_qual_script branch August 13, 2025 20:10
pthmas pushed a commit to 01builders/v4-chain that referenced this pull request Sep 26, 2025
pthmas pushed a commit to 01builders/v4-chain that referenced this pull request Sep 29, 2025
@jusbar23
Copy link
Contributor Author

@Mergifyio backport release/protocol/v9.x

@mergify
Copy link
Contributor

mergify bot commented Oct 23, 2025

backport release/protocol/v9.x

✅ Backports have been created

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Development

Successfully merging this pull request may close these issues.

3 participants