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
- 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.
- 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.
- Verify the verification coordinator's tracer configuration (tracer type/options) has not dropped the `to` field via custom filtering or field selection.
- 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
- Use a callTracer implementation that always populates `to` for CALL/STATICCALL/DELEGATECALL frames.
- Exclude or explicitly reject contract-creation frames before validation (the validator separately forbids them).
- Avoid proxies that strip trace fields; fetch traces directly from a full node.
- Pin and test the node version used for tracing in CI so frame shape regressions are caught early.
- Log frames with null `to` (with call_type) to identify creation vs malformed frames.
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
- Verified call trace child has an invalid caller context
- Finalized block {} does not contain transaction {}
- No deployed bytecode at {description} address {address}
- Chain ID mismatch at connect: expected {expected_chain_id},
- Verified call trace contains a forbidden state-changing oper
AI-assisted analysis of nautechsystems/nautilus_trader@18893faf8b (2026-09-08).
Data as JSON: /api/errors/690dd587a9e44919.
Report an issue: GitHub.