From 9c3bc988c276387564b57ce35808a1a2c9539b20 Mon Sep 17 00:00:00 2001 From: Henry de Valence Date: Wed, 27 Mar 2024 15:31:11 -0700 Subject: [PATCH] Improve `update_position` API and position execution events. (#4121) --- .../src/gen/proto_descriptor.bin.no_lfs | Bin 95074 -> 98383 bytes .../src/component/circuit_breaker/value.rs | 2 +- .../core/component/dex/src/component/dex.rs | 3 +- .../dex/src/component/position_manager.rs | 178 +++++++++++------- .../dex/src/component/router/fill_route.rs | 18 +- .../dex/src/component/router/path.rs | 3 +- .../dex/src/component/router/tests.rs | 8 +- crates/core/component/dex/src/event.rs | 19 +- crates/core/component/dex/src/lp/position.rs | 2 +- crates/core/component/dex/src/lp/reserves.rs | 2 +- .../src/gen/penumbra.core.component.dex.v1.rs | 9 + .../penumbra.core.component.dex.v1.serde.rs | 53 ++++++ .../proto/src/gen/proto_descriptor.bin.no_lfs | Bin 376419 -> 380322 bytes .../penumbra/core/component/dex/v1/dex.proto | 6 + 14 files changed, 218 insertions(+), 85 deletions(-) diff --git a/crates/cnidarium/src/gen/proto_descriptor.bin.no_lfs b/crates/cnidarium/src/gen/proto_descriptor.bin.no_lfs index d50e1bd8818eb8b4d521b590c16703d42bad4e23..67d781308dc15349cc1633e0986b9be4d49fda1f 100644 GIT binary patch delta 23133 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 QsWnq;W;8wa^^V#931P9$$^ZZW delta 19801 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 Result<()> { - match self.get_raw(&state_key::position_by_id(id)).await? { - Some(_) => Err(anyhow::anyhow!("position id {:?} already used", id)), - None => Ok(()), - } - } - async fn best_position( &self, pair: &DirectedTradingPair, @@ -140,19 +133,37 @@ impl PositionRead for T {} #[async_trait] pub trait PositionManager: StateWrite + PositionRead { /// Close a position by id, removing it from the state. + /// + /// If the position is already closed, this is a no-op. + /// /// # Errors + /// /// Returns an error if the position does not exist. async fn close_position_by_id(&mut self, id: &position::Id) -> Result<()> { tracing::debug!(?id, "closing position, first fetch it"); - let mut position = self + let prev_state = self .position_by_id(id) .await .expect("fetching position should not fail") - .ok_or_else(|| anyhow::anyhow!("position not found"))?; + .ok_or_else(|| anyhow::anyhow!("could not find position {} to close", id))?; + + anyhow::ensure!( + matches!( + prev_state.state, + position::State::Opened | position::State::Closed, + ), + "attempted to close a position with state {:?}, expected Opened or Closed", + prev_state.state + ); + + let new_state = { + let mut new_state = prev_state.clone(); + new_state.state = position::State::Closed; + new_state + }; + + self.update_position(Some(prev_state), new_state).await?; - tracing::debug!(?id, "position found, close it"); - position.state = position::State::Closed; - self.update_position(position).await?; Ok(()) } @@ -164,18 +175,13 @@ pub trait PositionManager: StateWrite + PositionRead { } /// Close all positions that have been queued for closure. - async fn close_queued_positions(&mut self) -> () { + async fn close_queued_positions(&mut self) -> Result<()> { let to_close = self.pending_position_closures(); for id in to_close { - match self.close_position_by_id(&id).await { - Ok(()) => tracing::debug!(?id, "position closed"), - // The position was already closed, which in and of itself is not an error. - // It's possible that the position was closed by the engine, for example - // because it was a limit-order. - Err(e) => tracing::debug!(?id, "failed to close position: {}", e), - } + self.close_position_by_id(&id).await?; } self.object_delete(state_key::pending_position_closures()); + Ok(()) } /// Opens a new position, updating all necessary indexes and checking for @@ -188,7 +194,13 @@ pub trait PositionManager: StateWrite + PositionRead { } // Validate that the position ID doesn't collide - self.check_position_id_unused(&position.id()).await?; + if let Some(existing) = self.position_by_id(&position.id()).await? { + anyhow::bail!( + "attempted to open a position with ID {}, which already exists with state {:?}", + position.id(), + existing + ); + } // Credit the DEX for the inflows from this position. self.vcb_credit(position.reserves_1()).await?; @@ -196,30 +208,65 @@ pub trait PositionManager: StateWrite + PositionRead { // Finally, record the new position state. self.record_proto(event::position_open(&position)); - self.update_position(position).await?; + self.update_position(None, position).await?; Ok(()) } /// Record execution against an opened position. + /// + /// The `context` parameter records the global context of the path in which + /// the position execution happened. This may be completely different than + /// the trading pair of the position itself, and is used to link the + /// micro-scale execution (processed by this method) with the macro-scale + /// context (a swap or arbitrage). #[tracing::instrument(level = "debug", skip_all)] - async fn position_execution(&mut self, mut position: Position) -> Result<()> { + async fn position_execution( + &mut self, + mut new_state: Position, + context: DirectedTradingPair, + ) -> Result<()> { + let prev_state = self + .position_by_id(&new_state.id()) + .await? + .ok_or_else(|| anyhow::anyhow!("withdrew from unknown position {}", new_state.id()))?; + + anyhow::ensure!( + matches!(&prev_state.state, position::State::Opened), + "attempted to execute against a position with state {:?}, expected Opened", + prev_state.state + ); + anyhow::ensure!( + matches!(&new_state.state, position::State::Opened), + "supplied post-execution state {:?}, expected Opened", + prev_state.state + ); + // Handle "close-on-fill": automatically flip the position state to "closed" if // either of the reserves are zero. - if position.close_on_fill { - if position.reserves.r1 == 0u64.into() || position.reserves.r2 == 0u64.into() { + if new_state.close_on_fill { + if new_state.reserves.r1 == 0u64.into() || new_state.reserves.r2 == 0u64.into() { tracing::debug!( - id = ?position.id(), - r1 = ?position.reserves.r1, - r2 = ?position.reserves.r2, + id = ?new_state.id(), + r1 = ?new_state.reserves.r1, + r2 = ?new_state.reserves.r2, "marking position as closed due to close-on-fill" ); - position.state = position::State::Closed; + new_state.state = position::State::Closed; } } - self.record_proto(event::position_execution(&position)); - self.update_position(position).await?; + // Optimization: it's possible that the position's reserves haven't + // changed, and that we're about to do a no-op update. This can happen + // when saving a frontier, for instance, since the FillRoute code saves + // the entire frontier when it finishes. + // + // If so, skip the write, but more importantly, skip emitting an event, + // so tooling doesn't get confused about a no-op execution. + if prev_state != new_state { + self.record_proto(event::position_execution(&prev_state, &new_state, context)); + self.update_position(Some(prev_state), new_state).await?; + } Ok(()) } @@ -233,7 +280,7 @@ pub trait PositionManager: StateWrite + PositionRead { position_id: position::Id, sequence: u64, ) -> Result { - let mut metadata = self + let prev_state = self .position_by_id(&position_id) .await? .ok_or_else(|| anyhow::anyhow!("withdrew from unknown position {}", position_id))?; @@ -246,17 +293,17 @@ pub trait PositionManager: StateWrite + PositionRead { // This is just a check that sequence == current_sequence + 1, with extra logic // so that we treat "closed" as "sequence -1". if sequence == 0 { - if metadata.state != position::State::Closed { + if prev_state.state != position::State::Closed { anyhow::bail!( "attempted to withdraw position {} with state {}, expected Closed", position_id, - metadata.state + prev_state.state ); } } else { if let position::State::Withdrawn { sequence: current_sequence, - } = metadata.state + } = prev_state.state { if current_sequence + 1 != sequence { anyhow::bail!( @@ -270,34 +317,34 @@ pub trait PositionManager: StateWrite + PositionRead { anyhow::bail!( "attempted to withdraw position {} with state {}, expected Withdrawn", position_id, - metadata.state + prev_state.state ); } } // Record an event prior to updating the position state, so we have access to // the current reserves. - self.record_proto(event::position_withdraw(position_id, &metadata)); + self.record_proto(event::position_withdraw(position_id, &prev_state)); // Grab a copy of the final reserves of the position to return to the caller. - let reserves = metadata.reserves.balance(&metadata.phi.pair); + let reserves = prev_state.reserves.balance(&prev_state.phi.pair); // Debit the DEX for the outflows from this position. - // TODO: in a future PR, split current PositionManager to PositionManagerInner - // and fold this into a position open method - self.vcb_debit(metadata.reserves_1()).await?; - self.vcb_debit(metadata.reserves_2()).await?; + self.vcb_debit(prev_state.reserves_1()).await?; + self.vcb_debit(prev_state.reserves_2()).await?; // Finally, update the position. This has two steps: // - update the state with the correct sequence number; // - zero out the reserves, to prevent double-withdrawals. - metadata.state = position::State::Withdrawn { + let new_state = { + let mut new_state = prev_state.clone(); // We just checked that the supplied sequence number is incremented by 1 from prev. - sequence, + new_state.state = position::State::Withdrawn { sequence }; + new_state.reserves = Reserves::zero(); + new_state }; - metadata.reserves = Reserves::zero(); - self.update_position(metadata).await?; + self.update_position(Some(prev_state), new_state).await?; Ok(reserves) } @@ -311,36 +358,38 @@ pub(crate) trait Inner: StateWrite { /// /// This should be the SOLE ENTRYPOINT for writing positions to the state. /// All other position changes exposed by the `PositionManager` should run through here. - #[tracing::instrument(level = "debug", skip(self, position), fields(id = ?position.id()))] - async fn update_position(&mut self, position: position::Position) -> Result<()> { - let id = position.id(); - tracing::debug!(?position, "fetch position's previous state from storage"); - // We pull the position from the state unconditionally, since we will - // always need to update the position's liquidity index. - let prev = self - .position_by_id(&id) - .await - .expect("fetching position should not fail"); + #[tracing::instrument(level = "debug", skip_all, fields(id = ?new_state.id()))] + async fn update_position( + &mut self, + prev_state: Option, + new_state: Position, + ) -> Result<()> { + tracing::debug!(?prev_state, ?new_state, "updating position state"); + + let id = new_state.id(); // Clear any existing indexes of the position, since changes to the // reserves or the position state might have invalidated them. - self.deindex_position_by_price(&position); + if let Some(prev_state) = prev_state.as_ref() { + self.deindex_position_by_price(&prev_state, &id); + } // Only index the position's liquidity if it is active. - if position.state == position::State::Opened { - self.index_position_by_price(&position); + if new_state.state == position::State::Opened { + self.index_position_by_price(&new_state, &id); } // Update the available liquidity for this position's trading pair. - self.update_available_liquidity(&position, &prev).await?; + // TODO: refactor and streamline this method while implementing eviction. + self.update_available_liquidity(&new_state, &prev_state) + .await?; - self.put(state_key::position_by_id(&id), position); + self.put(state_key::position_by_id(&id), new_state); Ok(()) } - fn index_position_by_price(&mut self, position: &position::Position) { + fn index_position_by_price(&mut self, position: &position::Position, id: &position::Id) { let (pair, phi) = (position.phi.pair, &position.phi); - let id = position.id(); if position.reserves.r2 != 0u64.into() { // Index this position for trades FROM asset 1 TO asset 2, since the position has asset 2 to give out. let pair12 = DirectedTradingPair { @@ -370,8 +419,7 @@ pub(crate) trait Inner: StateWrite { } } - fn deindex_position_by_price(&mut self, position: &Position) { - let id = position.id(); + fn deindex_position_by_price(&mut self, position: &Position, id: &position::Id) { tracing::debug!("deindexing position"); let pair12 = DirectedTradingPair { start: position.phi.pair.asset_1(), diff --git a/crates/core/component/dex/src/component/router/fill_route.rs b/crates/core/component/dex/src/component/router/fill_route.rs index 1f8ea5e1a1..ac35ab746a 100644 --- a/crates/core/component/dex/src/component/router/fill_route.rs +++ b/crates/core/component/dex/src/component/router/fill_route.rs @@ -12,12 +12,10 @@ use penumbra_num::{ fixpoint::{Error, U128x128}, Amount, }; -use penumbra_proto::StateWriteProto as _; use tracing::instrument; use crate::{ component::{metrics, PositionManager, PositionRead}, - event, lp::{ position::{self, Position}, Reserves, @@ -411,12 +409,14 @@ impl Frontier { } async fn save(&mut self) -> Result<()> { + let context = DirectedTradingPair { + start: self.pairs.first().expect("pairs is nonempty").start, + end: self.pairs.last().expect("pairs is nonempty").end, + }; for position in &self.positions { - self.state.position_execution(position.clone()).await?; - - // Create an ABCI event signaling that the position was executed against self.state - .record_proto(event::position_execution(&position)); + .position_execution(position.clone(), context.clone()) + .await?; } Ok(()) } @@ -491,8 +491,12 @@ impl Frontier { // discard it, so write its updated reserves before we replace it on the // frontier. The other positions will be written out either when // they're fully consumed, or when we finish filling. + let context = DirectedTradingPair { + start: self.pairs.first().expect("pairs is nonempty").start, + end: self.pairs.last().expect("pairs is nonempty").end, + }; self.state - .position_execution(self.positions[index].clone()) + .position_execution(self.positions[index].clone(), context) .await .expect("writing to storage should not fail"); diff --git a/crates/core/component/dex/src/component/router/path.rs b/crates/core/component/dex/src/component/router/path.rs index 330bc2222f..a9fd283590 100644 --- a/crates/core/component/dex/src/component/router/path.rs +++ b/crates/core/component/dex/src/component/router/path.rs @@ -74,7 +74,8 @@ impl Path { // Deindex the position we "consumed" in this and all descendant state forks, // ensuring we don't double-count liquidity while traversing cycles. use super::super::position_manager::Inner as _; - self.state.deindex_position_by_price(&best_price_position); + self.state + .deindex_position_by_price(&best_price_position, &best_price_position.id()); // Compute the effective price of a trade in the direction self.end()=>new_end let hop_price = best_price_position diff --git a/crates/core/component/dex/src/component/router/tests.rs b/crates/core/component/dex/src/component/router/tests.rs index 6618d902ec..bd9c5f4de6 100644 --- a/crates/core/component/dex/src/component/router/tests.rs +++ b/crates/core/component/dex/src/component/router/tests.rs @@ -496,6 +496,10 @@ async fn test_multiple_similar_position() -> anyhow::Result<()> { state_tx.open_position(buy_1.clone()).await.unwrap(); state_tx.open_position(buy_2.clone()).await.unwrap(); + // We don't really care about the value, but the API requires + // that we make up a context here. + let context = pair_1.into_directed_trading_pair(); + let mut p_1 = state_tx .best_position(&pair_1.into_directed_trading_pair()) .await @@ -503,7 +507,7 @@ async fn test_multiple_similar_position() -> anyhow::Result<()> { .expect("we just posted two positions"); assert_eq!(p_1.nonce, buy_1.nonce); p_1.reserves = p_1.reserves.flip(); - state_tx.position_execution(p_1).await.unwrap(); + state_tx.position_execution(p_1, context).await.unwrap(); let mut p_2 = state_tx .best_position(&pair_1.into_directed_trading_pair()) @@ -512,7 +516,7 @@ async fn test_multiple_similar_position() -> anyhow::Result<()> { .expect("there is one position remaining"); assert_eq!(p_2.nonce, buy_2.nonce); p_2.reserves = p_2.reserves.flip(); - state_tx.position_execution(p_2).await.unwrap(); + state_tx.position_execution(p_2, context).await.unwrap(); assert!(state_tx .best_position(&pair_1.into_directed_trading_pair()) diff --git a/crates/core/component/dex/src/event.rs b/crates/core/component/dex/src/event.rs index 22832b2bf9..8aae026868 100644 --- a/crates/core/component/dex/src/event.rs +++ b/crates/core/component/dex/src/event.rs @@ -5,7 +5,7 @@ use crate::{ }, swap::Swap, swap_claim::SwapClaim, - BatchSwapOutputData, SwapExecution, + BatchSwapOutputData, DirectedTradingPair, SwapExecution, }; use penumbra_asset::asset; @@ -66,12 +66,19 @@ pub fn position_withdraw( } } -pub fn position_execution(post_execution_state: &Position) -> pb::EventPositionExecution { +pub fn position_execution( + prev_state: &Position, + new_state: &Position, + context: DirectedTradingPair, +) -> pb::EventPositionExecution { pb::EventPositionExecution { - position_id: Some(post_execution_state.id().into()), - trading_pair: Some(post_execution_state.phi.pair.into()), - reserves_1: Some(post_execution_state.reserves.r1.into()), - reserves_2: Some(post_execution_state.reserves.r2.into()), + position_id: Some(new_state.id().into()), + trading_pair: Some(new_state.phi.pair.into()), + reserves_1: Some(new_state.reserves.r1.into()), + reserves_2: Some(new_state.reserves.r2.into()), + prev_reserves_1: Some(prev_state.reserves.r1.into()), + prev_reserves_2: Some(prev_state.reserves.r2.into()), + context: Some(context.into()), } } diff --git a/crates/core/component/dex/src/lp/position.rs b/crates/core/component/dex/src/lp/position.rs index 05dabc0d74..1e2e9ebbc4 100644 --- a/crates/core/component/dex/src/lp/position.rs +++ b/crates/core/component/dex/src/lp/position.rs @@ -19,7 +19,7 @@ pub const MAX_FEE_BPS: u32 = 5000; /// Encapsulates the immutable parts of the position (phi/nonce), along /// with the mutable parts (state/reserves). -#[derive(Clone, Serialize, Deserialize)] +#[derive(Clone, Serialize, Deserialize, PartialEq, Eq)] #[serde(try_from = "pb::Position", into = "pb::Position")] pub struct Position { pub state: State, diff --git a/crates/core/component/dex/src/lp/reserves.rs b/crates/core/component/dex/src/lp/reserves.rs index 265603ea3c..215d12aa5c 100644 --- a/crates/core/component/dex/src/lp/reserves.rs +++ b/crates/core/component/dex/src/lp/reserves.rs @@ -12,7 +12,7 @@ use super::position::MAX_RESERVE_AMOUNT; /// between assets 1 and 2, without specifying what those assets are, to avoid /// duplicating data (each asset ID alone is four times the size of the /// reserves). -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct Reserves { pub r1: Amount, pub r2: Amount, diff --git a/crates/proto/src/gen/penumbra.core.component.dex.v1.rs b/crates/proto/src/gen/penumbra.core.component.dex.v1.rs index 27e6a5c183..316658f996 100644 --- a/crates/proto/src/gen/penumbra.core.component.dex.v1.rs +++ b/crates/proto/src/gen/penumbra.core.component.dex.v1.rs @@ -1399,6 +1399,15 @@ pub struct EventPositionExecution { /// The reserves of asset 2 of the position after execution. #[prost(message, optional, tag = "4")] pub reserves_2: ::core::option::Option, + /// The reserves of asset 1 of the position before execution. + #[prost(message, optional, tag = "5")] + pub prev_reserves_1: ::core::option::Option, + /// The reserves of asset 2 of the position before execution. + #[prost(message, optional, tag = "6")] + pub prev_reserves_2: ::core::option::Option, + /// Context: the end-to-end route that was being traversed during execution. + #[prost(message, optional, tag = "7")] + pub context: ::core::option::Option, } impl ::prost::Name for EventPositionExecution { const NAME: &'static str = "EventPositionExecution"; diff --git a/crates/proto/src/gen/penumbra.core.component.dex.v1.serde.rs b/crates/proto/src/gen/penumbra.core.component.dex.v1.serde.rs index 14e166d4f8..b7294296fc 100644 --- a/crates/proto/src/gen/penumbra.core.component.dex.v1.serde.rs +++ b/crates/proto/src/gen/penumbra.core.component.dex.v1.serde.rs @@ -1645,6 +1645,15 @@ impl serde::Serialize for EventPositionExecution { if self.reserves_2.is_some() { len += 1; } + if self.prev_reserves_1.is_some() { + len += 1; + } + if self.prev_reserves_2.is_some() { + len += 1; + } + if self.context.is_some() { + len += 1; + } let mut struct_ser = serializer.serialize_struct("penumbra.core.component.dex.v1.EventPositionExecution", len)?; if let Some(v) = self.position_id.as_ref() { struct_ser.serialize_field("positionId", v)?; @@ -1658,6 +1667,15 @@ impl serde::Serialize for EventPositionExecution { if let Some(v) = self.reserves_2.as_ref() { struct_ser.serialize_field("reserves2", v)?; } + if let Some(v) = self.prev_reserves_1.as_ref() { + struct_ser.serialize_field("prevReserves1", v)?; + } + if let Some(v) = self.prev_reserves_2.as_ref() { + struct_ser.serialize_field("prevReserves2", v)?; + } + if let Some(v) = self.context.as_ref() { + struct_ser.serialize_field("context", v)?; + } struct_ser.end() } } @@ -1676,6 +1694,11 @@ impl<'de> serde::Deserialize<'de> for EventPositionExecution { "reserves1", "reserves_2", "reserves2", + "prev_reserves_1", + "prevReserves1", + "prev_reserves_2", + "prevReserves2", + "context", ]; #[allow(clippy::enum_variant_names)] @@ -1684,6 +1707,9 @@ impl<'de> serde::Deserialize<'de> for EventPositionExecution { TradingPair, Reserves1, Reserves2, + PrevReserves1, + PrevReserves2, + Context, __SkipField__, } impl<'de> serde::Deserialize<'de> for GeneratedField { @@ -1710,6 +1736,9 @@ impl<'de> serde::Deserialize<'de> for EventPositionExecution { "tradingPair" | "trading_pair" => Ok(GeneratedField::TradingPair), "reserves1" | "reserves_1" => Ok(GeneratedField::Reserves1), "reserves2" | "reserves_2" => Ok(GeneratedField::Reserves2), + "prevReserves1" | "prev_reserves_1" => Ok(GeneratedField::PrevReserves1), + "prevReserves2" | "prev_reserves_2" => Ok(GeneratedField::PrevReserves2), + "context" => Ok(GeneratedField::Context), _ => Ok(GeneratedField::__SkipField__), } } @@ -1733,6 +1762,9 @@ impl<'de> serde::Deserialize<'de> for EventPositionExecution { let mut trading_pair__ = None; let mut reserves_1__ = None; let mut reserves_2__ = None; + let mut prev_reserves_1__ = None; + let mut prev_reserves_2__ = None; + let mut context__ = None; while let Some(k) = map_.next_key()? { match k { GeneratedField::PositionId => { @@ -1759,6 +1791,24 @@ impl<'de> serde::Deserialize<'de> for EventPositionExecution { } reserves_2__ = map_.next_value()?; } + GeneratedField::PrevReserves1 => { + if prev_reserves_1__.is_some() { + return Err(serde::de::Error::duplicate_field("prevReserves1")); + } + prev_reserves_1__ = map_.next_value()?; + } + GeneratedField::PrevReserves2 => { + if prev_reserves_2__.is_some() { + return Err(serde::de::Error::duplicate_field("prevReserves2")); + } + prev_reserves_2__ = map_.next_value()?; + } + GeneratedField::Context => { + if context__.is_some() { + return Err(serde::de::Error::duplicate_field("context")); + } + context__ = map_.next_value()?; + } GeneratedField::__SkipField__ => { let _ = map_.next_value::()?; } @@ -1769,6 +1819,9 @@ impl<'de> serde::Deserialize<'de> for EventPositionExecution { trading_pair: trading_pair__, reserves_1: reserves_1__, reserves_2: reserves_2__, + prev_reserves_1: prev_reserves_1__, + prev_reserves_2: prev_reserves_2__, + context: context__, }) } } diff --git a/crates/proto/src/gen/proto_descriptor.bin.no_lfs b/crates/proto/src/gen/proto_descriptor.bin.no_lfs index 45cac790c0812776b6a1b6906be3c49ead01d25d..8a26d29b046550f594d789bdc692d546f5431d8b 100644 GIT binary patch delta 24735 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