Skip to content

Commit 03fdea6

Browse files
fix: filter input account_transactions for unknown trees (#335)
1 parent dc088e2 commit 03fdea6

File tree

3 files changed

+81
-9
lines changed

3 files changed

+81
-9
lines changed

src/api/method/get_validity_proof/prover/helpers.rs

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -232,7 +232,6 @@ mod tests {
232232
hex::decode(padded).unwrap().try_into().unwrap()
233233
}
234234

235-
236235
/// combined proofs incorrectly returned non_inclusion_chain instead of
237236
/// hash(inclusion_chain, non_inclusion_chain).
238237
#[test]
@@ -251,10 +250,8 @@ mod tests {
251250
let non_inclusion_chain =
252251
create_two_inputs_hash_chain(&[address_root], &[address]).unwrap();
253252

254-
255253
let old_result = non_inclusion_chain;
256254

257-
258255
let correct_result =
259256
create_two_inputs_hash_chain(&[inclusion_chain], &[non_inclusion_chain]).unwrap();
260257

src/ingester/fetchers/grpc.rs

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,9 @@ use solana_pubkey::Pubkey as SdkPubkey;
1717
use solana_signature::Signature;
1818
use tokio::time::sleep;
1919
use tracing::error;
20-
use yellowstone_grpc_client::{ClientTlsConfig, GeyserGrpcBuilderResult, GeyserGrpcClient, Interceptor};
20+
use yellowstone_grpc_client::{
21+
ClientTlsConfig, GeyserGrpcBuilderResult, GeyserGrpcClient, Interceptor,
22+
};
2123
use yellowstone_grpc_proto::convert_from::create_tx_error;
2224
use yellowstone_grpc_proto::geyser::{
2325
subscribe_update::UpdateOneof, CommitmentLevel, SubscribeRequest, SubscribeRequestPing,
@@ -158,7 +160,11 @@ fn is_healthy(slot: u64) -> bool {
158160
(LATEST_SLOT.load(Ordering::SeqCst) as i64 - slot as i64) <= HEALTH_CHECK_SLOT_DISTANCE
159161
}
160162

161-
fn get_grpc_block_stream(endpoint: String, auth_header: String, mut last_indexed_slot: Option<u64>) -> impl Stream<Item = BlockInfo> {
163+
fn get_grpc_block_stream(
164+
endpoint: String,
165+
auth_header: String,
166+
mut last_indexed_slot: Option<u64>,
167+
) -> impl Stream<Item = BlockInfo> {
162168
stream! {
163169
loop {
164170
let mut grpc_tx;
@@ -255,7 +261,10 @@ fn generate_random_string(len: usize) -> String {
255261
}
256262

257263
fn get_block_subscribe_request(from_slot: Option<u64>) -> SubscribeRequest {
258-
info!("Subscribing to gRPC block stream from slot {}", from_slot.unwrap_or(0));
264+
info!(
265+
"Subscribing to gRPC block stream from slot {}",
266+
from_slot.unwrap_or(0)
267+
);
259268
SubscribeRequest {
260269
blocks: HashMap::from_iter(vec![(
261270
generate_random_string(20),

src/ingester/parser/state_update.rs

Lines changed: 69 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)