diff --git a/base_layer/core/src/chain_storage/lmdb_db/lmdb_db.rs b/base_layer/core/src/chain_storage/lmdb_db/lmdb_db.rs index 0e47058b03..7bb5908911 100644 --- a/base_layer/core/src/chain_storage/lmdb_db/lmdb_db.rs +++ b/base_layer/core/src/chain_storage/lmdb_db/lmdb_db.rs @@ -744,29 +744,63 @@ impl LMDBDatabase { "block_accumulated_data_db", )?; - let output_rows = - lmdb_delete_keys_starting_with::(&write_txn, &self.utxos_db, &hash_hex)?; + self.delete_block_inputs_outputs(write_txn, &hash_hex)?; + self.delete_block_kernels(write_txn, &hash_hex)?; + Ok(()) + } + + fn delete_block_inputs_outputs(&self, txn: &WriteTransaction<'_>, hash: &str) -> Result<(), ChainStorageError> { + let output_rows = lmdb_delete_keys_starting_with::(txn, &self.utxos_db, hash)?; debug!(target: LOG_TARGET, "Deleted {} outputs...", output_rows.len()); + let inputs = lmdb_delete_keys_starting_with::(txn, &self.inputs_db, hash)?; + debug!(target: LOG_TARGET, "Deleted {} input(s)...", inputs.len()); + for utxo in &output_rows { trace!(target: LOG_TARGET, "Deleting UTXO `{}`", to_hex(&utxo.hash)); lmdb_delete( - &write_txn, + txn, &self.txos_hash_to_index_db, utxo.hash.as_slice(), "txos_hash_to_index_db", )?; if let Some(ref output) = utxo.output { + let output_hash = output.hash(); + // if an output was already spent in the block, it was never created as unspent, so dont delete it as it + // does not exist here + if inputs.iter().any(|r| r.input.output_hash() == output_hash) { + continue; + } lmdb_delete( - &write_txn, + txn, &*self.utxo_commitment_index, output.commitment.as_bytes(), "utxo_commitment_index", )?; } } - let kernels = - lmdb_delete_keys_starting_with::(&write_txn, &self.kernels_db, &hash_hex)?; + // Move inputs in this block back into the unspent set, any outputs spent within this block they will be removed + // by deleting all the block's outputs below + for row in inputs { + // If input spends an output in this block, don't add it to the utxo set + let output_hash = row.input.output_hash(); + if output_rows.iter().any(|r| r.hash == output_hash) { + continue; + } + trace!(target: LOG_TARGET, "Input moved to UTXO set: {}", row.input); + lmdb_insert( + txn, + &*self.utxo_commitment_index, + row.input.commitment.as_bytes(), + &row.input.output_hash(), + "utxo_commitment_index", + )?; + } + Ok(()) + } + + fn delete_block_kernels(&self, txn: &WriteTransaction<'_>, hash: &str) -> Result<(), ChainStorageError> { + let kernels = lmdb_delete_keys_starting_with::(txn, &self.kernels_db, hash)?; debug!(target: LOG_TARGET, "Deleted {} kernels...", kernels.len()); for kernel in kernels { trace!( @@ -775,7 +809,7 @@ impl LMDBDatabase { kernel.kernel.excess.to_hex() ); lmdb_delete( - &write_txn, + txn, &self.kernel_excess_index, kernel.kernel.excess.as_bytes(), "kernel_excess_index", @@ -789,33 +823,12 @@ impl LMDBDatabase { to_hex(&excess_sig_key) ); lmdb_delete( - &write_txn, + txn, &self.kernel_excess_sig_index, excess_sig_key.as_slice(), "kernel_excess_sig_index", )?; } - - let inputs = lmdb_delete_keys_starting_with::(&write_txn, &self.inputs_db, &hash_hex)?; - debug!(target: LOG_TARGET, "Deleted {} input(s)...", inputs.len()); - // Move inputs in this block back into the unspent set, any outputs spent within this block they will be removed - // by deleting all the block's outputs below - for row in inputs { - // If input spends an output in this block, don't add it to the utxo set - let output_hash = row.input.output_hash(); - if output_rows.iter().any(|r| r.hash == output_hash) { - continue; - } - trace!(target: LOG_TARGET, "Input moved to UTXO set: {}", row.input); - lmdb_insert( - &write_txn, - &*self.utxo_commitment_index, - row.input.commitment.as_bytes(), - &row.input.output_hash(), - "utxo_commitment_index", - )?; - } - Ok(()) } diff --git a/integration_tests/features/Reorgs.feature b/integration_tests/features/Reorgs.feature index 218201b2a0..008df39e8e 100644 --- a/integration_tests/features/Reorgs.feature +++ b/integration_tests/features/Reorgs.feature @@ -94,6 +94,49 @@ Feature: Reorgs When I submit transaction TX2 to PNODE1 Then PNODE1 has TX2 in MEMPOOL state + @critical @reorg + Scenario: Zero-conf reorg with spending + Given I have a base node NODE1 connected to all seed nodes + Given I have a base node NODE2 connected to node NODE1 + When I mine 14 blocks on NODE1 + When I mine a block on NODE1 with coinbase CB1 + When I mine 4 blocks on NODE1 + When I create a custom fee transaction TX1 spending CB1 to UTX1 with fee 100 + When I create a custom fee transaction TX11 spending UTX1 to UTX11 with fee 100 + When I submit transaction TX1 to NODE1 + When I submit transaction TX11 to NODE1 + When I mine 1 blocks on NODE1 + Then NODE1 has TX1 in MINED state + And NODE1 has TX11 in MINED state + And all nodes are at height 20 + And I stop node NODE1 + And node NODE2 is at height 20 + When I mine a block on NODE2 with coinbase CB2 + When I mine 3 blocks on NODE2 + When I create a custom fee transaction TX2 spending CB2 to UTX2 with fee 100 + When I create a custom fee transaction TX21 spending UTX2 to UTX21 with fee 100 + When I submit transaction TX2 to NODE2 + When I submit transaction TX21 to NODE2 + When I mine 1 blocks on NODE2 + Then node NODE2 is at height 25 + And NODE2 has TX2 in MINED state + And NODE2 has TX21 in MINED state + And I stop node NODE2 + When I start base node NODE1 + And node NODE1 is at height 20 + When I mine a block on NODE1 with coinbase CB3 + When I mine 3 blocks on NODE1 + When I create a custom fee transaction TX3 spending CB3 to UTX3 with fee 100 + When I create a custom fee transaction TX31 spending UTX3 to UTX31 with fee 100 + When I submit transaction TX3 to NODE1 + When I submit transaction TX31 to NODE1 + When I mine 1 blocks on NODE1 + Then NODE1 has TX3 in MINED state + And NODE1 has TX31 in MINED state + And node NODE1 is at height 25 + When I start base node NODE2 + Then all nodes are on the same chain tip + Scenario Outline: Massive multiple reorg # # Chain 1a: