Skip to content

core: Fix duplicate VID when multiple offchain triggers fire in the same block#6336

Merged
lutter merged 4 commits intographprotocol:masterfrom
hudsonhrh:fix/vid-seq-offchain-collision
Feb 14, 2026
Merged

core: Fix duplicate VID when multiple offchain triggers fire in the same block#6336
lutter merged 4 commits intographprotocol:masterfrom
hudsonhrh:fix/vid-seq-offchain-collision

Conversation

@hudsonhrh
Copy link
Contributor

@hudsonhrh hudsonhrh commented Feb 7, 2026

Each iteration of the offchain trigger loop in handle_offchain_triggers creates a fresh BlockState with vid_seq reset to RESERVED_VIDS (100). When two or more offchain triggers (e.g. file/ipfs data sources) fire in the same block and write to the same entity table, they produce identical VIDs, causing a unique constraint violation.

This threads vid_seq from the onchain EntityCache through the offchain trigger loop so each trigger continues the sequence where the previous one left off. Also adds unit tests demonstrating both the collision and the fix.

There are tests for illustrative purposes showing the bug and the fix

Fixes #6335

@hudsonhrh hudsonhrh marked this pull request as ready for review February 7, 2026 21:07
@hudsonhrh hudsonhrh force-pushed the fix/vid-seq-offchain-collision branch from 20bba68 to f24825a Compare February 7, 2026 21:21
@hudsonhrh
Copy link
Contributor Author

@lutter fixed the styling issue with 8ab10ca
thanks for triggering the workflow run

@lutter
Copy link
Collaborator

lutter commented Feb 11, 2026

@lutter fixed the styling issue with 8ab10ca thanks for triggering the workflow run

No Problem. One thing I am not entirely sure about is that now everywhere an EntityCache is created, we need to remember to preserve the vid_seq. I would like to have something more automatic that is harder to screw up. Maybe we introduce an EntityCacheBuilder or some such and have as_modifications return that. The builder would then be responsible for preserving state from one cache to the next. But I haven't fully wrapped my head around how the cache and its data move through the code.

@hudsonhrh
Copy link
Contributor Author

@lutter fixed the styling issue with 8ab10ca thanks for triggering the workflow run

No Problem. One thing I am not entirely sure about is that now everywhere an EntityCache is created, we need to remember to preserve the vid_seq. I would like to have something more automatic that is harder to screw up. Maybe we introduce an EntityCacheBuilder or some such and have as_modifications return that. The builder would then be responsible for preserving state from one cache to the next. But I haven't fully wrapped my head around how the cache and its data move through the code.

Good call. I looked into it a bit more and the blast radius is actually pretty narrow. onchain triggers share a single EntityCache passed by value, so vid_seq accumulates naturally. It's only the offchain trigger loop that needs manual threading since each trigger gets an isolated cache.

the manual read-before-consume / write-after-create pattern is definitely fragile. I think the simplest fix is adding vid_seq to ModificationsAndCache that struct already carries the LFU cache forward, so vid_seq should travel with it. then you can’t forget it because it’s part of the return value you’re already destructuring. No new abstractions needed just using existing patterns.

how does that approach sound to you i could go ahead and implement it if you think it sounds good

@lutter
Copy link
Collaborator

lutter commented Feb 13, 2026

Reading the code in more detail, I think this is fine as-is

Copy link
Collaborator

@lutter lutter left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you rebase this on top of latest master? I'll merge it then

When specVersion >= 1.3.0, VIDs are computed deterministically as
(block_number << 32) + vid_seq. Each offchain trigger in
handle_offchain_triggers creates a fresh EntityCache with vid_seq
reset to RESERVED_VIDS (100). When multiple file data source triggers
fire in the same block and write to the same entity table, they
produce identical VIDs, causing PostgreSQL primary key violations:

  "duplicate key value violates unique constraint task_metadata_pkey"

Fix: Pass the onchain EntityCache's final vid_seq into
handle_offchain_triggers and accumulate it across loop iterations,
so each offchain trigger continues the sequence from where the
previous one left off.
Two tests demonstrate the bug where multiple offchain triggers (e.g. file
data sources) in the same block each create a fresh EntityCache with
vid_seq reset to RESERVED_VIDS (100), producing duplicate VIDs that
violate the primary key constraint.

- offchain_trigger_vid_collision_without_fix: proves VIDs collide
- offchain_trigger_vid_no_collision_with_fix: proves threading vid_seq
  across triggers prevents collisions
@hudsonhrh hudsonhrh force-pushed the fix/vid-seq-offchain-collision branch from 8ab10ca to 9c4a052 Compare February 13, 2026 20:14
@hudsonhrh
Copy link
Contributor Author

Can you rebase this on top of latest master? I'll merge it then

should be good now

@lutter lutter merged commit 2a468a4 into graphprotocol:master Feb 14, 2026
11 of 12 checks passed
@lutter
Copy link
Collaborator

lutter commented Feb 14, 2026

Thanks for the fix!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bug: Duplicate VID constraint violation when multiple offchain triggers fire in the same block

2 participants