@@ -185,9 +185,48 @@ impl StateUpdate {
185185 // Track which account hashes we're keeping for filtering account_transactions later
186186 let mut kept_account_hashes = HashSet :: new ( ) ;
187187
188- // Add input (spent) account hashes - these don't have tree info but should be kept
189- // for account_transactions tracking
190- kept_account_hashes. extend ( self . in_accounts . iter ( ) . cloned ( ) ) ;
188+ // Only keep in_accounts whose hashes exist in the accounts table.
189+ // Input accounts from unknown trees were never persisted as outputs,
190+ // so referencing them in account_transactions would violate the FK constraint.
191+ if !self . in_accounts . is_empty ( ) {
192+ let hash_bytes: Vec < Vec < u8 > > = self . in_accounts . iter ( ) . map ( |h| h. to_vec ( ) ) . collect ( ) ;
193+ let placeholders: Vec < String > =
194+ ( 1 ..=hash_bytes. len ( ) ) . map ( |i| format ! ( "${}" , i) ) . collect ( ) ;
195+ let sql = format ! (
196+ "SELECT hash FROM accounts WHERE hash IN ({})" ,
197+ placeholders. join( ", " )
198+ ) ;
199+ let values: Vec < sea_orm:: Value > = hash_bytes
200+ . iter ( )
201+ . map ( |b| sea_orm:: Value :: Bytes ( Some ( Box :: new ( b. clone ( ) ) ) ) )
202+ . collect ( ) ;
203+ let stmt = sea_orm:: Statement :: from_sql_and_values (
204+ sea_orm:: DatabaseBackend :: Postgres ,
205+ & sql,
206+ values,
207+ ) ;
208+ let rows = txn. query_all ( stmt) . await . map_err ( |e| {
209+ PhotonApiError :: UnexpectedError ( format ! (
210+ "Failed to query existing input accounts: {}" ,
211+ e
212+ ) )
213+ } ) ?;
214+ let existing_hashes: HashSet < Hash > = rows
215+ . iter ( )
216+ . filter_map ( |row| {
217+ let bytes: Vec < u8 > = row. try_get ( "" , "hash" ) . ok ( ) ?;
218+ Hash :: try_from ( bytes) . ok ( )
219+ } )
220+ . collect ( ) ;
221+ let filtered_count = self . in_accounts . len ( ) - existing_hashes. len ( ) ;
222+ if filtered_count > 0 {
223+ debug ! (
224+ "Filtered {} input account hashes not found in accounts table" ,
225+ filtered_count
226+ ) ;
227+ }
228+ kept_account_hashes. extend ( existing_hashes) ;
229+ }
191230
192231 // Filter out_accounts
193232 let out_accounts: Vec < _ > = self
@@ -576,4 +615,31 @@ mod tests {
576615 . iter( )
577616 . any( |tx| tx. hash == unknown_hash) ) ;
578617 }
618+
619+ #[ tokio:: test]
620+ async fn test_filter_by_known_trees_filters_input_account_transactions_for_missing_accounts ( ) {
621+ let db = setup_test_db ( ) . await ;
622+
623+ let mut state_update = StateUpdate :: new ( ) ;
624+
625+ // Simulate input accounts that were never persisted (from unknown trees in earlier txs)
626+ let missing_hash = crate :: common:: typedefs:: hash:: Hash :: new_unique ( ) ;
627+ state_update. in_accounts . insert ( missing_hash. clone ( ) ) ;
628+
629+ // Add an account_transaction referencing the missing input hash
630+ state_update
631+ . account_transactions
632+ . insert ( AccountTransaction {
633+ hash : missing_hash. clone ( ) ,
634+ signature : Signature :: default ( ) ,
635+ } ) ;
636+
637+ // Filter the state update
638+ let result = state_update. filter_by_known_trees ( & db) . await . unwrap ( ) ;
639+
640+ // The missing input hash should NOT be in kept_account_hashes,
641+ // so the account_transaction referencing it should be filtered out.
642+ // This prevents FK violations in persist_account_transactions.
643+ assert_eq ! ( result. state_update. account_transactions. len( ) , 0 ) ;
644+ }
579645}
0 commit comments