Skip to content

Duplicate transaction submissions can strand callers until deadline #41

Description

@Dodecahedr0x

Problem

A transaction submitted twice is deduplicated correctly, but a duplicate submitted after the original settles is never told the outcome. It blocks until its own deadline and reports a timeout for a transaction that executed successfully.

AccountAccessor::create and AccountAccessor::update await the status with an 8-second EXECUTION_TIMEOUT, so a caller that races itself sees Engine("internal error: deadline has elapsed") even though the transaction succeeded.

Reproduction

This is reachable from ordinary validator behavior. magicblock-chainlink's ensure_delegation_action_dependencies force-refreshes a bundle's writable dependencies and issued two identical clones of the same account 0.8 ms apart:

06:40:25.856166  Cloning account pubkey=3jd362SA… slot=62199 owner=Tokenkeg…
06:40:25.857008  Cloning account pubkey=3jd362SA… slot=62199 owner=Tokenkeg…

Identical MagicRoot clone transactions share a signature, so the second was deduplicated. The clone landed at 06:36:39 with err: None, and chainlink gave up at 06:36:47.44, exactly eight seconds later. Chainlink then read that timeout as "post-delegation actions unsatisfiable," discarded the bundle, and undelegated the account instead. This reproduced identically in debug and release builds.

The result depends on timing:

  • Original still in flight: Subscribers::subscribe returns a receiver on the existing channel for that signature, so commit_execution reaches both submitters.
  • Original already settled: the terminal broadcast removes the channel. A later duplicate creates a fresh channel that nothing will write to.
  • During settlement: commit_execution caches the status after the terminal broadcast, leaving a narrower window in which a subscriber finds neither a live channel nor a cached result.

Expected

Every duplicate submission should observe the original transaction's outcome without waiting for its own deadline. Settling a transaction should not expose a window with neither a live subscription nor a cached result.

Context

Sequencer::schedule drops the duplicate and returns without notifying subscribers:

// processor/src/sequencer/mod.rs
if !self.replay && !self.state.transactions().append(&txn).await? {
    metrics::failed_transaction(FailureKind::SequencerDrop);
    return Ok(());
};

append returns Ok(false) when caches.signatures.push finds the signature already present. Nothing is published to subscriptions.signatures for that submission.

The settlement ordering creates the additional window:

// keeper/src/accessor.rs
subs.signatures.send(&commit.signature, &commit.status, true);
self.keeper.caches.signatures.update(&commit.signature, Some(commit.status));

A possible fix is to replay the cached outcome to duplicate subscribers and update the cache before the terminal broadcast. A cached None means the original is still in flight, where the shared channel already serves both submitters.

This was implemented locally against a5e9f2d with regression tests in keeper/src/tests/subscriptions.rs. The workspace did not build standalone at that revision because solana-account failed on a wincode version split, so the tests could not be executed there.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions