nautechsystems/nautilus_trader · critical · anyhow::Error

Verified call trace contains an operation without a target

Error message

Verified call trace contains an operation without a target

What it means

This error is raised in `validate_internal_calls` when a child frame in the verified call trace has no target: `call.to` is `None`. Every internal EVM call frame must name the contract (or account) it called; a frame without a target cannot be checked against the deployment manifest's reviewed call edges, so verification cannot proceed and fails explicitly. This guards against malformed tracer output being silently accepted.

Source

Thrown at crates/adapters/blockchain/src/execution/client.rs:4428

            && trace.success == receipt_success,
        "Verified call-trace root differs from the authenticated transaction"
    );
    validate_internal_calls(&trace.calls, signed.to, purpose, manifest)
}

fn validate_internal_calls(
    calls: &[VerifiedCallTrace],
    caller_context: Address,
    purpose: &str,
    manifest: &BlockchainDeploymentManifest,
) -> anyhow::Result<()> {
    for call in calls {
        anyhow::ensure!(
            call.from == caller_context,
            "Verified call trace child has an invalid caller context"
        );
        let target = call.to.ok_or_else(|| {
            anyhow::anyhow!("Verified call trace contains an operation without a target")
        })?;
        let call_type = match call.call_type {
            RpcCallType::Call => "call",
            RpcCallType::Callcode => "callcode",
            RpcCallType::Delegatecall => "delegatecall",
            RpcCallType::Staticcall => "staticcall",
            RpcCallType::Create | RpcCallType::Create2 | RpcCallType::Selfdestruct => {
                anyhow::bail!("Verified call trace contains a forbidden state-changing operation")
            }
        };
        let permitted = manifest.call_edges.iter().any(|edge| {
            edge.purpose == purpose
                && edge.call_type.eq_ignore_ascii_case(call_type)
                && Address::from_str(&edge.caller).ok() == Some(call.from)
                && Address::from_str(&edge.target).ok() == Some(target)
        });
        anyhow::ensure!(
            permitted,

View on GitHub (pinned to 18893faf8b)

Solutions

  1. Inspect the full trace frame at the failure point; if it is a CREATE/CREATE2 frame, note that such operations are separately rejected as forbidden state-changing operations — ensure the tracer's frames are correctly typed so creation frames do not masquerade as calls.
  2. Re-fetch the trace from a node whose callTracer always populates `to` for CALL/STATICCALL/DELEGATECALL frames (e.g. current geth/erigon); upgrade the node if it emits null targets.
  3. Verify the verification coordinator's tracer configuration (tracer type/options) has not dropped the `to` field via custom filtering or field selection.
  4. If precompile or native-frame edge cases produce null targets, confirm the node version and switch to one with correct callTracer semantics before re-running verification.

Example fix

// before: custom tracer that omits `to` on some frames
let trace = node.debug_trace_transaction(tx_hash, CustomTracer { fields: &["from", "input"] }).await?;
validate_internal_calls(&trace.calls, ctx, purpose, manifest)?; // frames lack `to`

// after: standard callTracer guarantees `to` on call frames
let trace = node.debug_trace_transaction(tx_hash, GethCallTracer).await?;
validate_internal_calls(&trace.calls, ctx, purpose, manifest)?;
Defensive patterns

Strategy: validation

Validate before calling

// Filter/flag frames without targets before verification.
fn all_children_have_targets(calls: &[VerifiedCallTrace]) -> bool {
    calls.iter().all(|c| c.to.is_some())
}

Type guard

fn has_target(call: &VerifiedCallTrace) -> bool {
    call.to.is_some()
}

Try / catch

if let Err(e) = validate_internal_calls(&trace.calls, ctx, purpose, manifest) {
    if e.to_string().contains("without a target") {
        // usually a CREATE frame or malformed tracer output: re-trace with standard callTracer
        let fresh = trusted_node.debug_trace_transaction(tx_hash, GethCallTracer).await?;
        return validate_internal_calls(&fresh.calls, ctx, purpose, manifest);
    }
    return Err(e);
}

Prevention

When it happens

Trigger: `validate_internal_calls` encounters any `VerifiedCallTrace` child whose `to` field is `None` (the `ok_or_else` at client.rs:4427). Happens when the tracing RPC returns frames with a missing/null `to` — typically for CREATE/CREATE2 frames (which legitimately have no `to`) surfacing where the validator expects a CALL/STATICCALL/DELEGATECALL frame, or when the tracer omits the field on malformed input.

Common situations: A tracer that reports contract-creation frames without a `to` address feeding into this validator's input; nodes with differing callTracer implementations that leave `to` null on edge cases (precompile calls, self-destruct edge frames); proxy nodes that strip or reformat trace fields; traces for transactions containing contract deployments where the validator's upstream filtering did not exclude creation frames.

Related errors


AI-assisted analysis of nautechsystems/nautilus_trader@18893faf8b (2026-09-08). Data as JSON: /api/errors/690dd587a9e44919. Report an issue: GitHub.