diff --git a/crates/autopilot/src/database/competition.rs b/crates/autopilot/src/database/competition.rs index 464fa9d1af..2180c4b9a3 100644 --- a/crates/autopilot/src/database/competition.rs +++ b/crates/autopilot/src/database/competition.rs @@ -149,7 +149,7 @@ impl super::Postgres { deserialize_solver_competition( row.json, row.id, - row.tx_hash.map(|hash| H256(hash.0)), + row.tx_hashes.iter().map(|hash| H256(hash.0)).collect(), ) }) .transpose() @@ -159,13 +159,13 @@ impl super::Postgres { fn deserialize_solver_competition( json: JsonValue, auction_id: model::auction::AuctionId, - transaction_hash: Option, + transaction_hashes: Vec, ) -> anyhow::Result { let common: SolverCompetitionDB = serde_json::from_value(json).context("deserialize SolverCompetitionDB")?; Ok(SolverCompetitionAPI { auction_id, - transaction_hash, + transaction_hashes, common, }) } diff --git a/crates/database/src/solver_competition.rs b/crates/database/src/solver_competition.rs index def53d0bde..8771eb6647 100644 --- a/crates/database/src/solver_competition.rs +++ b/crates/database/src/solver_competition.rs @@ -20,7 +20,8 @@ VALUES ($1, $2) pub struct LoadCompetition { pub json: JsonValue, pub id: AuctionId, - pub tx_hash: Option, + // Multiple settlements can be associated with a single competition. + pub tx_hashes: Vec, } pub async fn load_by_id( @@ -28,11 +29,12 @@ pub async fn load_by_id( id: AuctionId, ) -> Result, sqlx::Error> { const QUERY: &str = r#" -SELECT sc.json, sc.id, s.tx_hash +SELECT sc.json, sc.id, COALESCE(ARRAY_AGG(s.tx_hash) FILTER (WHERE s.tx_hash IS NOT NULL), '{}') AS tx_hashes FROM solver_competitions sc -- outer joins because the data might not have been indexed yet LEFT OUTER JOIN settlements s ON sc.id = s.auction_id WHERE sc.id = $1 +GROUP BY sc.id ;"#; sqlx::query_as(QUERY).bind(id).fetch_optional(ex).await } @@ -41,10 +43,11 @@ pub async fn load_latest_competition( ex: &mut PgConnection, ) -> Result, sqlx::Error> { const QUERY: &str = r#" -SELECT sc.json, sc.id, s.tx_hash +SELECT sc.json, sc.id, COALESCE(ARRAY_AGG(s.tx_hash) FILTER (WHERE s.tx_hash IS NOT NULL), '{}') AS tx_hashes FROM solver_competitions sc -- outer joins because the data might not have been indexed yet LEFT OUTER JOIN settlements s ON sc.id = s.auction_id +GROUP BY sc.id ORDER BY sc.id DESC LIMIT 1 ;"#; @@ -56,10 +59,17 @@ pub async fn load_by_tx_hash( tx_hash: &TransactionHash, ) -> Result, sqlx::Error> { const QUERY: &str = r#" -SELECT sc.json, sc.id, s.tx_hash +WITH competition AS ( + SELECT sc.id + FROM solver_competitions sc + JOIN settlements s ON sc.id = s.auction_id + WHERE s.tx_hash = $1 +) +SELECT sc.json, sc.id, COALESCE(ARRAY_AGG(s.tx_hash) FILTER (WHERE s.tx_hash IS NOT NULL), '{}') AS tx_hashes FROM solver_competitions sc JOIN settlements s ON sc.id = s.auction_id -WHERE s.tx_hash = $1 +WHERE sc.id = (SELECT id FROM competition) +GROUP BY sc.id ;"#; sqlx::query_as(QUERY).bind(tx_hash).fetch_optional(ex).await } @@ -70,7 +80,7 @@ mod tests { super::*, crate::{ byte_array::ByteArray, - events::{Event, EventIndex, Settlement}, + events::{EventIndex, Settlement}, }, sqlx::Connection, }; @@ -84,61 +94,76 @@ mod tests { let value = JsonValue::Bool(true); save(&mut db, 0, &value).await.unwrap(); + + // load by id works let value_ = load_by_id(&mut db, 0).await.unwrap().unwrap(); assert_eq!(value, value_.json); - assert!(value_.tx_hash.is_none()); + assert!(value_.tx_hashes.is_empty()); + // load as latest works + let value_ = load_latest_competition(&mut db).await.unwrap().unwrap(); + assert_eq!(value, value_.json); + assert!(value_.tx_hashes.is_empty()); + // load by tx doesn't work, as there is no settlement yet + assert!(load_by_tx_hash(&mut db, &ByteArray([0u8; 32])) + .await + .unwrap() + .is_none()); + // non-existent auction returns none assert!(load_by_id(&mut db, 1).await.unwrap().is_none()); - } - - #[tokio::test] - #[ignore] - async fn postgres_by_hash() { - let mut db = PgConnection::connect("postgresql://").await.unwrap(); - let mut db = db.begin().await.unwrap(); - crate::clear_DANGER_(&mut db).await.unwrap(); - - let id: i64 = 5; - let value = JsonValue::Bool(true); - let hash = ByteArray([1u8; 32]); - save(&mut db, id, &value).await.unwrap(); - - let value_by_id = load_by_id(&mut db, id).await.unwrap().unwrap(); - assert_eq!(value, value_by_id.json); - // no hash because hash columns isn't used to find it - assert!(value_by_id.tx_hash.is_none()); - - // Fails because the tx_hash stored directly in the solver_competitions table is - // no longer used to look the competition up. - assert!(load_by_tx_hash(&mut db, &hash).await.unwrap().is_none()); - - // Now insert the proper settlement event and account-nonce. - - let index = EventIndex::default(); - let event = Event::Settlement(Settlement { - solver: Default::default(), - transaction_hash: hash, - }); - crate::events::append(&mut db, &[(index, event)]) - .await - .unwrap(); - crate::settlements::update_settlement_auction( + // insert two settlement events for the same auction id + crate::events::insert_settlement( + &mut db, + &EventIndex { + block_number: 0, + log_index: 0, + }, + &Settlement { + solver: Default::default(), + transaction_hash: ByteArray([0u8; 32]), + }, + ) + .await + .unwrap(); + crate::events::insert_settlement( &mut db, - index.block_number, - index.log_index, - id, + &EventIndex { + block_number: 0, + log_index: 1, + }, + &Settlement { + solver: Default::default(), + transaction_hash: ByteArray([1u8; 32]), + }, ) .await .unwrap(); + crate::settlements::update_settlement_auction(&mut db, 0, 0, 0) + .await + .unwrap(); + crate::settlements::update_settlement_auction(&mut db, 0, 1, 0) + .await + .unwrap(); + + // load by id works, and finds two hashes + let value_ = load_by_id(&mut db, 0).await.unwrap().unwrap(); + assert!(value_.tx_hashes.len() == 2); - // Now succeeds. - let value_by_hash = load_by_tx_hash(&mut db, &hash).await.unwrap().unwrap(); - assert_eq!(value, value_by_hash.json); - assert_eq!(id, value_by_hash.id); + // load as latest works, and finds two hashes + let value_ = load_latest_competition(&mut db).await.unwrap().unwrap(); + assert!(value_.tx_hashes.len() == 2); - // By id also sees the hash now. - let value_by_id = load_by_id(&mut db, id).await.unwrap().unwrap(); - assert_eq!(hash, value_by_id.tx_hash.unwrap()); + // load by tx works, and finds two hashes, no matter which tx hash is used + let value_ = load_by_tx_hash(&mut db, &ByteArray([0u8; 32])) + .await + .unwrap() + .unwrap(); + assert!(value_.tx_hashes.len() == 2); + let value_ = load_by_tx_hash(&mut db, &ByteArray([1u8; 32])) + .await + .unwrap() + .unwrap(); + assert!(value_.tx_hashes.len() == 2); } } diff --git a/crates/model/src/solver_competition.rs b/crates/model/src/solver_competition.rs index 150f586023..d21660be4e 100644 --- a/crates/model/src/solver_competition.rs +++ b/crates/model/src/solver_competition.rs @@ -25,7 +25,7 @@ pub struct SolverCompetitionDB { pub struct SolverCompetitionAPI { #[serde(default)] pub auction_id: AuctionId, - pub transaction_hash: Option, + pub transaction_hashes: Vec, #[serde(flatten)] pub common: SolverCompetitionDB, } @@ -126,7 +126,7 @@ mod tests { "auctionId": 0, "auctionStartBlock": 13u64, "competitionSimulationBlock": 15u64, - "transactionHash": "0x1111111111111111111111111111111111111111111111111111111111111111", + "transactionHashes": ["0x1111111111111111111111111111111111111111111111111111111111111111"], "auction": { "orders": [ "0x1111111111111111111111111111111111111111111111111111111111111111\ @@ -173,7 +173,7 @@ mod tests { let orig = SolverCompetitionAPI { auction_id: 0, - transaction_hash: Some(H256([0x11; 32])), + transaction_hashes: vec![H256([0x11; 32])], common: SolverCompetitionDB { auction_start_block: 13, competition_simulation_block: 15, @@ -360,7 +360,7 @@ mod tests { } } ], - "transactionHash": "0x044499c2a830890cb0a8ecf9aec6c5621e8310092a58d369cdef726254d3d108", + "transactionHashes": ["0x044499c2a830890cb0a8ecf9aec6c5621e8310092a58d369cdef726254d3d108"], "auctionStartBlock": 15173535, "liquidityCollectedBlock": 15173535, "competitionSimulationBlock": 15173535 diff --git a/crates/orderbook/src/database/solver_competition.rs b/crates/orderbook/src/database/solver_competition.rs index cfabb1065c..00d4bb9704 100644 --- a/crates/orderbook/src/database/solver_competition.rs +++ b/crates/orderbook/src/database/solver_competition.rs @@ -14,13 +14,13 @@ use { fn deserialize_solver_competition( json: JsonValue, auction_id: AuctionId, - transaction_hash: Option, + transaction_hashes: Vec, ) -> Result { let common: SolverCompetitionDB = serde_json::from_value(json).context("deserialize SolverCompetitionDB")?; Ok(SolverCompetitionAPI { auction_id, - transaction_hash, + transaction_hashes, common, }) } @@ -45,14 +45,20 @@ impl SolverCompetitionStoring for Postgres { deserialize_solver_competition( row.json, row.id, - row.tx_hash.map(|hash| H256(hash.0)), + row.tx_hashes.iter().map(|hash| H256(hash.0)).collect(), ) }), Identifier::Transaction(hash) => { database::solver_competition::load_by_tx_hash(&mut ex, &ByteArray(hash.0)) .await .context("solver_competition::load_by_tx_hash")? - .map(|row| deserialize_solver_competition(row.json, row.id, Some(hash))) + .map(|row| { + deserialize_solver_competition( + row.json, + row.id, + row.tx_hashes.iter().map(|hash| H256(hash.0)).collect(), + ) + }) } } .ok_or(LoadSolverCompetitionError::NotFound)? @@ -74,7 +80,7 @@ impl SolverCompetitionStoring for Postgres { deserialize_solver_competition( row.json, row.id, - row.tx_hash.map(|hash| H256(hash.0)), + row.tx_hashes.iter().map(|hash| H256(hash.0)).collect(), ) }) .ok_or(LoadSolverCompetitionError::NotFound)?