Skip to content

(fast3) Gate placement of fast-path orders on config - #4858

Open
MartinquaXD wants to merge 1 commit into
fast-path-2from
fast-path-3
Open

(fast3) Gate placement of fast-path orders on config#4858
MartinquaXD wants to merge 1 commit into
fast-path-2from
fast-path-3

Conversation

@MartinquaXD

Copy link
Copy Markdown
Contributor

Description

So far all orders that have enableFastPath: true in the appdata were rejected because the feature was disabled entirely.

Changes

This PR adds an optional config parameter min-fast-path-exclusivity which acts as a feature gate.
Only if this value is configured will fast path orders be allowed.

How to test

extended / updated existing unit tests

@MartinquaXD
MartinquaXD requested a review from a team as a code owner September 3, 2026 12:41
@claude

claude Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Claude finished @MartinquaXD's task in 1m 42s —— View job


Review complete

I reviewed the fast-path config gating changes. The mechanism (config gate → reject when unset, otherwise compute valid_from) is clean and well-tested. Two findings, posted inline:

  • valid_from doesn't enforce a minimum (discussion) — The doc says exclusivity applies "unless the user set a later one" and the field is min_fast_path_exclusivity, but .or_else(...) preserves any user-set validFrom, including one earlier than now + exclusivity (or in the past), bypassing the intended floor. Suggested a .max() clamp and an additional test case.
  • Disabled-feature rejection runs late (discussion) — FastPathDisabled is returned only at the end of validation, after quote/balance/signature work, though enable_fast_path is known early. Minor efficiency/UX nit.

The rest (config plumbing, error mapping in post_order.rs, removal of the old rejections in quoter.rs, test refactor) looks good.

Comment on lines +1049 to +1053
app_data
.inner
.protocol
.valid_from
.or_else(|| Some(time::now_in_epoch_seconds() + exclusivity.as_secs() as u32))

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.

The code diverges from the documented behavior. The config doc says the exclusivity is applied "unless the user set a later one", and the field is named min_fast_path_exclusivity — both imply a floor. But .or_else(...) only fills in a value when the user provided none: if a user sets a validFrom earlier than now + exclusivity (or in the past), it is preserved verbatim, shrinking or eliminating the exclusivity window and bypassing the intended minimum.

If the floor is intended, clamp to the later of the two:

Suggested change
app_data
.inner
.protocol
.valid_from
.or_else(|| Some(time::now_in_epoch_seconds() + exclusivity.as_secs() as u32))
let min_valid_from =
time::now_in_epoch_seconds() + exclusivity.as_secs() as u32;
Some(
app_data
.inner
.protocol
.valid_from
.map_or(min_valid_from, |user| user.max(min_valid_from)),
)

The existing tests wouldn't catch this — the user-validFrom test only exercises a value later than the floor (now + 100 vs now + 30). Worth adding a case with an earlier validFrom to pin down the intended behavior either way.

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.

Seems like a good catch, otherwise we should call it fast_path_default_exclusivity.

Comment on lines +1045 to +1048
let valid_from = if app_data.inner.protocol.enable_fast_path {
let Some(exclusivity) = self.min_fast_path_exclusivity else {
return Err(ValidationError::FastPathDisabled);
};

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.

Minor: enable_fast_path is known from app-data very early (the previous code rejected it during app-data validation). Now the FastPathDisabled rejection runs at the very end of validate_and_construct_order, i.e. after quote fetching, balance checks and signature validation. For an order that can never succeed because the feature is off, that's wasted work and a slower error response. Consider gating earlier (where the old enable_fast_path rejection lived) and only computing the effective valid_from here.

@MartinquaXD MartinquaXD changed the title Gate placement of fast-path orders on config (fast3) Gate placement of fast-path orders on config Sep 3, 2026
Adds a `min_fast_path_exclusivity` orderbook config option and an
`OrderValidator::with_min_fast_path_exclusivity` chainable setter. Orders
whose app data has `metadata.enableFastPath = true` now:

* fail validation with a new `FastPathDisabled` variant (HTTP 400) if
  the config knob is unset,
* otherwise get `valid_from = now + exclusivity`, holding them out of
  the regular auction until the fast-path solver has had its exclusivity
  window.

Also drops the leftover "'enableFastPath' is not yet supported" stubs
from the quoter and order validator, since the flag is now handled.

Signed-off-by: MartinquaXD <martin@cow.fi>

@fleupold fleupold left a comment

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.

mainly nits

/// later one). Unset disables the fast path: orders requesting it are
/// rejected.
#[serde(with = "humantime_serde", default)]
pub min_fast_path_exclusivity: Option<Duration>,

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.

Why the "minimum"? Can it be held out for longer? I assume it's because the solver can also settle it in the first block after the deadline. While technically correct, I wouldn't carry this detail in the name and just call it fast_path_default_exclusivity.

config.order_validation.max_gas_per_order,
config.order_validation.same_tokens_policy,
)
.with_min_fast_path_exclusivity(config.order_validation.min_fast_path_exclusivity),

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.

Any reason we are using the builder pattern here now, if all other parameters are constructor args? Feels inconsistent.

Comment on lines +1049 to +1053
app_data
.inner
.protocol
.valid_from
.or_else(|| Some(time::now_in_epoch_seconds() + exclusivity.as_secs() as u32))

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.

Seems like a good catch, otherwise we should call it fast_path_default_exclusivity.

@jmg-duarte jmg-duarte left a comment

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.

Felix raised the points I'd raise too 😅

@fleupold fleupold left a comment

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 sure if this is the right PR, but we should probably make sure the following edge cases are handled correctly:

  1. If a fast path order isn't able to find its quote, it should be rejected. I believe currently, we may fetch a new quote with fastPath: false and then try to hold solvers that didn't know accountable to that
  2. Same with orders that changed the limit price compared to its quote. Currently I believe they just get re-classified as out of market, but in this case the winning solver will not be willing to settle them.
  3. PreSign and ERC1271 orders should not be able to use fast path since they are inserted before they are "ready" (so the fast path cannot succed)

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.

3 participants