nautechsystems/nautilus_trader · error
Pool state at block {} has no ingestion-time block hash; ref
Error message
Pool state at block {} has no ingestion-time block hash; refresh the profiler before execution What it means
Before executing a swap, the library anchors the quote to the profiler's ingestion-time pool-state snapshot, which must record the block hash of the block it was taken at. If the snapshot row has no block hash, the quote cannot be verified against canonical chain state, so execution refuses to proceed and asks the user to refresh the profiler data.
Source
Thrown at crates/adapters/blockchain/src/execution/client.rs:3940
.database
.mark_execution_event_emitted(intent.id, "terminal")
.await?;
executor.release_slot();
Ok(())
}
InclusionOutcome::Pending(message) => anyhow::bail!(message),
}
}
async fn validate_swap_quote(
position: &BlockPosition,
plan: &SwapPlan,
max_age_blocks: u64,
verification: &VerificationCoordinator,
manifest: &BlockchainDeploymentManifest,
) -> anyhow::Result<SwapQuoteAnchors> {
let block_hash = position.block_hash.as_deref().ok_or_else(|| {
anyhow::anyhow!(
"Pool state at block {} has no ingestion-time block hash; refresh the profiler before execution",
position.number
)
})?;
let expected_block_hash = B256::from_str(block_hash)
.with_context(|| format!("Invalid profiler block hash {block_hash}"))?;
let now_unix_secs = current_unix_secs()?;
let head = verified_value(
verification.verify_decision_header(now_unix_secs).await,
"swap decision header",
)?;
validate_quote_age(position.number, head.number, max_age_blocks)?;
let canonical_block = verified_value(
verification.verify_block(position.number).await,
"profiler watermark header",
)?;
anyhow::ensure!(
canonical_block.hash == expected_block_hash,View on GitHub (pinned to 18893faf8b)
Solutions
- Refresh the profiler so the pool-state snapshot at the given block is re-ingested with its block hash.
- Re-run the ingestion/indexer job covering that block to backfill block_hash.
- Pin execution to a newer snapshot block that has a valid hash.
- Check that the indexer version writes block_hash for every pool-state row.
Defensive patterns
Strategy: validation
Validate before calling
if position.block_hash.as_deref().map_or(true, |h| h.is_empty()) {
// refresh the profiler before building a SwapPlan on this snapshot
refresh_profiler(position.number)?;
} Type guard
fn has_ingestion_block_hash(position: &PoolStatePosition) -> bool {
position.block_hash.as_deref().map_or(false, |h| !h.is_empty())
} Try / catch
match build_quote_anchors(position, plan, max_age, verification, manifest) {
Ok(anchors) => execute(anchors),
Err(e) if e.to_string().contains("no ingestion-time block hash") => {
refresh_profiler(position.number)?;
retry();
}
Err(e) => return Err(e),
} Prevention
- Use an indexer version that always persists block_hash with pool-state rows.
- Refuse to execute on snapshots missing block hash at plan-build time.
- Schedule profiler refreshes before execution windows.
When it happens
Trigger: Calling the quote-anchor validation with a profiler pool-state row whose block_hash field is NULL/empty (position.block_hash is None) while preparing a SwapPlan.
Common situations: Profiler snapshots ingested by an older indexer version that didn't persist block hashes; partially written snapshot rows; running execution against a stale or hand-built dataset; DB schema change/migration that dropped the column values.
Understand the failure class
Background: "must not be empty", "cannot be empty" — required-field validation errors across open-source libraries — this error's family across 41 libraries.
Related errors
- Missing fee in topic3 when parsing pool created event
- Initialize event missing topics: expected 4, was {topics_len
- Initialize event missing topics: expected 4, was {log_topics
- Verified decision block {} has no base fee
- Pool state block {} changed from {} to {}; refresh the profi
AI-assisted analysis of nautechsystems/nautilus_trader@18893faf8b (2026-09-08).
Data as JSON: /api/errors/8725248aa6678e07.
Report an issue: GitHub.