From fb9ad961e7a95baeb2ecf6e84700a9bab1056b6b Mon Sep 17 00:00:00 2001 From: tee8z Date: Sun, 18 Aug 2024 16:02:26 -0400 Subject: [PATCH 1/2] d --- oracle/src/db/event_data.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/oracle/src/db/event_data.rs b/oracle/src/db/event_data.rs index a30fd76..0f22df6 100644 --- a/oracle/src/db/event_data.rs +++ b/oracle/src/db/event_data.rs @@ -598,14 +598,14 @@ impl EventData { "observation_date::TEXT", "locations", "total_allowed_entries", - "event_entries.total_entries as total_entries", + "COALESCE(event_entries.total_entries,0) as total_entries", "number_of_places_win", "number_of_values_per_entry", "attestation_signature", )) .from( "events" - .join("event_entries") + .left_join("event_entries") .on("event_entries.event_id = events.id"), ); if let Some(ids) = filter.event_ids.clone() { @@ -696,14 +696,14 @@ impl EventData { "observation_date::TEXT", "locations", "total_allowed_entries", - "total_entries", + "COALESCE(event_entries.total_entries, 0) as total_entries", "number_of_places_win", "number_of_values_per_entry", "attestation_signature", )) .from( "events" - .join("event_entries") + .left_join("event_entries") .on("event_entries.event_id = events.id"), ) .where_("attestation_signature IS NULL"); //Only filter out events that have been signed From e4ed90c2122562894290dd7f08c031fd76b20e67 Mon Sep 17 00:00:00 2001 From: tee8z Date: Sun, 18 Aug 2024 16:46:40 -0400 Subject: [PATCH 2/2] fixing issues with get events and multi outcomes --- oracle/src/db/event_data.rs | 37 ++--- oracle/src/oracle.rs | 5 +- oracle/tests/api/etl_workflow.rs | 248 +++++++++++++++++++++++++++++++ 3 files changed, 266 insertions(+), 24 deletions(-) diff --git a/oracle/src/db/event_data.rs b/oracle/src/db/event_data.rs index 0f22df6..b8c93ad 100644 --- a/oracle/src/db/event_data.rs +++ b/oracle/src/db/event_data.rs @@ -602,6 +602,7 @@ impl EventData { "number_of_places_win", "number_of_values_per_entry", "attestation_signature", + "nonce", )) .from( "events" @@ -863,6 +864,11 @@ impl CreateEventData { let possible_outcome_rankings: Vec> = possible_scores .iter() .combinations(number_of_places_win) + //Sort possible combinations in desc order + .map(|mut combos| { + combos.sort_by_key(|n| i64::MAX - *n); + combos + }) .filter(|combination| { // Check if the combination is sorted in descending order, if not filter out of possible outcomes combination.windows(2).all(|window| window[0] >= window[1]) @@ -1196,6 +1202,8 @@ pub struct EventSummary { pub weather: Vec, /// When added it means the oracle has signed that the current data is the final result pub attestation: Option, + /// Used to sign the result of the event being watched + pub nonce: Scalar, } impl EventSummary { @@ -1287,34 +1295,17 @@ impl<'a> TryFrom<&Row<'a>> for EventSummary { } }) .map_err(|e| duckdb::Error::FromSqlConversionFailure(8, Type::Any, Box::new(e)))?, - weather: row + nonce: row .get::(9) .map(|raw| { - let list_weather = match raw { - Value::List(list) => list, + let blob = match raw { + Value::Blob(val) => val, _ => vec![], }; - let mut weather_data = vec![]; - for value in list_weather.iter() { - if let Value::Struct(data) = value { - let weather: Weather = match data.try_into() { - Ok(val) => val, - Err(e) => return Err(e), - }; - weather_data.push(weather) - } - } - Ok(weather_data) + serde_json::from_slice(&blob) })? - .map_err(|e| { - duckdb::Error::DuckDBFailure( - ffi::Error { - code: ErrorCode::TypeMismatch, - extended_code: 0, - }, - Some(e.to_string()), - ) - })?, + .map_err(|e| duckdb::Error::FromSqlConversionFailure(9, Type::Any, Box::new(e)))?, + weather: vec![], }; event_summary.update_status(); Ok(event_summary) diff --git a/oracle/src/oracle.rs b/oracle/src/oracle.rs index 78281a0..6949816 100644 --- a/oracle/src/oracle.rs +++ b/oracle/src/oracle.rs @@ -10,6 +10,7 @@ use dlctix::{ musig2::secp256k1::{rand, PublicKey, SecretKey}, secp::Point, }; +use itertools::Itertools; use log::{debug, error, info, warn}; use nostr::{key::Keys, nips::nip19::ToBech32}; use pem_rfc7468::{decode_vec, encode_string}; @@ -501,10 +502,12 @@ impl Oracle { for event in events.iter_mut() { let mut entries = self.event_data.get_event_weather_entries(&event.id).await?; entries.sort_by_key(|entry| cmp::Reverse(entry.score)); + // NOTE: there may be issues here if number of unique scores isn't as large as number_of_places_win let winners: Vec = entries .iter() - .take(event.number_of_places_win as usize) .map(|entry| entry.score.unwrap_or_default()) // default means '0' was winning score + .unique() + .take(event.number_of_places_win as usize) .collect(); let winner_bytes: Vec = winners diff --git a/oracle/tests/api/etl_workflow.rs b/oracle/tests/api/etl_workflow.rs index 551844c..6869f44 100644 --- a/oracle/tests/api/etl_workflow.rs +++ b/oracle/tests/api/etl_workflow.rs @@ -282,6 +282,254 @@ async fn can_get_event_run_etl_and_see_it_signed() { assert_eq!(attested_outcome, res.attestation); } +#[tokio::test] +async fn can_get_event_run_etl_and_see_it_signed_multiple_winners() { + let mut weather_data = MockWeatherAccess::new(); + //called twice per ETL process + weather_data + .expect_forecasts_data() + .times(2) + .returning(|_, _| Ok(mock_forecast_data())); + weather_data + .expect_observation_data() + .times(2) + .returning(|_, _| Ok(mock_observation_data())); + + let test_app = spawn_app(Arc::new(weather_data)).await; + + // This makes the event window 1 day (what is used by the oracle) + let observation_date = OffsetDateTime::parse("2024-08-12T00:00:00+00:00", &Rfc3339).unwrap(); + let signing_date = OffsetDateTime::parse("2024-08-13T00:00:00+00:00", &Rfc3339).unwrap(); + + let new_event_1 = CreateEvent { + id: Uuid::now_v7(), + observation_date, + signing_date, + locations: vec![ + String::from("PFNO"), + String::from("KSAW"), + String::from("PAPG"), + String::from("KWMC"), + ], + total_allowed_entries: 4, + number_of_places_win: 2, + number_of_values_per_entry: 6, + }; + info!("above create event"); + let event = test_app.oracle.create_event(new_event_1).await.unwrap(); + let entry_1 = AddEventEntry { + id: Uuid::now_v7(), + event_id: event.id, + expected_observations: vec![ + WeatherChoices { + stations: String::from("PFNO"), + temp_low: Some(oracle::ValueOptions::Under), + temp_high: None, + wind_speed: Some(oracle::ValueOptions::Over), + }, + WeatherChoices { + stations: String::from("KSAW"), + temp_low: None, + temp_high: None, + wind_speed: Some(oracle::ValueOptions::Over), + }, + WeatherChoices { + stations: String::from("KWMC"), + temp_low: Some(oracle::ValueOptions::Par), + temp_high: Some(oracle::ValueOptions::Under), + wind_speed: Some(oracle::ValueOptions::Par), + }, + ], + }; + let entry_2 = AddEventEntry { + id: Uuid::now_v7(), + event_id: event.id, + expected_observations: vec![ + WeatherChoices { + stations: String::from("PFNO"), + temp_low: Some(oracle::ValueOptions::Par), + temp_high: None, + wind_speed: Some(oracle::ValueOptions::Par), + }, + WeatherChoices { + stations: String::from("KSAW"), + temp_low: Some(oracle::ValueOptions::Par), + temp_high: None, + wind_speed: Some(oracle::ValueOptions::Over), + }, + WeatherChoices { + stations: String::from("KWMC"), + temp_low: Some(oracle::ValueOptions::Par), + temp_high: Some(oracle::ValueOptions::Under), + wind_speed: None, + }, + ], + }; + let entry_3 = AddEventEntry { + id: Uuid::now_v7(), + event_id: event.id, + expected_observations: vec![ + WeatherChoices { + stations: String::from("PFNO"), + temp_low: Some(oracle::ValueOptions::Par), + temp_high: None, + wind_speed: Some(oracle::ValueOptions::Under), + }, + WeatherChoices { + stations: String::from("KSAW"), + temp_low: Some(oracle::ValueOptions::Over), + temp_high: None, + wind_speed: Some(oracle::ValueOptions::Over), + }, + WeatherChoices { + stations: String::from("KWMC"), + temp_low: Some(oracle::ValueOptions::Par), + temp_high: None, + wind_speed: Some(oracle::ValueOptions::Under), + }, + ], + }; + let entry_4 = AddEventEntry { + id: Uuid::now_v7(), + event_id: event.id, + expected_observations: vec![ + WeatherChoices { + stations: String::from("PFNO"), + temp_low: Some(oracle::ValueOptions::Over), + temp_high: None, + wind_speed: Some(oracle::ValueOptions::Par), + }, + WeatherChoices { + stations: String::from("KSAW"), + temp_low: None, + temp_high: Some(oracle::ValueOptions::Under), + wind_speed: Some(oracle::ValueOptions::Over), + }, + WeatherChoices { + stations: String::from("KWMC"), + temp_low: Some(oracle::ValueOptions::Par), + temp_high: None, + wind_speed: Some(oracle::ValueOptions::Under), + }, + ], + }; + test_app + .oracle + .add_event_entry(entry_1.clone()) + .await + .unwrap(); + test_app + .oracle + .add_event_entry(entry_2.clone()) + .await + .unwrap(); + test_app + .oracle + .add_event_entry(entry_3.clone()) + .await + .unwrap(); + test_app + .oracle + .add_event_entry(entry_4.clone()) + .await + .unwrap(); + + // 1) get event before etl + let request = Request::builder() + .method(Method::GET) + .uri(format!("/oracle/events/{}", event.id)) + .header(header::CONTENT_TYPE, "application/json") + .body(Body::empty()) + .unwrap(); + + let response = test_app + .app + .clone() + .oneshot(request) + .await + .expect("Failed to execute request."); + assert!(response.status().is_success()); + let body = to_bytes(response.into_body(), usize::MAX).await.unwrap(); + let res: Event = from_slice(&body).unwrap(); + assert_eq!(res.status, EventStatus::Completed); + assert!(res.attestation.is_none()); + + // 2) request etl to run + let request = Request::builder() + .method(Method::POST) + .uri(String::from("/oracle/update")) + .header(header::CONTENT_TYPE, "application/json") + .body(Body::empty()) + .unwrap(); + + let response = test_app + .app + .clone() + .oneshot(request) + .await + .expect("Failed to execute request."); + assert!(response.status().is_success()); + + // wait for etl to run in background + sleep(std::time::Duration::from_secs(2)).await; + + // 3) get event after etl + let request = Request::builder() + .method(Method::GET) + .uri(format!("/oracle/events/{}", event.id)) + .header(header::CONTENT_TYPE, "application/json") + .body(Body::empty()) + .unwrap(); + + let response = test_app + .app + .oneshot(request) + .await + .expect("Failed to execute request."); + assert!(response.status().is_success()); + let body = to_bytes(response.into_body(), usize::MAX).await.unwrap(); + let res: Event = from_slice(&body).unwrap(); + + // Verify the event was signed and status changed + assert_eq!(res.status, EventStatus::Signed); + assert!(res.attestation.is_some()); + + let mut entries = res.entries; + entries.sort_by_key(|entry| cmp::Reverse(entry.score)); + info!("entries: {:?}", entries); + //Make sure the expected entries won and calculated the correct score for each + // We expect a tie between entry_1 and entry_3 with 4 pts + let entry_1_res = entries.iter().find(|entry| entry.id == entry_1.id).unwrap(); + assert_eq!(entry_1_res.score.unwrap(), 4); + let entry_2_res = entries.iter().find(|entry| entry.id == entry_2.id).unwrap(); + assert_eq!(entry_2_res.score.unwrap(), 3); + let entry_3_res = entries.iter().find(|entry| entry.id == entry_3.id).unwrap(); + assert_eq!(entry_3_res.score.unwrap(), 4); + let entry_4_res = entries.iter().find(|entry| entry.id == entry_4.id).unwrap(); + assert_eq!(entry_4_res.score.unwrap(), 1); + + let winning_score_bytes: Vec = vec![4, 3] + .into_iter() + .flat_map(|val: i64| val.to_be_bytes()) + .collect(); + + let outcome_index = event + .event_annoucement + .outcome_messages + .iter() + .position(|outcome| *outcome == winning_score_bytes) + .unwrap(); + + let attested_outcome = res.event_annoucement.attestation_secret( + outcome_index, + test_app.oracle.raw_private_key(), + res.nonce, + ); + + // Verify the attestation matches what we calculate in the test + assert_eq!(attested_outcome, res.attestation); +} + fn mock_forecast_data() -> Vec { vec![ Forecast {