Jelajahi Sumber

refactor: remove redundant input_ys and blinded_secrets from saga table (#1457)

The saga table was storing serialized lists of input_ys and blinded_secrets,
but this data is redundant since the proof and blind_signature tables already
store these values with operation_id as a foreign key.
tsk 1 bulan lalu
induk
melakukan
e72466c5db

+ 18 - 0
crates/cdk-common/src/database/mint/mod.rs

@@ -235,6 +235,12 @@ pub trait ProofsTransaction {
         &self,
         quote_id: &QuoteId,
     ) -> Result<Vec<PublicKey>, Self::Err>;
+
+    /// Get proof ys by operation id
+    async fn get_proof_ys_by_operation_id(
+        &self,
+        operation_id: &uuid::Uuid,
+    ) -> Result<Vec<PublicKey>, Self::Err>;
 }
 
 /// Mint Proof Database trait
@@ -261,6 +267,12 @@ pub trait ProofsDatabase {
 
     /// Get total proofs redeemed by keyset id
     async fn get_total_redeemed(&self) -> Result<HashMap<Id, Amount>, Self::Err>;
+
+    /// Get proof ys by operation id
+    async fn get_proof_ys_by_operation_id(
+        &self,
+        operation_id: &uuid::Uuid,
+    ) -> Result<Vec<PublicKey>, Self::Err>;
 }
 
 #[async_trait]
@@ -310,6 +322,12 @@ pub trait SignaturesDatabase {
 
     /// Get total amount issued by keyset id
     async fn get_total_issued(&self) -> Result<HashMap<Id, Amount>, Self::Err>;
+
+    /// Get blinded secrets (B values) by operation id
+    async fn get_blinded_secrets_by_operation_id(
+        &self,
+        operation_id: &uuid::Uuid,
+    ) -> Result<Vec<PublicKey>, Self::Err>;
 }
 
 #[async_trait]

+ 0 - 42
crates/cdk-common/src/database/mint/test/saga.rs

@@ -1,7 +1,5 @@
 //! Saga database tests
 
-use cashu::SecretKey;
-
 use crate::database::mint::{Database, Error};
 use crate::mint::{MeltSagaState, OperationKind, Saga, SagaStateEnum, SwapSagaState};
 
@@ -15,14 +13,6 @@ where
         operation_id,
         operation_kind: OperationKind::Swap,
         state: SagaStateEnum::Swap(SwapSagaState::SetupComplete),
-        blinded_secrets: vec![
-            SecretKey::generate().public_key(),
-            SecretKey::generate().public_key(),
-        ],
-        input_ys: vec![
-            SecretKey::generate().public_key(),
-            SecretKey::generate().public_key(),
-        ],
         quote_id: None,
         created_at: 1234567890,
         updated_at: 1234567890,
@@ -41,8 +31,6 @@ where
     assert_eq!(retrieved.operation_id, saga.operation_id);
     assert_eq!(retrieved.operation_kind, saga.operation_kind);
     assert_eq!(retrieved.state, saga.state);
-    assert_eq!(retrieved.blinded_secrets, saga.blinded_secrets);
-    assert_eq!(retrieved.input_ys, saga.input_ys);
     assert_eq!(retrieved.quote_id, saga.quote_id);
     tx.commit().await.unwrap();
 }
@@ -57,8 +45,6 @@ where
         operation_id,
         operation_kind: OperationKind::Swap,
         state: SagaStateEnum::Swap(SwapSagaState::SetupComplete),
-        blinded_secrets: vec![SecretKey::generate().public_key()],
-        input_ys: vec![SecretKey::generate().public_key()],
         quote_id: None,
         created_at: 1234567890,
         updated_at: 1234567890,
@@ -86,8 +72,6 @@ where
         operation_id,
         operation_kind: OperationKind::Swap,
         state: SagaStateEnum::Swap(SwapSagaState::SetupComplete),
-        blinded_secrets: vec![SecretKey::generate().public_key()],
-        input_ys: vec![SecretKey::generate().public_key()],
         quote_id: None,
         created_at: 1234567890,
         updated_at: 1234567890,
