Skip to main content

Signer Protocol

This document covers the entire signer infrastructure of the QTG v3 movement orchestration framework. It includes the protocol interface, data model, implementations, binding resolution, the signing flow inside dispatch, registry/health synchronization, CCTP caller-address validation, the local AWS KMS signer, and the remote proxy.
Source of truth:
  • src/qtg/domain/protocols.py — SignerProtocol, SignRequest, SignResult, SigningIntent
  • src/qtg/infrastructure/signers/noop, aws_kms_evm, remote_proxy, local_private_key_evm, registry
  • src/qtg/application/services/dispatch.py — signing flow
  • src/qtg/application/services/registry.py — health sync
  • src/qtg/application/services/signer_metadata.py — caller verification
  • src/qtg/infrastructure/db/models.py — SignerRegistryEntry

Current snapshot

  • There are currently four signer implementations.
    1. NoopSigner
    2. AwsKmsEvmSigner — default EVM / KMS path
    3. RemoteSignerProxy
    4. LocalPrivateKeySignerEvm — development-only (Tier-3, key in process memory)
  • The current default path for the EVM signer-required proving lane (CCTP, Gateway) is AwsKmsEvmSigner.
  • RemoteSignerProxy is still supported, but according to the current repository docs the production-readiness path is the local AWS KMS signer.
  • LocalPrivateKeySignerEvm is for local evaluation / dev only — see local-private-key-signer.md. Do not use it for production fund movement.

1. SignerProtocol Interface

@runtime_checkable protocol defined in domain/protocols.py.

Method description

Core properties

  • signer_key: unique identifier inside the registry. Referenced from the node’s signer_binding dict.
  • runtime_checkable: type checks are possible with isinstance(obj, SignerProtocol).

health() contract

For an EVM signer, health() may include a signer_address value. Registry health refresh validates and normalizes it, then writes it only to observed_signer_address; it is not the operator-pinned identity and is not copied into capabilities.

Optional SignerIdentityProtocol

EVM signers that support operator enrollment also implement the optional, runtime-checkable SignerIdentityProtocol:
The enrollment endpoint resolves this value with fresh=True, requires a nonzero canonical EVM address, and compares it with the submitted expected address. KMS and identity-capable remote adapters may implement the protocol; signer-free and non-EVM adapters simply do not, so identity enrollment is unavailable rather than guessed.

2. SigningIntent

This is the signing-intent declaration passed to the signer. It allows the signer to verify in a human-readable form “what is being signed.”

Field details

Security role of intent

SigningIntent exists to prevent blind signing. The signer should compare at least the following two:
  1. the intent’s action, asset, amount, destination
  2. allowed_payload_hash and the actual payload hash

3. SignRequest

This is the full signing request passed to the signer by combining the PreparedAction returned from executor.prepare() with ExecutionContext.

Field details


4. SignResult

Signing result returned by the signer.

Field details

signature_format values

AwsKmsEvmSigner and LocalPrivateKeySignerEvm both produce two distinct formats depending on what is being signed:

Use in executors

In dispatch.py, sign_result is serialized to JSON and passed to executor.submit(). The CCTP burn/mint executors allow only signed raw transactions where signature_format == "evm_raw_tx"; the Gateway intent executor consumes the eip712_compact form.

5. NoopSigner

Implementation for tests and signer-free lane bindings.

When it is used


6. AwsKmsEvmSigner

Current default implementation for the EVM signer-required lane.

Role

  • Derives the signer EOA from an AWS KMS ECC_SECG_P256K1 key.
  • Exposes signer_address from health().
  • Validates the payload hash in sign() and signs the EVM raw transaction.
  • Follows the current v1 architecture that reuses the same signer EOA across multiple EVM chains.

Required env

  • MG_LOCAL_SIGNER_BACKEND=aws_kms_evmload-bearing (selects the KMS backend)
  • MG_LOCAL_SIGNER_KMS_KEY_IDload-bearing (the KMS key to derive the EOA from)
  • MG_LOCAL_SIGNER_AWS_REGIONload-bearing
  • MG_LOCAL_SIGNER_KEY — the registry signer_key label for this signer. Optional for KMS readiness (it names the registry row, it is not key material). If set on a Stage 3 KMS-only path it can become a signer-runtime blocker; when unset it is not required.
  • MG_LOCAL_SIGNER_AWS_PROFILE (optional — AWS SDK default credential chain is used when absent)
  • MG_LOCAL_SIGNER_AWS_ENDPOINT_URL (optional)
  • MG_LOCAL_SIGNER_TIMEOUT_SECONDS
MG_LOCAL_SIGNER_KEY is a registry label, not a secret. The cryptographic key material never leaves AWS KMS — KMS_KEY_ID, AWS_REGION, and BACKEND are the load-bearing values.

Bootstrap rules

  • bootstrap_runtime() automatically registers AwsKmsEvmSigner when local signer env vars are configured.
  • Bootstrap fails if MG_LOCAL_SIGNER_BACKEND is not set to aws_kms_evm.
  • Bootstrap fails if the local signer and remote signer share the same signer_key.

health example

Operational references

  • preflight: uv run python -m qtg.interfaces.tools.cctp_live_preflight
  • retained TESTNET one-shot:
The one-shot additionally requires an AWS KMS signer, the durable TESTNET database, and fresh human approval for the exact run. The command and this document do not grant live authority; pytest cannot arm CCTP signing or broadcast.

7. RemoteSignerProxy

Proxy implementation that delegates over HTTP to a remote signer service.

Current position

  • The code remains in the repository
  • Bootstrap is still possible (MG_REMOTE_SIGNER_*)
  • However, it is not the default EVM signer path in the current repository docs

Constructor

Error mapping

The current implementation does not raise raw httpx exceptions directly. It converts them into MovementError variants. This allows the dispatch signing phase to handle the node as FAILED without crashing the worker.

7b. LocalPrivateKeySignerEvm

Development-only signer (MG_LOCAL_SIGNER_BACKEND=local_private_key_evm) that holds the EVM private key in process memory. Produces the same evm_raw_tx and eip712_compact signature formats and the same lowercase signer_address derivation as AwsKmsEvmSigner, so the proving lane and the signed-recipient guards behave identically.
  • Key source backends: op (1Password CLI, recommended Tier-3) or plain (env var, dev only).
  • Applies process hygiene at bootstrap (prctl(PR_SET_DUMPABLE=0) on Linux, setrlimit(RLIMIT_CORE, 0) on Linux + macOS) and fails closed if hygiene cannot be applied.
  • Not for production fund movement. The AWS-KMS-only operator preflights do not cover it.
Full operator reference: local-private-key-signer.md.

8. Registry / caller verification

  • GET /v3/signers exposes expected_signer_address, observed_signer_address, and the derived identity_state separately for EVM rows; non-EVM rows report identity as not applicable.
  • PUT /v3/signers/{signer_key}/expected-identity is the sole enrollment writer for expected_signer_address; it uses a reasoned compare-and-set request and a fresh SignerIdentityProtocol observation.
  • Health refresh is the sole writer for observed_signer_address. It clears that value when a signer becomes orphaned, so stale runtime evidence is never presented as current.
  • PATCH /v3/signers/{signer_key} can switch the signer state between active and disabled.
  • CCTP burn dispatch precheck and mint preflight cross-check the signer address against destination_caller; a mismatch fails closed as SIGNER_CALLER_MISMATCH.
  • This contract does not add movement-time or rotation-time drift enforcement; that boundary is a separate follow-up.

9. Practical getting-started

  1. Uncomment the MG_LOCAL_SIGNER_* and MG_EVM_RPC_ENDPOINTS_JSON sections in .env.example and fill in the values.
  2. Run preflight:
  1. Run the retained TESTNET one-shot only after fresh human approval:
The environment flag and explicit acknowledgement are both required software gates, but neither grants live authority. A fresh reviewed human approval is still required for the exact run. At the current point, the project already has preflight / retained CLI / runbook + a successful staged testnet E2E record. The main remaining follow-ups are protocol signature verification, stronger registry-driven binding resolution, and operator audit-trail cleanup.