> ## Documentation Index
> Fetch the complete documentation index at: https://jephalabs.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Executor & Signer Protocols

> Protocol interfaces that all executors and signers must implement

# Executor & Signer Protocols

> Source of truth: `src/qtg/domain/protocols.py`
>
> This document describes `ExecutorProtocol` and `SignerProtocol`, the core
> execution interfaces of the v3 movement orchestration framework, along with
> their related dataclasses.
> All executor/signer implementations must follow this protocol.

***

## Table of Contents

1. [Overview](#overview)
2. [ExecutorProtocol](#executorprotocol)
3. [Executor Dataclasses](#executor-dataclasses)
4. [Executor execution lifecycle](#executor-execution-lifecycle)
5. [SignerProtocol](#signerprotocol)
6. [Signer Dataclasses](#signer-dataclasses)
7. [Probe Protocols](#probe-protocols)
8. [Registry pattern](#registry-pattern)
9. [Binding Resolution](#binding-resolution)
10. [Implementation categories](#implementation-categories)

***

## Overview

The v3 framework uses a **structural typing** Protocol pattern.
The `@runtime_checkable` decorator is applied so `isinstance()` checks are
possible, but actual registration happens through the in-memory dict in the
registry module.

```
domain/protocols.py          pure interface definitions (no I/O)
infrastructure/executors/    implementations: LocalExecutorAdapter, RemoteExecutorProxy, CEX/CCTP/Gateway/EVM/CCIP/USDT0/Hyperliquid executors
infrastructure/signers/      implementations: NoopSigner, AwsKmsEvmSigner, RemoteSignerProxy, LocalPrivateKeySignerEvm
(Pro distribution)           Pro implementations (excluded from OSS): Stargate send + dst_observe
```

All dataclasses inherit from `pydantic.BaseModel`, so JSON
serialization/deserialization is straightforward.
This allows local executors and remote executors (HTTP proxy) to operate under
the same contract.

***

## ExecutorProtocol

```python theme={null}
@runtime_checkable
class ExecutorProtocol(Protocol):
    executor_key: str

    async def preflight(self, context: ExecutionContext) -> ExecutionResult | None: ...
    async def prepare(self, context: ExecutionContext) -> ExecutionResult: ...
    async def submit(
        self,
        context: ExecutionContext,
        prepared_action: PreparedAction,
        *,
        session: AsyncSession | None = None,
        **kwargs: Any,
    ) -> ExecutionResult: ...
    async def observe(self, context: ExecutionContext) -> ExecutionResult: ...
    async def recover(
        self,
        context: ExecutionContext,
        *,
        session: AsyncSession | None = None,
        **kwargs: Any,
    ) -> ExecutionResult: ...
    async def health(self) -> dict[str, Any] | None: ...
```

`submit()` and `recover()` receive the dispatcher's live `AsyncSession`, so an executor can read or write inside the same transaction that commits the node state. Executors that do not need it accept and ignore it.

### Required attributes

| Attribute      | Type  | Description                                                                                                           |
| -------------- | ----- | --------------------------------------------------------------------------------------------------------------------- |
| `executor_key` | `str` | Unique key that identifies this executor in the registry. Example: `"exec.cex.withdrawal_action"`, `"exec.cctp.burn"` |

### Method details

#### `preflight(context) -> ExecutionResult | None`

**When called**: invoked during dispatch, before `prepare()`. Called immediately after the node transitions `READY` -> `PREPARING`.

**Purpose**: validate preconditions before execution. Checks include exchange whitelist address, withdrawal availability, balance, wallet service status, and so on.

**Return value**:

* `None` -- all preconditions met. The dispatcher proceeds to the `prepare()` stage.
* `ExecutionResult` -- precondition failure. `next_state` becomes the node's new state (usually `"FAILED"`). The dispatcher commits and exits immediately.

**CEX withdrawal example**: performs whitelist validation, `check_withdrawal_available()`, min/max amount validation, and `check_wallet_service()`. If any of these fails, it returns a result such as `ExecutionResult(next_state="FAILED", error_code="ADDRESS_NOT_WHITELISTED")`.

**Special note**: for CCTP burn, the dispatcher calls `validate_cctp_burn_caller_alignment()` first. If that returns a non-`None` result, the executor's `preflight()` is skipped.

***

#### `prepare(context) -> ExecutionResult`

**When called**: after `preflight()` returns `None`.

**Purpose**: construct the transaction payload to be submitted. Does not yet cause any side effect on external systems.

**Return value**: must return `ExecutionResult` with `PreparedAction` included in the `prepared_action` field.

**Failure handling**: if `prepared_action` is `None`, the dispatcher transitions the node to `FAILED`.

**Artifact persistence**: the dispatcher stores the returned `PreparedAction` in the `MovementArtifact` table with `artifact_type='prepared_action'`. `artifact_digest` uses the value of `prepared_action.payload_hash`.

**CEX example**:

```python theme={null}
async def prepare(self, ctx: ExecutionContext) -> ExecutionResult:
    provider_request = {
        "exchange": common.source_exchange(ctx),
        "asset": common.asset(ctx),
        "network": common.network(ctx),
        "amount": str(common.amount(ctx)),
        "address": common.address(ctx),
        "memo": common.memo(ctx),
    }
    return ExecutionResult(
        next_state="SUBMITTING",
        prepared_action=common.build_provider_request_action(
            action_type="cex_withdrawal",
            payload=provider_request,
        ),
    )
```

***

#### `submit(context, prepared_action, **kwargs) -> ExecutionResult`

**When called**: after `prepare()` completes. When signing is required (`prepared_action.signing_required == True`), the signer is invoked first and the signature result is then passed as `sign_result` in `kwargs`.

**Purpose**: submit the actual transaction to the external system. **Side effects begin at this point.**

**Arguments**:

* `context` -- execution context for the current node
* `prepared_action` -- the action payload produced by `prepare()`
* `session` -- the dispatcher's `AsyncSession`, for executors that must read or write in the same transaction
* `**kwargs` -- additional data such as the signature result. When signing is required, it includes `sign_result: dict`.

**Optional `pre_submit_check(context, session)` hook**: if the executor (or its `_handler`) defines this coroutine, the dispatcher awaits it immediately before `submit()` — inside the same transaction, after the node is already `SUBMITTING`. It is the last gate before a side effect, and it can hold a row lock across the submit. Raising `TemporaryMovementError` retries the node (`PRE_SUBMIT_CHECK_TEMPORARY_ERROR`); `FatalMovementError` or any other exception fails it and propagates (`PRE_SUBMIT_CHECK_FATAL_ERROR` / `PRE_SUBMIT_CHECK_UNEXPECTED_ERROR`). Used by the CCIP and Stargate send lanes.

**Return value**: `ExecutionResult`

* `next_state`: typically `"COMPLETED"` (synchronous completion) or `"SUBMITTED"` (asynchronous, observation required)
* `provider_refs`: exchange/chain reference IDs. Example: `{"exchange_withdrawal_id": "uuid-xxx"}`
* `provider_state`: the state string on the exchange/protocol side

**Error handling**:

* network timeout -> `next_state="UNKNOWN"` (recover target)
* business error -> `next_state="FAILED"` (terminal)

**Dispatcher post-processing**: the submit result's `provider_refs`, `provider_state`, and `generated_artifacts` are stored on the node. If `next_state` is `COMPLETED`, successor nodes are unblocked; if `FAILED`/`UNKNOWN`, the request state is derived accordingly.

***

#### `observe(context) -> ExecutionResult`

**When called**: while the node is in `SUBMITTED` or `OBSERVING` state, called periodically by the observe worker.

**Purpose**: poll the progress of asynchronous work. Examples include exchange withdrawal/deposit state checks, chain transaction checks, and protocol completion checks.

**Return value**: `ExecutionResult`

* `next_state="COMPLETED"` -- work completed. The observer unblocks successor nodes.
* `next_state="OBSERVING"` -- still in progress. The next polling time is determined by `retry_after_seconds`.
* `next_state="FAILED"` -- work confirmed as failed.
* `next_state="UNKNOWN"` -- state cannot be determined. Transitioned to the recover target.

**Polling interval**: the observe worker uses `result.retry_after_seconds` when set; otherwise it uses the per-`action_type` default from `DEFAULT_OBSERVE_INTERVALS`:

| action\_type                            | default interval (seconds) |
| --------------------------------------- | -------------------------- |
| `cex_withdrawal_status`                 | 5                          |
| `cex_deposit_status`                    | 10                         |
| `destination_chain_receive_observe`     | 10                         |
| `destination_chain_finality_observe`    | 15                         |
| `protocol_observe` / `cctp_attestation` | 15                         |
| `cctp_mint_status`                      | 5                          |
| `ccip_delivery`                         | 60                         |
| `debridge_fulfillment`                  | 30                         |
| other                                   | 10                         |

***

#### `recover(context) -> ExecutionResult`

**When called**: while the node is in `UNKNOWN` state, called by the recovery worker.

**Purpose**: recover from an uncertain state. For example, when a timeout prevented confirmation of submission, query the exchange API again to check whether submission actually occurred.

**Return value**: `ExecutionResult`

* `next_state="SUBMITTED"` -- submission confirmed. Transition to observe.
* `next_state="COMPLETED"` -- already completed.
* `next_state="FAILED"` -- recovery not possible.

**CEX example**: if `provider_context` contains `exchange_withdrawal_id`, the submission is judged to have occurred and `COMPLETED` is returned. Otherwise `FAILED` is returned.

***

#### `health() -> dict[str, Any] | None`

**When called**: at the health check endpoint or during system diagnostics.

**Purpose**: check the availability of the executor implementation.

**Return value**: a state dict (e.g. `{"ok": True, "executor_key": "exec.cex.withdrawal_action", "mode": "local"}`) or `None`.

***

#### `close()` (informal)

Not included in the Protocol definition, but both `LocalExecutorAdapter` and `RemoteExecutorProxy` implement a `close()` method. It is used for resource cleanup (closing the HTTP client, etc.).

***

## Executor Dataclasses

### ExecutionContext

An immutable snapshot containing **all context information** for the currently executing node.
Built by the `build_execution_context()` function from the DB.

```python theme={null}
class ExecutionContext(BaseModel):
    protocol_version: str
    request_id: str
    request_node_id: str
    compiled_plan_hash: str
    template_key: str
    template_version: int
    node_key: str
    node_kind: str
    action_type: str
    attempt_no: int
    intent: dict[str, Any]
    input_params: dict[str, Any]
    resolved_bindings: dict[str, Any]
    risk_controls: dict[str, Any]
    prior_artifacts: list[dict[str, Any]]
    node_config: dict[str, Any]
    provider_context: dict[str, Any]
    timeout_policy: dict[str, Any]
```

| Field                | Type         | Source                                                  | Description                                                                                                                     |
| -------------------- | ------------ | ------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
| `protocol_version`   | `str`        | hard-coded `"1.0"`                                      | Protocol version. Used for signer/executor compatibility checks.                                                                |
| `request_id`         | `str`        | `MovementRequest.id` (UUID)                             | Unique ID of the current movement request                                                                                       |
| `request_node_id`    | `str`        | `MovementRequestNode.id` (UUID)                         | Unique ID of the currently executing node instance                                                                              |
| `compiled_plan_hash` | `str`        | `MovementRequest.compiled_plan_hash`                    | Deterministic hash of the plan finalized at approval time. `"sha256:..."` format                                                |
| `template_key`       | `str`        | `MovementPlanTemplate.template_key`                     | Plan template identifier. Example: `"cex_transfer"`, `"cctp_bridge"`                                                            |
| `template_version`   | `int`        | `MovementPlanVersion.version`                           | Template version number                                                                                                         |
| `node_key`           | `str`        | `MovementRequestNode.node_key`                          | Unique key of this node within the graph. Example: `"withdrawal_action"`, `"deposit_observe"`                                   |
| `node_kind`          | `str`        | `MovementPlanNode.node_kind`                            | Node kind: `"action"`, `"observe"`, `"manual_gate"`, `"compensation"`, `"post_action"`                                          |
| `action_type`        | `str`        | `MovementPlanNode.action_type`                          | Specific action type. Example: `"cex_withdrawal"`, `"exec.cctp.burn"`, `"cex_deposit_status"`                                   |
| `attempt_no`         | `int`        | `MovementRequestNode.attempt_no + 1`                    | Current attempt count (1-based). Increases on each retry                                                                        |
| `intent`             | `dict`       | `MovementRequest.intent`                                | High-level movement intent. Example: `{"asset": "USDC", "amount": "1000", "destination": {"address": "0x..."}}`                 |
| `input_params`       | `dict`       | `MovementRequest.input_params`                          | Input parameters finalized at compile time. Exchange name, network, address, memo, etc.                                         |
| `resolved_bindings`  | `dict`       | per-node executor/signer bindings                       | Shape `{"executor": {...}, "signer": {...} \| None}`                                                                            |
| `risk_controls`      | `dict`       | `MovementPlanVersion.risk_controls`                     | Policy parameters such as hardcap and rate limit                                                                                |
| `prior_artifacts`    | `list[dict]` | `MovementArtifact` of predecessor nodes                 | Artifacts produced by predecessor nodes. `[{"artifact_type": "...", "artifact_digest": "..."}]`                                 |
| `node_config`        | `dict`       | `MovementPlanNode.config` after runtime resolution      | Node-specific config. When passed to the executor, the top-level `$ref:node_key.field` is already resolved.                     |
| `provider_context`   | `dict`       | flat merge of direct predecessor nodes' `provider_refs` | Merge of `provider_refs` from `COMPLETED` direct predecessor nodes. Example: `{"exchange_withdrawal_id": "...", "txid": "..."}` |
| `timeout_policy`     | `dict`       | `MovementPlanNode.timeout_policy`                       | Timeout settings. Example: `{"max_observe_seconds": 3600}`                                                                      |

**How `provider_context` is constructed**: `build_execution_context()` collects `provider_refs` from `COMPLETED` direct predecessors of the current node and merges them into a single flat dict. This lets successor nodes (observe) reference the predecessor node's (action) exchange ID or txid.

For template-config `$ref:` resolution, the runtime builds a separate synthetic namespaced ref map (`{node_key}.{field}`), but that value is not exposed to the executor. In other words, the executor contract remains a flat `provider_context` plus an already-resolved `node_config`.

***

### ExecutionResult

The **unified return type** of executor methods. All methods (`preflight`, `prepare`, `submit`, `observe`, `recover`) return this type.

```python theme={null}
class ExecutionResult(BaseModel):
    next_state: str
    provider_state: str | None = None
    provider_refs: dict[str, Any] = Field(default_factory=dict)
    proof: dict[str, Any] = Field(default_factory=dict)
    error_code: str | None = None
    error_detail: str | None = None
    retry_after_seconds: int | None = None
    manual_intervention_required: bool = False
    artifacts: list[ArtifactRef] = Field(default_factory=list)
    generated_artifacts: list[GeneratedArtifact] = Field(default_factory=list)
    prepared_action: PreparedAction | None = None
```

| Field                          | Type                      | Description                                                                                                                                                                                                                                                             |
| ------------------------------ | ------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `next_state`                   | `str`                     | **required**. The node's next state. A string from the `NodeState` enum: `"COMPLETED"`, `"SUBMITTED"`, `"OBSERVING"`, `"FAILED"`, `"UNKNOWN"`, etc.                                                                                                                     |
| `provider_state`               | `str \| None`             | The raw state value from the external system (exchange/chain). Example: `"DONE"`, `"PENDING"`                                                                                                                                                                           |
| `provider_refs`                | `dict`                    | Reference IDs from the external system. Example: `{"exchange_withdrawal_id": "uuid", "txid": "0xabc"}`. Stored in the node's `provider_refs` column and propagated as flat `provider_context` to direct successors. Namespaced keys used by `$ref` are not stored here. |
| `proof`                        | `dict`                    | Execution evidence. Block number, confirmation count, etc. collected by observe. Stored as `MovementArtifact` with `artifact_type='proof'`                                                                                                                              |
| `error_code`                   | `str \| None`             | Error code. Example: `"ADDRESS_NOT_WHITELISTED"`, `"TIMEOUT"`, `"RECOVERY_FAILED"`                                                                                                                                                                                      |
| `error_detail`                 | `str \| None`             | Human-readable error description                                                                                                                                                                                                                                        |
| `retry_after_seconds`          | `int \| None`             | Wait time (in seconds) until the next polling during observe. If `None`, the per-`action_type` default is used                                                                                                                                                          |
| `manual_intervention_required` | `bool`                    | If `True`, automatic recovery is not possible and operator intervention is required                                                                                                                                                                                     |
| `artifacts`                    | `list[ArtifactRef]`       | References to existing artifacts                                                                                                                                                                                                                                        |
| `generated_artifacts`          | `list[GeneratedArtifact]` | Newly produced artifacts from this execution. Persisted to the DB after submit/observe                                                                                                                                                                                  |
| `prepared_action`              | `PreparedAction \| None`  | Used only in `prepare()` results. Carries the action payload to be submitted                                                                                                                                                                                            |

***

### PreparedAction

The **immutable snapshot of the transaction to be submitted**, produced in the `prepare()` stage.

```python theme={null}
class PreparedAction(BaseModel):
    prepared_action_id: str
    action_type: str
    payload_format: str
    payload: str | None = None
    payload_hash: str
    signing_required: bool
    provider_hints: dict[str, Any] = Field(default_factory=dict)
    expires_at: datetime | None = None
```

| Field                | Type               | Description                                                                                                                                                                                                                       |
| -------------------- | ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `prepared_action_id` | `str`              | Unique ID of this prepared action (UUID, etc.)                                                                                                                                                                                    |
| `action_type`        | `str`              | Action type. Example: `"cex_withdrawal"`, `"exec.cctp.burn"`, `"cctp_mint"`                                                                                                                                                       |
| `payload_format`     | `str`              | Serialization format of the payload. Example: `"json"`, `"hex"` (EVM raw tx)                                                                                                                                                      |
| `payload`            | `str \| None`      | Serialized payload body. CEX JSON, CCTP hex-encoded raw tx                                                                                                                                                                        |
| `payload_hash`       | `str`              | Deterministic hash of the payload. `"sha256:..."` format. Used for artifact digest and signature validation                                                                                                                       |
| `signing_required`   | `bool`             | If `True`, the dispatcher calls the signer, obtains a signature, then passes it to submit. CEX executors are `False` (authenticated via exchange API keys); CCTP executors are `True` (on-chain transaction signatures required). |
| `provider_hints`     | `dict`             | Hints for the executor implementation. Example: `{"gas_limit": 200000, "chain_id": 1}`                                                                                                                                            |
| `expires_at`         | `datetime \| None` | Payload validity period. On expiration, re-prepare is required                                                                                                                                                                    |

**`signing_required` behavior flow**:

```
signing_required == False:
  PREPARING -> prepare -> SUBMITTING -> submit -> ...

signing_required == True:
  PREPARING -> prepare -> AWAITING_SIGNATURE -> signer.sign() -> SUBMITTING -> submit -> ...
```

***

### SigningIntent

An **intent declaration** included in the signing request. Human-readable metadata that lets the signer check "what is being signed".

```python theme={null}
class SigningIntent(BaseModel):
    action: str
    asset: str
    amount: str
    destination: str
    max_fee_usd: str | None = None
    chain_family: str
    allowed_payload_hash: str
```

| Field                  | Type          | Description                                                                                                            |
| ---------------------- | ------------- | ---------------------------------------------------------------------------------------------------------------------- |
| `action`               | `str`         | Copied from `PreparedAction.action_type`. Example: `"exec.cctp.burn"`                                                  |
| `asset`                | `str`         | Asset being moved. Extracted from `context.intent["asset"]`. Example: `"USDC"`                                         |
| `amount`               | `str`         | Amount being moved. `context.intent["amount"]` or `context.input_params["amount"]`                                     |
| `destination`          | `str`         | Destination address. Extracted from `context.intent["destination"]`. If it is a dict, the `["address"]` key is used    |
| `max_fee_usd`          | `str \| None` | Maximum allowed fee (USD). The current implementation sets this to `None`                                              |
| `chain_family`         | `str`         | Chain family. Extracted from `context.input_params["chain_family"]`. Default `"evm"`                                   |
| `allowed_payload_hash` | `str`         | Same as `PreparedAction.payload_hash`. The signer compares this hash with the actual payload hash to prevent tampering |

***

### ArtifactRef / GeneratedArtifact

```python theme={null}
class ArtifactRef(BaseModel):
    artifact_type: str      # e.g. "prepared_action", "proof", "attestation"
    artifact_digest: str    # "sha256:..." hash

class GeneratedArtifact(BaseModel):
    artifact_type: str          # artifact kind
    content_format: str         # "json", "hex", "base64", etc.
    content_inline: str | None  # inline content (small data)
    content_uri: str | None     # URI reference (large data)
    artifact_metadata: dict     # additional metadata
    artifact_digest: str | None # content hash
```

Generated artifacts are persisted to the `MovementArtifact` table via `persist_generated_artifacts()`.

***

## Executor execution lifecycle

The full node execution flow, organized with state transitions.

```mermaid theme={null}
flowchart TB
    BLOCKED["BLOCKED<br/>(waiting for predecessor nodes)"] -->|predecessor COMPLETED| READY["READY<br/>(dispatch target)"]
    READY -->|dispatcher picks up| PREPARING
    PREPARING -->|preflight()| PF{preflight result?}
    PF -->|returns None| PREP[prepare]
    PF -->|returns result| FAIL1[FAILED]
    PREP --> SIG{signing_required?}
    SIG -->|No| SUBMITTING
    SIG -->|Yes| AS[AWAITING_SIGNATURE]
    AS -->|signer.sign()| SUBMITTING
    SUBMITTING -->|submit()| SR{submit result}
    SR --> FAIL2[FAILED]
    SR --> SUBCOMP["SUBMITTED / COMPLETED"]
    SR --> UNK[UNKNOWN]
    UNK -->|recover()| RECRES["SUBMITTED / FAILED"]
    SUBCOMP --> OBSERVING
    OBSERVING -->|observe() periodic| OR{observe result}
    OR --> FAIL3[FAILED]
    OR --> OBSCONT["OBSERVING<br/>(continue)"]
    OR --> COMPLETED
```

### Dispatcher (`dispatch.py`) processing order

1. Acquire one `READY` node via `SELECT ... FOR UPDATE SKIP LOCKED`
2. If the request is `APPROVED`, transition it to `EXECUTING`
3. Transition the node to `PREPARING`
4. Call `preflight()` -- if non-`None`, transition state and exit immediately
5. Call `prepare()` -- if `prepared_action` is `None`, set `FAILED`
6. Persist `PreparedAction` to `MovementArtifact`
7. If `signing_required`: transition to `AWAITING_SIGNATURE` -> look up signer -> build `SignRequest` -> call `signer.sign()`
8. Transition to `SUBMITTING`, record `started_at`/`attempt_no`, commit
9. Call `submit()`
10. Store `provider_state`, `provider_refs`, `provider_ref_id`
11. Transition the node per `next_state` and derive request state

### Observer (`observe.py`) processing order

1. Acquire up to 10 `SUBMITTED`/`OBSERVING` nodes (skip locked)
2. Skip if `next_observe_at` has not yet arrived
3. `build_execution_context()` -> call `executor.observe()`
4. Store `provider_state`, `provider_refs`, proof, artifacts
5. If `COMPLETED`, unblock successors (`advance_after_completion`)
6. If `OBSERVING`, compute the next polling time
7. If `FAILED`/`UNKNOWN`, derive request state

### Recovery (`recover.py`) processing order

1. Acquire up to 10 `UNKNOWN` nodes (skip locked)
2. `build_execution_context()` -> call `executor.recover()`
3. Store `provider_state`, `provider_refs`, proof, artifacts
4. If `FAILED`, derive request state

***

## SignerProtocol

```python theme={null}
@runtime_checkable
class SignerProtocol(Protocol):
    signer_key: str

    async def sign(self, request: SignRequest) -> SignResult: ...
    async def health(self) -> dict[str, Any] | None: ...
```

### Required attributes

| Attribute    | Type  | Description                                                                                      |
| ------------ | ----- | ------------------------------------------------------------------------------------------------ |
| `signer_key` | `str` | Unique key that identifies this signer in the registry. Example: `"noop"`, `"remote_evm_signer"` |

### Method details

#### `sign(request: SignRequest) -> SignResult`

**When called**: when the dispatcher sees `prepared_action.signing_required == True` and the node has a `signer_binding`.

**Purpose**: signs the transaction payload. The signer can confirm the meaning of what is being signed via `SigningIntent`.

**Security contract**: the signer must verify that `signing_intent.allowed_payload_hash` matches `request.payload_hash`. On mismatch it must refuse to sign.

#### `health() -> dict[str, Any] | None`

Checks signer availability. For remote signers, calls the HTTP health endpoint.

***

## Signer Dataclasses

### SignRequest

```python theme={null}
class SignRequest(BaseModel):
    protocol_version: str
    request_id: str
    request_node_id: str
    signing_intent: SigningIntent
    payload_format: str
    payload: str
    payload_hash: str
    metadata: dict[str, Any]
```

| Field              | Type            | Description                                              |
| ------------------ | --------------- | -------------------------------------------------------- |
| `protocol_version` | `str`           | Copied from `ExecutionContext.protocol_version`. `"1.0"` |
| `request_id`       | `str`           | `ExecutionContext.request_id`                            |
| `request_node_id`  | `str`           | `ExecutionContext.request_node_id`                       |
| `signing_intent`   | `SigningIntent` | Signing intent metadata (see above)                      |
| `payload_format`   | `str`           | `PreparedAction.payload_format`                          |
| `payload`          | `str`           | `PreparedAction.payload` (empty string if `""`)          |
| `payload_hash`     | `str`           | `PreparedAction.payload_hash`                            |
| `metadata`         | `dict`          | `ExecutionContext.input_params` is passed in full        |

**Construction example in the dispatcher** (`dispatch.py`):

```python theme={null}
sign_request = SignRequest(
    protocol_version=context.protocol_version,
    request_id=context.request_id,
    request_node_id=context.request_node_id,
    signing_intent=SigningIntent(
        action=prepared_action.action_type,
        asset=str(context.intent.get('asset', '')),
        amount=str(context.intent.get('amount', context.input_params.get('amount', ''))),
        destination=str(destination_value),
        max_fee_usd=None,
        chain_family=str(context.input_params.get('chain_family', 'evm')),
        allowed_payload_hash=prepared_action.payload_hash,
    ),
    payload_format=prepared_action.payload_format,
    payload=prepared_action.payload or '',
    payload_hash=prepared_action.payload_hash,
    metadata=context.input_params,
)
```

### SignResult

```python theme={null}
class SignResult(BaseModel):
    signature: str
    signature_format: str
    signer_ref: str
```

| Field              | Type  | Description                                                                                              |
| ------------------ | ----- | -------------------------------------------------------------------------------------------------------- |
| `signature`        | `str` | Signature value. NoopSigner returns an empty string; real implementations return a hex-encoded signature |
| `signature_format` | `str` | Signature format. `"noop"`, `"evm_v_r_s"`, `"raw_hex"`, etc.                                             |
| `signer_ref`       | `str` | Signer identification reference. Example: `"noop:my-signer"`, `"remote:https://signer.example.com"`      |

The signature result is passed to `executor.submit()` as the `sign_result` key in `kwargs`, in the form `sign_result.model_dump(mode='json')`.

***

## Probe Protocols

Beyond executors, three additional **probe protocols** are defined. These are auxiliary interfaces used inside observe executors.

### ChainReceiveProbe

```python theme={null}
@runtime_checkable
class ChainReceiveProbe(Protocol):
    async def check_receive(
        self,
        context: ExecutionContext,
        *,
        match_mode: str,
        targets: dict[str, Any],
    ) -> ChainReceiveProbeResult: ...
```

Detects on-chain receive transactions. `match_mode` specifies the matching strategy (e.g. `"txid"`, `"address_amount"`), and `targets` carries the matching conditions.

**ChainReceiveProbeResult**:

| field                 | description                               |
| --------------------- | ----------------------------------------- |
| `matched`             | whether a receive transaction was found   |
| `provider_state`      | chain/provider-side state                 |
| `provider_refs`       | references such as txid and block\_number |
| `proof`               | evidence data                             |
| `retry_after_seconds` | wait time until next polling              |

### ChainFinalityProbe

```python theme={null}
@runtime_checkable
class ChainFinalityProbe(Protocol):
    async def check_finality(
        self,
        context: ExecutionContext,
        *,
        txid: str,
        confirmations_required: int,
    ) -> ChainFinalityProbeResult: ...
```

Checks the finality of a specific transaction.

**ChainFinalityProbeResult**:

| field                     | description                                     |
| ------------------------- | ----------------------------------------------- |
| `finalized`               | whether the required confirmation count was met |
| `confirmations`           | current confirmation count                      |
| `provider_state`          | chain-side state                                |
| `provider_refs` / `proof` | references and evidence                         |
| `retry_after_seconds`     | wait time until next polling                    |

### ProtocolProbe

```python theme={null}
@runtime_checkable
class ProtocolProbe(Protocol):
    async def check_proof(
        self,
        context: ExecutionContext,
        *,
        protocol_ref: str,
        proof_mode: str | None = None,
    ) -> ProtocolProbeResult: ...
```

Checks protocol-level completion evidence. CCTP attestation, bridge delivery, etc.

**ProtocolProbeResult**:

| field                     | description                                 |
| ------------------------- | ------------------------------------------- |
| `completed`               | whether the protocol-level work is complete |
| `provider_state`          | protocol-side state                         |
| `provider_refs` / `proof` | references and evidence                     |
| `generated_artifacts`     | produced artifacts (e.g. attestation bytes) |
| `retry_after_seconds`     | wait time until next polling                |

***

## Registry pattern

Both Executor and Signer use **the same in-memory registry pattern**.

### Executor Registry

> Source: `src/qtg/infrastructure/executors/registry.py`

```python theme={null}
_EXECUTOR_REGISTRY: dict[str, object] = {}

def clear_executor_registry() -> None:
    _EXECUTOR_REGISTRY.clear()

def register_executor(executor) -> None:
    _EXECUTOR_REGISTRY[executor.executor_key] = executor

def get_executor(binding: str | Mapping[str, object]):
    if isinstance(binding, str):
        key = binding
    else:
        key = str(binding["executor_key"])
    return _EXECUTOR_REGISTRY[key]

def iter_executors():
    return tuple(_EXECUTOR_REGISTRY.values())
```

### Signer Registry

> Source: `src/qtg/infrastructure/signers/registry.py`

```python theme={null}
_SIGNER_REGISTRY: dict[str, object] = {}

def clear_signer_registry() -> None:
    _SIGNER_REGISTRY.clear()

def register_signer(signer) -> None:
    _SIGNER_REGISTRY[signer.signer_key] = signer

def get_signer(binding: str | Mapping[str, object]):
    if isinstance(binding, str):
        key = binding
    else:
        key = str(binding["signer_key"])
    return _SIGNER_REGISTRY[key]

def iter_signers():
    return tuple(_SIGNER_REGISTRY.values())
```

### Registry API summary

| Function                                                  | Description                                                                                                                                |
| --------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
| `register_executor(executor)` / `register_signer(signer)` | Register in the registry under `executor_key` / `signer_key`                                                                               |
| `get_executor(binding)` / `get_signer(binding)`           | Extract the key from binding and look it up in the registry. If `str`, use as is; if `dict`, extract `["executor_key"]` / `["signer_key"]` |
| `iter_executors()` / `iter_signers()`                     | Return all registered implementations as a tuple. Used for iterating health checks, etc.                                                   |
| `clear_executor_registry()` / `clear_signer_registry()`   | Initialization for tests                                                                                                                   |

### Registry initialization timing

During the bootstrap stage (`infrastructure/bootstrap.py`), all executor/signer implementations are constructed and registered in the registry. Afterwards, the dispatch/observe/recover workers retrieve them via `get_executor()` / `get_signer()`.

***

## Binding Resolution

A node's `executor_binding` and `signer_binding` can take two forms:

### 1. String binding

```python theme={null}
node.executor_binding = "exec.cex.withdrawal_action"
```

Looked up directly in the registry under this key.

### 2. Dictionary binding (Dict / Mapping)

```python theme={null}
node.executor_binding = {
    "executor_key": "exec.cex.withdrawal_action",
    "exchange": "upbit",
    "mode": "live"
}
```

`binding["executor_key"]` is used to look up the registry. The remaining fields can be referenced inside the executor.

### Signer binding resolution

How the dispatcher finds the signer:

```python theme={null}
# dispatch.py
if prepared_action.signing_required:
    if not node.signer_binding:
        # FAILED if signer_binding is missing
        ...
    signer = get_signer(node.signer_binding)
```

If `signer_binding` is `None` and `signing_required` is `True`, the dispatcher transitions the node to `FAILED` and records `"missing signer binding"` in detail.

***

## Implementation categories

### Executor implementations

| Class                          | Location                                             | Description                                                                                                                                                      |
| ------------------------------ | ---------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `LocalExecutorAdapter`         | `infrastructure/executors/local_adapter.py`          | Adapter wrapping an internal handler. Delegates all methods to the handler                                                                                       |
| `RemoteExecutorProxy`          | `infrastructure/executors/remote_proxy.py`           | HTTP-based remote executor proxy. Translates each method into `POST /preflight`, `POST /prepare`, etc.                                                           |
| `CexWithdrawalActionExecutor`  | `infrastructure/executors/cex/withdrawal_action.py`  | CEX withdrawal execution                                                                                                                                         |
| `CexWithdrawalObserveExecutor` | `infrastructure/executors/cex/withdrawal_observe.py` | CEX withdrawal state observation                                                                                                                                 |
| `CexDepositObserveExecutor`    | `infrastructure/executors/cex/deposit_observe.py`    | CEX deposit state observation                                                                                                                                    |
| `CctpBurnExecutor`             | `infrastructure/executors/cctp/burn.py`              | CCTP burn transaction execution                                                                                                                                  |
| `CctpMintExecutor`             | `infrastructure/executors/cctp/mint.py`              | CCTP mint transaction execution                                                                                                                                  |
| Gateway executors              | `infrastructure/executors/gateway/`                  | Circle Gateway family — approve / deposit / intent (EIP-712) / mint + their observe lanes (`exec.gateway.*`, 6 keys)                                             |
| EVM transfer executor          | `infrastructure/executors/evm/erc20_transfer.py`     | Direct ERC-20 transfer (`exec.evm.erc20_transfer`)                                                                                                               |
| CCIP executor                  | `infrastructure/executors/ccip/`                     | Chainlink CCIP send (`exec.ccip.send`, gated by `ccip_enabled`)                                                                                                  |
| USDT0 executor                 | `infrastructure/executors/usdt0/`                    | USDT0 / LayerZero native-OFT send (`exec.usdt0.send`, gated by `usdt0_enabled`)                                                                                  |
| Hyperliquid top-up             | `infrastructure/executors/hyperliquid_topup.py`      | Hyperliquid USDC top-up (`exec.hyperliquid.topup`)                                                                                                               |
| Stargate executors (Pro)       | *Pro distribution (excluded from OSS)*               | Stargate bridge send + destination observe (`exec.stargate.send`, `exec.stargate.dst_observe`) — registered via the `pro_loader` boundary, **excluded from OSS** |
| Observe executors              | `infrastructure/executors/observe/`                  | Chain/protocol observation executor family (incl. `exec.observe.evm_balance` / `exec.observe.evm_finality`)                                                      |

For the full key list, gating, and bootstrap order, see [executors/overview.md § 5.2 / § 7](/reference/executors/overview#52-built-in-executor-key-list).

### Signer implementations

| Class                      | Location                                          | Description                                                                                                                                                               |
| -------------------------- | ------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `NoopSigner`               | `infrastructure/signers/noop.py`                  | Returns an empty result without signing. Used in dry-run or CEX-only environments                                                                                         |
| `AwsKmsEvmSigner`          | `infrastructure/signers/aws_kms_evm.py`           | Default EVM signer. Derives the EOA from an AWS KMS `secp256k1` key; signs `evm_raw_tx` and `eip712_compact` formats without exposing key material                        |
| `RemoteSignerProxy`        | `infrastructure/signers/remote_proxy.py`          | HTTP-based remote signer. `POST /sign` for signing requests, `GET /health` for status checks                                                                              |
| `LocalPrivateKeySignerEvm` | `infrastructure/signers/local_private_key_evm.py` | Development-only signer holding the key in process memory (`op` or `plain` backend). Same signature formats / address derivation as `AwsKmsEvmSigner`; not for production |

### Remote Signer HTTP Protocol

The remote signer HTTP interface used by `RemoteSignerProxy`:

**POST /sign**

* Request: `SignRequest` JSON body
* Response: `SignResult` JSON body
* Auth: `Authorization: Bearer {token}` (optional)

**GET /health**

* Response: signer state JSON

```python theme={null}
class RemoteSignerProxy:
    async def sign(self, request: SignRequest) -> SignResult:
        response = await self._client.post(
            f"{self.base_url}/sign",
            json=request.model_dump(mode="json"),
            headers=self._headers(),
        )
        response.raise_for_status()
        return SignResult.model_validate(response.json())
```

### Remote Executor HTTP Protocol

The remote executor HTTP interface used by `RemoteExecutorProxy`:

| Endpoint     | Method | Request Body                                         | Response                                 |
| ------------ | ------ | ---------------------------------------------------- | ---------------------------------------- |
| `/preflight` | POST   | `ExecutionContext` JSON                              | `ExecutionResult` JSON or 204 No Content |
| `/prepare`   | POST   | `ExecutionContext` JSON                              | `ExecutionResult` JSON                   |
| `/submit`    | POST   | `{"context": ..., "prepared_action": ..., **kwargs}` | `ExecutionResult` JSON                   |
| `/observe`   | POST   | `ExecutionContext` JSON                              | `ExecutionResult` JSON                   |
| `/recover`   | POST   | `ExecutionContext` JSON                              | `ExecutionResult` JSON                   |
| `/cancel`    | POST   | `ExecutionContext` JSON                              | `ExecutionResult` JSON                   |
| `/health`    | GET    | -                                                    | state JSON                               |

A 204 response from `preflight` means "precondition passed" (equivalent to returning `None`).

***

## Cross-References

* State transition rules: [domain/states-and-transitions.md](/reference/domain/states-and-transitions)
* Type definitions: [domain/types-and-enums.md](/reference/domain/types-and-enums)
* Error taxonomy: [domain/error-taxonomy.md](/reference/domain/error-taxonomy)
* Plan compilation: [compiler/plan-compilation.md](/reference/compiler/plan-compilation)
* CEX executor details: [executors/cex-lane.md](/reference/executors/cex-lane)
* CCTP executor details: [executors/cctp-lane.md](/reference/executors/bridges/cctp-lane)