@@ -125,8 +109,6 @@ where
         operation_id,
         operation_kind: OperationKind::Swap,
         state: SagaStateEnum::Swap(SwapSagaState::SetupComplete),
-        blinded_secrets: vec![SecretKey::generate().public_key()],
-        input_ys: vec![SecretKey::generate().public_key()],
         quote_id: None,
         created_at: 1234567890,
         updated_at: 1234567890,
@@ -164,8 +146,6 @@ where
         operation_id: uuid::Uuid::new_v4(),
         operation_kind: OperationKind::Swap,
         state: SagaStateEnum::Swap(SwapSagaState::SetupComplete),
-        blinded_secrets: vec![SecretKey::generate().public_key()],
-        input_ys: vec![SecretKey::generate().public_key()],
         quote_id: None,
         created_at: 1234567890,
         updated_at: 1234567890,
@@ -175,8 +155,6 @@ where
         operation_id: uuid::Uuid::new_v4(),
         operation_kind: OperationKind::Swap,
         state: SagaStateEnum::Swap(SwapSagaState::Signed),
-        blinded_secrets: vec![SecretKey::generate().public_key()],
-        input_ys: vec![SecretKey::generate().public_key()],
         quote_id: None,
         created_at: 1234567891,
         updated_at: 1234567891,
@@ -187,8 +165,6 @@ where
         operation_id: uuid::Uuid::new_v4(),
         operation_kind: OperationKind::Melt,
         state: SagaStateEnum::Melt(MeltSagaState::SetupComplete),
-        blinded_secrets: vec![SecretKey::generate().public_key()],
-        input_ys: vec![SecretKey::generate().public_key()],
         quote_id: Some("test_quote_id".to_string()),
         created_at: 1234567892,
         updated_at: 1234567892,
@@ -227,8 +203,6 @@ where
         operation_id: uuid::Uuid::new_v4(),
         operation_kind: OperationKind::Melt,
         state: SagaStateEnum::Melt(MeltSagaState::SetupComplete),
-        blinded_secrets: vec![SecretKey::generate().public_key()],
-        input_ys: vec![SecretKey::generate().public_key()],
         quote_id: Some("melt_quote_1".to_string()),
         created_at: 1234567890,
         updated_at: 1234567890,
@@ -238,8 +212,6 @@ where
         operation_id: uuid::Uuid::new_v4(),
         operation_kind: OperationKind::Melt,
         state: SagaStateEnum::Melt(MeltSagaState::PaymentAttempted),
-        blinded_secrets: vec![SecretKey::generate().public_key()],
-        input_ys: vec![SecretKey::generate().public_key()],
         quote_id: Some("melt_quote_2".to_string()),
         created_at: 1234567891,
         updated_at: 1234567891,
@@ -250,8 +222,6 @@ where
         operation_id: uuid::Uuid::new_v4(),
         operation_kind: OperationKind::Swap,
         state: SagaStateEnum::Swap(SwapSagaState::SetupComplete),
-        blinded_secrets: vec![SecretKey::generate().public_key()],
-        input_ys: vec![SecretKey::generate().public_key()],
         quote_id: None,
         created_at: 1234567892,
         updated_at: 1234567892,
@@ -344,8 +314,6 @@ where
         operation_id,
         operation_kind: OperationKind::Melt,
         state: SagaStateEnum::Melt(MeltSagaState::SetupComplete),
-        blinded_secrets: vec![SecretKey::generate().public_key()],
-        input_ys: vec![SecretKey::generate().public_key()],
         quote_id: Some(quote_id.to_string()),
         created_at: 1234567890,
         updated_at: 1234567890,
@@ -373,8 +341,6 @@ where
         operation_id,
         operation_kind: OperationKind::Swap,
         state: SagaStateEnum::Swap(SwapSagaState::SetupComplete),
-        blinded_secrets: vec![SecretKey::generate().public_key()],
-        input_ys: vec![SecretKey::generate().public_key()],
         quote_id: None,
         created_at: 1234567890,
         updated_at: 1234567890,
@@ -402,8 +368,6 @@ where
             operation_id: uuid::Uuid::new_v4(),
             operation_kind: OperationKind::Swap,
             state: SagaStateEnum::Swap(SwapSagaState::SetupComplete),
