Repository navigation
Conversation
talosmachina
left a comment
There was a problem hiding this comment.
No findings. Adds record_rbf_boost and a fee-rate-preserving onchain snapshot upsert, so an RBF replacement keeps the selected rate while the original keeps its own. Reviewed ae01932, full tier.
What I checked, and 7 candidates I ruled out
Read: the diff in src/lib.rs, activity/mod.rs, activity/README.md, all of activity/rbf_tests.rs, and in activity/implementation.rs record_rbf_boost, upsert_onchain_activities_with_fee_rate_policy, get_pre_activity_metadata_in_connection, transfer_pre_activity_metadata_in_transaction, apply_pre_activity_metadata_for_onchain, add_pre_activity_metadata, the schema and the activities_update_trigger.
Consumers traced: ldk-node bump_fee_by_rbf and update_payment_store_inner (master 0527e43), bitkit-ios processOnchainPayment and boostOnchainTransaction (master), and the call sites in synonymdev/bitkit-ios#879.
Ran: cargo test --locked --lib modules::activity at ae01932 on Linux: 207 passed, 0 failed, all 11 rbf_tests included. cargo fmt --check clean.
CI: green (Android and iOS bindgen, manifests, usdt-tests).
Ruled out
- Duplicate activity for the replacement: ldk-node builds
PaymentId(txid.to_byte_array()), so the payment id is the internal byte order, not the displayed txid, and a snapshot'sidandtx_iddiffer. The preserving path resolves the row by(wallet_id, tx_id)before falling back toactivity.id, so the replacement lands on one row whichever arrives first. - Replacement arrives before the boost is recorded:
record_rbf_boostfinds the row by txid and applies the just-written metadata in the same transaction (recording_after_replacement_arrives_preserves_other_fields_and_tags). - Original confirms instead of the replacement: the original's
feeandfee_rateare never written, onlyis_boostedwhile pending (original_fee_rate_survives_boost_recording_confirmation_and_reopen). The pending row keyed by the losing replacement txid stays behind with empty tags, which nothing ever matches; the same holds today for a send whose tx never shows up. - Pending metadata vs the preserved rate: the metadata transfer runs after the upsert inside the transaction, so a recorded boost rate wins over a stored fallback, and the row is deleted on first use, so a later stale snapshot cannot re-apply it.
- Behaviour change on errors: metadata transfer failures on the preserving path now roll back the upsert instead of being swallowed after commit. The rollback test covers it, and the iOS sync loop catches per payment, so one failure does not stop a sync.
created_at = strftime('%s','now')is text: the column has INTEGER affinity, so SQLite stores an integer; the existing update trigger uses the same expression.- Chained boost: a replacement is itself a
sentactivity with a txid different from the next one, so it passes the original check, and the generic upsert/update APIs still overwrite the rate for deliberate corrections (full_updates_and_upserts_still_allow_deliberate_fee_corrections).
Merge confidence: 4/5, the Rust side is covered by rbf_tests.rs, but the new UniFFI calls are exercised only through bitkit-ios#879, which I did not build here (no iOS toolchain on Linux), and I read implementation.rs around the change rather than whole.
coreyphillips
left a comment
There was a problem hiding this comment.
Two independent reviews, nothing blocking a merge.
worth doing, does not block
- Pending replacement fee metadata is orphaned when the original confirms (src/modules/activity/implementation.rs:795). If the original transaction wins the race and confirms, the pending
pre_activity_metadatarow thatrecord_rbf_boostwrote under the replacement txid stays in the table forever. The row is written atsrc/modules/activity/implementation.rs:795. It is only deleted when an activity with the replacement txid shows up. That can never happen once the original confirms, because the replacement conflicts with it. The row carries no link back to the original, so nothing can find it to clean it up. It will also be included in everyget_all_pre_activity_metadatabackup. I reproduced this in a scratch copy:rust db.insert_onchain_activity(&base("o", "tx-o"))?; db.record_rbf_boost(DEFAULT_WALLET_ID, "o", "tx-r", 25)?; let mut c = db.get_activity_by_tx_id(DEFAULT_WALLET_ID, "tx-o")?.unwrap(); c.confirmed = true; c.confirm_timestamp = Some(200); db.upsert_onchain_activity_preserving_fee_rate(&c)?; // get_all_pre_activity_metadata() -> [("tx-r", 25)]It does no functional harm, because the row can never be applied to anything. But this is new state the old consumer flow never created, and the PR explicitly covers the case where the original confirms instead. Either record enough information to drop the row when the original confirms, or document that it is expected to be left behind. - Snapshots leave unknown fee rates stuck at zero (src/modules/activity/implementation.rs:1093). A stored zero rate prevents later snapshots from filling missing fee information.
watch_only_activity_from_detailemits zero when the rate is unavailable. I reproduced two snapshot upserts for the same wallet and txid, first at zero and then at 25: the absolute fee updated but the rate stayed zero. Preserve positive stored rates while allowing an unknown zero rate to be filled.
coreyphillips
left a comment
There was a problem hiding this comment.
Rechecked after replies, with no new commit since ae01932. Nothing blocking remains.
still open
- Pending replacement fee metadata is orphaned when the original confirms (src/modules/activity/implementation.rs:795). If the original transaction wins the race and confirms, the pending
pre_activity_metadatarow thatrecord_rbf_boostwrote under the replacement txid stays in the table forever. The row is written atsrc/modules/activity/implementation.rs:795. It is only deleted when an activity with the replacement txid shows up. That can never happen once the original confirms, because the replacement conflicts with it. The row carries no link back to the original, so nothing can find it to clean it up. It will also be included in everyget_all_pre_activity_metadatabackup. I reproduced this in a scratch copy:rust db.insert_onchain_activity(&base("o", "tx-o"))?; db.record_rbf_boost(DEFAULT_WALLET_ID, "o", "tx-r", 25)?; let mut c = db.get_activity_by_tx_id(DEFAULT_WALLET_ID, "tx-o")?.unwrap(); c.confirmed = true; c.confirm_timestamp = Some(200); db.upsert_onchain_activity_preserving_fee_rate(&c)?; // get_all_pre_activity_metadata() -> [("tx-r", 25)]It does no functional harm, because the row can never be applied to anything. But this is new state the old consumer flow never created, and the PR explicitly covers the case where the original confirms instead. Either record enough information to drop the row when the original confirms, or document that it is expected to be left behind. HEAD is still ae01932, andgit diff ae01932 HEADis empty. The insert at implementation.rs:795 is unchanged, and the new README section on RBF linkage does not mention the leftover row. @talosmachina's review agrees the row "stays behind", so the point is confirmed rather than shown wrong. The comparison to a send whose tx never shows up is only partly fair: bitkit-ios deletes its pre-activity metadata when a send fails (SendConfirmationView.swift:1454). To close this, add a cleanup path or one README sentence saying the row is expected to remain when the original confirms. - Snapshots leave unknown fee rates stuck at zero (src/modules/activity/implementation.rs:1093). A stored zero rate prevents later snapshots from filling missing fee information.
watch_only_activity_from_detailemits zero when the rate is unavailable. I reproduced two snapshot upserts for the same wallet and txid, first at zero and then at 25: the absolute fee updated but the rate stayed zero. Preserve positive stored rates while allowing an unknown zero rate to be filled. Implementation.rs:1093 still readsCASE WHEN ?16 THEN onchain_activity.fee_rate ELSE excluded.fee_rate END, whilefeeon the line above is still overwritten. A stored zero (unknown) rate therefore still blocks a later snapshot from filling it in. None of the responses addresses this. AddingAND onchain_activity.fee_rate > 0to the WHEN condition would fix it.
|
Approved and green, ready for you to merge and release. |
coreyphillips
left a comment
There was a problem hiding this comment.
Rechecked after replies, with no new commit since ae01932. Nothing blocking remains.
still open
- Pending replacement fee metadata is orphaned when the original confirms (src/modules/activity/implementation.rs:795). If the original transaction wins the race and confirms, the pending
pre_activity_metadatarow thatrecord_rbf_boostwrote under the replacement txid stays in the table forever. The row is written atsrc/modules/activity/implementation.rs:795. It is only deleted when an activity with the replacement txid shows up. That can never happen once the original confirms, because the replacement conflicts with it. The row carries no link back to the original, so nothing can find it to clean it up. It will also be included in everyget_all_pre_activity_metadatabackup. I reproduced this in a scratch copy:rust db.insert_onchain_activity(&base("o", "tx-o"))?; db.record_rbf_boost(DEFAULT_WALLET_ID, "o", "tx-r", 25)?; let mut c = db.get_activity_by_tx_id(DEFAULT_WALLET_ID, "tx-o")?.unwrap(); c.confirmed = true; c.confirm_timestamp = Some(200); db.upsert_onchain_activity_preserving_fee_rate(&c)?; // get_all_pre_activity_metadata() -> [("tx-r", 25)]It does no functional harm, because the row can never be applied to anything. But this is new state the old consumer flow never created, and the PR explicitly covers the case where the original confirms instead. Either record enough information to drop the row when the original confirms, or document that it is expected to be left behind. HEAD is still ae01932, and the insert at implementation.rs:793-800 is unchanged. Nothing removes the replacement-txidpre_activity_metadatarow when the original confirms. The new README section on RBF linkage does not say the row is expected to be left behind. The only response is an approval comment, and it does not engage with this point. - Snapshots leave unknown fee rates stuck at zero (src/modules/activity/implementation.rs:1093). A stored zero rate prevents later snapshots from filling missing fee information.
watch_only_activity_from_detailemits zero when the rate is unavailable. I reproduced two snapshot upserts for the same wallet and txid, first at zero and then at 25: the absolute fee updated but the rate stayed zero. Preserve positive stored rates while allowing an unknown zero rate to be filled. Implementation.rs:1093 still readsCASE WHEN ?16 THEN onchain_activity.fee_rate ELSE excluded.fee_rate END. A stored zero rate is still never filled by a later snapshot, whilefeeis still overwritten. The approval comment does not rebut this. Adding afee_rate > 0guard to the WHEN would settle it.
|
@coreyphillips you are right about my comparison, so I am correcting it. bitkit-ios deletes the send's pre-activity metadata when the send fails (
Neither point blocks the merge. |
coreyphillips
left a comment
There was a problem hiding this comment.
Rechecked after replies, with no new commit since ae01932. Nothing blocking remains.
still open
- Pending replacement fee metadata is orphaned when the original confirms (src/modules/activity/implementation.rs:795). If the original transaction wins the race and confirms, the pending
pre_activity_metadatarow thatrecord_rbf_boostwrote under the replacement txid stays in the table forever. The row is written atsrc/modules/activity/implementation.rs:795. It is only deleted when an activity with the replacement txid shows up. That can never happen once the original confirms, because the replacement conflicts with it. The row carries no link back to the original, so nothing can find it to clean it up. It will also be included in everyget_all_pre_activity_metadatabackup. I reproduced this in a scratch copy:rust db.insert_onchain_activity(&base("o", "tx-o"))?; db.record_rbf_boost(DEFAULT_WALLET_ID, "o", "tx-r", 25)?; let mut c = db.get_activity_by_tx_id(DEFAULT_WALLET_ID, "tx-o")?.unwrap(); c.confirmed = true; c.confirm_timestamp = Some(200); db.upsert_onchain_activity_preserving_fee_rate(&c)?; // get_all_pre_activity_metadata() -> [("tx-r", 25)]It does no functional harm, because the row can never be applied to anything. But this is new state the old consumer flow never created, and the PR explicitly covers the case where the original confirms instead. Either record enough information to drop the row when the original confirms, or document that it is expected to be left behind. The insert atsrc/modules/activity/implementation.rs:794-801still writes the replacement row with only the replacement txid and nothing that links it to the original. The README (lines 116-131) still does not say the row is left behind. The reply agrees this is new state and lists two ways to close it, but neither has landed. Either store the original id on the row and delete it when that original confirms, or add the README sentence. - Snapshots leave unknown fee rates stuck at zero (src/modules/activity/implementation.rs:1093). A stored zero rate prevents later snapshots from filling missing fee information.
watch_only_activity_from_detailemits zero when the rate is unavailable. I reproduced two snapshot upserts for the same wallet and txid, first at zero and then at 25: the absolute fee updated but the rate stayed zero. Preserve positive stored rates while allowing an unknown zero rate to be filled. Line 1093 still readsCASE WHEN ?16 THEN onchain_activity.fee_rate ELSE excluded.fee_rate END, so a stored zero is still kept over a later positive rate. The reply says no current caller hits this, and that checks out in this repo:upsert_activitiesuses the overwriting path atsrc/lib.rs:1038. But that does not rebut the point, which was already scoped to the public API, and the reply calls theAND onchain_activity.fee_rate > 0guard cheap insurance. The guard has not been added.
Related: synonymdev/bitkit-ios#485
Companion: iOS #879.
This PR preserves an outgoing RBF replacement's selected fee rate when activity arrives or is refreshed, so consumer apps can show the correct confirmation estimate.
Description
Out of Scope
Preview
N/A — shared persistence APIs; no UI changes.
QA Notes
Manual Tests
The initial implementation was verified in the iOS simulator using a local SDK built on the apps' existing 0.5.18-compatible base. That historical simulator result predates the latest original-confirmation correction. Native app verification against the updated SDK remains pending publication/dependency adoption.
Automated Checks
rbf_tests.rs— eleven regressions cover replacement arrival order, persistence across database reopen, stale snapshots, wallet isolation, invalid inputs, metadata preservation, and atomic rollback/retry.Latest review follow-up verification
Verified on macOS at
ae0193272bd965a297a2546121af4d2e5b60e37e(2026-10-22):CARGO_INCREMENTAL=0 cargo test --locked --offline --lib modules::activity— 207 passed, zero failures, including all eleven RBF regressions.CARGO_INCREMENTAL=0 cargo clippy --locked --offline --lib --tests --message-format=short— passed with pre-existing warnings; no warnings in the changed implementation or RBF tests.cargo fmt --checkandgit diff --check— passed.rusqlite::params!.No public signatures, binding declarations, schema, or dependency versions changed in this follow-up. A new native app/device run was not performed for it.