Skip to content

sagaxyz/signer

Repository files navigation

Saga Signer Service

A gRPC/HTTP service for transaction signing and TMKMS (Tendermint Key Management System) management in the Saga ecosystem.

⚠️ Disclaimer: This software has not yet been battle-tested in production. While it includes comprehensive tests, use it at your own risk and perform thorough testing before deploying to any production environment.

What it does

  • Transaction Signing: Signs Cosmos SDK transactions using private keys
  • TMKMS Management: Manages Tendermint Key Management System (TMKMS) configuration for validator operations. Unlike traditional TMKMS which requires manual configuration file edits and service restarts, the signer enables dynamic management of consensus chains through API calls. It automatically generates TMKMS configuration files, manages the TMKMS process lifecycle (spawning, restarting on crashes, graceful shutdown), and performs atomic configuration updates to ensure TMKMS always sees a consistent state.

Consensus Key Providers

The signer service supports the same consensus key providers as TMKMS, allowing validators to use hardware-backed keys for enhanced security or file-based keys for simpler setups.

Supported Providers

Provider Implemented Tested Description
File (Softsign) File-based Ed25519 keys stored on disk
Fortanix DSM Fortanix Data Security Manager cloud HSM
Ledger ⚠️ Ledger hardware wallet with Tendermint Validator app. Not tested because the Tendermint Validator app doesn't appear to be published in Ledger Live/Wallet anymore.
YubiHSM ⚠️ Partial ⚠️ YubiHSM2 hardware security module. Not tested due to lack of hardware. TMKMS config generation supports both USB and HTTP adapters. GetConsensusKeys only works with HTTP adapter type.

The signer service generates TMKMS configuration files that are compatible with all supported consensus key providers. Each provider requires specific configuration in the signer's config file - see internal/config/config.yaml for examples.

Note: Ledger is implemented but untested. YubiHSM implementation is partial - USB adapter type is not supported for GetConsensusKeys because the Go yubihsm-go library only supports HTTP connectors. Both should be thoroughly tested with actual hardware before use in production.

Building

  1. Install Go (version 1.25 or later)
  2. Build the signer service:
    make build

Running

Running Locally Built Binary

Run the signer service with a configuration file:

./build/signer --config config.yaml

Note: The TMKMS binary path must be specified in the configuration file's tmkms.binary_path field (e.g., /usr/local/bin/tmkms). For now, only TMKMS 0.15.0 is supported.

See internal/config/config.yaml for an example configuration file.

Running with Docker

Run the signer service using the Docker image:

docker run -d \
  --name saga-signer \
  -v /path/to/config.yaml:/etc/signer/config.yaml:ro \
  -v /path/to/tx_keys:/var/signer/tx_keys:ro \
  -v /path/to/consensus.key:/var/signer/consensus.key:ro \
  -v /path/to/workdir:/var/signer/ \
  -p 9090:9090 \
  -p 8080:8080 \
  -p 8081:8081 \
  sagaxyz/signer:latest

See internal/config/config.yaml for an example configuration file.

Volumes:

  • /etc/signer/config.yaml (read-only): Configuration file
  • /var/signer/tx_keys (read-only): Directory containing transaction signing key files
  • /var/signer/consensus.key (read-only): Consensus key file
  • /var/signer/: Working directory (read-write). This volume is only used for persistence - the signer will create the tmkms symlink and necessary data and configuration files automatically.

Ports:

  • 9090: gRPC server
  • 8080: HTTP gateway
  • 8081: Admin server (metrics and health)

The Docker image includes both the signer binary and the TMKMS binary, so no additional setup is required.

Testing

Unit Tests

Run the unit tests:

make test

Integration Tests

Integration tests require Rust to be installed since TMKMS needs to be built:

  1. Install Rust (https://rustup.rs/)
  2. Run integration tests:
    make test-integration

The integration tests will automatically build TMKMS and run comprehensive tests against the full service stack.

API Endpoints

gRPC Endpoints

  • SetConsensusChains: Configure validator addresses for different chains to enable privval signing of consensus messages (via TMKMS). This method replaces the entire consensus chain configuration and is idempotent - calling it multiple times with the same data produces the same result. Returns an empty response indicating success.
  • GetConsensusKeys: Returns the CometBFT consensus public keys for the specified chain IDs.
  • SignTx: Sign transactions for broadcasting
  • GetSignTxKeys: Returns the Cosmos SDK public keys for the specified key entries. Each entry includes a key_id and optional chain_id (currently unused, reserved for future use when keys may be chain-specific).

HTTP Gateway

The service also exposes HTTP endpoints via gRPC-Gateway for REST API access to the same functionality:

  • SetConsensusChains: POST /v1/set-consensus-chains
  • GetConsensusKeys: POST /v1/get-consensus-keys
  • SignTx: POST /v1/sign-tx
  • GetSignTxKeys: POST /v1/get-sign-tx-keys

Example Usage

SetConsensusChains:

Request:

{
  "chains": [
    {
      "chain_id": "my-chain-1",
      "validator_address": "[email protected]:26658"
    },
    {
      "chain_id": "test-network-2",
      "validator_address": "[email protected]:26658"
    }
  ]
}

Response:

{}

GetConsensusKeys:

Request:

{
  "chain_ids": ["my-chain-1", "test-network-2"]
}

Response:

{
  "chains": [
    {
      "chain_id": "my-chain-1",
      "public_key": {
        "sum": {
          "ed25519": "dGVzdGNvbnNlbnN1c2tleWV4YW1wbGVmb3JkYXNoYm9hcmQ="
        }
      }
    },
    {
      "chain_id": "test-network-2",
      "public_key": {
        "sum": {
          "ed25519": "dGVzdGNvbnNlbnN1c2tleWV4YW1wbGVmb3JkYXNoYm9hcmQ="
        }
      }
    }
  ]
}

The ed25519 field contains the Ed25519 public key bytes encoded in base64 (32 bytes when decoded).

SignTx:

Request:

{
  "key_id": "validator-key-1",
  "chain_id": "my-chain-1",
  "account_number": 12345,
  "tx_body": {
    "messages": [...],
    "memo": "",
    "timeout_height": "0"
  },
  "auth_info": {
    "signer_infos": [...],
    "fee": {
      "amount": [...],
      "gas_limit": "200000"
    }
  }
}

Response:

{
  "tx": {
    "body": {
      "messages": [...],
      "memo": "",
      "timeout_height": "0"
    },
    "auth_info": {
      "signer_infos": [...],
      "fee": {
        "amount": [...],
        "gas_limit": "200000"
      }
    },
    "signatures": ["base64_encoded_signature_here"]
  },
  "tx_hash": "a1b2c3d4e5f6789012345678901234567890abcdef1234567890abcdef123456"
}

GetSignTxKeys:

Request:

{
  "entries": [
    {
      "key_id": "validator-key-1",
      "chain_id": ""
    },
    {
      "key_id": "validator-key-2",
      "chain_id": ""
    }
  ]
}

Response:

{
  "keys": [
    {
      "key_id": "validator-key-1",
      "chain_id": "",
      "public_key": {
        "@type": "/cosmos.crypto.secp256k1.PubKey",
        "key": "AhyVB+S6Z1pE7ycwYglemafp5qIDXKAgQwac0GeeHAce"
      }
    },
    {
      "key_id": "validator-key-2",
      "chain_id": "",
      "public_key": {
        "@type": "/cosmos.crypto.secp256k1.PubKey",
        "key": "BhyVB+S6Z1pE7ycwYglemafp5qIDXKAgQwac0GeeHAcf"
      }
    }
  ]
}

The chain_id field is optional and currently unused (reserved for future use when keys may be chain-specific).

The public keys are returned as Cosmos SDK secp256k1.PubKey, compatible with Cosmos SDK account types.

Monitoring

Grafana Dashboard

We provide a Grafana dashboard (monitoring/grafana-dashboard.json) to visualize service metrics:

  • TMKMS Health: Monitor TMKMS health status and chain count over time
  • API RED Metrics: Visualize API request metrics (Rate, Errors, Duration) for all API methods

Prometheus Alerting Rules

We provide Prometheus alerting rules (monitoring/prometheus-alerts.yml) for monitoring the signer service:

  • TMKMSUnhealthy: Fires when TMKMS health status is unhealthy (tmkms_healthy == 0) for more than 5 minutes with page severity

Monitoring API

The signer service exposes Prometheus metrics and health checks through an optional admin HTTP server:

  • Metrics: /metrics - Prometheus metrics endpoint
  • Health: /health - Health check endpoint that reports the service health status

Configure the admin server in your config file by setting the listen_admin field (e.g., "localhost:8080").

Metrics Endpoint

The /metrics endpoint exposes Prometheus-formatted metrics for monitoring and alerting. The following metrics are available:

gRPC Metrics (standard metrics for all API methods):

  • grpc_server_handled_total: Total number of RPCs completed, labeled by method, service, and status code
  • grpc_server_handling_seconds: Histogram of response latency for all gRPC methods

SignTx-specific Metrics (with chainID labels):

  • grpc_server_handled_by_chain_total: Total number of SignTx RPCs completed, labeled by chainID and status code
  • grpc_server_handling_by_chain_seconds: Histogram of SignTx response latency, labeled by chainID

TMKMS Metrics:

  • tmkms_healthy: TMKMS health status gauge (1 if healthy, 0 if unhealthy)
  • tmkms_chain_count: Number of consensus chains currently configured for TMKMS

Health Endpoint

The /health endpoint returns a JSON response with the current health status:

Example response (healthy):

{
  "status": "ok",
  "tmkms": {
    "monitor": {
      "healthy": true,
      "for_seconds": 30,
      "last_checked": "2025-11-01T03:00:00Z"
    },
    "chain_count": 2
  }
}

Example response (unhealthy):

{
  "status": "tmkms_unhealthy",
  "tmkms": {
    "monitor": {
      "healthy": false,
      "for_seconds": 5,
      "last_error": "ping failed: connection refused",
      "last_checked": "2025-11-01T03:00:00Z"
    },
    "chain_count": 1
  }
}

Example response (TMKMS monitoring unavailable):

{
  "status": "tmkms_status_unknown",
  "tmkms": {
    "chain_count": 0
  }
}

Field Descriptions:

  • status (string): Overall health status. "ok" when healthy.

  • tmkms.monitor.healthy (boolean, optional): Whether the monitor detected a healthy state. true = TMKMS is responding correctly, false = error detected. Present when TMKMS monitoring is functioning as expected.

  • tmkms.monitor.for_seconds (integer, optional): How long (in seconds) monitor.healthy had been that state when the last health check was performed. Resets to 0 when the health state changes. Present when TMKMS monitoring is functioning as expected.

  • tmkms.monitor.last_error (string, optional): Error message from the last failed health check. Present when healthy is false.

  • tmkms.monitor.last_checked (string, optional): ISO 8601 timestamp (RFC 3339 format) in UTC (GMT) of when the last health check was performed. Present when TMKMS monitoring is functioning as expected.

  • tmkms.chain_count (integer): Number of consensus chains currently configured for TMKMS signing.

HTTP Status Codes:

  • 200 OK: Service is healthy
  • 503 Service Unavailable: Service is unhealthy or TMKMS monitoring is unavailable

About

Saga signer service

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages