-
Notifications
You must be signed in to change notification settings - Fork 305
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
dex: refactor internal indexing into ext traits #4188
Merged
Merged
Changes from all commits
Commits
Show all changes
11 commits
Select commit
Hold shift + click to select a range
312691a
dex: break out engine idx into modules
erwanor 62adefe
dex: implement update index methods
erwanor ffbd9a0
dex: make `update_position` use update index methods
erwanor 9024634
dex: DiD guard against bad position transitions
erwanor aa0d66b
dex: don't expose dex engine internals
erwanor 4e150c1
dex: refactor base liquidity index
erwanor 61d65cd
dex(position_manager): restrict interface to internal indices
erwanor 7cd61d4
dex: expose a position api to other components
erwanor 0f5e516
dex(position_counter): limit visibility of counter internals
erwanor 9d430fd
dex: spurious index update from test
erwanor 78fbf51
dex: pr review improvements
erwanor File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
339 changes: 56 additions & 283 deletions
339
crates/core/component/dex/src/component/position_manager.rs
Large diffs are not rendered by default.
Oops, something went wrong.
180 changes: 180 additions & 0 deletions
180
crates/core/component/dex/src/component/position_manager/base_liquidity_index.rs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,180 @@ | ||
use anyhow::Result; | ||
use cnidarium::StateWrite; | ||
use penumbra_num::Amount; | ||
use position::State::*; | ||
|
||
use crate::lp::position::{self, Position}; | ||
use crate::state_key::engine; | ||
use crate::DirectedTradingPair; | ||
use penumbra_proto::{StateReadProto, StateWriteProto}; | ||
|
||
pub(crate) trait AssetByLiquidityIndex: StateWrite { | ||
/// Update the base liquidity index, used by the DEX engine during path search. | ||
/// | ||
/// # Overview | ||
/// Given a directed trading pair `A -> B`, the index tracks the amount of | ||
/// liquidity available to convert the quote asset B, into a base asset A. | ||
/// | ||
/// # Index schema | ||
/// The liquidity index schema is as follow: | ||
/// - A primary index that maps a "start" asset A (aka. base asset) | ||
/// to an "end" asset B (aka. quote asset) ordered by the amount of | ||
/// liquidity available for B -> A (not a typo). | ||
/// - An auxilliary index that maps a directed trading pair `A -> B` | ||
/// to the aggregate liquidity for B -> A (used in the primary composite key) | ||
/// | ||
/// # Diagram | ||
/// | ||
/// Liquidity index: | ||
/// For an asset `A`, surface asset | ||
/// `B` with the best liquidity | ||
/// score. | ||
/// ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐ | ||
/// | ||
/// ┌──┐ ▼ ┌─────────┐ │ | ||
/// ▲ │ │ ┌──────────────────┐ │ │ | ||
/// │ │ ─┼───▶│{asset_A}{agg_liq}│──▶│{asset_B}│ │ | ||
/// │ ├──┤ └──────────────────┘ │ │ | ||
/// sorted │ │ └─────────┘ │ | ||
/// by agg │ │ | ||
/// liq ├──┤ │ | ||
/// │ │ │ used in the | ||
/// │ ├──┤ composite | ||
/// │ │ │ key | ||
/// │ │ │ Auxiliary look-up index: │ | ||
/// │ │ │ "Find the aggregate liquidity | ||
/// │ │ │ per directed trading pair" │ | ||
/// │ │ │ ┌───────┐ ┌─────────┐ | ||
/// │ │ │ ├───────┤ ┌──────────────────┐ │ │ | ||
/// │ │ │ │ ────┼─▶│{asset_A}{asset_B}│────▶│{agg_liq}│ | ||
/// │ ├──┤ ├───────┤ └──────────────────┘ │ │ | ||
/// │ │ │ ├───────┤ └─────────┘ | ||
/// │ │ │ ├───────┤ | ||
/// │ │ │ ├───────┤ | ||
/// │ ├──┤ └───────┘ | ||
/// │ │ │ | ||
/// │ │ │ | ||
/// │ └──┘ | ||
async fn update_asset_by_base_liquidity_index( | ||
&mut self, | ||
prev_state: &Option<Position>, | ||
new_state: &Position, | ||
id: &position::Id, | ||
) -> Result<()> { | ||
// We need to reconstruct the position's previous contribution and compute | ||
// its new contribution to the index. We do this for each asset in the pair | ||
// and short-circuit if all contributions are zero. | ||
let canonical_pair = new_state.phi.pair; | ||
let pair_ab = DirectedTradingPair::new(canonical_pair.asset_1(), canonical_pair.asset_2()); | ||
|
||
// We reconstruct the position's *previous* contribution so that we can deduct them later: | ||
let (prev_a, prev_b) = match prev_state { | ||
// The position was just created, so its previous contributions are zero. | ||
None => (Amount::zero(), Amount::zero()), | ||
Some(prev) => match prev.state { | ||
// The position was previously closed or withdrawn, so its previous contributions are zero. | ||
Closed | Withdrawn { sequence: _ } => (Amount::zero(), Amount::zero()), | ||
// The position's previous contributions are the reserves for the start and end assets. | ||
_ => ( | ||
prev.reserves_for(pair_ab.start) | ||
.expect("asset ids match for start"), | ||
prev.reserves_for(pair_ab.end) | ||
.expect("asset ids match for end"), | ||
), | ||
}, | ||
}; | ||
|
||
// For each asset, we compute the new position's contribution to the index: | ||
let (new_a, new_b) = if matches!(new_state.state, Closed | Withdrawn { sequence: _ }) { | ||
// The position is being closed or withdrawn, so its new contributions are zero. | ||
// Note a withdrawn position MUST have zero reserves, so hardcoding this is extra. | ||
(Amount::zero(), Amount::zero()) | ||
} else { | ||
( | ||
// The new amount of asset A: | ||
new_state | ||
.reserves_for(pair_ab.start) | ||
.expect("asset ids match for start"), | ||
// The new amount of asset B: | ||
new_state | ||
.reserves_for(pair_ab.end) | ||
.expect("asset ids match for end"), | ||
) | ||
}; | ||
|
||
// If all contributions are zero, we can skip the update. | ||
// This can happen if we're processing inactive transitions like `Closed -> Withdrawn`. | ||
if prev_a == Amount::zero() | ||
&& new_a == Amount::zero() | ||
&& prev_b == Amount::zero() | ||
&& new_b == Amount::zero() | ||
{ | ||
return Ok(()); | ||
} | ||
|
||
// A -> B | ||
self.update_asset_by_base_liquidity_index_inner(id, pair_ab, prev_a, new_a) | ||
.await?; | ||
// B -> A | ||
self.update_asset_by_base_liquidity_index_inner(id, pair_ab.flip(), prev_b, new_b) | ||
.await?; | ||
|
||
Ok(()) | ||
} | ||
} | ||
|
||
impl<T: StateWrite + ?Sized> AssetByLiquidityIndex for T {} | ||
|
||
trait Inner: StateWrite { | ||
async fn update_asset_by_base_liquidity_index_inner( | ||
&mut self, | ||
id: &position::Id, | ||
pair: DirectedTradingPair, | ||
old_contrib: Amount, | ||
new_contrib: Amount, | ||
) -> Result<()> { | ||
let aggregate_key = &engine::routable_assets::lookup_base_liquidity_by_pair(&pair); | ||
|
||
let prev_tally: Amount = self | ||
.nonverifiable_get(aggregate_key) | ||
.await? | ||
.unwrap_or_default(); | ||
|
||
// To compute the new aggregate liquidity, we deduct the old contribution | ||
// and add the new contribution. We use saturating arithmetic defensively. | ||
let new_tally = prev_tally | ||
.saturating_sub(&old_contrib) | ||
.saturating_add(&new_contrib); | ||
|
||
// If the update operation is a no-op, we can skip the update and return early. | ||
if prev_tally == new_tally { | ||
tracing::debug!( | ||
?prev_tally, | ||
?pair, | ||
?id, | ||
"skipping routable asset index update" | ||
); | ||
return Ok(()); | ||
} | ||
|
||
// Update the primary and auxiliary indices: | ||
let old_primary_key = engine::routable_assets::key(&pair.start, prev_tally).to_vec(); | ||
// This could make the `StateDelta` more expensive to scan, but this doesn't show on profiles yet. | ||
self.nonverifiable_delete(old_primary_key); | ||
|
||
let new_primary_key = engine::routable_assets::key(&pair.start, new_tally).to_vec(); | ||
self.nonverifiable_put(new_primary_key, pair.end); | ||
tracing::debug!(?pair, ?new_tally, "base liquidity entry updated"); | ||
|
||
let auxiliary_key = engine::routable_assets::lookup_base_liquidity_by_pair(&pair).to_vec(); | ||
self.nonverifiable_put(auxiliary_key, new_tally); | ||
tracing::debug!( | ||
?pair, | ||
"base liquidity heuristic marked directed pair as routable" | ||
); | ||
|
||
Ok(()) | ||
} | ||
} | ||
|
||
impl<T: StateWrite + ?Sized> Inner for T {} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@erwanor @zbuc maybe this comment could be clarified / expanded in light of today's discussion in the dex channel on discord, explaining that although the index may seem "backwards", we think it's a heuristic that's both useful and also hard to forge?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we could do this in #4193
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the entire index description could be workshopped, we should put some examples