Skip to content

solana-autopilot: persist competitions and hold out in-flight orders - #4931

Open
squadgazzz wants to merge 11 commits into
mainfrom
solana-autopilot/be-220-competition-persistence
Open

squadgazzz wants to merge 11 commits into
mainfrom
solana-autopilot/be-220-competition-persistence

Conversation

@squadgazzz

@squadgazzz squadgazzz commented Sep 17, 2026 •

Copy link
Copy Markdown
Contributor

Description

The Solana autopilot keeps no record of its competitions, and the next cut can re-offer an order whose settlement is still in flight (seen on staging, one order settled twice by the same solver). Accounting and debugging need what the EVM autopilot persists, so this mirrors it with the Solana counterparts of the V072 competition tables. Auction ids come from the solana.auctions identity column, so they are serial and survive restarts. Every proposed solution is stored under an autopilot-generated per-auction uid, settlement windows key on that uid, and the cut holds out the orders of winning solutions the way the EVM fetch_in_flight_orders does: until a blockhash lifetime past the deadline slot runs out, a landed settlement of the auction trades the order, or the driver rejects the solution before sending it. Landing is checked per order through the settlement's trades, since settlements.solution_uid is unattributed and one solver may win several solutions of one auction. S3 archival of the auction is left for later.

The in-memory hold-out from #4937 and #4938 goes away. Its two release triggers were table state already: an observed settlement is a solana.settlements row with its solana.trades, a provable non-send is a rejected window. Its blockhash grace moves into the query as MAX_PROCESSING_AGE, and a timed-out window keeps the hold for that span. So the hold no longer waits on the deadline sweep and survives a restart without seeding. #4932 still adds the idle sweep so timed-out windows get flagged on a quiet chain.

Changes

  • solana.auctions, solana.competition_auctions, solana.proposed_solutions (with filtered_out), solana.proposed_trade_executions, solana.reference_scores, plus a deadline index for the hold-out query
  • Auction ids move from unix seconds to the solana.auctions sequence, like the EVM auctions.id
  • The observer persists the ranked competition and the reference scores before dispatch in one transaction with batched inserts, a failed write skips the dispatch. Solution uids are positions in the ranking, winners first, as on EVM
  • Settlement windows key on the generated solution uid, the settle request keeps the driver-local id, and a window closes as landed on the settlement that traded its solution's orders
  • The cut excludes orders of unsettled winning solutions until their deadline plus the blockhash lifetime, records them as filtered, and skips the cycle when the lookup fails
  • The in-memory InFlightOrders and its hold and release calls are removed

How to test

Postgres tests: in_flight_orders over the deadline plus blockhash lifetime, timeout, rejection and settlement, and the open-orders predicates. The full-cycle test checks the persisted competition rows and the hold across the deadline sweep.

Related issues

BE-220, BE-245 (windows close one-to-one through the persisted trade executions), BE-317 (reference scores)

@linear-code

linear-code Bot commented Sep 17, 2026

Copy link
Copy Markdown

BE-220

Base automatically changed from solana-autopilot/be-192-native-prices to main September 18, 2026 17:47
…20-competition-persistence

# Conflicts:
#	crates/autopilot-svm/example.toml
#	crates/autopilot-svm/src/infra/config.rs
#	crates/autopilot-svm/src/infra/db.rs
#	crates/autopilot-svm/src/infra/prices.rs
#	crates/autopilot-svm/src/infra/provider.rs
#	crates/autopilot-svm/src/run.rs
#	crates/autopilot-svm/src/tests.rs
… dropped

The observer persists ranked and filtered-out solutions alike, so without a flag a debugger cannot tell a filtered solution from one that lost on score. Mirrors the EVM proposed_solutions.filtered_out column.
…rows

The cut now holds out orders of winning solutions until their deadline slot passes, the indexer records the solver's settlement, or the execution window closes early, the shape of the EVM fetch_in_flight_orders. The hold no longer depends on a window row being opened or on the deadline sweep running, and it survives a restart. Held orders get a filtered order event, a failed lookup skips the cut.

The competition inserts go through QueryBuilder, one statement per table, like the EVM save_solutions.
The competition rows carry the hold on their own, the EVM shape. The blockhash grace the map provided moves into the query as MAX_PROCESSING_AGE past the deadline slot, and a timed-out window keeps the hold for that span. The map's two release triggers were table state already: an observed settlement is a settlements row, a provable non-send a rejected window. The hold now survives a restart without any seeding.
@squadgazzz

Copy link
Copy Markdown
Contributor Author

@claude review please and ensure the logic mirrors the evm implementation and each line of code makes sense. we're going to have a separate settlement program instance for prod, so same auction ids from staging and prod shouldn't be a problem for indexing.

@claude

claude Bot commented Sep 24, 2026 •

Copy link
Copy Markdown
Contributor

Claude finished @squadgazzz's task in 6m 24s —— View job


Review complete

I reviewed the full diff against origin/main, focusing on whether the logic mirrors the EVM implementation and whether each line holds up. Overall this is a clean, well-documented mirror of the V072 competition tables. The intentional Solana-specific divergences all check out:

  • Blockhash grace — in_flight_orders holds through deadline_slot + MAX_PROCESSING_AGE (>= vs EVM's strict deadline > block), and a timeout window keeps the hold (EVM releases on any non-success). Correct for the "tx can still land until the blockhash expires" model, and the new DB test covers the deadline / timeout / reject / settle transitions plus the restart case.
  • Sequential auction ids from the solana.auctions identity column mirror EVM auctions.id; per your note, a separate prod settlement-program instance means staging/prod id overlap is a non-issue for the indexer.
  • Persist-before-dispatch ordering (run_cycle → persist_competition_ranking → execute) means the hold is live in the DB before any settlement goes out, and a failed persist / failed in-flight lookup safely skips the cycle rather than risking a double-settle.
  • Removing the in-memory InFlightOrders in favor of table state (settlements row = observed; rejected window = provable non-send) is sound, and store_detached correctly keeps order-event writes off the cycle path.
  • Column widths (numeric(20,0)) match Solana's Amount = u64.

One finding worth confirming (posted inline on db.rs:246): the settlement-release check matches by solver, not by solution_uid. The comment assumes "one solver wins at most one solution per auction," but pick_winners only enforces disjoint token pairs — with max_winners > 1 a solver could win two disjoint solutions and settling one would release the other prematurely. This preserves the old in-memory behavior (not a regression) but diverges from EVM's solution_uid-based match.

Minor (not blocking): the solana.auctions.json snapshot is written and then deleted on the next cut, so it's currently a write-only column reserved for the future S3 archival you mention — fine, just flagging it reads as dead until then.
· solana-autopilot/be-220-competition-persistence

Comment thread crates/autopilot-svm/src/infra/db.rs Outdated
…n any settlement by its solver

Winner selection only keeps token pairs disjoint, so one solver can win several solutions of one auction. Matching the landed settlement by solver released all of them at once. Matching the settlement's trades releases exactly the orders that landed.
…ecuted its solution

A settlement row carries no solution uid, and one solver may hold several windows of one auction. Matching by solver closed all of them on the first landing, with the first signature. The window's solution has persisted trade executions, so the settlement whose trades cover one of them is the one that executed it.
@squadgazzz
squadgazzz marked this pull request as ready for review September 24, 2026 14:05
@squadgazzz
squadgazzz requested a review from a team as a code owner September 24, 2026 14:05
@claude

claude Bot commented Sep 24, 2026 •

Copy link
Copy Markdown
Contributor

Claude finished @squadgazzz's task in 4m 35s —— View job


Review complete

I re-reviewed the full diff against origin/main, focusing on whether the logic mirrors the EVM implementation and whether each line holds up now that the earlier solver-matching concern has been addressed.

The main finding from my previous review is fixed. Both release paths now match a settlement to a solution per order through the trades, not by solver:

  • close_landed_windows (db.rs:190) closes a window only when the solver's settlement traded one of that solution's proposed executions — the new one-to-one test (observation.rs, a_landing_closes_only_the_window_it_executed) confirms a second window stays open until its own settlement lands.
  • in_flight_orders (db.rs:239) releases an order only once a settlement of that auction actually traded it (or the window is rejected, or deadline + MAX_PROCESSING_AGE has passed). The max_winners > 1 double-settle edge I flagged before is closed.

Everything else checks out:

  • Hold semantics — held from persist time (ps.is_winner), through deadline_slot + MAX_PROCESSING_AGE, with a timeout window keeping the hold. All three release triggers (traded / rejected / blockhash-expired) are individually sound against double-settle, and the release-too-early paths are all cases where the order is either already filled or provably un-sent. DB test covers deadline/timeout/reject/settle plus the two-winning-solutions case.
  • Persist-before-dispatch — run_cycle persists the ranking (and thus the hold) before execute, and a failed persist / failed in-flight lookup returns early rather than risking a dispatch without the hold.
  • Auction ids — sourced from the solana.auctions identity column, sequential across restarts like EVM auctions.id; replace_current_auction consumes an id only as the last step of a successful cut, so skipped cuts leave no partial state.
  • Column widths / types — numeric(20,0) fits Solana's Amount = u64 for score and executed amounts.
  • Removing the in-memory InFlightOrders in favor of table state, and store_detached keeping order-event writes off the cycle path, are both clean.

CI is green.

One non-blocking note (posted inline on arbitrator.rs:74): uids is keyed by (solver, id); a collision on that key now collapses two solutions to one uid and would fail the whole persist_competition transaction (PK violation → cycle skipped), rather than silently collapsing the way the existing drivers map does. Same invariant already assumed elsewhere and unreachable in the one-driver-per-solver setup — just worth confirming.
· solana-autopilot/be-220-competition-persistence

Comment thread crates/autopilot-svm/src/domain/arbitrator.rs Outdated
…nk position

Reference scores come from the generic arbitrator, one per winning solver, and go into solana.reference_scores with the solutions, the rewards baseline accounting asked for.

The persisted solution uid is now the position in ranked followed by filtered_out, as the EVM autopilot enumerates its ranking. The map keyed by (solver, solution id) collapsed two solutions sharing a key into one uid and failed the whole persist on the primary key.
The EVM columns exist but are written empty, they are not a feature the Solana table lacks.
Comment on lines +4 to +8
CREATE TABLE solana.auctions (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
tip_slot bigint NOT NULL,
json jsonb NOT NULL
);

@tilacog tilacog Sep 24, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Not super important, but since this is a singleton table, maybe it's worth adding a singleton check to enforce it, like

CREATE TABLE solana.auctions (
    -- …
    lock     boolean NOT NULL DEFAULT true UNIQUE CHECK (lock)
);

No change needed in replace_current_auction.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good suggestion! Even tho EVM doesn't have it, I'd add it. Thanks!

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.

2 participants