15
15
16
16
use std:: {
17
17
cmp:: Reverse ,
18
- collections:: { btree_map:: Entry , BTreeMap , BTreeSet } ,
18
+ collections:: { btree_map:: Entry , BTreeMap , BTreeSet , VecDeque } ,
19
19
ops:: Add ,
20
20
} ;
21
21
@@ -750,7 +750,7 @@ impl OutputCache {
750
750
& mut self ,
751
751
confirmed_tx : & Transaction ,
752
752
block_id : Id < GenBlock > ,
753
- ) -> WalletResult < Vec < Id < Transaction > > > {
753
+ ) -> WalletResult < Vec < ( Id < Transaction > , WalletTx ) > > {
754
754
struct ConflictCheck {
755
755
frozen_token_id : Option < TokenId > ,
756
756
confirmed_account_nonce : Option < ( AccountType , AccountNonce ) > ,
@@ -802,76 +802,97 @@ impl OutputCache {
802
802
for unconfirmed in self . unconfirmed_descendants . keys ( ) {
803
803
let unconfirmed_tx = self . txs . get ( unconfirmed) . expect ( "must be present" ) ;
804
804
805
- if let WalletTx :: Tx ( tx) = unconfirmed_tx {
806
- if let Some ( frozen_token_id) = conflict_check. frozen_token_id {
807
- if self . violates_frozen_token ( unconfirmed_tx, & frozen_token_id) {
808
- conflicting_txs. insert ( tx. get_transaction ( ) . get_id ( ) ) ;
809
- continue ;
805
+ match unconfirmed_tx {
806
+ WalletTx :: Tx ( tx) => {
807
+ if let Some ( frozen_token_id) = conflict_check. frozen_token_id {
808
+ if self . violates_frozen_token ( unconfirmed_tx, & frozen_token_id) {
809
+ conflicting_txs. insert ( tx. get_transaction ( ) . get_id ( ) ) ;
810
+ continue ;
811
+ }
810
812
}
811
- }
812
813
813
- if let Some ( ( confirmed_account, confirmed_account_nonce) ) =
814
- conflict_check. confirmed_account_nonce
815
- {
816
- if confirmed_tx. get_id ( ) != tx. get_transaction ( ) . get_id ( )
817
- && uses_conflicting_nonce (
818
- unconfirmed_tx,
819
- confirmed_account,
820
- confirmed_account_nonce,
821
- )
814
+ if let Some ( ( confirmed_account, confirmed_account_nonce) ) =
815
+ conflict_check. confirmed_account_nonce
822
816
{
823
- conflicting_txs. insert ( tx. get_transaction ( ) . get_id ( ) ) ;
824
- continue ;
817
+ if confirmed_tx. get_id ( ) != tx. get_transaction ( ) . get_id ( )
818
+ && uses_conflicting_nonce (
819
+ unconfirmed_tx,
820
+ confirmed_account,
821
+ confirmed_account_nonce,
822
+ )
823
+ {
824
+ conflicting_txs. insert ( tx. get_transaction ( ) . get_id ( ) ) ;
825
+ continue ;
826
+ }
825
827
}
826
828
}
827
- }
829
+ WalletTx :: Block ( _) => {
830
+ utils:: debug_panic_or_log!( "Cannot be block reward" ) ;
831
+ }
832
+ } ;
828
833
}
829
834
}
830
835
831
836
// Remove all descendants of conflicting txs
832
837
let mut conflicting_txs_with_descendants = vec ! [ ] ;
833
838
839
+ // Note: `conflicting_txs` can contain not only parent txs but also children if they have their
840
+ // own descendants.
841
+ // Because `conflicting_txs` is a set we can't tell the order of processing, so have to track all txs
842
+ // to avoid marking as conflicted txs that has already been marked because they are descendants.
843
+ let mut processed_transactions = BTreeSet :: new ( ) ;
844
+
834
845
for conflicting_tx in conflicting_txs {
835
- let txs_to_rollback = self . remove_from_unconfirmed_descendants ( conflicting_tx) ;
846
+ let tx_ids_to_rollback = self . remove_from_unconfirmed_descendants ( conflicting_tx) ;
836
847
837
848
// Mark conflicting tx and its descendants as Conflicting and update OutputCache data accordingly
838
849
let mut tx_to_rollback_data = vec ! [ ] ;
839
- for tx_id in txs_to_rollback. iter ( ) . rev ( ) . copied ( ) {
840
- match self . txs . entry ( tx_id. into ( ) ) {
841
- Entry :: Occupied ( mut entry) => match entry. get_mut ( ) {
842
- WalletTx :: Block ( _) => {
843
- Err ( WalletError :: TransactionIdCannotMapToBlock ( tx_id) )
844
- }
845
- WalletTx :: Tx ( tx) => match tx. state ( ) {
846
- TxState :: Inactive ( _) | TxState :: InMempool ( _) => {
847
- tx. set_state ( TxState :: Conflicted ( block_id) ) ;
848
- tx_to_rollback_data. push ( entry. get ( ) . clone ( ) ) ;
849
- Ok ( ( ) )
850
+ for tx_id in tx_ids_to_rollback. into_iter ( ) . rev ( ) {
851
+ if !processed_transactions. contains ( & tx_id) {
852
+ match self . txs . entry ( tx_id. into ( ) ) {
853
+ Entry :: Occupied ( mut entry) => match entry. get_mut ( ) {
854
+ WalletTx :: Block ( _) => {
855
+ Err ( WalletError :: TransactionIdCannotMapToBlock ( tx_id) )
850
856
}
851
- TxState :: Conflicted ( ..)
852
- | TxState :: Abandoned
853
- | TxState :: Confirmed ( ..) => {
854
- Err ( WalletError :: CannotMarkTxAsConflictedIfInState ( * tx. state ( ) ) )
857
+ WalletTx :: Tx ( tx) => {
858
+ match tx. state ( ) {
859
+ TxState :: Inactive ( _) | TxState :: InMempool ( _) => {
860
+ tx. set_state ( TxState :: Conflicted ( block_id) ) ;
861
+ tx_to_rollback_data. push ( WalletTx :: Tx ( tx. clone ( ) ) ) ;
862
+ }
863
+ TxState :: Conflicted ( ..)
864
+ | TxState :: Abandoned
865
+ | TxState :: Confirmed ( ..) => {
866
+ return Err ( WalletError :: CannotMarkTxAsConflictedIfInState (
867
+ * tx. state ( ) ,
868
+ ) )
869
+ }
870
+ }
871
+ conflicting_txs_with_descendants
872
+ . push ( ( tx_id, WalletTx :: Tx ( tx. clone ( ) ) ) ) ;
873
+ processed_transactions. insert ( tx_id) ;
874
+ Ok ( ( ) )
855
875
}
856
876
} ,
857
- } ,
858
- Entry :: Vacant ( _ ) => {
859
- Err ( WalletError :: CannotFindDescendantTransactionWithId ( tx_id ) )
860
- }
861
- } ? ;
877
+ Entry :: Vacant ( _ ) => {
878
+ Err ( WalletError :: CannotFindDescendantTransactionWithId ( tx_id ) )
879
+ }
880
+ } ? ;
881
+ }
862
882
}
863
883
864
884
for tx in tx_to_rollback_data {
865
885
self . rollback_tx_data ( & tx) ?;
866
886
}
867
-
868
- conflicting_txs_with_descendants. extend ( txs_to_rollback. into_iter ( ) ) ;
869
887
}
870
888
871
889
Ok ( conflicting_txs_with_descendants)
872
890
}
873
891
874
892
fn violates_frozen_token ( & self , unconfirmed_tx : & WalletTx , frozen_token_id : & TokenId ) -> bool {
893
+ // We check only inputs because currently it's not possible to have a token in an output
894
+ // without having it in an input.
895
+ // But potentially we should check outputs too.
875
896
unconfirmed_tx. inputs ( ) . iter ( ) . any ( |inp| match inp {
876
897
TxInput :: Utxo ( outpoint) => self . txs . get ( & outpoint. source_id ( ) ) . is_some_and ( |tx| {
877
898
let output =
@@ -947,9 +968,9 @@ impl OutputCache {
947
968
TxState :: Confirmed ( _, _, _) => false ,
948
969
} ;
949
970
950
- if is_unconfirmed && !already_present {
951
- self . unconfirmed_descendants . insert ( tx_id. clone ( ) , BTreeSet :: new ( ) ) ;
952
- } else if !is_unconfirmed {
971
+ if is_unconfirmed {
972
+ self . unconfirmed_descendants . entry ( tx_id. clone ( ) ) . or_default ( ) ;
973
+ } else {
953
974
self . unconfirmed_descendants . remove ( & tx_id) ;
954
975
}
955
976
@@ -1197,7 +1218,6 @@ impl OutputCache {
1197
1218
if let Some ( descendants) = data
1198
1219
. last_parent
1199
1220
. as_ref ( )
1200
- . filter ( |parent_tx_id| * parent_tx_id != tx_id)
1201
1221
. and_then ( |parent_tx_id| unconfirmed_descendants. get_mut ( parent_tx_id) )
1202
1222
{
1203
1223
descendants. insert ( tx_id. clone ( ) ) ;
@@ -1229,7 +1249,6 @@ impl OutputCache {
1229
1249
if let Some ( descendants) = data
1230
1250
. last_parent
1231
1251
. as_ref ( )
1232
- . filter ( |parent_tx_id| * parent_tx_id != tx_id)
1233
1252
. and_then ( |parent_tx_id| unconfirmed_descendants. get_mut ( parent_tx_id) )
1234
1253
{
1235
1254
descendants. insert ( tx_id. clone ( ) ) ;
@@ -1264,7 +1283,6 @@ impl OutputCache {
1264
1283
if let Some ( descendants) = data
1265
1284
. last_parent
1266
1285
. as_ref ( )
1267
- . filter ( |parent_tx_id| * parent_tx_id != tx_id)
1268
1286
. and_then ( |parent_tx_id| unconfirmed_descendants. get_mut ( parent_tx_id) )
1269
1287
{
1270
1288
descendants. insert ( tx_id. clone ( ) ) ;
@@ -1275,7 +1293,7 @@ impl OutputCache {
1275
1293
1276
1294
pub fn remove_confirmed_tx ( & mut self , tx_id : & OutPointSourceId ) -> WalletResult < ( ) > {
1277
1295
if let Some ( tx) = self . txs . remove ( tx_id) {
1278
- matches ! ( tx. state( ) , TxState :: Confirmed ( ..) ) ;
1296
+ assert ! ( matches!( tx. state( ) , TxState :: Confirmed ( ..) ) ) ;
1279
1297
self . rollback_tx_data ( & tx) ?;
1280
1298
}
1281
1299
@@ -1474,16 +1492,21 @@ impl OutputCache {
1474
1492
}
1475
1493
1476
1494
// Removes a tx and all its descendants from unconfirmed descendants collection.
1477
- // Returns provided tx and all the descendants in that specific order.
1495
+ // Returns provided tx and all the descendants in such way that parent always comes before its
1496
+ // descendants. Returned collection contains no duplicates.
1478
1497
fn remove_from_unconfirmed_descendants (
1479
1498
& mut self ,
1480
1499
tx_id : Id < Transaction > ,
1481
1500
) -> Vec < Id < Transaction > > {
1482
1501
let mut all_txs = Vec :: new ( ) ;
1483
- let mut to_update = BTreeSet :: from_iter ( [ OutPointSourceId :: from ( tx_id) ] ) ;
1502
+ let mut all_txs_as_set = BTreeSet :: new ( ) ;
1503
+ let mut to_update: VecDeque < OutPointSourceId > = [ tx_id. into ( ) ] . into ( ) ;
1484
1504
1485
- while let Some ( outpoint_source_id) = to_update. pop_first ( ) {
1486
- all_txs. push ( * outpoint_source_id. get_tx_id ( ) . expect ( "must be a transaction" ) ) ;
1505
+ while let Some ( outpoint_source_id) = to_update. pop_front ( ) {
1506
+ let tx_id = * outpoint_source_id. get_tx_id ( ) . expect ( "must be a transaction" ) ;
1507
+ if all_txs_as_set. insert ( tx_id) {
1508
+ all_txs. push ( tx_id) ;
1509
+ }
1487
1510
1488
1511
if let Some ( descendants) = self . unconfirmed_descendants . remove ( & outpoint_source_id) {
1489
1512
to_update. extend ( descendants. into_iter ( ) )
@@ -1498,7 +1521,8 @@ impl OutputCache {
1498
1521
fn rollback_tx_data ( & mut self , tx : & WalletTx ) -> WalletResult < ( ) > {
1499
1522
let tx_id = tx. id ( ) ;
1500
1523
1501
- // Iterate in reverse to handle situations where an account is modified twice in the same tx
1524
+ // Iterate in reverse to handle situations where an account is modified twice in the same tx.
1525
+ // It's currently impossible to have more than 1 account command but it's safer to use rev() anyway.
1502
1526
for input in tx. inputs ( ) . iter ( ) . rev ( ) {
1503
1527
match input {
1504
1528
TxInput :: Utxo ( outpoint) => {
@@ -1572,7 +1596,7 @@ impl OutputCache {
1572
1596
}
1573
1597
}
1574
1598
1575
- for output in tx. outputs ( ) {
1599
+ for output in tx. outputs ( ) . iter ( ) . rev ( ) {
1576
1600
match output {
1577
1601
TxOutput :: CreateStakePool ( pool_id, _) => {
1578
1602
self . pools . remove ( pool_id) ;
@@ -1613,43 +1637,47 @@ impl OutputCache {
1613
1637
}
1614
1638
1615
1639
/// Mark a transaction and its descendants as abandoned
1616
- /// Returns a Vec of the transaction Ids that have been abandoned
1640
+ /// Returns a Vec of the transactions and the Ids that have been abandoned
1617
1641
pub fn abandon_transaction (
1618
1642
& mut self ,
1619
1643
tx_id : Id < Transaction > ,
1620
- ) -> WalletResult < Vec < Id < Transaction > > > {
1644
+ ) -> WalletResult < Vec < ( Id < Transaction > , WalletTx ) > > {
1621
1645
let all_abandoned = self . remove_from_unconfirmed_descendants ( tx_id) ;
1646
+ let mut all_abandoned_txs = Vec :: with_capacity ( all_abandoned. len ( ) ) ;
1622
1647
1623
1648
let mut txs_to_rollback = vec ! [ ] ;
1624
1649
for tx_id in all_abandoned. iter ( ) . rev ( ) . copied ( ) {
1625
1650
match self . txs . entry ( tx_id. into ( ) ) {
1626
1651
Entry :: Occupied ( mut entry) => match entry. get_mut ( ) {
1627
1652
WalletTx :: Block ( _) => Err ( WalletError :: CannotFindTransactionWithId ( tx_id) ) ,
1628
- WalletTx :: Tx ( tx) => match tx. state ( ) {
1629
- TxState :: Inactive ( _) => {
1630
- tx. set_state ( TxState :: Abandoned ) ;
1631
- txs_to_rollback. push ( entry. get ( ) . clone ( ) ) ;
1632
- Ok ( ( ) )
1633
- }
1634
- TxState :: Conflicted ( _) => {
1635
- tx. set_state ( TxState :: Abandoned ) ;
1636
- Ok ( ( ) )
1653
+ WalletTx :: Tx ( tx) => {
1654
+ all_abandoned_txs. push ( WalletTx :: Tx ( tx. clone ( ) ) ) ;
1655
+ match tx. state ( ) {
1656
+ TxState :: Inactive ( _) => {
1657
+ tx. set_state ( TxState :: Abandoned ) ;
1658
+ txs_to_rollback. push ( entry. get ( ) . clone ( ) ) ;
1659
+ Ok ( ( ) )
1660
+ }
1661
+ TxState :: Conflicted ( _) => {
1662
+ tx. set_state ( TxState :: Abandoned ) ;
1663
+ Ok ( ( ) )
1664
+ }
1665
+ state => Err ( WalletError :: CannotChangeTransactionState (
1666
+ * state,
1667
+ TxState :: Abandoned ,
1668
+ ) ) ,
1637
1669
}
1638
- state => Err ( WalletError :: CannotChangeTransactionState (
1639
- * state,
1640
- TxState :: Abandoned ,
1641
- ) ) ,
1642
- } ,
1670
+ }
1643
1671
} ,
1644
1672
Entry :: Vacant ( _) => Err ( WalletError :: CannotFindTransactionWithId ( tx_id) ) ,
1645
1673
} ?;
1646
1674
}
1647
1675
1648
- for tx in txs_to_rollback {
1649
- self . rollback_tx_data ( & tx) ?;
1676
+ for tx in & txs_to_rollback {
1677
+ self . rollback_tx_data ( tx) ?;
1650
1678
}
1651
1679
1652
- Ok ( all_abandoned)
1680
+ Ok ( all_abandoned. into_iter ( ) . zip_eq ( all_abandoned_txs ) . collect ( ) )
1653
1681
}
1654
1682
1655
1683
pub fn get_transaction ( & self , transaction_id : Id < Transaction > ) -> WalletResult < & TxData > {
@@ -1915,3 +1943,6 @@ fn uses_conflicting_nonce(
1915
1943
}
1916
1944
} )
1917
1945
}
1946
+
1947
+ #[ cfg( test) ]
1948
+ mod tests;
0 commit comments