-
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
base: main
Are you sure you want to change the base?
Fix evicted transation #605
Conversation
👋 Thanks for assigning @tnull as a reviewer! |
de9c632
to
0a9bee3
Compare
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
@@ -1121,7 +1122,12 @@ impl BitcoindClient { | |||
let mempool_entries_cache = mempool_entries_cache.lock().await; | |||
let evicted_txids = unconfirmed_txids | |||
.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.
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.
@@ -1109,7 +1109,7 @@ impl BitcoindClient { | |||
let mempool_entries_cache = mempool_entries_cache.lock().await; | |||
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!
@@ -754,7 +754,7 @@ impl BitcoindClient { | |||
async fn get_raw_transaction_rpc( | |||
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
@@ -1074,6 +1110,170 @@ pub(crate) fn do_channel_full_cycle<E: ElectrumApi>( | |||
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
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.
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.
- 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.
d28e051
to
456b3ab
Compare
This PR fixes an issue where
BitcoinD
failed to detect when a transaction was removed from the mempool, causing inconsistencies compared toElectrum
andEsplora.
The root cause was an endianness mismatch in the
evicted_txids
check: 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: