From 9f5bf0f332533d10426a9688ff69012e94a11319 Mon Sep 17 00:00:00 2001 From: pgherveou Date: Fri, 30 Jan 2026 16:45:04 +0100 Subject: [PATCH] fix(revive): handle transaction hash conflicts during re-org When a blockchain re-org occurs and the same transaction is included in a different block, the INSERT into transaction_hashes would fail with a UNIQUE constraint violation if the server had restarted (losing the in-memory block_number_to_hashes map used for pruning). This change uses INSERT OR REPLACE to update the block_hash when a transaction is re-included in a different block after a re-org, matching the pattern already used for insert_block_mapping. Fixes: UNIQUE constraint failed: transaction_hashes.transaction_hash --- ...adf2d552140a20d84edbf90583da4619bcf2a.json | 12 ----- ...285ccf101a8e36d68455e05a76df3b366420e.json | 12 +++++ .../frame/revive/rpc/src/receipt_provider.rs | 52 ++++++++++++++++++- 3 files changed, 63 insertions(+), 13 deletions(-) delete mode 100644 substrate/frame/revive/rpc/.sqlx/query-5c0ea8efbd2591e3ede3833acfcadf2d552140a20d84edbf90583da4619bcf2a.json create mode 100644 substrate/frame/revive/rpc/.sqlx/query-b296f5ac320c7537133dca1ede0285ccf101a8e36d68455e05a76df3b366420e.json diff --git a/substrate/frame/revive/rpc/.sqlx/query-5c0ea8efbd2591e3ede3833acfcadf2d552140a20d84edbf90583da4619bcf2a.json b/substrate/frame/revive/rpc/.sqlx/query-5c0ea8efbd2591e3ede3833acfcadf2d552140a20d84edbf90583da4619bcf2a.json deleted file mode 100644 index a3d8e8236527d..0000000000000 --- a/substrate/frame/revive/rpc/.sqlx/query-5c0ea8efbd2591e3ede3833acfcadf2d552140a20d84edbf90583da4619bcf2a.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n\t\t\t\t\tINSERT INTO transaction_hashes (transaction_hash, block_hash, transaction_index)\n\t\t\t\t\tVALUES ($1, $2, $3)\n\t\t\t\t\t", - "describe": { - "columns": [], - "parameters": { - "Right": 3 - }, - "nullable": [] - }, - "hash": "5c0ea8efbd2591e3ede3833acfcadf2d552140a20d84edbf90583da4619bcf2a" -} diff --git a/substrate/frame/revive/rpc/.sqlx/query-b296f5ac320c7537133dca1ede0285ccf101a8e36d68455e05a76df3b366420e.json b/substrate/frame/revive/rpc/.sqlx/query-b296f5ac320c7537133dca1ede0285ccf101a8e36d68455e05a76df3b366420e.json new file mode 100644 index 0000000000000..76ad4a8c1fe95 --- /dev/null +++ b/substrate/frame/revive/rpc/.sqlx/query-b296f5ac320c7537133dca1ede0285ccf101a8e36d68455e05a76df3b366420e.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "\n\t\t\t\t\tINSERT OR REPLACE INTO transaction_hashes (transaction_hash, block_hash, transaction_index)\n\t\t\t\t\tVALUES ($1, $2, $3)\n\t\t\t\t\t", + "describe": { + "columns": [], + "parameters": { + "Right": 3 + }, + "nullable": [] + }, + "hash": "b296f5ac320c7537133dca1ede0285ccf101a8e36d68455e05a76df3b366420e" +} diff --git a/substrate/frame/revive/rpc/src/receipt_provider.rs b/substrate/frame/revive/rpc/src/receipt_provider.rs index cacbf064e188f..e598e7186300b 100644 --- a/substrate/frame/revive/rpc/src/receipt_provider.rs +++ b/substrate/frame/revive/rpc/src/receipt_provider.rs @@ -334,7 +334,7 @@ impl ReceiptProvider { query!( r#" - INSERT INTO transaction_hashes (transaction_hash, block_hash, transaction_index) + INSERT OR REPLACE INTO transaction_hashes (transaction_hash, block_hash, transaction_index) VALUES ($1, $2, $3) "#, transaction_hash, @@ -765,6 +765,56 @@ mod tests { return Ok(()); } + #[sqlx::test] + async fn test_reorg_same_transaction_hash(pool: SqlitePool) -> anyhow::Result<()> { + let provider = setup_sqlite_provider(pool).await; + + // Build two blocks at the same height with the same transaction hash + let tx_hash = H256::from([42u8; 32]); + + // Block A at height 1 + let block_a = MockBlockInfo { hash: H256::from([1u8; 32]), number: 1 }; + let ethereum_hash_a = H256::from([2u8; 32]); + let receipts_a = vec![( + TransactionSigned::default(), + ReceiptInfo { + transaction_hash: tx_hash, + transaction_index: U256::from(0), + ..Default::default() + }, + )]; + + provider.insert(&block_a, &receipts_a, ðereum_hash_a).await?; + + // Verify transaction points to block A + let (found_hash, _) = provider.find_transaction(&tx_hash).await.unwrap(); + assert_eq!(found_hash, block_a.hash); + + // Clear the in-memory map to simulate server restart + provider.block_number_to_hashes.lock().await.clear(); + + // Block B at same height 1 (re-org) with SAME transaction + let block_b = MockBlockInfo { hash: H256::from([3u8; 32]), number: 1 }; + let ethereum_hash_b = H256::from([4u8; 32]); + let receipts_b = vec![( + TransactionSigned::default(), + ReceiptInfo { + transaction_hash: tx_hash, // Same tx hash! + transaction_index: U256::from(0), + ..Default::default() + }, + )]; + + // This should NOT fail with UNIQUE constraint violation + provider.insert(&block_b, &receipts_b, ðereum_hash_b).await?; + + // Transaction should now point to block B + let (found_hash, _) = provider.find_transaction(&tx_hash).await.unwrap(); + assert_eq!(found_hash, block_b.hash); + + Ok(()) + } + #[sqlx::test] async fn test_receipts_count_per_block(pool: SqlitePool) -> anyhow::Result<()> { let provider = setup_sqlite_provider(pool).await;