-            blinded_secrets: vec![SecretKey::generate().public_key()],
-            input_ys: vec![SecretKey::generate().public_key()],
             quote_id: None,
             created_at: 1234567890,
             updated_at: 1234567890,
@@ -412,8 +376,6 @@ where
             operation_id: uuid::Uuid::new_v4(),
             operation_kind: OperationKind::Swap,
             state: SagaStateEnum::Swap(SwapSagaState::Signed),
-            blinded_secrets: vec![SecretKey::generate().public_key()],
-            input_ys: vec![SecretKey::generate().public_key()],
             quote_id: None,
             created_at: 1234567891,
             updated_at: 1234567891,
@@ -422,8 +384,6 @@ where
             operation_id: uuid::Uuid::new_v4(),
             operation_kind: OperationKind::Melt,
             state: SagaStateEnum::Melt(MeltSagaState::SetupComplete),
-            blinded_secrets: vec![SecretKey::generate().public_key()],
-            input_ys: vec![SecretKey::generate().public_key()],
             quote_id: Some("quote1".to_string()),
             created_at: 1234567892,
             updated_at: 1234567892,
@@ -432,8 +392,6 @@ where
             operation_id: uuid::Uuid::new_v4(),
             operation_kind: OperationKind::Melt,
             state: SagaStateEnum::Melt(MeltSagaState::PaymentAttempted),
-            blinded_secrets: vec![SecretKey::generate().public_key()],
-            input_ys: vec![SecretKey::generate().public_key()],
             quote_id: Some("quote2".to_string()),
             created_at: 1234567893,
             updated_at: 1234567893,

+ 2 - 21
crates/cdk-common/src/mint.rs

