Summary
When an original transaction fails to broadcast due to low fee rate and is subsequently replaced by a successful RBF transaction, the sender wallet shows incorrect balance (0 tokens instead of expected change amount). The receiver wallet state is correct.
Environment
Test Scenario
This issue was discovered during comprehensive RBF edge case testing. The specific scenario:
- Initial State: Sender has 600 tokens, receiver has 0 tokens
- Original Transaction: Create transfer of 400 tokens with 100 sats fee (intentionally low)
- Broadcast Failure: Original transaction fails with
"min relay fee not met, 100 < 153"
- RBF Transaction: Create replacement transaction with 1000 sats fee
- RBF Success: RBF transaction broadcasts and confirms successfully
- State Issue: Sender shows 0 balance instead of expected 200 tokens
Expected Behavior
After successful RBF transaction:
- Sender Balance: 200 tokens (600 - 400 transferred)
- Receiver Balance: 400 tokens (amount received)
- State Consistency: Both
state_all and state_own should reflect correct balances
Actual Behavior
After successful RBF transaction:
- Sender Balance: 0 tokens (incorrect)
- Receiver Balance: 400 tokens (correct)
- State Inconsistency:
state_all(contract_id).owned contains genesis state (600 tokens)
state_own(contract_id).owned returns empty array []
Reproduction Steps
#[test]
fn rbf_original_unbroadcast_rbf_success() {
initialize();
let mut wlt_1 = get_wallet(&DescriptorType::Wpkh);
let mut wlt_2 = get_wallet(&DescriptorType::Wpkh);
// Issue NIA asset with 600 tokens
let mut params = NIAIssueParams::new("RBFTestAsset", "RBF", "centiMilli", 600);
let outpoint = wlt_1.get_utxo(None);
params.add_allocation(outpoint, 600);
let contract_id = wlt_1.issue_nia_with_params(params);
wlt_1.send_contract("RBFTestAsset", &mut wlt_2);
wlt_2.reload_runtime();
let invoice = wlt_2.invoice(contract_id, 400, false, Some(0), None);
// Stop mining to test RBF behavior
stop_mining();
let initial_height = get_height();
// Create original transaction with low fee (will fail to broadcast)
let params = TxParams::with(Sats(100)); // Below minimum relay fee
let (psbt, payment) = wlt_1
.runtime
.pay_invoice(
&invoice,
wlt_1.coinselect_strategy,
params,
Some(Sats(2000)),
)
.unwrap();
let (consignment_1, first_tx) = wlt_1.consign(
contract_id,
psbt,
&payment.terminals,
Duration::default(),
false,
None,
);
// Verify original transaction broadcast fails
assert!(wlt_1.broadcast_tx(&first_tx).is_err());
let first_txid = first_tx.txid();
// Receiver accepts the transfer
wlt_2.accept_transfer(&consignment_1, None).unwrap();
// Sync wallets (should cause state rollback due to unbroadcast transaction)
wlt_1.sync();
wlt_2.sync();
// Create and broadcast successful RBF transaction
let (consignment_2, tx) = wlt_1.transfer_rbf(contract_id, payment, 1000, None);
let second_txid = tx.txid();
// Broadcast and confirm the RBF transaction
wlt_1.mine_tx(&tx.txid(), true);
// Receiver accepts the successful RBF transfer
wlt_2.accept_transfer(&consignment_2, None).unwrap();
// Sync both wallets
wlt_1.sync();
wlt_2.sync();
// BUG: This assertion fails
wlt_1.check_allocations(contract_id, AssetSchema::RGB20, vec![200]); // Expected: 200, Actual: 0
wlt_2.check_allocations(contract_id, AssetSchema::RGB20, vec![400]); // This passes correctly
}
Error Output
thread 'rbf_original_unbroadcast_rbf_success' panicked at tests/utils/helper/wallet.rs:606:9:
assertion `left == right` failed
left: []
right: [200]
Debug Information
State Analysis:
// After RBF transaction success
dbg!(wlt_1.runtime.state_all(contract_id).owned);
// Output: Contains genesis state with 600 tokens
dbg!(wlt_1.runtime.state_own(contract_id).owned);
// Output: Empty array []
Transaction Status:
- Original TX:
Unknown status (failed to broadcast)
- RBF TX:
Mined(MiningInfo { height: 234, ... }) (successfully confirmed)
Original TX Broadcast Error (as expected):
HttpResponse {
status: 400,
message: "sendrawtransaction RPC error: {\"code\":-26,\"message\":\"min relay fee not met, 100 < 153\"}"
}
Analysis
The issue appears to be related to RGB state management during RBF scenarios where:
- Original transaction is created but fails to broadcast
- RBF transaction successfully replaces and confirms
- Wallet state synchronization doesn't properly handle the transition from failed original to successful RBF
Additional Context
This issue was discovered during comprehensive RBF edge case testing. Other RBF scenarios work correctly:
- ✅ Standard RBF (original tx in mempool, RBF replaces): Works correctly
- ✅ Both transactions fail to broadcast: State correctly rolls back
- ✅ RBF with fee overflow: Error handling works correctly
- ❌ Original fails, RBF succeeds: This specific scenario shows the state issue
Related Files
Summary
When an original transaction fails to broadcast due to low fee rate and is subsequently replaced by a successful RBF transaction, the sender wallet shows incorrect balance (0 tokens instead of expected change amount). The receiver wallet state is correct.
Environment
Test Scenario
This issue was discovered during comprehensive RBF edge case testing. The specific scenario:
"min relay fee not met, 100 < 153"Expected Behavior
After successful RBF transaction:
state_allandstate_ownshould reflect correct balancesActual Behavior
After successful RBF transaction:
state_all(contract_id).ownedcontains genesis state (600 tokens)state_own(contract_id).ownedreturns empty array[]Reproduction Steps
Error Output
Debug Information
State Analysis:
Transaction Status:
Unknownstatus (failed to broadcast)Mined(MiningInfo { height: 234, ... })(successfully confirmed)Original TX Broadcast Error (as expected):
Analysis
The issue appears to be related to RGB state management during RBF scenarios where:
Additional Context
This issue was discovered during comprehensive RBF edge case testing. Other RBF scenarios work correctly:
Related Files