Skip to content

Commit

Permalink
fix: chain error caused by zero-conf transactions and reorgs (#3223)
Browse files Browse the repository at this point in the history
<!--- Provide a general summary of your changes in the Title above -->

## Description
<!--- Describe your changes in detail -->
If a zero-conf transaction was in a block and the block is rewound. The block_chain backend will try to delete in input which was never marked as unspent as it was immediately spent. When we rewound this block we should check this and not try and delete an output that was never an unspent output on the chain. 

## Motivation and Context
<!--- Why is this change required? What problem does it solve? -->
<!--- If it fixes an open issue, please link to the issue here. -->

## How Has This Been Tested?
<!--- Please describe in detail how you tested your changes. -->
<!--- Include details of your testing environment, and the tests you ran to -->
<!--- see how your change affects other areas of the code, etc. -->

## Checklist:
<!--- Go over all the following points, and put an `x` in all the boxes that apply. -->
* [x] I'm merging against the `development` branch.
* [x] I have squashed my commits into a single commit.
  • Loading branch information
SWvheerden authored Aug 20, 2021
1 parent 9af90e7 commit f040427
Show file tree
Hide file tree
Showing 2 changed files with 85 additions and 29 deletions.
71 changes: 42 additions & 29 deletions base_layer/core/src/chain_storage/lmdb_db/lmdb_db.rs
Original file line number Diff line number Diff line change
Expand Up @@ -744,29 +744,63 @@ impl LMDBDatabase {
"block_accumulated_data_db",
)?;

let output_rows =
lmdb_delete_keys_starting_with::<TransactionOutputRowData>(&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::<TransactionOutputRowData>(txn, &self.utxos_db, hash)?;
debug!(target: LOG_TARGET, "Deleted {} outputs...", output_rows.len());
let inputs = lmdb_delete_keys_starting_with::<TransactionInputRowData>(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::<TransactionKernelRowData>(&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::<TransactionKernelRowData>(txn, &self.kernels_db, hash)?;
debug!(target: LOG_TARGET, "Deleted {} kernels...", kernels.len());
for kernel in kernels {
trace!(
Expand All @@ -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",
Expand All @@ -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::<TransactionInputRowData>(&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(())
}

Expand Down
43 changes: 43 additions & 0 deletions integration_tests/features/Reorgs.feature
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down

0 comments on commit f040427

Please sign in to comment.