@@ -162,10 +162,6 @@ pub struct Saga {
     pub operation_kind: OperationKind,
     /// Current saga state (operation-specific)
     pub state: SagaStateEnum,
-    /// Blinded secrets (B values) from output blinded messages
-    pub blinded_secrets: Vec<PublicKey>,
-    /// Y values (public keys) from input proofs
-    pub input_ys: Vec<PublicKey>,
     /// Quote ID for melt operations (used for payment status lookup during recovery)
     /// None for swap operations
     pub quote_id: Option<String>,
@@ -177,19 +173,12 @@ pub struct Saga {
 
 impl Saga {
     /// Create new swap saga
-    pub fn new_swap(
-        operation_id: Uuid,
-        state: SwapSagaState,
-        blinded_secrets: Vec<PublicKey>,
-        input_ys: Vec<PublicKey>,
-    ) -> Self {
+    pub fn new_swap(operation_id: Uuid, state: SwapSagaState) -> Self {
         let now = unix_time();
         Self {
             operation_id,
             operation_kind: OperationKind::Swap,
             state: SagaStateEnum::Swap(state),
-            blinded_secrets,
-            input_ys,
             quote_id: None,
             created_at: now,
             updated_at: now,
@@ -203,20 +192,12 @@ impl Saga {
     }
 
     /// Create new melt saga
-    pub fn new_melt(
-        operation_id: Uuid,
-        state: MeltSagaState,
-        input_ys: Vec<PublicKey>,
-        blinded_secrets: Vec<PublicKey>,
-        quote_id: String,
-    ) -> Self {
+    pub fn new_melt(operation_id: Uuid, state: MeltSagaState, quote_id: String) -> Self {
         let now = unix_time();
         Self {
             operation_id,
             operation_kind: OperationKind::Melt,
             state: SagaStateEnum::Melt(state),
-            blinded_secrets,
-            input_ys,
             quote_id: Some(quote_id),
             created_at: now,
             updated_at: now,

+ 5 - 0
crates/cdk-sql-common/src/mint/migrations/postgres/20251223000000_remove_saga_redundant_columns.sql

@@ -0,0 +1,5 @@
+-- Remove blinded_secrets and input_ys columns from saga_state table
+-- These values can be looked up from proof and blind_signature tables using operation_id
+
+ALTER TABLE saga_state DROP COLUMN IF EXISTS blinded_secrets;
+ALTER TABLE saga_state DROP COLUMN IF EXISTS input_ys;

+ 29 - 0
crates/cdk-sql-common/src/mint/migrations/sqlite/20251223000000_remove_saga_redundant_columns.sql

@@ -0,0 +1,29 @@
+-- Remove blinded_secrets and input_ys columns from saga_state table
+-- These values can be looked up from proof and blind_signature tables using operation_id
+
+-- SQLite doesn't support DROP COLUMN directly, so we need to recreate the table
+
+-- Step 1: Create new table without the redundant columns
+CREATE TABLE IF NOT EXISTS saga_state_new (
+    operation_id TEXT PRIMARY KEY,
+    operation_kind TEXT NOT NULL,
+    state TEXT NOT NULL,
+    quote_id TEXT,
+    created_at INTEGER NOT NULL,
+    updated_at INTEGER NOT NULL
+);
+
+-- Step 2: Copy data from old table to new table
+INSERT INTO saga_state_new (operation_id, operation_kind, state, quote_id, created_at, updated_at)
+SELECT operation_id, operation_kind, state, quote_id, created_at, updated_at
+FROM saga_state;
+
+-- Step 3: Drop old table
+DROP TABLE saga_state;
+
+-- Step 4: Rename new table to original name
+ALTER TABLE saga_state_new RENAME TO saga_state;
+
+-- Step 5: Recreate indexes
+CREATE INDEX IF NOT EXISTS idx_saga_state_operation_kind ON saga_state(operation_kind);
+CREATE INDEX IF NOT EXISTS idx_saga_state_quote_id ON saga_state(quote_id);

+ 88 - 26
crates/cdk-sql-common/src/mint/mod.rs

@@ -325,6 +325,34 @@ where
         .collect::<Result<Vec<Proof>, _>>()?
         .ys()?)
     }
+
+    async fn get_proof_ys_by_operation_id(
+        &self,
+        operation_id: &uuid::Uuid,
+    ) -> Result<Vec<PublicKey>, Self::Err> {
+        Ok(query(
+            r#"
+            SELECT
+                y
+            FROM
+                proof
+            WHERE
+                operation_id = :operation_id
+            "#,
+        )?
+        .bind("operation_id", operation_id.to_string())
+        .fetch_all(&self.inner)
+        .await?
+        .into_iter()
+        .map(|row| -> Result<PublicKey, Error> {
+            Ok(column_as_string!(
+                &row[0],
+                PublicKey::from_hex,
+                PublicKey::from_slice
+            ))
+        })
+        .collect::<Result<Vec<_>, _>>()?)
+    }
 }
 
 #[async_trait]
@@ -1658,6 +1686,35 @@ where
         .map(sql_row_to_hashmap_amount)
         .collect()
     }
+
+    async fn get_proof_ys_by_operation_id(
+        &self,
+        operation_id: &uuid::Uuid,
+    ) -> Result<Vec<PublicKey>, Self::Err> {
+        let conn = self.pool.get().map_err(|e| Error::Database(Box::new(e)))?;
+        query(
+            r#"
+            SELECT
+                y
+            FROM
+                proof
+            WHERE
+                operation_id = :operation_id
+            "#,
+        )?
+        .bind("operation_id", operation_id.to_string())
+        .fetch_all(&*conn)
+        .await?
+        .into_iter()
+        .map(|row| -> Result<PublicKey, Error> {
+            Ok(column_as_string!(
+                &row[0],
+                PublicKey::from_hex,
+                PublicKey::from_slice
+            ))
+        })
+        .collect::<Result<Vec<_>, _>>()
+    }
 }
 
 #[async_trait]
@@ -1992,6 +2049,35 @@ where
         .map(sql_row_to_hashmap_amount)
         .collect()
     }
+
+    async fn get_blinded_secrets_by_operation_id(
+        &self,
+        operation_id: &uuid::Uuid,
+    ) -> Result<Vec<PublicKey>, Self::Err> {
+        let conn = self.pool.get().map_err(|e| Error::Database(Box::new(e)))?;
+        query(
+            r#"
+            SELECT
+                blinded_message
+            FROM
+                blind_signature
+            WHERE
+                operation_id = :operation_id
+            "#,
+        )?
+        .bind("operation_id", operation_id.to_string())
+        .fetch_all(&*conn)
+        .await?
+        .into_iter()
+        .map(|row| -> Result<PublicKey, Error> {
+            Ok(column_as_string!(
+                &row[0],
+                PublicKey::from_hex,
+                PublicKey::from_slice
+            ))
+        })
+        .collect::<Result<Vec<_>, _>>()
+    }
 }
 
 #[async_trait]
@@ -2115,8 +2201,6 @@ where
                 operation_id,
                 operation_kind,
                 state,
-                blinded_secrets,
-                input_ys,
                 quote_id,
                 created_at,
                 updated_at
@@ -2137,25 +2221,17 @@ where
     async fn add_saga(&mut self, saga: &mint::Saga) -> Result<(), Self::Err> {
         let current_time = unix_time();
 
-        let blinded_secrets_json = serde_json::to_string(&saga.blinded_secrets)
-            .map_err(|e| Error::Internal(format!("Failed to serialize blinded_secrets: {e}")))?;
-
-        let input_ys_json = serde_json::to_string(&saga.input_ys)
-            .map_err(|e| Error::Internal(format!("Failed to serialize input_ys: {e}")))?;
-
         query(
             r#"
             INSERT INTO saga_state
-            (operation_id, operation_kind, state, blinded_secrets, input_ys, quote_id, created_at, updated_at)
+            (operation_id, operation_kind, state, quote_id, created_at, updated_at)
             VALUES
-            (:operation_id, :operation_kind, :state, :blinded_secrets, :input_ys, :quote_id, :created_at, :updated_at)
+            (:operation_id, :operation_kind, :state, :quote_id, :created_at, :updated_at)
             "#,
         )?
         .bind("operation_id", saga.operation_id.to_string())
         .bind("operation_kind", saga.operation_kind.to_string())
         .bind("state", saga.state.state())
-        .bind("blinded_secrets", blinded_secrets_json)
-        .bind("input_ys", input_ys_json)
         .bind("quote_id", saga.quote_id.as_deref())
         .bind("created_at", saga.created_at as i64)
         .bind("updated_at", current_time as i64)
@@ -2221,8 +2297,6 @@ where
                 operation_id,
                 operation_kind,
                 state,
-                blinded_secrets,
-                input_ys,
                 quote_id,
                 created_at,
                 updated_at
@@ -2668,8 +2742,6 @@ fn sql_row_to_saga(row: Vec<Column>) -> Result<mint::Saga, Error> {
             operation_id,
             operation_kind,
             state,
-            blinded_secrets,
-            input_ys,
             quote_id,
             created_at,
             updated_at
@@ -2688,14 +2760,6 @@ fn sql_row_to_saga(row: Vec<Column>) -> Result<mint::Saga, Error> {
     let state = mint::SagaStateEnum::new(operation_kind, &state_str)
         .map_err(|e| Error::Internal(format!("Invalid saga state: {e}")))?;
 
-    let blinded_secrets_str = column_as_string!(&blinded_secrets);
-    let blinded_secrets: Vec<PublicKey> = serde_json::from_str(&blinded_secrets_str)
-        .map_err(|e| Error::Internal(format!("Failed to deserialize blinded_secrets: {e}")))?;
-
-    let input_ys_str = column_as_string!(&input_ys);
-    let input_ys: Vec<PublicKey> = serde_json::from_str(&input_ys_str)
-        .map_err(|e| Error::Internal(format!("Failed to deserialize input_ys: {e}")))?;
-
     let quote_id = match &quote_id {
         Column::Text(s) => {
             if s.is_empty() {
@@ -2715,8 +2779,6 @@ fn sql_row_to_saga(row: Vec<Column>) -> Result<mint::Saga, Error> {
         operation_id,
         operation_kind,
         state,
-        blinded_secrets,
-        input_ys,
         quote_id,
         created_at,
         updated_at,

+ 0 - 2
crates/cdk/src/mint/melt/melt_saga/mod.rs

@@ -378,8 +378,6 @@ impl MeltSaga<Initial> {
         let saga = Saga::new_melt(
             self.operation_id,
             MeltSagaState::SetupComplete,
-            input_ys.clone(),
-            blinded_secrets.clone(),
             quote.id.to_string(),
         );
 

+ 33 - 13
crates/cdk/src/mint/melt/melt_saga/tests.rs

@@ -95,16 +95,21 @@ async fn test_saga_state_persistence_after_setup() {
         _ => panic!("Expected Melt saga state"),
     }
 
-    // STEP 6: Verify input_ys are stored
+    // STEP 6: Verify input_ys can be looked up by operation_id
     let input_ys = proofs.ys().unwrap();
+    let stored_input_ys = mint
+        .localstore
+        .get_proof_ys_by_operation_id(&persisted_saga.operation_id)
+        .await
+        .unwrap();
     assert_eq!(
-        persisted_saga.input_ys.len(),
+        stored_input_ys.len(),
         input_ys.len(),
         "Should store all input Ys"
     );
     for y in &input_ys {
         assert!(
-            persisted_saga.input_ys.contains(y),
+            stored_input_ys.contains(y),
             "Input Y should be stored: {:?}",
             y
         );
@@ -124,10 +129,15 @@ async fn test_saga_state_persistence_after_setup() {
         "Timestamps should match for new saga"
     );
 
-    // STEP 8: Verify blinded_secrets is empty (not used for melt)
+    // STEP 8: Verify blinded_secrets lookup returns empty (not used for melt without change)
+    let stored_blinded_secrets = mint
+        .localstore
+        .get_blinded_secrets_by_operation_id(&persisted_saga.operation_id)
+        .await
+        .unwrap();
     assert!(
-        persisted_saga.blinded_secrets.is_empty(),
-        "Melt saga should not store blinded_secrets"
+        stored_blinded_secrets.is_empty(),
+        "Melt saga without change should have no blinded_secrets"
     );
 
     // SUCCESS: Saga persisted correctly!
@@ -1213,17 +1223,22 @@ async fn test_saga_content_validation() {
         _ => panic!("Expected Melt saga state, got {:?}", persisted_saga.state),
     }
 
-    // STEP 7: Verify input_ys are stored correctly
+    // STEP 7: Verify input_ys can be looked up by operation_id
+    let stored_input_ys = mint
+        .localstore
+        .get_proof_ys_by_operation_id(&persisted_saga.operation_id)
+        .await
+        .unwrap();
     assert_eq!(
-        persisted_saga.input_ys.len(),
+        stored_input_ys.len(),
         input_ys.len(),
         "Should store all input Ys"
     );
 
-    // Verify each Y is present and in correct order
+    // Verify each Y is present
     for (i, expected_y) in input_ys.iter().enumerate() {
         assert!(
-            persisted_saga.input_ys.contains(expected_y),
+            stored_input_ys.contains(expected_y),
             "Input Y at index {} should be stored: {:?}",
             i,
             expected_y
@@ -1261,10 +1276,15 @@ async fn test_saga_content_validation() {
         "Timestamps should match for newly created saga"
     );
 
-    // STEP 9: Verify blinded_secrets is empty (not used for melt)
+    // STEP 9: Verify blinded_secrets lookup returns empty (not used for melt without change)
+    let stored_blinded_secrets = mint
+        .localstore
+        .get_blinded_secrets_by_operation_id(&persisted_saga.operation_id)
+        .await
+        .unwrap();
     assert!(
-        persisted_saga.blinded_secrets.is_empty(),
-        "Melt saga should not use blinded_secrets field"
+        stored_blinded_secrets.is_empty(),
+        "Melt saga without change should have no blinded_secrets"
     );
 
     // SUCCESS: All saga content validated!

+ 31 - 13
crates/cdk/src/mint/start_up_check.rs

@@ -126,11 +126,21 @@ impl Mint {
                 saga.updated_at
             );
 
+            // Look up input_ys and blinded_secrets from the proof and blind_signature tables
+            let input_ys = self
+                .localstore
+                .get_proof_ys_by_operation_id(&saga.operation_id)
+                .await?;
+            let blinded_secrets = self
+                .localstore
+                .get_blinded_secrets_by_operation_id(&saga.operation_id)
+                .await?;
+
             // Use the same compensation logic as in-process failures
             // Saga deletion is included in the compensation transaction
             let compensation = RemoveSwapSetup {
-                blinded_secrets: saga.blinded_secrets.clone(),
-                input_ys: saga.input_ys.clone(),
+                blinded_secrets,
+                input_ys,
                 operation_id: saga.operation_id,
             };
 
@@ -200,6 +210,16 @@ impl Mint {
                 saga.updated_at
             );
 
+            // Look up input_ys and blinded_secrets from the proof and blind_signature tables
+            let input_ys = self
+                .localstore
+                .get_proof_ys_by_operation_id(&saga.operation_id)
+                .await?;
+            let blinded_secrets = self
+                .localstore
+                .get_blinded_secrets_by_operation_id(&saga.operation_id)
+                .await?;
+
             // Get quote_id from saga (new field added for efficient lookup)
             let quote_id = match saga.quote_id {
                 Some(ref qid) => qid.clone(),
@@ -228,9 +248,9 @@ impl Mint {
                         let proof_ys = tx.get_proof_ys_by_quote_id(&quote.id).await?;
                         tx.rollback().await?;
 
-                        if !saga.input_ys.is_empty()
+                        if !input_ys.is_empty()
                             && !proof_ys.is_empty()
-                            && saga.input_ys.iter().any(|y| proof_ys.contains(y))
+                            && input_ys.iter().any(|y| proof_ys.contains(y))
                         {
                             quote_id_found = Some(quote.id.clone());
                             break;
@@ -515,20 +535,18 @@ impl Mint {
 
             // Compensate if needed
             if should_compensate {
-                // Use saga data directly for compensation (like swap does)
                 tracing::info!(
                     "Compensating melt saga {} (removing {} proofs, {} change outputs)",
                     saga.operation_id,
-                    saga.input_ys.len(),
-                    saga.blinded_secrets.len()
+                    input_ys.len(),
+                    blinded_secrets.len()
                 );
 
-                // Compensate using saga data only - don't rely on quote state
                 let mut tx = self.localstore.begin_transaction().await?;
 
                 // Remove blinded messages (change outputs)
-                if !saga.blinded_secrets.is_empty() {
-                    if let Err(e) = tx.delete_blinded_messages(&saga.blinded_secrets).await {
+                if !blinded_secrets.is_empty() {
+                    if let Err(e) = tx.delete_blinded_messages(&blinded_secrets).await {
                         tracing::error!(
                             "Failed to delete blinded messages for saga {}: {}",
                             saga.operation_id,
@@ -539,9 +557,9 @@ impl Mint {
                     }
                 }
 
-                // Remove proofs (inputs) - use None for quote_id like swap does
-                if !saga.input_ys.is_empty() {
-                    match tx.remove_proofs(&saga.input_ys, None).await {
+                // Remove proofs (inputs)
+                if !input_ys.is_empty() {
+                    match tx.remove_proofs(&input_ys, None).await {
                         Ok(()) => {}
                         Err(DatabaseError::AttemptRemoveSpentProof) => {
                             // Proofs are already spent or missing - this is okay for compensation.

+ 1 - 6
crates/cdk/src/mint/swap/swap_saga/mod.rs

@@ -253,12 +253,7 @@ impl<'a> SwapSaga<'a, Initial> {
             .collect();
 
         // Persist saga state for crash recovery (atomic with TX1)
-        let saga = Saga::new_swap(
-            self.operation_id,
-            SwapSagaState::SetupComplete,
-            blinded_secrets.clone(),
-            ys.clone(),
-        );
+        let saga = Saga::new_swap(self.operation_id, SwapSagaState::SetupComplete);
 
         if let Err(err) = tx.add_saga(&saga).await {
             tx.rollback().await?;

+ 33 - 18
crates/cdk/src/mint/swap/swap_saga/tests.rs

@@ -1208,24 +1208,34 @@ async fn test_saga_state_persistence_after_setup() {
     // Verify operation_id matches
     assert_eq!(saga.operation_id, *operation_id);
 
-    // Verify blinded_secrets are stored correctly
+    // Verify blinded_secrets can be looked up by operation_id
     let expected_blinded_secrets: Vec<_> = output_blinded_messages
         .iter()
         .map(|bm| bm.blinded_secret)
         .collect();
-    assert_eq!(saga.blinded_secrets.len(), expected_blinded_secrets.len());
+    let stored_blinded_secrets = mint
+        .localstore()
+        .get_blinded_secrets_by_operation_id(&saga.operation_id)
+        .await
+        .unwrap();
+    assert_eq!(stored_blinded_secrets.len(), expected_blinded_secrets.len());
     for bs in &expected_blinded_secrets {
         assert!(
-            saga.blinded_secrets.contains(bs),
-            "Blinded secret should be in saga"
+            stored_blinded_secrets.contains(bs),
+            "Blinded secret should be stored"
         );
     }
 
-    // Verify input_ys are stored correctly
+    // Verify input_ys can be looked up by operation_id
     let expected_ys = input_proofs.ys().unwrap();
-    assert_eq!(saga.input_ys.len(), expected_ys.len());
+    let stored_input_ys = mint
+        .localstore()
+        .get_proof_ys_by_operation_id(&saga.operation_id)
+        .await
+        .unwrap();
+    assert_eq!(stored_input_ys.len(), expected_ys.len());
     for y in &expected_ys {
-        assert!(saga.input_ys.contains(y), "Input Y should be in saga");
+        assert!(stored_input_ys.contains(y), "Input Y should be stored");
     }
 }
 
@@ -1471,16 +1481,26 @@ async fn test_saga_content_validation() {
         SagaStateEnum::Swap(SwapSagaState::SetupComplete)
     );
 
-    // Validate blinded secrets
-    assert_eq!(saga.blinded_secrets.len(), expected_blinded_secrets.len());
+    // Validate blinded secrets can be looked up by operation_id
+    let stored_blinded_secrets = mint
+        .localstore()
+        .get_blinded_secrets_by_operation_id(&saga.operation_id)
+        .await
+        .unwrap();
+    assert_eq!(stored_blinded_secrets.len(), expected_blinded_secrets.len());
     for bs in &expected_blinded_secrets {
-        assert!(saga.blinded_secrets.contains(bs));
+        assert!(stored_blinded_secrets.contains(bs));
     }
 
-    // Validate input Ys
-    assert_eq!(saga.input_ys.len(), expected_ys.len());
+    // Validate input Ys can be looked up by operation_id
+    let stored_input_ys = mint
+        .localstore()
+        .get_proof_ys_by_operation_id(&saga.operation_id)
+        .await
+        .unwrap();
+    assert_eq!(stored_input_ys.len(), expected_ys.len());
     for y in &expected_ys {
-        assert!(saga.input_ys.contains(y));
+        assert!(stored_input_ys.contains(y));
     }
 
     // Validate timestamps
@@ -1578,11 +1598,6 @@ async fn test_saga_state_updates_persisted() {
 
     // Verify other fields unchanged
     assert_eq!(state_after_sign.operation_id, operation_id);
-    assert_eq!(
-        state_after_sign.blinded_secrets,
-        state_after_setup.blinded_secrets
-    );
-    assert_eq!(state_after_sign.input_ys, state_after_setup.input_ys);
     assert_eq!(state_after_sign.created_at, initial_created_at);
 
     // updated_at might not change since state wasn't updated

+ 1 - 2
crates/cdk/src/test_helpers/mint.rs

@@ -1,6 +1,7 @@
 #![cfg(test)]
 //! Test helpers for creating test mints and related utilities
 
+use std::cell::RefCell;
 use std::collections::{HashMap, HashSet};
 use std::str::FromStr;
 use std::sync::Arc;
@@ -20,8 +21,6 @@ use crate::mint::{Mint, MintBuilder, MintMeltLimits};
 use crate::types::{FeeReserve, QuoteTTL};
 use crate::Error;
 
-use std::cell::RefCell;
-
 thread_local! {
     /// Thread-local storage for test failure flags.
     /// Using thread-local instead of env vars prevents race conditions