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.
NoopSigner
AwsKmsEvmSigner — default EVM / KMS path
RemoteSignerProxy
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:
- the intent’s
action, asset, amount, destination
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
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_evm — load-bearing (selects the KMS backend)
MG_LOCAL_SIGNER_KMS_KEY_ID — load-bearing (the KMS key to derive the EOA from)
MG_LOCAL_SIGNER_AWS_REGION — load-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
- Uncomment the
MG_LOCAL_SIGNER_* and MG_EVM_RPC_ENDPOINTS_JSON sections in .env.example and fill in the values.
- Run preflight:
- 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.