Skip to content

Commit 823b687

Browse files
committed
Check outputs in OutputCache::violates_frozen_token
1 parent fc6628f commit 823b687

File tree

2 files changed

+189
-32
lines changed

2 files changed

+189
-32
lines changed

wallet/src/account/output_cache/mod.rs

Lines changed: 31 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -890,36 +890,33 @@ impl OutputCache {
890890
}
891891

892892
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.
896-
unconfirmed_tx.inputs().iter().any(|inp| match inp {
893+
let output_uses_frozen_token = |output: &TxOutput| match output {
894+
TxOutput::Transfer(v, _)
895+
| TxOutput::LockThenTransfer(v, _, _)
896+
| TxOutput::Burn(v)
897+
| TxOutput::Htlc(v, _) => match v {
898+
OutputValue::TokenV1(token_id, _) => frozen_token_id == token_id,
899+
OutputValue::TokenV0(_) | OutputValue::Coin(_) => false,
900+
},
901+
TxOutput::CreateOrder(data) => [data.ask(), data.give()].iter().any(|v| match v {
902+
OutputValue::TokenV1(token_id, _) => frozen_token_id == token_id,
903+
OutputValue::TokenV0(_) | OutputValue::Coin(_) => false,
904+
}),
905+
TxOutput::IssueNft(_, _, _)
906+
| TxOutput::DataDeposit(_)
907+
| TxOutput::CreateStakePool(_, _)
908+
| TxOutput::DelegateStaking(_, _)
909+
| TxOutput::CreateDelegationId(_, _)
910+
| TxOutput::IssueFungibleToken(_)
911+
| TxOutput::ProduceBlockFromStake(_, _) => false,
912+
};
913+
914+
let in_inputs = unconfirmed_tx.inputs().iter().any(|input| match input {
897915
TxInput::Utxo(outpoint) => self.txs.get(&outpoint.source_id()).is_some_and(|tx| {
898916
let output =
899917
tx.outputs().get(outpoint.output_index() as usize).expect("must be present");
900918

901-
match output {
902-
TxOutput::Transfer(v, _)
903-
| TxOutput::LockThenTransfer(v, _, _)
904-
| TxOutput::Burn(v)
905-
| TxOutput::Htlc(v, _) => match v {
906-
OutputValue::TokenV1(token_id, _) => frozen_token_id == token_id,
907-
OutputValue::TokenV0(_) | OutputValue::Coin(_) => false,
908-
},
909-
TxOutput::CreateOrder(data) => {
910-
[data.ask(), data.give()].iter().any(|v| match v {
911-
OutputValue::TokenV1(token_id, _) => frozen_token_id == token_id,
912-
OutputValue::TokenV0(_) | OutputValue::Coin(_) => false,
913-
})
914-
}
915-
TxOutput::IssueNft(_, _, _)
916-
| TxOutput::DataDeposit(_)
917-
| TxOutput::CreateStakePool(_, _)
918-
| TxOutput::DelegateStaking(_, _)
919-
| TxOutput::CreateDelegationId(_, _)
920-
| TxOutput::IssueFungibleToken(_)
921-
| TxOutput::ProduceBlockFromStake(_, _) => false,
922-
}
919+
output_uses_frozen_token(output)
923920
}),
924921
TxInput::AccountCommand(_, cmd) => match cmd {
925922
AccountCommand::LockTokenSupply(token_id)
@@ -952,7 +949,14 @@ impl OutputCache {
952949
OrderAccountCommand::FreezeOrder(_) => false,
953950
},
954951
TxInput::Account(_) => false,
955-
})
952+
});
953+
954+
// Note: it's possible to have a token in tx outputs but not in inputs:
955+
// 1) zero amount transfers don't require the token to be present in inputs;
956+
// 2) for a CreateOrder output the ask currency won't be present in the inputs either.
957+
let in_outputs = unconfirmed_tx.outputs().iter().any(output_uses_frozen_token);
958+
959+
in_inputs || in_outputs
956960
}
957961

958962
pub fn add_tx(&mut self, tx_id: OutPointSourceId, tx: WalletTx) -> WalletResult<()> {

wallet/src/account/output_cache/tests.rs

Lines changed: 158 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,14 @@
1313
// See the License for the specific language governing permissions and
1414
// limitations under the License.
1515

16+
use rstest::rstest;
17+
1618
use chainstate_test_framework::{empty_witness, TransactionBuilder};
17-
use common::{chain::signature::inputsig::InputWitness, primitives::H256};
19+
use common::{
20+
chain::{signature::inputsig::InputWitness, timelock::OutputTimeLock, OrderData},
21+
primitives::H256,
22+
};
1823
use randomness::Rng;
19-
use rstest::rstest;
2024
use test_utils::random::{make_seedable_rng, Seed};
2125

2226
use super::*;
@@ -48,6 +52,10 @@ fn diamond_unconfirmed_descendants(#[case] seed: Seed) {
4852
OutputValue::Coin(Amount::from_atoms(rng.gen())),
4953
Destination::AnyoneCanSpend,
5054
))
55+
.add_output(TxOutput::Transfer(
56+
OutputValue::Coin(Amount::from_atoms(rng.gen())),
57+
Destination::AnyoneCanSpend,
58+
))
5159
.build();
5260
let tx_a_id = tx_a.transaction().get_id();
5361
output_cache
@@ -79,7 +87,7 @@ fn diamond_unconfirmed_descendants(#[case] seed: Seed) {
7987
// C
8088
let tx_c = TransactionBuilder::new()
8189
.add_input(
82-
TxInput::from_utxo(tx_a_id.into(), 0),
90+
TxInput::from_utxo(tx_a_id.into(), 1),
8391
empty_witness(&mut rng),
8492
)
8593
.add_output(TxOutput::Transfer(
@@ -140,7 +148,7 @@ fn diamond_unconfirmed_descendants(#[case] seed: Seed) {
140148
assert!(output_cache.unconfirmed_descendants.is_empty());
141149
}
142150

143-
// Create 2 unconfirmed txs B and C that spends tokens:
151+
// Create 2 unconfirmed txs B and C that spend tokens:
144152
//
145153
// /-->B-->C
146154
// A
@@ -151,7 +159,7 @@ fn diamond_unconfirmed_descendants(#[case] seed: Seed) {
151159
#[rstest]
152160
#[trace]
153161
#[case(Seed::from_entropy())]
154-
fn conflict_parent_and_child(#[case] seed: Seed) {
162+
fn update_conflicting_txs_parent_and_child(#[case] seed: Seed) {
155163
let mut rng = make_seedable_rng(seed);
156164

157165
let mut output_cache = OutputCache::empty();
@@ -245,3 +253,148 @@ fn conflict_parent_and_child(#[case] seed: Seed) {
245253
]
246254
);
247255
}
256+
257+
// Create unconfirmed txs Bi that use a token in their outputs only:
258+
// a) by transferring zero amount of the token (a legit situation which doesn't require the token
259+
// to be present in the inputs);
260+
// b) creating an order that asks for the token.
261+
//
262+
// /-->Bi
263+
// A
264+
// \-->C
265+
//
266+
// Freeze token in C.
267+
// Check that Bi got marked as conflicted.
268+
#[rstest]
269+
#[trace]
270+
#[case(Seed::from_entropy())]
271+
fn update_conflicting_txs_frozen_token_only_in_outputs(#[case] seed: Seed) {
272+
let mut rng = make_seedable_rng(seed);
273+
274+
let mut output_cache = OutputCache::empty();
275+
let token_id = TokenId::random_using(&mut rng);
276+
277+
let genesis_tx_id = Id::<Transaction>::new(H256::random_using(&mut rng));
278+
let tx_a = TransactionBuilder::new()
279+
.add_input(
280+
TxInput::from_utxo(genesis_tx_id.into(), 0),
281+
InputWitness::NoSignature(None),
282+
)
283+
// Note: only coin outputs here
284+
.add_output(TxOutput::Transfer(
285+
OutputValue::Coin(Amount::from_atoms(rng.gen())),
286+
Destination::AnyoneCanSpend,
287+
))
288+
.add_output(TxOutput::Transfer(
289+
OutputValue::Coin(Amount::from_atoms(rng.gen())),
290+
Destination::AnyoneCanSpend,
291+
))
292+
.add_output(TxOutput::Transfer(
293+
OutputValue::Coin(Amount::from_atoms(rng.gen())),
294+
Destination::AnyoneCanSpend,
295+
))
296+
.build();
297+
let tx_a_id = tx_a.transaction().get_id();
298+
output_cache
299+
.add_tx(
300+
tx_a_id.into(),
301+
WalletTx::Tx(TxData::new(
302+
tx_a,
303+
TxState::Confirmed(BlockHeight::zero(), BlockTimestamp::from_int_seconds(0), 0),
304+
)),
305+
)
306+
.unwrap();
307+
308+
// Transfer zero amount
309+
let tx_b1 = TransactionBuilder::new()
310+
.add_input(
311+
TxInput::from_utxo(tx_a_id.into(), 0),
312+
empty_witness(&mut rng),
313+
)
314+
.add_output(TxOutput::Transfer(
315+
OutputValue::TokenV1(token_id, Amount::ZERO),
316+
Destination::AnyoneCanSpend,
317+
))
318+
.build();
319+
let tx_b1_id = tx_b1.transaction().get_id();
320+
output_cache
321+
.add_tx(
322+
tx_b1_id.into(),
323+
WalletTx::Tx(TxData::new(tx_b1.clone(), TxState::Inactive(0))),
324+
)
325+
.unwrap();
326+
327+
// LockThenTransfer zero amount
328+
let tx_b2 = TransactionBuilder::new()
329+
.add_input(
330+
TxInput::from_utxo(tx_a_id.into(), 1),
331+
empty_witness(&mut rng),
332+
)
333+
.add_output(TxOutput::LockThenTransfer(
334+
OutputValue::TokenV1(token_id, Amount::ZERO),
335+
Destination::AnyoneCanSpend,
336+
OutputTimeLock::ForBlockCount(1),
337+
))
338+
.build();
339+
let tx_b2_id = tx_b2.transaction().get_id();
340+
output_cache
341+
.add_tx(
342+
tx_b2_id.into(),
343+
WalletTx::Tx(TxData::new(tx_b2.clone(), TxState::Inactive(0))),
344+
)
345+
.unwrap();
346+
347+
let tx_b3 = TransactionBuilder::new()
348+
.add_input(
349+
TxInput::from_utxo(tx_a_id.into(), 2),
350+
empty_witness(&mut rng),
351+
)
352+
.add_output(TxOutput::CreateOrder(Box::new(OrderData::new(
353+
Destination::AnyoneCanSpend,
354+
OutputValue::TokenV1(token_id, Amount::from_atoms(rng.gen())),
355+
OutputValue::Coin(Amount::from_atoms(rng.gen())),
356+
))))
357+
.build();
358+
let tx_b3_id = tx_b3.transaction().get_id();
359+
output_cache
360+
.add_tx(
361+
tx_b3_id.into(),
362+
WalletTx::Tx(TxData::new(tx_b3.clone(), TxState::Inactive(0))),
363+
)
364+
.unwrap();
365+
366+
let tx_d = TransactionBuilder::new()
367+
.add_input(
368+
TxInput::AccountCommand(
369+
AccountNonce::new(0),
370+
AccountCommand::FreezeToken(token_id, IsTokenUnfreezable::No),
371+
),
372+
empty_witness(&mut rng),
373+
)
374+
.build();
375+
376+
let block_id = Id::<GenBlock>::new(H256::random_using(&mut rng));
377+
let result = output_cache
378+
.update_conflicting_txs(tx_d.transaction(), block_id)
379+
.unwrap()
380+
.into_iter()
381+
.collect::<BTreeMap<_, _>>();
382+
383+
assert_eq!(
384+
result,
385+
BTreeMap::from([
386+
(
387+
tx_b1_id,
388+
WalletTx::Tx(TxData::new(tx_b1, TxState::Conflicted(block_id)))
389+
),
390+
(
391+
tx_b2_id,
392+
WalletTx::Tx(TxData::new(tx_b2, TxState::Conflicted(block_id)))
393+
),
394+
(
395+
tx_b3_id,
396+
WalletTx::Tx(TxData::new(tx_b3, TxState::Conflicted(block_id)))
397+
),
398+
])
399+
);
400+
}

0 commit comments

Comments
 (0)