Skip to content

Wallet handle txs with conflicting account nonces #1864

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 13 commits into from
Apr 15, 2025

Conversation

azarovh
Copy link
Member

@azarovh azarovh commented Jan 15, 2025

Unconfirmed txs in the wallet which uses outdated nonce are marked as conflicted now if confirmed tx is encountered on mainchain and the state of OutputCache is updated accordingly.

The behaviour is similar to abandoning tx.

Besides tests I had a wallet with conflicting orders which I couldn't open previously because of InconsistentOrderDuplicateNonce which is now resolved.

@azarovh azarovh marked this pull request as draft January 15, 2025 14:33
@azarovh azarovh force-pushed the fix/wallet_remove_conflicting_txs branch 2 times, most recently from 3a2fd08 to 6c83ac1 Compare January 21, 2025 09:11
@azarovh azarovh marked this pull request as ready for review January 21, 2025 09:17
@azarovh azarovh requested a review from OBorce January 21, 2025 09:17

conflicting_txs.push(unconfirmed);
if let Some((confirmed_account, confirmed_account_nonce)) = confirmed_account_nonce
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this can probably be an else if, as the transactions that use a token id will be a superset of the transactions that use the token id and have a specific nonce.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that it'd make the code more confusing especially when modifying in the future. I replaced the vec with set to avoid duplicates.

@azarovh azarovh force-pushed the fix/wallet_remove_conflicting_txs branch from 8be3bb5 to 3327b22 Compare January 29, 2025 09:56
@azarovh
Copy link
Member Author

azarovh commented Jan 30, 2025

Fixed the problem when abandoning a tx that is already marked as conflicted leads to invalid state of internal fields of OutputCache (like account nonces).

}
},
},
Entry::Vacant(_) => Err(WalletError::CannotFindTransactionWithId(tx_id)),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this also an invariant? If so, it deserves its own error.

But I also see that remove_tx removes the tx from txs unconditionally, possibly leaving it inside unconfirmed_descendants. If this situation is legitimate, we probably shouldn't return an error here.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To me it looks like an invariant. Because removing a tx removes its descendants as well.

Comment on lines 784 to 875
TxState::Abandoned
| TxState::Confirmed(..)
| TxState::InMempool(..)
| TxState::Conflicted(..) => {
Err(WalletError::CannotChangeTransactionState(
*tx.state(),
TxState::Conflicted(block_id),
))
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't look nice. This whole function is called during block processing, so if this situation happens, the wallet will stop working, right?

Abandoned, Confirmed and Conflicted probably can't happen here and are invariant violations, am I right? If so, a separate error would be nice.
But what about InMempool? It sounds like it should be possible. If so, we must handle it somehow. Would it be wrong to change the state from InMempool to Conflicted?

Copy link
Member Author

@azarovh azarovh Feb 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like InMempool -> Conflicted is theoretically possible. Changed the logic to mark it as conflicted.

@azarovh azarovh force-pushed the fix/wallet_remove_conflicting_txs branch from caa2c45 to f87218f Compare March 5, 2025 14:10
Comment on lines 814 to 818
for conflicting_tx in conflicting_txs {
let txs_to_rollback = self.remove_from_unconfirmed_descendants(conflicting_tx);

// Mark conflicting tx and its descendants as Conflicting and update OutputCache data accordingly
for tx_id in txs_to_rollback.iter().rev().copied() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is a problem that currently I think cannot happen but can happen in the future when we add TXs from Mempool.

The problem is with the rollback_tx_data, as the TXs are iterated in random order by tx_id, in a situation like:
Having unconfirmed txs for a delegation or token:
A(nonce 1) -> B(2) -> C(3) -> D(4)
If for some reason the conflict happens in the middle of the chain e.g. nonce 2 (currently it should always happen at the start of the chain from confirmed blocks, unless I am mistaken)
So if it happens at 2, all 2, 3, and 4 will be inside conflicting_txs, but not sorted by tx_id. So the rollback can happen to finish at 3 or 4, which will be unable to find last_parent, the tx A as a still unconfirmed tx for this delegation or token.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thinking about it maybe you can have filter them out in n * log(d).
something like:
conflicting_descendents = BTreeSet::new();
for tx in conflicting_txs {
conflicting_descendents += find_descendents(tx);
}
conflicting_txs = conflicting_txs.filter(|tx| conflicting_descendetns.contains(tx))

This should also fix the double Conflict -> Conflict state

Copy link
Member Author

@azarovh azarovh Mar 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Am I getting it right that you suggest to filter out conflicting txs to be able to rollback descendants first and then conflicting txs themself?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

txs_to_rollback is already sorted with the parent first and all descendants after it, and the loop is in reverse.
only the conflicting_txs are not sorted, so if we filter them out before this loop, everything will be ok and there will be no duplicate txs and no Conflict -> Conflict state.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So I checked the collection and I don't think filtering out parents is necessary as a result of remove_from_unconfirmed_descendants function call doesn't contain any duplicates anymore.

@azarovh azarovh force-pushed the fix/wallet_remove_conflicting_txs branch 2 times, most recently from 13bf848 to c1e3856 Compare March 17, 2025 11:02
@azarovh azarovh requested review from ImplOfAnImpl and OBorce March 17, 2025 11:02
@azarovh azarovh force-pushed the fix/wallet_remove_conflicting_txs branch from c1e3856 to ca1a378 Compare March 17, 2025 13:12
let mut to_abandon = BTreeSet::from_iter([OutPointSourceId::from(tx_id)]);
) -> Vec<Id<Transaction>> {
let mut all_txs = Vec::new();
let mut to_update = BTreeSet::from_iter([OutPointSourceId::from(tx_id)]);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've just noticed that it's actually a set and not a vec. Why?
This can make descendants appear before parents in the result.

E.g. consider the following dependency graph:

 /-->B-->\
A         D
 \-->C-->/

We call remove_from_unconfirmed_descendants for A, so B and C are inserted into to_update. Let's assume Id(D) < Id(B) < Id(C), then on the next step B will be handled and D will be inserted into to_update before C. The resulting order will be "A, B, D, C, D" and this is what will be written to all_txs.

Using a vec would change it to "A, B, C, D, D".

But I guess returning duplicates is also not a good idea, so probably there should be an additional set, e.g. all_txs_as_set, that will be used to check whether a tx id already exists in all_txs.

A test that reproduces this would be nice to have.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed, test added

Comment on lines 832 to 847
for conflicting_tx in conflicting_txs {
let txs_to_rollback = self.remove_from_unconfirmed_descendants(conflicting_tx);

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think there are still some problems here:

  1. As Boris has said, the txs in conflicting_txs are in a random order and descendants may come before parents.
  2. remove_from_unconfirmed_descendants unconditionally puts ids from to_update into the result and to_update is initialized with the provided tx. So here txs_to_rollback may contain ids of trasansactions already rolled back on the previous iteration of the loop. Which will lead to CannotMarkTxAsConflictedIfInState(TxState::Conflicted).

But I'm not sure why we would need transactions from the mempool to reproduce this (which Boris also mentioned), it seems like it can happen anyway.
If so, a test that reproduces this problem would be nice to have.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe this is fixed in the previous comment with diamond deps

}
}

for output in tx.outputs() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could be nitpicking, but I'd suggest rolling back stuff in the exact opposit order of how it was added. I.e. first roll back outputs (in the reverse order too) and then inputs.
Currently it doesn't matter of course, but maybe our chain will evolve in the future and make the order matter.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I changed the order to rev(), but processing outputs before inputs break logic that searches for utxo parent, so I'd leave it this way

}

fn uses_token(&self, unconfirmed_tx: &WalletTx, frozen_token_id: &TokenId) -> bool {
fn violates_frozen_token(&self, unconfirmed_tx: &WalletTx, frozen_token_id: &TokenId) -> bool {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Btw, probably this should check tx outputs too?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure that it's possible to have tokens in the outputs without inputs

@azarovh azarovh force-pushed the fix/wallet_remove_conflicting_txs branch 2 times, most recently from 3528e2c to 34b383f Compare April 8, 2025 10:40
@azarovh azarovh force-pushed the fix/wallet_remove_conflicting_txs branch from 34b383f to 9722bd7 Compare April 8, 2025 10:45
@azarovh azarovh requested a review from ImplOfAnImpl April 8, 2025 11:15
@azarovh azarovh force-pushed the fix/wallet_remove_conflicting_txs branch from 9722bd7 to e8db935 Compare April 14, 2025 16:29
@azarovh azarovh force-pushed the fix/wallet_remove_conflicting_txs branch from e8db935 to be11da1 Compare April 15, 2025 08:09
@azarovh azarovh changed the base branch from master to fix/generate_transaction_graph_fee April 15, 2025 08:09
Base automatically changed from fix/generate_transaction_graph_fee to master April 15, 2025 11:16
@azarovh azarovh merged commit 2729c09 into master Apr 15, 2025
18 checks passed
@azarovh azarovh deleted the fix/wallet_remove_conflicting_txs branch April 15, 2025 11:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants