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 private AWS KMS signer service, and the remote proxy.Source of truth:
src/qtg/domain/protocols.py— SignerProtocol, SignRequest, SignResult, SigningIntentsrc/qtg/infrastructure/signers/—noop,aws_kms_evm,remote_proxy,local_private_key_evm,registrysrc/qtg/application/services/dispatch.py— signing flowsrc/qtg/application/services/signing_jobs.py— durable one-use movement job/result contractsrc/qtg/application/services/message_signing.py— strict SIWE message jobs and explicit attemptssrc/qtg/application/services/registry.py— health syncsrc/qtg/application/services/signer_metadata.py— caller verificationsrc/qtg/infrastructure/db/models.py— SignerRegistryEntry
Current snapshot
- There are currently four signer implementations.
NoopSignerAwsKmsEvmSigner— runs in the private signer service or an explicit one-off toolRemoteSignerProxy— the only signer implementation accepted byqtg.mainLocalPrivateKeySignerEvm— development-only explicit-tool path (Tier-3)
- The EVM signer-required runtime path is Core
RemoteSignerProxy-> privateAwsKmsEvmSignerservice -> AWS KMS. qtg.mainrejects every non-emptyMG_LOCAL_SIGNER_BACKEND/_KEY/_KMS_KEY_ID/_AWS_REGION/_AWS_PROFILE/_AWS_ENDPOINT_URL/_KEY_SOURCE/_KEY_REFfield andMG_EXTRA_SIGNERS_JSON; explicit operator tools retain those direct inputs.LocalPrivateKeySignerEvmis 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’ssigner_bindingdict.runtime_checkable: type checks are possible withisinstance(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:
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. Signing intents
SignRequest.signing_intent is a three-variant union (SigningIntent | MessageSigningIntent | HyperliquidApproveAgentIntent). Movement callers use SigningIntent; strict SIWE
message jobs use MessageSigningIntent; browser typed-data jobs (Hyperliquid approve-agent,
JEP KMS-wallet opt-in) use HyperliquidApproveAgentIntent. The signer rejects a payload format whose intent variant
does not match.
Movement SigningIntent
This is the movement 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_hashand the actual payload hash
Message MessageSigningIntent
The message variant is intentionally narrower than generic personal_sign:
base64: and independently recomputes
both SHA-256 payload binding and the EIP-191 version 0x45 digest before KMS I/O. It does not accept
an opaque caller-supplied digest.
3. SignRequest
This is the full signing request passed to the signer by combining thePreparedAction returned from executor.prepare() with ExecutionContext.
Field details
4. SignResult
Signing result returned by the signer.Field details
signature_format values
The supported result formats are:LocalPrivateKeySignerEvm deliberately rejects message intents. Browser-facing message signing is
KMS-only through the private signer plane; there is no dev-key fallback.
Use in executors
Indispatch.py, QTG commits the movement signing job and attempt as SIGNING before calling the
signer. It then commits the exact SignResult with SUCCEEDED before any checkpoint, pre-submit,
or submit work. A same-digest recovery may reuse that result; a movement job never starts a second
signer attempt. Temporary, cancellation, unexpected, and stale in-flight outcomes are ambiguous and
escalate to manual intervention rather than returning the node to automatic signing.
The persisted 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
KMS implementation hosted by the private signer service and retained by explicit one-off tools.Role
- Derives the signer EOA from an AWS KMS
ECC_SECG_P256K1key. - Exposes
signer_addressfromhealth(). - Validates the payload hash in
sign()and signs EVM raw transactions, EIP-712 digests, or the independently derived EIP-191 digest for a strict SIWE message intent. - Follows the current v1 architecture that reuses the same signer EOA across multiple EVM chains.
Required env
The private service usesMG_SIGNER_SIGNER_KEY, MG_SIGNER_KMS_KEY_ID,
MG_SIGNER_AWS_REGION, optional profile/endpoint inputs, its nonce-HMAC credential, and a
PostgreSQL nonce-store URL. Core receives only the matching MG_REMOTE_SIGNER_* settings and a
pinned expected EVM address. See
private-signer-service.md.
Explicit one-off tools continue to use MG_LOCAL_SIGNER_BACKEND=aws_kms_evm,
MG_LOCAL_SIGNER_KMS_KEY_ID, MG_LOCAL_SIGNER_AWS_REGION, and the tool-specific reviewed
acknowledgements. Those variables are rejected by qtg.main.
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
qtg.maincallsbootstrap_runtime(..., in_process_signers_allowed=False)and never constructsAwsKmsEvmSigner.- The private signer service constructs exactly one
AwsKmsEvmSignerand fails startup if KMS health or persistent nonce recording is unavailable. - Explicit tools use the default signer-capable bootstrap mode and retain existing local KMS preflight rules.
health example
AwsKmsEvmSigner.health() does not return its registry label, KMS key ID,
alias, or ARN. The logical registry signer_key remains available as the
top-level registry field instead of being duplicated inside raw health. The
registry projection additionally removes KMS identifier variants from custom or
remote signer health before API responses or DB capability persistence. The
configured KMS identifier remains internal to the signer for AWS API calls.
Operational references
- preflight:
uv run python -m qtg.interfaces.tools.cctp_live_preflight - retained TESTNET one-shot:
7. RemoteSignerProxy
Proxy implementation that delegates over HTTP to a remote signer service.Current position
- It is the only signer implementation accepted by the long-running
qtg.mainprocess. - Every
/healthand/signrequest uses a fresh timestamp/nonce HMAC bound to method, path, raw query, and exact body bytes. - Core pins the expected EVM address and verifies authenticated health immediately before signing.
- The private server commits each nonce to PostgreSQL before signer I/O; replay-store failure is fail-closed.
Constructor
Error mapping
The implementation does not raise rawhttpx exceptions directly. It converts them into
MovementError variants. JEP-735 then treats a movement signing-call failure conservatively; a
transport or server error does not authorize an automatic second signing attempt.
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) orplain(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.
8. Registry / caller verification
GET /v3/signersexposesexpected_signer_address,observed_signer_address, and the derivedidentity_stateseparately for EVM rows; non-EVM rows report identity as not applicable.PUT /v3/signers/{signer_key}/expected-identityis the sole enrollment writer forexpected_signer_address; it uses a reasoned compare-and-set request and a freshSignerIdentityProtocolobservation.- 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 betweenactiveanddisabled.- CCTP burn dispatch precheck and mint preflight cross-check the signer address against
destination_caller; a mismatch fails closed asSIGNER_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
- For a Core runtime, configure the private signer service and matching
MG_REMOTE_SIGNER_*values from private-signer-service.md. Do not put AWS/KMS settings inqtg.main. - For an explicitly approved one-off tool, configure its documented
MG_LOCAL_SIGNER_*inputs and run its read-only preflight:
- Run the retained TESTNET one-shot only after fresh human approval: