-
Notifications
You must be signed in to change notification settings - Fork 114
Fix evicted transation #605
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
Fix evicted transation #605
Conversation
|
👋 Thanks for assigning @tnull as a reviewer! |
de9c632 to
0a9bee3
Compare
tnull
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, good catch! I think we should fix this right at deserialization time though.
src/chain/bitcoind.rs
Outdated
| .into_iter() | ||
| .filter(|txid| mempool_entries_cache.contains_key(txid)) | ||
| .filter(|txid| { | ||
| let mut bytes = txid.to_byte_array(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think we should fix this here, but when we first parse in the Txid, i.e., before we even add it to the cache.
While you're doing this, mind adding a few end-to-end tests for all the JsonResponse de/serialization, i.e., asserting that we get the same objects when we encode and decode GetRawMempoolResponse,GetMempoolEntryResponse, etc. These might also good candidates for simple proptests.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Got it, thanks! I’ll make those changes and add the tests as suggested.
tnull
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this branch also needs a rebase by now.
0a9bee3 to
5961e27
Compare
|
I updated the implementation to replace I also added property-based tests to ensure that Additionally, I expanded the RBF functional tests to cover two scenarios:
For this, I introduced |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for catching this!
Changes themselves look mostly good, but I'd prefer to keep the test setup more minimal, as all this boilerplate doesn't provide any real benefit, and the complexity renders CI pretty AFAICT.
| let evicted_txids = unconfirmed_txids | ||
| .into_iter() | ||
| .filter(|txid| mempool_entries_cache.contains_key(txid)) | ||
| .filter(|txid| !mempool_entries_cache.contains_key(txid)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Aaah, good catch. Mind renaming unconfirmed_txids to bdk_mempool_txids or bdk_unconfirmed_txids. I think a bit cleaner naming would have likely avoided that bug in the first place.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done!
| rpc_client: Arc<RpcClient>, txid: &Txid, | ||
| ) -> std::io::Result<Option<Transaction>> { | ||
| let txid_hex = bitcoin::consensus::encode::serialize_hex(txid); | ||
| let txid_hex = txid.to_string(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ugh, classic mistake. Thanks for catching this.
tests/common/mod.rs
Outdated
| println!("\nB stopped"); | ||
| } | ||
|
|
||
| pub(crate) struct MultiNodeTestSetup { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a lot of boilerplate, and would use a lot of CI resources for no added benefit. I think it would be preferable if the test_rbf tests would just reuse the existing test utilities. FWIW, to test RBF works as expected you'll only need to setup a single node (maybe one per chain source, as we do for the full_cycle) test, i.e., should just be able to use setup_node.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I initially made that struct for a different approach, but it turned out unnecessary. I removed it and now use one node per chain type, keeping the test lighter.
f2cf691 to
f9e0767
Compare
tnull
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This needs a rebase by now.
- Replace `bitcoin::consensus::encode::deserialize_hex()` with `hex_str.parse::<Txid>()` when parsing Txids from RPC, and `serialize_hex()` with `txid.to_string()` when sending to RPC, ensuring proper handling of Bitcoin Core's reversed-byte hexadecimal format. - Fix mempool eviction logic: transactions that are no longer in the mempool are now correctly removed from wallet consideration, preventing stale pending transactions from inflating unconfirmed balances. - Refactor `unconfirmed_txids` to `bdk_unconfirmed_txids` to make it easier to identify what are unconfirmed bdk transactions
f9e0767 to
d28e051
Compare
src/chain/bitcoind.rs
Outdated
| } | ||
|
|
||
| proptest! { | ||
| #![proptest_config(proptest::test_runner::Config::with_cases(250))] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
While good for test coverage initially, 250 seems like a high number if we run it every time we push to CI, no? Maybe 10 or 20 seems more reasonable?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good point. Reducing from 250 to 20 only saves a few milliseconds, but I agree 250 is excessive for CI. 20 runs seems more reasonable for these scenarios.
tests/integration_tests_rust.rs
Outdated
| match bitcoind.send_raw_transaction(&tx) { | ||
| Ok(res) => { | ||
| // Mine a block immediately so the transaction is confirmed | ||
| // before any node identifies it as a transaction that was in the mempool. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm, depending on the chain source that might be race-y, no? In particular, for bitcoind RPC we might happen to poll the mempool just before the block gets mined?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, race conditions are possible, but they don’t affect the test outcome. When is_insert_block is true, there are three possible scenarios:
- When the node checks the mempool, it sees the RBF transaction. The old transaction is removed to allow the RBF. I haven’t triggered this in my tests, but it wouldn’t break anything.
- When the node checks the mempool, it finds it empty, either because the transaction is already in a block or the block is being processed. Old unconfirmed transactions are removed and the correct transaction is picked from the block. This scenario is rare but safe.
- When the node checks confirmed transactions in the block, it sees the RBF transaction already included. The old unconfirmed transaction is removed. This is the most common scenario in the test and effectively tests the case where the node first encounters the RBF transaction via the block rather than the mempool.
- Validate roundtrip serialization/deserialization for Txids, transactions, mempool entries, and fee responses. - Ensure Txid parsing/serialization matches Bitcoin Core RPC expectations.
d28e051 to
456b3ab
Compare
tnull
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Mostly looks good, some minor comments.
tests/integration_tests_rust.rs
Outdated
| } | ||
| } | ||
| fee_output_index = option_fee_output_index.expect( | ||
| "No output available for fee pumping. Need at least one output not being modified.", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: bump?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done!
tests/integration_tests_rust.rs
Outdated
| }, | ||
| Err(_) => { | ||
| if _attempt == attempts - 1 { | ||
| panic!("Failed to pump fee after {} attempts", attempts); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: bump?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done!
| nodes.iter().map(|node| node.onchain_payment().new_address().unwrap()).collect::<Vec<_>>(); | ||
| let amount_sat = 2_100_000; | ||
| let mut txid; | ||
| macro_rules! distribute_funds_all_nodes { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we make all these helpers methods rather than macros? Macros have serveral drawbacks, in particular they're much harder to debug in case of a failure as the compiler won't output the correct line numbers then.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, though I think distribute_funds_all_nodes and validate_balances can stay as macros. The first just calls a function, and in the second the main failure point is assert_eq!, so keeping them as macro_rules! helps keep the helper functions cleaner.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm I would generally prefer to inline them or use methods, but won't hold this PR up over it.
- Test mempool-only RBF handling and balance adjustments. - Test RBF transactions confirmed in block, ensuring stale unconfirmed txs are removed. - Introduce `distribute_funds_unconfirmed` for creating unconfirmed outputs.
456b3ab to
f3aab90
Compare
tnull
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks again! Landing this!
| nodes.iter().map(|node| node.onchain_payment().new_address().unwrap()).collect::<Vec<_>>(); | ||
| let amount_sat = 2_100_000; | ||
| let mut txid; | ||
| macro_rules! distribute_funds_all_nodes { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm I would generally prefer to inline them or use methods, but won't hold this PR up over it.
This PR fixes an issue where
BitcoinDfailed to detect when a transaction was removed from the mempool, causing inconsistencies compared toElectrumandEsplora.The root cause was an endianness mismatch in the
evicted_txidscheck: one txid was in little-endian and the other in big-endian, making proper matching nearly impossible. The fix ensures consistent txid comparison by normalizing endianness.Additionally, this PR adds an RBF functional test to verify that the node correctly: