Skip to content

dpdanpittman/cosmos-auto-voter

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

2 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Cosmos Auto-Voter

Automated governance voting system for Cosmos SDK chains using the Authz module.

Overview

This application automatically votes "ABSTAIN" on all active governance proposals across Cosmos SDK chains. It uses the Authz module to vote on behalf of validators, allowing a single wallet to manage voting across multiple validator addresses.

Features

  • Authz-based voting - One wallet authorized to vote on behalf of validators
  • RPC/gRPC polling - Automatically discovers active proposals
  • Idempotent voting - Checks both local state and on-chain to prevent duplicate votes
  • Automatic restart - Exponential backoff retry logic for resilient operation
  • Comprehensive logging - Detailed logs for all voting decisions (voted, skipped, failed)
  • Custom chain support - Works with any Cosmos SDK chain (requires chain-specific address prefix configuration)
  • Full observability - Prometheus metrics, health checks, and structured logging
  • Dry-run mode - Test without actually submitting votes

Prerequisites

  • Go 1.21+
  • Access to a Cosmos SDK chain (RPC and gRPC endpoints)
  • A wallet with funds for transaction fees
  • Authz grant from the validator address(es) you want to vote on behalf of

Installation

Clone and Build

git clone <your-repo-url>
cd cosmos-auto-voter

# Build the binary (outputs to bin/autovoter)
make build

# Or install to /usr/local/bin (requires sudo)
make install

Docker Build

docker build -t cosmos-auto-voter:latest .

Configuration

All configuration is via environment variables:

Required Variables

Variable Description Example
CHAIN_ID Cosmos chain ID osmosis-1, cosmoshub-4, autovoter-1
RPC_ENDPOINT RPC endpoint URL http://localhost:26657, https://rpc.osmosis.zone
GRPC_ENDPOINT gRPC endpoint (host:port, no http://) localhost:9090, grpc.osmosis.zone:9090
GRANTER_ADDRESS Validator address that granted authz permission cosmos1abc..., osmo1xyz...
WALLET_MNEMONIC or WALLET_MNEMONIC_FILE BIP39 mnemonic phrase (24 words) or path to file containing it See Wallet Configuration below
GAS_PRICE Gas price with denomination 10000ncheq, 0.025uosmo, 0.005uatom

Optional Variables

Variable Default Description
CHAIN_NAME (empty) Human-readable chain name for logging/metrics
POLL_INTERVAL 60s How often to check for new proposals (supports s, m, h). Examples: 30s, 5m, 1h
GAS_LIMIT 200000 Maximum gas to use per transaction
STATE_DB /data/votes.db Path to SQLite database (auto-created, but parent directory must exist)
HTTP_PORT 8080 Port for health check endpoints
METRICS_PORT 9090 Port for Prometheus metrics endpoint
DRY_RUN false Test mode - polls and logs but doesn't submit votes (set to true for testing)
LOG_LEVEL info Logging verbosity: debug, info, warn, error
LOG_FORMAT json Log output format: json (production) or console (development)

Configuration Notes

WALLET_MNEMONIC vs WALLET_MNEMONIC_FILE:

  • Use WALLET_MNEMONIC to pass the mnemonic directly as an environment variable (not recommended for production)
  • Use WALLET_MNEMONIC_FILE to specify a file path containing the mnemonic (recommended for security)
  • Example file: echo "word1 word2 ... word24" > /secure/path/mnemonic.txt && chmod 600 /secure/path/mnemonic.txt

POLL_INTERVAL Format:

  • Supports Go duration format: s (seconds), m (minutes), h (hours)
  • Examples:
    • 30s - Poll every 30 seconds (aggressive, higher RPC load)
    • 60s - Poll every minute (default, balanced)
    • 5m - Poll every 5 minutes (conservative, lower RPC load)
    • 1h - Poll every hour (minimal load, slower vote response)
  • Recommendation: Use 60s (1 minute) for most chains. Increase if RPC rate-limiting occurs.

STATE_DB:

  • The SQLite database file will be automatically created on first run
  • Important: The parent directory must exist before starting
  • Example: If using /data/votes.db, run mkdir -p /data first
  • The database tracks which proposals have been voted on to prevent duplicates

GAS_PRICE:

  • Must match your chain's minimum gas price requirement
  • Check chain documentation or node config for minimum-gas-prices
  • If votes fail with "insufficient fee" errors, increase this value
  • Format: <amount><denom> where denom matches your chain's gas token

Usage

CLI Commands

The auto-voter supports the following commands:

# Show help menu
./autovoter --help
./autovoter help

# Show version
./autovoter version

# Start the auto-voter
./autovoter start

Running ./autovoter without any command will display the help menu.

Setting Up Authz Grants

Before running the auto-voter, you need to grant it permission to vote on behalf of your validator:

# Grant voting permission (using your chain's CLI)
<chain-cli> tx authz grant <auto-voter-address> generic \
  --msg-type /cosmos.gov.v1.MsgVote \
  --from <validator-address> \
  --chain-id <chain-id>

Running Locally

Step 1: Test with Dry-Run Mode

export CHAIN_ID=test
export CHAIN_NAME=test
export RPC_ENDPOINT=http://localhost:26657
export GRPC_ENDPOINT=localhost:9090
export GRANTER_ADDRESS=cheqd193lqq9x62wqpuxnsczxyxh8tzsgas48rhhqhln
export WALLET_MNEMONIC_FILE=/path/to/mnemonic.txt
export GAS_PRICE=10000ncheq
export STATE_DB=/path/to/votes.db
export METRICS_PORT=9091
export DRY_RUN=true
export LOG_FORMAT=console
export LOG_LEVEL=info

./autovoter start

Step 2: Run with Actual Voting

Once you've verified it works in dry-run mode:

export DRY_RUN=false
./autovoter start

Monitoring

While the app is running, you can:

Check Health:

curl http://localhost:8080/health

Check Voting Health (tests chain connection):

curl http://localhost:8080/health/voting

View Prometheus Metrics:

curl http://localhost:9090/metrics

View Status Page:

curl http://localhost:8080/status

Verifying Votes

Query the chain to verify your vote was submitted:

# Check a specific vote
<chain-cli> query gov vote <proposal-id> <voter-address> \
  --node <rpc-endpoint> \
  --chain-id <chain-id>

# Example for cheqd:
cheqd query gov vote 1 cheqd1dgkrgxmsuqtxtyxq0la88ze9er6lfnlw8s7ttf \
  --node http://localhost:26657 \
  --chain-id test

Adding Support for New Chains

The app supports any Cosmos SDK chain, but you need to add the chain's address prefix configuration:

  1. Edit cmd/autovoter/main.go and add your chain to the getAddressPrefix function:
func getAddressPrefix(chainID string) string {
    prefixMap := map[string]string{
        // ... existing chains ...
        "your-chain-1": "yourprefix",
    }
    // ...
}
  1. Edit internal/voter/voter.go and add to getAddressPrefixFromChain:
func getAddressPrefixFromChain(chainID string) string {
    prefixMap := map[string]string{
        // ... existing chains ...
        "your-chain": "yourprefix",
    }
    // ...
}
  1. Edit pkg/cosmos/wallet.go and add to getAddressPrefix:
func getAddressPrefix(chainID string) string {
    prefixMap := map[string]string{
        // ... existing chains ...
        "your-chain-1": "yourprefix",
    }
    // ...
}
  1. Rebuild the app:
make build

Deployment

Docker

docker run -d \
  -e CHAIN_ID=test \
  -e RPC_ENDPOINT=http://localhost:26657 \
  -e GRPC_ENDPOINT=localhost:9090 \
  -e GRANTER_ADDRESS=cheqd1abc... \
  -e WALLET_MNEMONIC="your mnemonic here" \
  -e GAS_PRICE=10000ncheq \
  -e DRY_RUN=false \
  -v /path/to/data:/data \
  -p 8080:8080 \
  -p 9090:9090 \
  cosmos-auto-voter:latest

HashiCorp Nomad

See example.nomad.hcl for a complete Nomad job specification.

nomad job run \
  -var="chain_id=test" \
  -var="rpc_endpoint=http://localhost:26657" \
  -var="grpc_endpoint=localhost:9090" \
  -var="granter_address=cheqd1abc..." \
  -var="gas_price=10000ncheq" \
  example.nomad.hcl

Prometheus Metrics

The application exposes the following metrics on the configured metrics port (default 9091):

Vote Tracking Metrics (Critical for Zabbix Monitoring)

  • cosmos_voter_votes_submitted_total{chain_id, result} - Total successful votes submitted

    • Labels: result=success
  • cosmos_voter_votes_skipped_total{chain_id, reason} - Total votes skipped (already voted)

    • Labels: reason=already_voted_local, reason=already_voted_onchain
  • cosmos_voter_votes_failed_total{chain_id, reason} - Total failed vote attempts ⚠️

    • Labels:
      • reason=insufficient_balance - Wallet has no funds
      • reason=insufficient_fees - Gas price too low
      • reason=connection_error - Network issues
      • reason=authz_error - Authz grant missing/expired
      • reason=unknown - Other errors
  • cosmos_voter_proposals_without_vote{chain_id} - Current proposals without successful votes ⚠️

    • Gauge showing count of proposals that failed to vote in last poll
    • Use this in Zabbix to alert when > 0

Operational Metrics

  • cosmos_voter_proposals_checked_total{chain_id, status} - Total proposals checked
  • cosmos_voter_poll_errors_total{chain_id, error_type} - Total polling errors
  • cosmos_voter_last_successful_poll_timestamp{chain_id} - Last successful poll timestamp
  • cosmos_voter_active_proposals{chain_id} - Current active proposals count
  • cosmos_voter_wallet_balance{chain_id, denom} - Current wallet balance
  • cosmos_voter_health_status{chain_id} - Health status (1=healthy, 0=unhealthy)
  • cosmos_voter_poll_duration_seconds{chain_id} - Time to poll proposals
  • cosmos_voter_vote_latency_seconds{chain_id} - Time to submit vote

Zabbix Monitoring Recommendations

Critical Alerts:

  1. Alert when cosmos_voter_proposals_without_vote > 0

    • Indicates active proposals that don't have votes
    • Action: Check logs, verify wallet balance, check authz grants
  2. Alert when cosmos_voter_votes_failed_total increases

    • Check the reason label to determine cause
    • Action:
      • insufficient_balance: Add funds to wallet
      • insufficient_fees: Increase GAS_PRICE
      • authz_error: Renew authz grant
      • connection_error: Check RPC/gRPC endpoints
  3. Alert when cosmos_voter_wallet_balance is low

    • Set threshold based on expected voting frequency
    • Action: Top up wallet before it runs out

Example Zabbix Item Configuration:

Item: cosmos_voter_proposals_without_vote{chain_id="osmosis-1"}
Trigger: last()>0
Severity: High
Message: Active proposals without votes on osmosis-1! Check auto-voter logs.

Error Handling & Logging

Automatic Restart

The application includes robust error handling with automatic restart capabilities:

  • Exponential backoff: Starts with 5 second backoff, doubles on each retry, capped at 5 minutes
  • Max retries: Will attempt up to 10 restarts before giving up
  • Context-aware: Respects shutdown signals during retry loops
  • Detailed logging: Logs every restart attempt with retry count and backoff duration

When the voter encounters an error, it will:

  1. Log the error with full context
  2. Wait for the calculated backoff period
  3. Automatically restart the voting process
  4. Continue retrying until successful or max retries reached

Logging Behavior

The application provides comprehensive logging for all voting decisions:

Vote Status Logs:

  • "vote submitted successfully" - Vote was successfully broadcast to the chain
  • ⏭️ "vote skipped - already voted (local state)" - Vote was previously submitted (found in local database)
  • ⏭️ "vote skipped - already voted on-chain" - Vote was previously submitted (found on chain, updates local state)
  • ℹ️ "no active proposals to vote on" - No proposals in voting period
  • ⚠️ "DRY RUN MODE - vote not actually submitted" - Dry-run mode active

Error Logs:

  • "failed to process proposal" - Error processing specific proposal (continues with others)
  • "insufficient wallet balance" - Wallet has insufficient funds to pay transaction fees
  • "poll failed" - Error during proposal polling (triggers retry)
  • "voter crashed" - Fatal error (triggers automatic restart)

Restart Logs:

  • 🔄 "starting voter" - Voter starting (includes retry count)
  • 🔄 "restarting voter after backoff" - Automatic restart triggered with backoff duration
  • 🛑 "max retries reached, giving up" - Fatal: exceeded retry limit

Example Log Output

INFO    starting voter    {"retry_count": 0}
INFO    polling for active proposals
INFO    found active proposals    {"count": 2}
INFO    submitting abstain vote    {"proposal_id": 1, "title": "Upgrade v2.0"}
INFO    vote submitted successfully    {"proposal_id": 1}
INFO    vote skipped - already voted (local state)    {"proposal_id": 2}
INFO    no active proposals to vote on
ERROR   voter crashed    {"error": "connection timeout", "retry_count": 1}
WARN    restarting voter after backoff    {"backoff": "5s", "retry_count": 1}

Troubleshooting

Vote Not Submitting

Check the logs:

# If running locally
./autovoter

# If running in Docker
docker logs <container-id>

Common issues:

  • Authz grant doesn't exist or has expired
  • Wallet has insufficient funds for transaction fees
  • Gas price too low (check chain's minimum gas price)
  • Wrong address prefix for the chain

Insufficient Fee Error

If you see an error like:

got: 200000000ncheq required: 2000000000ncheq, minGasPrice: 10000ncheq

Increase your GAS_PRICE environment variable to match the chain's minimum gas price:

export GAS_PRICE=10000ncheq  # Match the minGasPrice from the error

Wallet Balance Issues

The app checks wallet balance before attempting to vote. If you see errors like:

ERROR   insufficient wallet balance    {"error": "insufficient balance: have 0ncheq, need 2000000000ncheq"}
ERROR   failed to process proposal    {"proposal_id": 1, "error": "cannot vote due to insufficient balance"}

Actions to take:

  1. Check your wallet balance:

    <chain-cli> query bank balances <wallet-address> --node <rpc-endpoint>
  2. Send funds to the wallet:

    • The wallet address is logged at startup: "wallet created" {"address": "cheqd1..."}
    • Send enough tokens to cover multiple votes (calculate: gas_price × gas_limit × expected_votes)
    • Example: For 10 votes with GAS_PRICE=10000ncheq and GAS_LIMIT=200000:
      • Required: 10000 × 200000 × 10 = 20,000,000,000 ncheq (20 billion ncheq)
  3. Monitor balance:

    • Set up alerts when balance drops below a threshold
    • The app will log balance errors clearly when funds run out
    • App will continue retrying and will resume voting once funds are added

Note: The app performs a balance check before each vote attempt. If balance is insufficient, it will:

  • Log a clear error with current balance vs required amount
  • Skip the vote for that proposal
  • Continue operating and retry on next poll
  • Resume voting automatically once funds are added

Address Prefix Errors

If you see errors about "hrp does not match bech32 prefix":

  1. Ensure your chain is added to all three prefix mapping functions (see "Adding Support for New Chains")
  2. Rebuild the application after making changes
  3. Verify the chain ID matches exactly

Connection Errors

Verify RPC is reachable:

curl <rpc-endpoint>/status

Check gRPC:

grpcurl <grpc-endpoint> list

Security Considerations

Critical Risks

  1. Wallet compromise = full voting control

    • Store mnemonics securely (use Vault, hardware wallet, or encrypted storage)
    • Never commit mnemonics to version control
    • Rotate keys regularly
    • Monitor for unauthorized votes
  2. Authz scope

    • Grant ONLY cosmos.gov.v1.MsgVote permission
    • Set expiration times on grants
    • Automate renewal process
  3. Transaction fees

    • Monitor wallet balance
    • Set up alerts for low balance
    • Automate top-ups

Best Practices

  • Run in dry-run mode first to verify configuration
  • Start with one chain and monitor for 24h before expanding
  • Document your governance policy
  • Set up alerting for errors and health check failures
  • Regular security audits
  • Use separate wallets for different chains/validators

License

MIT

Support

For issues, questions, or contributions, please open an issue on GitHub.

About

Automated governance voting system for Cosmos SDK chains using the Authz module.

Topics

Resources

Security policy

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors