diff --git a/Cargo.lock b/Cargo.lock index 8bd2884647..a0c6a83eba 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -272,6 +272,7 @@ dependencies = [ "logging", "mempool", "node-comm", + "orders-accounting", "pos-accounting", "randomness", "rstest", @@ -1163,6 +1164,7 @@ dependencies = [ "mockall", "num", "oneshot", + "orders-accounting", "parity-scale-codec", "pos-accounting", "randomness", @@ -1213,6 +1215,7 @@ dependencies = [ "logging", "mockall", "num-traits", + "orders-accounting", "parity-scale-codec", "pos-accounting", "randomness", @@ -1240,6 +1243,7 @@ dependencies = [ "hex", "itertools 0.13.0", "logging", + "orders-accounting", "pos-accounting", "randomness", "rstest", @@ -1273,6 +1277,7 @@ dependencies = [ "hex", "itertools 0.13.0", "logging", + "orders-accounting", "pos-accounting", "randomness", "rstest", @@ -1573,6 +1578,7 @@ version = "0.5.1" dependencies = [ "common", "crypto", + "orders-accounting", "pos-accounting", "randomness", "rstest", @@ -4075,6 +4081,7 @@ dependencies = [ "mintscript", "mockall", "num-traits", + "orders-accounting", "p2p-types", "parking_lot 0.12.3", "pos-accounting", @@ -4201,6 +4208,7 @@ dependencies = [ "crypto", "expect-test", "hex", + "orders-accounting", "pos-accounting", "rstest", "serialization", @@ -4896,6 +4904,25 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "orders-accounting" +version = "0.5.1" +dependencies = [ + "accounting", + "chainstate-types", + "common", + "crypto", + "logging", + "parity-scale-codec", + "randomness", + "rstest", + "serialization", + "test-utils", + "thiserror", + "utils", + "variant_count", +] + [[package]] name = "ouroboros" version = "0.18.4" @@ -7531,6 +7558,7 @@ dependencies = [ "chainstate-types", "common", "crypto", + "logging", "parity-scale-codec", "randomness", "rstest", @@ -7982,6 +8010,7 @@ dependencies = [ "logging", "mintscript", "mockall", + "orders-accounting", "pos-accounting", "randomness", "replace_with", diff --git a/Cargo.toml b/Cargo.toml index f23e834e5b..e7987a9a41 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,6 +34,7 @@ members = [ "networking", # Pure networking implementations "node-gui", # Node GUI binary. "node-lib", # Node lib; the common library between daemon, tui and gui node executables. + "orders-accounting", # Orders accounting "p2p", # P2p communication interfaces and protocols. "p2p/backend-test-suite", # P2p backend agnostic tests. "p2p/types", # P2p support types with minimal dependencies. diff --git a/api-server/scanner-lib/Cargo.toml b/api-server/scanner-lib/Cargo.toml index 5ba90996ac..57c435093e 100644 --- a/api-server/scanner-lib/Cargo.toml +++ b/api-server/scanner-lib/Cargo.toml @@ -15,6 +15,7 @@ constraints-value-accumulator = { path = "../../chainstate/constraints-value-acc logging = { path = "../../logging" } mempool = { path = "../../mempool" } node-comm = { path = "../../wallet/wallet-node-client" } +orders-accounting = { path = "../../orders-accounting" } pos-accounting = { path = "../../pos-accounting" } randomness = { path = "../../randomness" } utils = { path = "../../utils" } diff --git a/api-server/scanner-lib/src/blockchain_state/mod.rs b/api-server/scanner-lib/src/blockchain_state/mod.rs index 3ff5d4c971..522ed80da8 100644 --- a/api-server/scanner-lib/src/blockchain_state/mod.rs +++ b/api-server/scanner-lib/src/blockchain_state/mod.rs @@ -396,7 +396,8 @@ async fn update_tables_from_block_reward( | TxOutput::DelegateStaking(_, _) | TxOutput::IssueFungibleToken(_) | TxOutput::IssueNft(_, _, _) - | TxOutput::Htlc(_, _) => {} + | TxOutput::Htlc(_, _) + | TxOutput::AnyoneCanTake(_) => {} TxOutput::ProduceBlockFromStake(_, _) => { set_utxo( outpoint, @@ -571,7 +572,8 @@ async fn calculate_fees( | TxOutput::DelegateStaking(_, _) | TxOutput::CreateDelegationId(_, _) | TxOutput::ProduceBlockFromStake(_, _) - | TxOutput::Htlc(_, _) => None, + | TxOutput::Htlc(_, _) + | TxOutput::AnyoneCanTake(_) => None, }) }) .collect(); @@ -604,7 +606,8 @@ async fn calculate_fees( | TxOutput::CreateDelegationId(_, _) | TxOutput::IssueFungibleToken(_) | TxOutput::ProduceBlockFromStake(_, _) - | TxOutput::Htlc(_, _) => None, + | TxOutput::Htlc(_, _) + | TxOutput::AnyoneCanTake(_) => None, }, TxInput::Account(_) => None, TxInput::AccountCommand(_, cmd) => match cmd { @@ -614,6 +617,7 @@ async fn calculate_fees( | AccountCommand::UnfreezeToken(token_id) | AccountCommand::LockTokenSupply(token_id) | AccountCommand::ChangeTokenAuthority(token_id, _) => Some(*token_id), + AccountCommand::ConcludeOrder(_) | AccountCommand::FillOrder(_, _, _) => None, }, }) .collect(); @@ -672,6 +676,56 @@ async fn token_decimals( Ok((token_id, decimals)) } +struct PoSAccountingAdapterToCheckFees { + pools: BTreeMap, +} + +impl PoSAccountingView for PoSAccountingAdapterToCheckFees { + type Error = pos_accounting::Error; + + fn pool_exists(&self, _pool_id: PoolId) -> Result { + unimplemented!() + } + + fn get_pool_balance(&self, _pool_id: PoolId) -> Result, Self::Error> { + unimplemented!() + } + + fn get_pool_data(&self, pool_id: PoolId) -> Result, Self::Error> { + Ok(self.pools.get(&pool_id).cloned()) + } + + fn get_pool_delegations_shares( + &self, + _pool_id: PoolId, + ) -> Result>, Self::Error> { + unimplemented!() + } + + fn get_delegation_balance( + &self, + _delegation_id: DelegationId, + ) -> Result, Self::Error> { + // only used for checks for attempted to print money but we don't need to check that here + Ok(Some(Amount::MAX)) + } + + fn get_delegation_data( + &self, + _delegation_id: DelegationId, + ) -> Result, Self::Error> { + unimplemented!() + } + + fn get_pool_delegation_share( + &self, + _pool_id: PoolId, + _delegation_id: DelegationId, + ) -> Result, Self::Error> { + unimplemented!() + } +} + async fn tx_fees( chain_config: &ChainConfig, block_height: BlockHeight, @@ -680,17 +734,18 @@ async fn tx_fees( new_outputs: &BTreeMap, ) -> Result { let inputs_utxos = collect_inputs_utxos(db_tx, tx.inputs(), new_outputs).await?; - let pools = prefetch_pool_amounts(&inputs_utxos, db_tx).await?; + let pools = prefetch_pool_data(&inputs_utxos, db_tx).await?; + let pos_accounting_adapter = PoSAccountingAdapterToCheckFees { pools }; - let staker_balance_getter = |pool_id: PoolId| Ok(pools.get(&pool_id).cloned()); - // only used for checks for attempted to print money but we don't need to check that here - let delegation_balance_getter = |_delegation_id: DelegationId| Ok(Some(Amount::MAX)); + // TODO(orders) + let orders_store = orders_accounting::InMemoryOrdersAccounting::new(); + let orders_db = orders_accounting::OrdersAccountingDB::new(&orders_store); let inputs_accumulator = ConstrainedValueAccumulator::from_inputs( chain_config, block_height, - staker_balance_getter, - delegation_balance_getter, + &orders_db, + &pos_accounting_adapter, tx.inputs(), &inputs_utxos, ) @@ -703,23 +758,18 @@ async fn tx_fees( Ok(consumed_accumulator) } -async fn prefetch_pool_amounts( +async fn prefetch_pool_data( inputs_utxos: &Vec>, db_tx: &mut T, -) -> Result, ApiServerStorageError> { +) -> Result, ApiServerStorageError> { let mut pools = BTreeMap::new(); for output in inputs_utxos { match output { Some( TxOutput::CreateStakePool(pool_id, _) | TxOutput::ProduceBlockFromStake(_, pool_id), ) => { - let amount = db_tx - .get_pool_data(*pool_id) - .await? - .expect("should exist") - .staker_balance() - .expect("no overflow"); - pools.insert(*pool_id, amount); + let data = db_tx.get_pool_data(*pool_id).await?.expect("should exist"); + pools.insert(*pool_id, data); } Some( TxOutput::Burn(_) @@ -730,7 +780,8 @@ async fn prefetch_pool_amounts( | TxOutput::DelegateStaking(_, _) | TxOutput::IssueNft(_, _, _) | TxOutput::IssueFungibleToken(_) - | TxOutput::Htlc(_, _), + | TxOutput::Htlc(_, _) + | TxOutput::AnyoneCanTake(_), ) => {} None => {} } @@ -1055,6 +1106,9 @@ async fn update_tables_from_transaction_inputs( ) .await; } + AccountCommand::ConcludeOrder(_) | AccountCommand::FillOrder(_, _, _) => { + // TODO(orders) + } }, TxInput::Account(outpoint) => { match outpoint.account() { @@ -1108,7 +1162,8 @@ async fn update_tables_from_transaction_inputs( | TxOutput::CreateDelegationId(_, _) | TxOutput::DelegateStaking(_, _) | TxOutput::IssueFungibleToken(_) - | TxOutput::Htlc(_, _) => {} + | TxOutput::Htlc(_, _) + | TxOutput::AnyoneCanTake(_) => {} TxOutput::CreateStakePool(pool_id, _) | TxOutput::ProduceBlockFromStake(_, pool_id) => { let pool_data = db_tx @@ -1161,8 +1216,9 @@ async fn update_tables_from_transaction_inputs( | TxOutput::CreateDelegationId(_, _) | TxOutput::DelegateStaking(_, _) | TxOutput::DataDeposit(_) - | TxOutput::IssueFungibleToken(_) => {} - | TxOutput::CreateStakePool(pool_id, _) + | TxOutput::IssueFungibleToken(_) + | TxOutput::AnyoneCanTake(_) => {} + TxOutput::CreateStakePool(pool_id, _) | TxOutput::ProduceBlockFromStake(_, pool_id) => { let pool_data = db_tx .get_pool_data(pool_id) @@ -1464,7 +1520,7 @@ async fn update_tables_from_transaction_outputs( .expect("Unable to encode address"); address_transactions.entry(staker_address).or_default().insert(transaction_id); } - | TxOutput::DelegateStaking(amount, delegation_id) => { + TxOutput::DelegateStaking(amount, delegation_id) => { // Update delegation pledge let delegation = db_tx @@ -1617,6 +1673,9 @@ async fn update_tables_from_transaction_outputs( } } TxOutput::Htlc(_, _) => {} // TODO(HTLC) + TxOutput::AnyoneCanTake(_) => { + // TODO(orders) + } } } @@ -1815,7 +1874,8 @@ fn get_tx_output_destination(txo: &TxOutput) -> Option<&Destination> { TxOutput::IssueFungibleToken(_) | TxOutput::Burn(_) | TxOutput::DelegateStaking(_, _) - | TxOutput::DataDeposit(_) => None, + | TxOutput::DataDeposit(_) + | TxOutput::AnyoneCanTake(_) => None, TxOutput::Htlc(_, _) => None, // TODO(HTLC) } } diff --git a/api-server/scanner-lib/src/sync/tests/simulation.rs b/api-server/scanner-lib/src/sync/tests/simulation.rs index ebbf2dd89e..16e18d4897 100644 --- a/api-server/scanner-lib/src/sync/tests/simulation.rs +++ b/api-server/scanner-lib/src/sync/tests/simulation.rs @@ -31,7 +31,7 @@ use api_server_common::storage::{ }, }; -use chainstate::{BlockSource, ChainstateConfig}; +use chainstate::{chainstate_interface::ChainstateInterface, BlockSource, ChainstateConfig}; use chainstate_test_framework::TestFramework; use common::{ chain::{ @@ -49,11 +49,70 @@ use crypto::{ key::{KeyKind, PrivateKey}, vrf::{VRFKeyKind, VRFPrivateKey}, }; -use pos_accounting::make_delegation_id; +use pos_accounting::{make_delegation_id, PoSAccountingView}; use randomness::Rng; use rstest::rstest; use test_utils::random::{make_seedable_rng, Seed}; +struct PoSAccountingAdapterToCheckFees<'a> { + chainstate: &'a dyn ChainstateInterface, +} + +impl<'a> PoSAccountingAdapterToCheckFees<'a> { + pub fn new(chainstate: &'a dyn ChainstateInterface) -> Self { + Self { chainstate } + } +} + +impl<'a> PoSAccountingView for PoSAccountingAdapterToCheckFees<'a> { + type Error = pos_accounting::Error; + + fn pool_exists(&self, _pool_id: PoolId) -> Result { + unimplemented!() + } + + fn get_pool_balance(&self, _pool_id: PoolId) -> Result, Self::Error> { + unimplemented!() + } + + fn get_pool_data( + &self, + pool_id: PoolId, + ) -> Result, Self::Error> { + Ok(self.chainstate.get_stake_pool_data(pool_id).unwrap()) + } + + fn get_pool_delegations_shares( + &self, + _pool_id: PoolId, + ) -> Result>, Self::Error> { + unimplemented!() + } + + fn get_delegation_balance( + &self, + _delegation_id: DelegationId, + ) -> Result, Self::Error> { + // only used for checks for attempted to print money but we don't need to check that here + Ok(Some(Amount::MAX)) + } + + fn get_delegation_data( + &self, + _delegation_id: DelegationId, + ) -> Result, Self::Error> { + unimplemented!() + } + + fn get_pool_delegation_share( + &self, + _pool_id: PoolId, + _delegation_id: DelegationId, + ) -> Result, Self::Error> { + unimplemented!() + } +} + #[rstest] #[trace] #[case(test_utils::random::Seed::from_entropy(), 20, 50)] @@ -152,6 +211,10 @@ async fn simulation( let mut delegations = BTreeSet::new(); let mut token_ids = BTreeSet::new(); + // TODO(orders) + let orders_store = orders_accounting::InMemoryOrdersAccounting::new(); + let orders_db = orders_accounting::OrdersAccountingDB::new(&orders_store); + let mut data_per_block_height = BTreeMap::new(); data_per_block_height.insert( BlockHeight::zero(), @@ -236,7 +299,7 @@ async fn simulation( let mut block_builder = tf.make_pos_block_builder().with_random_staking_pool(&mut rng); for _ in 0..rng.gen_range(10..max_tx_per_block) { - block_builder = block_builder.add_test_transaction(&mut rng, false); + block_builder = block_builder.add_test_transaction(&mut rng, false, false); } let block = block_builder.build(&mut rng); @@ -260,7 +323,8 @@ async fn simulation( | TxOutput::IssueFungibleToken(_) | TxOutput::ProduceBlockFromStake(_, _) | TxOutput::IssueNft(_, _, _) - | TxOutput::Htlc(_, _) => None, + | TxOutput::Htlc(_, _) + | TxOutput::AnyoneCanTake(_) => None, }); staking_pools.extend(new_pools); @@ -279,7 +343,8 @@ async fn simulation( | TxOutput::IssueFungibleToken(_) | TxOutput::ProduceBlockFromStake(_, _) | TxOutput::IssueNft(_, _, _) - | TxOutput::Htlc(_, _) => None, + | TxOutput::Htlc(_, _) + | TxOutput::AnyoneCanTake(_) => None, }); delegations.extend(new_delegations); @@ -295,7 +360,8 @@ async fn simulation( | TxOutput::DelegateStaking(_, _) | TxOutput::CreateDelegationId(_, _) | TxOutput::ProduceBlockFromStake(_, _) - | TxOutput::Htlc(_, _) => None, + | TxOutput::Htlc(_, _) + | TxOutput::AnyoneCanTake(_) => None, }); token_ids.extend(new_tokens); @@ -340,6 +406,7 @@ async fn simulation( | TxOutput::LockThenTransfer(_, _, _) | TxOutput::ProduceBlockFromStake(_, _) | TxOutput::Htlc(_, _) => {} + TxOutput::AnyoneCanTake(_) => unimplemented!(), // TODO(orders) }); tx.inputs().iter().for_each(|inp| match inp { @@ -360,7 +427,9 @@ async fn simulation( statistics .entry(CoinOrTokenStatistic::CirculatingSupply) .or_default() - .insert(CoinOrTokenId::TokenId(*token_id), *to_mint); + .entry(CoinOrTokenId::TokenId(*token_id)) + .and_modify(|amount| *amount = (*amount + *to_mint).unwrap()) + .or_insert(*to_mint); let token_supply_change_fee = chain_config.token_supply_change_fee(block_height); burn_coins(&mut statistics, token_supply_change_fee); @@ -384,6 +453,9 @@ async fn simulation( chain_config.token_change_authority_fee(block_height); burn_coins(&mut statistics, token_change_authority_fee); } + AccountCommand::ConcludeOrder(_) | AccountCommand::FillOrder(_, _, _) => { + unimplemented!() // TODO(orders) + } }, }); } @@ -402,6 +474,8 @@ async fn simulation( .and_modify(|amount| *amount = (*amount + block_subsidy).unwrap()) .or_insert(block_subsidy); + let pos_store = PoSAccountingAdapterToCheckFees::new(tf.chainstate.as_ref()); + let mut total_fees = AccumulatedFee::new(); for tx in block.transactions().iter() { let inputs_utxos: Vec<_> = tx @@ -415,25 +489,11 @@ async fn simulation( }) .collect(); - let staker_balance_getter = |pool_id: PoolId| { - Ok(Some( - tf.chainstate - .get_stake_pool_data(pool_id) - .unwrap() - .unwrap() - .staker_balance() - .unwrap(), - )) - }; - // only used for checks for attempted to print money but we don't need to check that here - let delegation_balance_getter = - |_delegation_id: DelegationId| Ok(Some(Amount::MAX)); - let inputs_accumulator = ConstrainedValueAccumulator::from_inputs( &chain_config, block_height, - staker_balance_getter, - delegation_balance_getter, + &orders_db, + &pos_store, tx.inputs(), &inputs_utxos, ) diff --git a/api-server/stack-test-suite/tests/v2/transaction.rs b/api-server/stack-test-suite/tests/v2/transaction.rs index 54caadf31a..60d6a297e0 100644 --- a/api-server/stack-test-suite/tests/v2/transaction.rs +++ b/api-server/stack-test-suite/tests/v2/transaction.rs @@ -194,7 +194,7 @@ async fn multiple_tx_in_same_block(#[case] seed: Seed) { "is_replaceable": transaction.is_replaceable(), "flags": transaction.flags(), "inputs": transaction.inputs().iter().zip(utxos).map(|(inp, utxo)| json!({ - "input": tx_input_to_json(inp, &chain_config), + "input": tx_input_to_json(inp, &chain_config, &TokenDecimals::Single(None)), "utxo": utxo.as_ref().map(|txo| txoutput_to_json(txo, &chain_config, &TokenDecimals::Single(None))), })).collect::>(), "outputs": transaction.outputs() @@ -338,7 +338,7 @@ async fn ok(#[case] seed: Seed) { "is_replaceable": transaction.is_replaceable(), "flags": transaction.flags(), "inputs": transaction.inputs().iter().zip(utxos).map(|(inp, utxo)| json!({ - "input": tx_input_to_json(inp, &chain_config), + "input": tx_input_to_json(inp, &chain_config, &TokenDecimals::Single(None)), "utxo": utxo.as_ref().map(|txo| txoutput_to_json(txo, &chain_config, &TokenDecimals::Single(None))), })).collect::>(), "outputs": transaction.outputs() diff --git a/api-server/web-server/src/api/json_helpers.rs b/api-server/web-server/src/api/json_helpers.rs index cf77b95a7a..73a88d971e 100644 --- a/api-server/web-server/src/api/json_helpers.rs +++ b/api-server/web-server/src/api/json_helpers.rs @@ -197,6 +197,14 @@ pub fn txoutput_to_json( }, }) } + TxOutput::AnyoneCanTake(data) => { + json!({ + "type": "AnyoneCanTake", + "conclude_key": Address::new(chain_config, data.conclude_key().clone()).expect("no error").as_str(), + "ask_value": outputvalue_to_json(data.ask(), chain_config, token_decimals), + "give_value": outputvalue_to_json(data.give(), chain_config, token_decimals), + }) + } } } @@ -246,7 +254,11 @@ pub fn utxo_outpoint_to_json(utxo: &UtxoOutPoint) -> serde_json::Value { } } -pub fn tx_input_to_json(inp: &TxInput, chain_config: &ChainConfig) -> serde_json::Value { +pub fn tx_input_to_json( + inp: &TxInput, + chain_config: &ChainConfig, + token_decimals: &TokenDecimals, +) -> serde_json::Value { match inp { TxInput::Utxo(utxo) => match utxo.source_id() { OutPointSourceId::Transaction(tx_id) => { @@ -333,6 +345,22 @@ pub fn tx_input_to_json(inp: &TxInput, chain_config: &ChainConfig) -> serde_json "nonce": nonce, }) } + AccountCommand::ConcludeOrder(order_id) => { + json!({ + "input_type": "AccountCommand", + "command": "ConcludeOrder", + "order_id": Address::new(chain_config, *order_id).expect("addressable").to_string(), + }) + } + AccountCommand::FillOrder(order_id, fill, dest) => { + json!({ + "input_type": "AccountCommand", + "command": "FillOrder", + "order_id": Address::new(chain_config, *order_id).expect("addressable").to_string(), + "fill_value": outputvalue_to_json(fill, chain_config, token_decimals), + "destination": Address::new(chain_config, dest.clone()).expect("no error").as_str(), + }) + } }, } } @@ -349,7 +377,7 @@ pub fn tx_to_json( "flags": tx.flags(), "fee": amount_to_json(additional_info.fee, chain_config.coin_decimals()), "inputs": tx.inputs().iter().zip(additional_info.input_utxos.iter()).map(|(inp, utxo)| json!({ - "input": tx_input_to_json(inp, chain_config), + "input": tx_input_to_json(inp, chain_config, &(&additional_info.token_decimals).into()), "utxo": utxo.as_ref().map(|txo| txoutput_to_json(txo, chain_config, &(&additional_info.token_decimals).into())), })).collect::>(), "outputs": tx.outputs() diff --git a/chainstate/Cargo.toml b/chainstate/Cargo.toml index 5316243241..adc79aca10 100644 --- a/chainstate/Cargo.toml +++ b/chainstate/Cargo.toml @@ -15,6 +15,7 @@ constraints-value-accumulator = { path = "./constraints-value-accumulator" } crypto = { path = "../crypto" } logging = { path = "../logging" } mintscript = { path = "../mintscript" } +orders-accounting = { path = "../orders-accounting" } pos-accounting = { path = "../pos-accounting" } randomness = { path = "../randomness" } rpc = { path = "../rpc" } diff --git a/chainstate/constraints-value-accumulator/Cargo.toml b/chainstate/constraints-value-accumulator/Cargo.toml index 37263ee904..bd41e3388d 100644 --- a/chainstate/constraints-value-accumulator/Cargo.toml +++ b/chainstate/constraints-value-accumulator/Cargo.toml @@ -10,6 +10,7 @@ rust-version.workspace = true [dependencies] common = { path = "../../common" } crypto = { path = "../../crypto" } +orders-accounting = { path = "../../orders-accounting" } pos-accounting = { path = "../../pos-accounting" } randomness = { path = "../../randomness" } utils = { path = "../../utils" } diff --git a/chainstate/constraints-value-accumulator/src/accounts_balances_tracker.rs b/chainstate/constraints-value-accumulator/src/accounts_balances_tracker.rs index 626e0dc23b..0472675d42 100644 --- a/chainstate/constraints-value-accumulator/src/accounts_balances_tracker.rs +++ b/chainstate/constraints-value-accumulator/src/accounts_balances_tracker.rs @@ -19,6 +19,7 @@ use common::{ chain::{AccountSpending, AccountType, DelegationId}, primitives::Amount, }; +use pos_accounting::PoSAccountingView; use crate::Error; @@ -43,20 +44,17 @@ impl From for AccountType { } } -pub struct AccountsBalancesTracker<'a, DelegationBalanceGetterFn> { +pub struct AccountsBalancesTracker

{ balances: BTreeMap, - delegation_balance_getter: &'a DelegationBalanceGetterFn, + pos_accounting_view: P, } -impl<'a, DelegationBalanceGetterFn> AccountsBalancesTracker<'a, DelegationBalanceGetterFn> -where - DelegationBalanceGetterFn: Fn(DelegationId) -> Result, Error>, -{ - pub fn new(delegation_balance_getter: &'a DelegationBalanceGetterFn) -> Self { +impl AccountsBalancesTracker

{ + pub fn new(pos_accounting_view: P) -> Self { Self { balances: BTreeMap::new(), - delegation_balance_getter, + pos_accounting_view, } } @@ -65,7 +63,10 @@ where Entry::Vacant(e) => { let (balance, spending) = match account { AccountSpending::DelegationBalance(id, spending) => { - let balance = (self.delegation_balance_getter)(id)? + let balance = self + .pos_accounting_view + .get_delegation_balance(id) + .map_err(|_| pos_accounting::Error::ViewFail)? .ok_or(Error::AccountBalanceNotFound(account.clone().into()))?; (balance, spending) } diff --git a/chainstate/constraints-value-accumulator/src/constraints_accumulator.rs b/chainstate/constraints-value-accumulator/src/constraints_accumulator.rs index 484d429023..2672c1513e 100644 --- a/chainstate/constraints-value-accumulator/src/constraints_accumulator.rs +++ b/chainstate/constraints-value-accumulator/src/constraints_accumulator.rs @@ -18,10 +18,12 @@ use std::{collections::BTreeMap, num::NonZeroU64}; use common::{ chain::{ output_value::OutputValue, timelock::OutputTimeLock, AccountCommand, AccountSpending, - ChainConfig, DelegationId, PoolId, TxInput, TxOutput, UtxoOutPoint, + AccountType, ChainConfig, TxInput, TxOutput, UtxoOutPoint, }, primitives::{Amount, BlockHeight, CoinOrTokenId, Fee, Subsidy}, }; +use orders_accounting::OrdersAccountingView; +use pos_accounting::PoSAccountingView; use utils::ensure; use crate::accounts_balances_tracker::AccountsBalancesTracker; @@ -55,27 +57,22 @@ impl ConstrainedValueAccumulator { }) } - pub fn from_inputs( + pub fn from_inputs( chain_config: &ChainConfig, block_height: BlockHeight, - staker_balance_getter: StakerBalanceGetterFn, - delegation_balance_getter: DelegationBalanceGetterFn, + orders_accounting_view: &impl OrdersAccountingView, + pos_accounting_view: &impl PoSAccountingView, inputs: &[TxInput], inputs_utxos: &[Option], - ) -> Result - where - StakerBalanceGetterFn: Fn(PoolId) -> Result, Error>, - DelegationBalanceGetterFn: Fn(DelegationId) -> Result, Error>, - { + ) -> Result { ensure!( inputs.len() == inputs_utxos.len(), Error::InputsAndInputsUtxosLengthMismatch(inputs.len(), inputs_utxos.len()) ); let mut accumulator = Self::new(); - let mut total_fee_deducted = Amount::ZERO; - let mut accounts_balances_tracker = - AccountsBalancesTracker::new(&delegation_balance_getter); + let mut total_to_deduct = BTreeMap::::new(); + let mut accounts_balances_tracker = AccountsBalancesTracker::new(pos_accounting_view); for (input, input_utxo) in inputs.iter().zip(inputs_utxos.iter()) { match input { @@ -85,7 +82,7 @@ impl ConstrainedValueAccumulator { accumulator.process_input_utxo( chain_config, block_height, - &staker_balance_getter, + pos_accounting_view, outpoint.clone(), input_utxo, )?; @@ -99,39 +96,38 @@ impl ConstrainedValueAccumulator { )?; } TxInput::AccountCommand(_, command) => { - let fee_to_deduct = accumulator.process_input_account_command( + let (id, to_deduct) = accumulator.process_input_account_command( chain_config, block_height, command, + orders_accounting_view, )?; - total_fee_deducted = (total_fee_deducted + fee_to_deduct) - .ok_or(Error::CoinOrTokenOverflow(CoinOrTokenId::Coin))?; + insert_or_increase(&mut total_to_deduct, id, to_deduct)?; } } } - decrease_or( - &mut accumulator.unconstrained_value, - CoinOrTokenId::Coin, - total_fee_deducted, - Error::AttemptToViolateFeeRequirements, - )?; + for (currency, amount) in total_to_deduct { + decrease_or( + &mut accumulator.unconstrained_value, + currency, + amount, + Error::AttemptToViolateFeeRequirements, + )?; + } Ok(accumulator) } - fn process_input_utxo( + fn process_input_utxo( &mut self, chain_config: &ChainConfig, block_height: BlockHeight, - staker_balance_getter: &StakerBalanceGetterFn, + pos_accounting_view: &impl PoSAccountingView, outpoint: UtxoOutPoint, input_utxo: &TxOutput, - ) -> Result<(), Error> - where - StakerBalanceGetterFn: Fn(PoolId) -> Result, Error>, - { + ) -> Result<(), Error> { match input_utxo { TxOutput::Transfer(value, _) | TxOutput::LockThenTransfer(value, _, _) @@ -152,8 +148,9 @@ impl ConstrainedValueAccumulator { } TxOutput::CreateDelegationId(..) | TxOutput::IssueFungibleToken(..) - | TxOutput::Burn(_) - | TxOutput::DataDeposit(_) => { + | TxOutput::Burn(..) + | TxOutput::DataDeposit(..) + | TxOutput::AnyoneCanTake(..) => { return Err(Error::SpendingNonSpendableOutput(outpoint.clone())); } TxOutput::IssueNft(token_id, _, _) => { @@ -167,8 +164,14 @@ impl ConstrainedValueAccumulator { insert_or_increase(&mut self.unconstrained_value, CoinOrTokenId::Coin, *coins)?; } TxOutput::CreateStakePool(pool_id, _) | TxOutput::ProduceBlockFromStake(_, pool_id) => { - let staker_balance = staker_balance_getter(*pool_id)? + let staker_balance = pos_accounting_view + .get_pool_data(*pool_id) + .map_err(|_| pos_accounting::Error::ViewFail)? + .map(|pool_data| pool_data.staker_balance()) + .transpose() + .map_err(Error::PoSAccountingError)? .ok_or(Error::PledgeAmountNotFound(*pool_id))?; + let maturity_distance = chain_config.staking_pool_spend_maturity_block_count(block_height); @@ -195,16 +198,13 @@ impl ConstrainedValueAccumulator { Ok(()) } - fn process_input_account( + fn process_input_account( &mut self, chain_config: &ChainConfig, block_height: BlockHeight, account: &AccountSpending, - accounts_balances_tracker: &mut AccountsBalancesTracker, - ) -> Result<(), Error> - where - DelegationBalanceGetterFn: Fn(DelegationId) -> Result, Error>, - { + accounts_balances_tracker: &mut AccountsBalancesTracker

, + ) -> Result<(), Error> { match account { AccountSpending::DelegationBalance(_, spend_amount) => { accounts_balances_tracker.spend_from_account(account.clone())?; @@ -240,7 +240,8 @@ impl ConstrainedValueAccumulator { chain_config: &ChainConfig, block_height: BlockHeight, command: &AccountCommand, - ) -> Result { + orders_accounting_view: &impl OrdersAccountingView, + ) -> Result<(CoinOrTokenId, Amount), Error> { match command { AccountCommand::MintTokens(token_id, amount) => { insert_or_increase( @@ -248,16 +249,68 @@ impl ConstrainedValueAccumulator { CoinOrTokenId::TokenId(*token_id), *amount, )?; - Ok(chain_config.token_supply_change_fee(block_height)) - } - AccountCommand::LockTokenSupply(_) | AccountCommand::UnmintTokens(_) => { - Ok(chain_config.token_supply_change_fee(block_height)) + Ok(( + CoinOrTokenId::Coin, + chain_config.token_supply_change_fee(block_height), + )) } - AccountCommand::FreezeToken(_, _) | AccountCommand::UnfreezeToken(_) => { - Ok(chain_config.token_freeze_fee(block_height)) + AccountCommand::LockTokenSupply(_) | AccountCommand::UnmintTokens(_) => Ok(( + CoinOrTokenId::Coin, + chain_config.token_supply_change_fee(block_height), + )), + AccountCommand::FreezeToken(_, _) | AccountCommand::UnfreezeToken(_) => Ok(( + CoinOrTokenId::Coin, + chain_config.token_freeze_fee(block_height), + )), + AccountCommand::ChangeTokenAuthority(_, _) => Ok(( + CoinOrTokenId::Coin, + chain_config.token_change_authority_fee(block_height), + )), + AccountCommand::ConcludeOrder(id) => { + let order_data = orders_accounting_view + .get_order_data(id) + .map_err(|_| orders_accounting::Error::ViewFail)? + .ok_or(orders_accounting::Error::OrderDataNotFound(*id))?; + let ask_balance = orders_accounting_view + .get_ask_balance(id) + .map_err(|_| orders_accounting::Error::ViewFail)? + .unwrap_or(Amount::ZERO); + let give_balance = orders_accounting_view + .get_give_balance(id) + .map_err(|_| orders_accounting::Error::ViewFail)? + .unwrap_or(Amount::ZERO); + + let initially_asked = output_value_amount(order_data.ask())?; + let filled_amount = (initially_asked - ask_balance) + .ok_or(Error::NegativeAccountBalance(AccountType::Order(*id)))?; + + let ask_currency = CoinOrTokenId::from_output_value(order_data.ask()) + .ok_or(Error::UnsupportedTokenVersion)?; + insert_or_increase(&mut self.unconstrained_value, ask_currency, filled_amount)?; + + let give_currency = CoinOrTokenId::from_output_value(order_data.give()) + .ok_or(Error::UnsupportedTokenVersion)?; + insert_or_increase(&mut self.unconstrained_value, give_currency, give_balance)?; + Ok((CoinOrTokenId::Coin, Amount::ZERO)) } - AccountCommand::ChangeTokenAuthority(_, _) => { - Ok(chain_config.token_change_authority_fee(block_height)) + AccountCommand::FillOrder(order_id, fill_value, _) => { + let filled_amount = orders_accounting::calculate_fill_order( + &orders_accounting_view, + *order_id, + fill_value, + )?; + + let order_data = orders_accounting_view + .get_order_data(order_id) + .map_err(|_| orders_accounting::Error::ViewFail)? + .ok_or(orders_accounting::Error::OrderDataNotFound(*order_id))?; + let give_currency = CoinOrTokenId::from_output_value(order_data.give()) + .ok_or(Error::UnsupportedTokenVersion)?; + insert_or_increase(&mut self.unconstrained_value, give_currency, filled_amount)?; + + let ask_currency = CoinOrTokenId::from_output_value(fill_value) + .ok_or(Error::UnsupportedTokenVersion)?; + Ok((ask_currency, output_value_amount(fill_value)?)) } } } @@ -325,6 +378,15 @@ impl ConstrainedValueAccumulator { CoinOrTokenId::Coin, chain_config.nft_issuance_fee(block_height), )?, + TxOutput::AnyoneCanTake(order_data) => { + let id = CoinOrTokenId::from_output_value(order_data.give()) + .ok_or(Error::UnsupportedTokenVersion)?; + insert_or_increase( + &mut accumulator.unconstrained_value, + id, + output_value_amount(order_data.give())?, + )?; + } }; } @@ -438,3 +500,10 @@ fn decrease_or( } Ok(()) } + +fn output_value_amount(value: &OutputValue) -> Result { + match value { + OutputValue::Coin(amount) | OutputValue::TokenV1(_, amount) => Ok(*amount), + OutputValue::TokenV0(_) => Err(Error::UnsupportedTokenVersion), + } +} diff --git a/chainstate/constraints-value-accumulator/src/error.rs b/chainstate/constraints-value-accumulator/src/error.rs index 03371dd2f0..1eba06a970 100644 --- a/chainstate/constraints-value-accumulator/src/error.rs +++ b/chainstate/constraints-value-accumulator/src/error.rs @@ -38,6 +38,8 @@ pub enum Error { MissingOutputOrSpent(UtxoOutPoint), #[error("PoS accounting error: `{0}`")] PoSAccountingError(#[from] pos_accounting::Error), + #[error("Orders accounting error: `{0}`")] + OrdersAccountingError(#[from] orders_accounting::Error), #[error("Pledge amount not found for pool: `{0}`")] PledgeAmountNotFound(PoolId), #[error("Spending non-spendable output: `{0:?}`")] @@ -48,4 +50,6 @@ pub enum Error { AccountBalanceNotFound(AccountType), #[error("Negative account balance for `{0:?}`")] NegativeAccountBalance(AccountType), + #[error("Unsupported token version")] + UnsupportedTokenVersion, } diff --git a/chainstate/constraints-value-accumulator/src/tests/constraints_tests.rs b/chainstate/constraints-value-accumulator/src/tests/constraints_tests.rs index 6e3df89c1c..bbfc176802 100644 --- a/chainstate/constraints-value-accumulator/src/tests/constraints_tests.rs +++ b/chainstate/constraints-value-accumulator/src/tests/constraints_tests.rs @@ -13,6 +13,8 @@ // See the License for the specific language governing permissions and // limitations under the License. +use std::collections::BTreeMap; + use common::{ chain::{ config::ChainType, output_value::OutputValue, stakelock::StakePoolData, @@ -25,6 +27,8 @@ use common::{ }, }; use crypto::vrf::{VRFKeyKind, VRFPrivateKey}; +use orders_accounting::{InMemoryOrdersAccounting, OrdersAccountingDB}; +use pos_accounting::{InMemoryPoSAccounting, PoSAccountingDB, PoolData}; use randomness::{CryptoRng, Rng}; use rstest::rstest; use test_utils::random::{make_seedable_rng, Seed}; @@ -63,8 +67,17 @@ fn allow_fees_from_decommission(#[case] seed: Seed) { let fee_atoms = rng.gen_range(1..100); let stake_pool_data = create_stake_pool_data(&mut rng, staked_atoms); - let pledge_getter = |_| Ok(Some(Amount::from_atoms(staked_atoms))); - let delegation_balance_getter = |_| Ok(None); + let pos_store = InMemoryPoSAccounting::from_values( + BTreeMap::from_iter([(pool_id, PoolData::from(stake_pool_data.clone()))]), + BTreeMap::new(), + BTreeMap::new(), + BTreeMap::new(), + BTreeMap::new(), + ); + let pos_db = PoSAccountingDB::new(&pos_store); + + let orders_store = InMemoryOrdersAccounting::new(); + let orders_db = OrdersAccountingDB::new(&orders_store); let inputs = vec![TxInput::Utxo(UtxoOutPoint::new( OutPointSourceId::BlockReward(Id::new(H256::random_using(&mut rng))), @@ -84,8 +97,8 @@ fn allow_fees_from_decommission(#[case] seed: Seed) { let inputs_accumulator = ConstrainedValueAccumulator::from_inputs( &chain_config, block_height, - pledge_getter, - delegation_balance_getter, + &orders_db, + &pos_db, &inputs, &input_utxos, ) @@ -122,8 +135,17 @@ fn allow_fees_from_spend_share(#[case] seed: Seed) { let delegated_atoms = rng.gen_range(100..1000); let fee_atoms = rng.gen_range(1..100); - let pledge_getter = |_| Ok(None); - let delegation_balance_getter = |_| Ok(Some(Amount::from_atoms(delegated_atoms))); + let pos_store = InMemoryPoSAccounting::from_values( + BTreeMap::new(), + BTreeMap::new(), + BTreeMap::new(), + BTreeMap::from_iter([(delegation_id, Amount::from_atoms(delegated_atoms))]), + BTreeMap::new(), + ); + let pos_db = PoSAccountingDB::new(&pos_store); + + let orders_store = InMemoryOrdersAccounting::new(); + let orders_db = OrdersAccountingDB::new(&orders_store); let inputs_utxos = vec![None]; let inputs = vec![TxInput::from_account( @@ -140,8 +162,8 @@ fn allow_fees_from_spend_share(#[case] seed: Seed) { let inputs_accumulator = ConstrainedValueAccumulator::from_inputs( &chain_config, block_height, - pledge_getter, - delegation_balance_getter, + &orders_db, + &pos_db, &inputs, &inputs_utxos, ) @@ -180,8 +202,17 @@ fn no_timelock_outputs_on_decommission(#[case] seed: Seed) { let less_than_staked_amount = Amount::from_atoms(rng.gen_range(1..staked_atoms)); let stake_pool_data = create_stake_pool_data(&mut rng, staked_atoms); - let pledge_getter = |_| Ok(Some(Amount::from_atoms(staked_atoms))); - let delegation_balance_getter = |_| Ok(None); + let pos_store = InMemoryPoSAccounting::from_values( + BTreeMap::from_iter([(pool_id, PoolData::from(stake_pool_data.clone()))]), + BTreeMap::new(), + BTreeMap::new(), + BTreeMap::new(), + BTreeMap::new(), + ); + let pos_db = PoSAccountingDB::new(&pos_store); + + let orders_store = InMemoryOrdersAccounting::new(); + let orders_db = OrdersAccountingDB::new(&orders_store); let inputs = vec![ TxInput::from_utxo( @@ -214,8 +245,8 @@ fn no_timelock_outputs_on_decommission(#[case] seed: Seed) { let inputs_accumulator = ConstrainedValueAccumulator::from_inputs( &chain_config, block_height, - pledge_getter, - delegation_balance_getter, + &orders_db, + &pos_db, &inputs, &inputs_utxos, ) @@ -242,8 +273,8 @@ fn no_timelock_outputs_on_decommission(#[case] seed: Seed) { let inputs_accumulator = ConstrainedValueAccumulator::from_inputs( &chain_config, block_height, - pledge_getter, - delegation_balance_getter, + &orders_db, + &pos_db, &inputs, &inputs_utxos, ) @@ -279,8 +310,17 @@ fn try_to_unlock_coins_with_smaller_timelock(#[case] seed: Seed) { let less_than_staked_amount = Amount::from_atoms(rng.gen_range(1..staked_atoms)); let stake_pool_data = create_stake_pool_data(&mut rng, staked_atoms); - let pledge_getter = |_| Ok(Some(Amount::from_atoms(staked_atoms))); - let delegation_balance_getter = |_| Ok(None); + let pos_store = InMemoryPoSAccounting::from_values( + BTreeMap::from_iter([(pool_id, PoolData::from(stake_pool_data.clone()))]), + BTreeMap::new(), + BTreeMap::new(), + BTreeMap::new(), + BTreeMap::new(), + ); + let pos_db = PoSAccountingDB::new(&pos_store); + + let orders_store = InMemoryOrdersAccounting::new(); + let orders_db = OrdersAccountingDB::new(&orders_store); let inputs = vec![ TxInput::from_utxo( @@ -323,8 +363,8 @@ fn try_to_unlock_coins_with_smaller_timelock(#[case] seed: Seed) { let inputs_accumulator = ConstrainedValueAccumulator::from_inputs( &chain_config, block_height, - pledge_getter, - delegation_balance_getter, + &orders_db, + &pos_db, &inputs, &inputs_utxos, ) @@ -362,8 +402,8 @@ fn try_to_unlock_coins_with_smaller_timelock(#[case] seed: Seed) { let inputs_accumulator = ConstrainedValueAccumulator::from_inputs( &chain_config, block_height, - pledge_getter, - delegation_balance_getter, + &orders_db, + &pos_db, &inputs, &inputs_utxos, ) @@ -413,8 +453,17 @@ fn check_timelock_saturation(#[case] seed: Seed) { let transferred_atoms = rng.gen_range(100..1000); - let pledge_getter = |_| Ok(Some(Amount::from_atoms(staked_atoms))); - let delegation_balance_getter = |_| Ok(Some(Amount::from_atoms(delegated_atoms))); + let pos_store = InMemoryPoSAccounting::from_values( + BTreeMap::from_iter([(pool_id, PoolData::from(stake_pool_data.clone()))]), + BTreeMap::new(), + BTreeMap::new(), + BTreeMap::from_iter([(delegation_id, Amount::from_atoms(delegated_atoms))]), + BTreeMap::new(), + ); + let pos_db = PoSAccountingDB::new(&pos_store); + + let orders_store = InMemoryOrdersAccounting::new(); + let orders_db = OrdersAccountingDB::new(&orders_store); let inputs = vec![ TxInput::from_utxo( @@ -457,8 +506,8 @@ fn check_timelock_saturation(#[case] seed: Seed) { let inputs_accumulator = ConstrainedValueAccumulator::from_inputs( &chain_config, block_height, - pledge_getter, - delegation_balance_getter, + &orders_db, + &pos_db, &inputs, &inputs_utxos, ) @@ -489,8 +538,8 @@ fn check_timelock_saturation(#[case] seed: Seed) { let inputs_accumulator = ConstrainedValueAccumulator::from_inputs( &chain_config, block_height, - pledge_getter, - delegation_balance_getter, + &orders_db, + &pos_db, &inputs, &inputs_utxos, ) @@ -523,8 +572,17 @@ fn try_to_overspend_on_spending_delegation(#[case] seed: Seed) { let delegation_balance = Amount::from_atoms(rng.gen_range(100..1000)); let overspent_amount = (delegation_balance + Amount::from_atoms(1)).unwrap(); - let pledge_getter = |_| Ok(None); - let delegation_balance_getter = |_| Ok(Some(delegation_balance)); + let pos_store = InMemoryPoSAccounting::from_values( + BTreeMap::new(), + BTreeMap::new(), + BTreeMap::new(), + BTreeMap::from_iter([(delegation_id, delegation_balance)]), + BTreeMap::new(), + ); + let pos_db = PoSAccountingDB::new(&pos_store); + + let orders_store = InMemoryOrdersAccounting::new(); + let orders_db = OrdersAccountingDB::new(&orders_store); // it's an error to spend more the balance let inputs = vec![TxInput::from_account( @@ -537,8 +595,8 @@ fn try_to_overspend_on_spending_delegation(#[case] seed: Seed) { let inputs_accumulator = ConstrainedValueAccumulator::from_inputs( &chain_config, block_height, - pledge_getter, - delegation_balance_getter, + &orders_db, + &pos_db, &inputs, &inputs_utxos, ); @@ -568,8 +626,8 @@ fn try_to_overspend_on_spending_delegation(#[case] seed: Seed) { let inputs_accumulator = ConstrainedValueAccumulator::from_inputs( &chain_config, block_height, - pledge_getter, - delegation_balance_getter, + &orders_db, + &pos_db, &inputs, &inputs_utxos, ) @@ -605,8 +663,8 @@ fn try_to_overspend_on_spending_delegation(#[case] seed: Seed) { let inputs_accumulator = ConstrainedValueAccumulator::from_inputs( &chain_config, block_height, - pledge_getter, - delegation_balance_getter, + &orders_db, + &pos_db, &inputs, &inputs_utxos, ) diff --git a/chainstate/constraints-value-accumulator/src/tests/homomorphism.rs b/chainstate/constraints-value-accumulator/src/tests/homomorphism.rs index 3f417e73d1..c7cab3ef28 100644 --- a/chainstate/constraints-value-accumulator/src/tests/homomorphism.rs +++ b/chainstate/constraints-value-accumulator/src/tests/homomorphism.rs @@ -58,6 +58,11 @@ fn create_stake_pool_data(rng: &mut (impl Rng + CryptoRng), atoms_to_stake: u128 #[trace] #[case(Seed::from_entropy())] fn accumulators_homomorphism(#[case] seed: Seed) { + use std::collections::BTreeMap; + + use orders_accounting::{InMemoryOrdersAccounting, OrdersAccountingDB}; + use pos_accounting::{InMemoryPoSAccounting, PoSAccountingDB, PoolData}; + let mut rng = make_seedable_rng(seed); let chain_config = common::chain::config::Builder::new(ChainType::Mainnet) @@ -88,8 +93,17 @@ fn accumulators_homomorphism(#[case] seed: Seed) { - spend_share_output, )); - let pledge_getter = |_| Ok(Some(Amount::from_atoms(staked_atoms))); - let delegation_balance_getter = |_| Ok(Some(delegation_balance)); + let pos_store = InMemoryPoSAccounting::from_values( + BTreeMap::from_iter([(pool_id, PoolData::from(stake_pool_data.clone()))]), + BTreeMap::new(), + BTreeMap::new(), + BTreeMap::from_iter([(delegation_id, delegation_balance)]), + BTreeMap::new(), + ); + let pos_db = PoSAccountingDB::new(&pos_store); + + let orders_store = InMemoryOrdersAccounting::new(); + let orders_db = OrdersAccountingDB::new(&orders_store); let (decommission_tx, decommission_tx_inputs_utxos) = { let decommission_pool_utxo = if rng.gen::() { @@ -211,8 +225,8 @@ fn accumulators_homomorphism(#[case] seed: Seed) { let inputs_accumulator = ConstrainedValueAccumulator::from_inputs( &chain_config, block_height, - pledge_getter, - delegation_balance_getter, + &orders_db, + &pos_db, &inputs, &inputs_utxos, ) @@ -235,8 +249,8 @@ fn accumulators_homomorphism(#[case] seed: Seed) { let decommission_inputs_accumulator = ConstrainedValueAccumulator::from_inputs( &chain_config, block_height, - pledge_getter, - delegation_balance_getter, + &orders_db, + &pos_db, decommission_tx.inputs(), &decommission_tx_inputs_utxos, ) @@ -252,8 +266,8 @@ fn accumulators_homomorphism(#[case] seed: Seed) { let spend_share_inputs_accumulator = ConstrainedValueAccumulator::from_inputs( &chain_config, block_height, - pledge_getter, - delegation_balance_getter, + &orders_db, + &pos_db, spend_share_tx.inputs(), &spend_share_inputs_utxos, ) diff --git a/chainstate/constraints-value-accumulator/src/tests/mod.rs b/chainstate/constraints-value-accumulator/src/tests/mod.rs index be5d5cef5a..5e440afd2b 100644 --- a/chainstate/constraints-value-accumulator/src/tests/mod.rs +++ b/chainstate/constraints-value-accumulator/src/tests/mod.rs @@ -15,3 +15,4 @@ mod constraints_tests; mod homomorphism; +mod orders_constraints; diff --git a/chainstate/constraints-value-accumulator/src/tests/orders_constraints.rs b/chainstate/constraints-value-accumulator/src/tests/orders_constraints.rs new file mode 100644 index 0000000000..87162b0c63 --- /dev/null +++ b/chainstate/constraints-value-accumulator/src/tests/orders_constraints.rs @@ -0,0 +1,734 @@ +// Copyright (c) 2024 RBB S.r.l +// opensource@mintlayer.org +// SPDX-License-Identifier: MIT +// Licensed under the MIT License; +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://github.com/mintlayer/mintlayer-core/blob/master/LICENSE +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::collections::BTreeMap; + +use common::{ + chain::{ + config::create_unit_test_config, output_value::OutputValue, tokens::TokenId, + AccountCommand, AccountNonce, Destination, OrderData, OrderId, OutPointSourceId, TxInput, + TxOutput, UtxoOutPoint, + }, + primitives::{Amount, BlockHeight, CoinOrTokenId, Fee, Id, H256}, +}; +use orders_accounting::{InMemoryOrdersAccounting, OrdersAccountingDB}; +use pos_accounting::{InMemoryPoSAccounting, PoSAccountingDB}; +use randomness::Rng; +use rstest::rstest; +use test_utils::random::{make_seedable_rng, Seed}; + +use crate::{ConstrainedValueAccumulator, Error}; + +#[rstest] +#[trace] +#[case(Seed::from_entropy())] +fn create_order_constraints(#[case] seed: Seed) { + let mut rng = make_seedable_rng(seed); + + let chain_config = create_unit_test_config(); + let block_height = BlockHeight::one(); + + let pos_store = InMemoryPoSAccounting::new(); + let pos_db = PoSAccountingDB::new(&pos_store); + + let give_amount = Amount::from_atoms(rng.gen_range(100..1000)); + let token_id = TokenId::random_using(&mut rng); + let ask_amount = Amount::from_atoms(rng.gen_range(100..1000)); + let order_data = Box::new(OrderData::new( + Destination::AnyoneCanSpend, + OutputValue::TokenV1(token_id, ask_amount), + OutputValue::Coin(give_amount), + )); + + let orders_store = InMemoryOrdersAccounting::new(); + let orders_db = OrdersAccountingDB::new(&orders_store); + + // not enough input coins + { + let inputs = vec![TxInput::Utxo(UtxoOutPoint::new( + OutPointSourceId::BlockReward(Id::new(H256::random_using(&mut rng))), + 0, + ))]; + let input_utxos = vec![Some(TxOutput::Transfer( + OutputValue::Coin((give_amount - Amount::from_atoms(1)).unwrap()), + Destination::AnyoneCanSpend, + ))]; + + let outputs = vec![TxOutput::AnyoneCanTake(order_data.clone())]; + + let inputs_accumulator = ConstrainedValueAccumulator::from_inputs( + &chain_config, + block_height, + &orders_db, + &pos_db, + &inputs, + &input_utxos, + ) + .unwrap(); + + let outputs_accumulator = + ConstrainedValueAccumulator::from_outputs(&chain_config, block_height, &outputs) + .unwrap(); + + let result = inputs_accumulator.satisfy_with(outputs_accumulator); + + assert_eq!( + result.unwrap_err(), + Error::AttemptToPrintMoneyOrViolateTimelockConstraints(CoinOrTokenId::Coin) + ); + } + + // input tokens instead of coins + { + let inputs = vec![TxInput::Utxo(UtxoOutPoint::new( + OutPointSourceId::BlockReward(Id::new(H256::random_using(&mut rng))), + 0, + ))]; + let input_utxos = vec![Some(TxOutput::Transfer( + OutputValue::TokenV1(token_id, give_amount), + Destination::AnyoneCanSpend, + ))]; + + let outputs = vec![TxOutput::AnyoneCanTake(order_data.clone())]; + + let inputs_accumulator = ConstrainedValueAccumulator::from_inputs( + &chain_config, + block_height, + &orders_db, + &pos_db, + &inputs, + &input_utxos, + ) + .unwrap(); + + let outputs_accumulator = + ConstrainedValueAccumulator::from_outputs(&chain_config, block_height, &outputs) + .unwrap(); + + let result = inputs_accumulator.satisfy_with(outputs_accumulator); + + assert_eq!( + result.unwrap_err(), + Error::AttemptToPrintMoneyOrViolateTimelockConstraints(CoinOrTokenId::Coin) + ); + } + + // print coins in output + { + let inputs = vec![TxInput::Utxo(UtxoOutPoint::new( + OutPointSourceId::BlockReward(Id::new(H256::random_using(&mut rng))), + 0, + ))]; + let input_utxos = vec![Some(TxOutput::Transfer( + OutputValue::Coin(give_amount), + Destination::AnyoneCanSpend, + ))]; + + let outputs = vec![ + TxOutput::AnyoneCanTake(order_data.clone()), + TxOutput::Transfer( + OutputValue::Coin(Amount::from_atoms(1)), + Destination::AnyoneCanSpend, + ), + ]; + + let inputs_accumulator = ConstrainedValueAccumulator::from_inputs( + &chain_config, + block_height, + &orders_db, + &pos_db, + &inputs, + &input_utxos, + ) + .unwrap(); + + let outputs_accumulator = + ConstrainedValueAccumulator::from_outputs(&chain_config, block_height, &outputs) + .unwrap(); + + let result = inputs_accumulator.satisfy_with(outputs_accumulator); + + assert_eq!( + result.unwrap_err(), + Error::AttemptToPrintMoneyOrViolateTimelockConstraints(CoinOrTokenId::Coin) + ); + } + + // print tokens in output + { + let inputs = vec![TxInput::Utxo(UtxoOutPoint::new( + OutPointSourceId::BlockReward(Id::new(H256::random_using(&mut rng))), + 0, + ))]; + let input_utxos = vec![Some(TxOutput::Transfer( + OutputValue::Coin(give_amount), + Destination::AnyoneCanSpend, + ))]; + + let outputs = vec![ + TxOutput::AnyoneCanTake(order_data.clone()), + TxOutput::Transfer( + OutputValue::TokenV1(token_id, Amount::from_atoms(1)), + Destination::AnyoneCanSpend, + ), + ]; + + let inputs_accumulator = ConstrainedValueAccumulator::from_inputs( + &chain_config, + block_height, + &orders_db, + &pos_db, + &inputs, + &input_utxos, + ) + .unwrap(); + + let outputs_accumulator = + ConstrainedValueAccumulator::from_outputs(&chain_config, block_height, &outputs) + .unwrap(); + + let result = inputs_accumulator.satisfy_with(outputs_accumulator); + + assert_eq!( + result.unwrap_err(), + Error::AttemptToPrintMoneyOrViolateTimelockConstraints(CoinOrTokenId::TokenId( + token_id + )) + ); + } + + // valid case + let inputs = vec![TxInput::Utxo(UtxoOutPoint::new( + OutPointSourceId::BlockReward(Id::new(H256::random_using(&mut rng))), + 0, + ))]; + let input_utxos = vec![Some(TxOutput::Transfer( + OutputValue::Coin(give_amount), + Destination::AnyoneCanSpend, + ))]; + + let outputs = vec![TxOutput::AnyoneCanTake(order_data)]; + + let inputs_accumulator = ConstrainedValueAccumulator::from_inputs( + &chain_config, + block_height, + &orders_db, + &pos_db, + &inputs, + &input_utxos, + ) + .unwrap(); + + let outputs_accumulator = + ConstrainedValueAccumulator::from_outputs(&chain_config, block_height, &outputs).unwrap(); + + let accumulated_fee = inputs_accumulator + .satisfy_with(outputs_accumulator) + .unwrap() + .map_into_block_fees(&chain_config, block_height) + .unwrap(); + + assert_eq!(accumulated_fee, Fee(Amount::ZERO)); +} + +#[rstest] +#[trace] +#[case(Seed::from_entropy())] +fn fill_order_constraints(#[case] seed: Seed) { + let mut rng = make_seedable_rng(seed); + + let chain_config = create_unit_test_config(); + let block_height = BlockHeight::one(); + + let pos_store = InMemoryPoSAccounting::new(); + let pos_db = PoSAccountingDB::new(&pos_store); + + let order_id = OrderId::random_using(&mut rng); + let give_amount = Amount::from_atoms(rng.gen_range(100..1000)); + let token_id = TokenId::random_using(&mut rng); + let ask_amount = Amount::from_atoms(rng.gen_range(100..1000)); + let order_data = OrderData::new( + Destination::AnyoneCanSpend, + OutputValue::TokenV1(token_id, ask_amount), + OutputValue::Coin(give_amount), + ); + + let orders_store = InMemoryOrdersAccounting::from_values( + BTreeMap::from_iter([(order_id, order_data.clone())]), + BTreeMap::from_iter([(order_id, ask_amount)]), + BTreeMap::from_iter([(order_id, give_amount)]), + ); + let orders_db = OrdersAccountingDB::new(&orders_store); + + // use in command more than provided in input + { + let inputs = vec![ + TxInput::Utxo(UtxoOutPoint::new( + OutPointSourceId::BlockReward(Id::new(H256::random_using(&mut rng))), + 0, + )), + TxInput::AccountCommand( + AccountNonce::new(0), + AccountCommand::FillOrder( + order_id, + OutputValue::TokenV1(token_id, (ask_amount + Amount::from_atoms(1)).unwrap()), + Destination::AnyoneCanSpend, + ), + ), + ]; + let input_utxos = vec![ + Some(TxOutput::Transfer( + OutputValue::TokenV1(token_id, ask_amount), + Destination::AnyoneCanSpend, + )), + None, + ]; + + let result = ConstrainedValueAccumulator::from_inputs( + &chain_config, + block_height, + &orders_db, + &pos_db, + &inputs, + &input_utxos, + ); + + assert_eq!( + result.unwrap_err(), + Error::OrdersAccountingError(orders_accounting::Error::OrderOverbid( + order_id, + ask_amount, + (ask_amount + Amount::from_atoms(1)).unwrap() + )) + ); + } + + // fill with coins instead of tokens + { + let inputs = vec![ + TxInput::Utxo(UtxoOutPoint::new( + OutPointSourceId::BlockReward(Id::new(H256::random_using(&mut rng))), + 0, + )), + TxInput::AccountCommand( + AccountNonce::new(0), + AccountCommand::FillOrder( + order_id, + OutputValue::Coin(ask_amount), + Destination::AnyoneCanSpend, + ), + ), + ]; + let input_utxos = vec![ + Some(TxOutput::Transfer( + OutputValue::Coin(ask_amount), + Destination::AnyoneCanSpend, + )), + None, + ]; + + let result = ConstrainedValueAccumulator::from_inputs( + &chain_config, + block_height, + &orders_db, + &pos_db, + &inputs, + &input_utxos, + ); + + assert_eq!( + result.unwrap_err(), + Error::OrdersAccountingError(orders_accounting::Error::CurrencyMismatch) + ); + } + + // try to print coins in output + { + let inputs = vec![ + TxInput::Utxo(UtxoOutPoint::new( + OutPointSourceId::BlockReward(Id::new(H256::random_using(&mut rng))), + 0, + )), + TxInput::AccountCommand( + AccountNonce::new(0), + AccountCommand::FillOrder( + order_id, + OutputValue::TokenV1(token_id, ask_amount), + Destination::AnyoneCanSpend, + ), + ), + ]; + let input_utxos = vec![ + Some(TxOutput::Transfer( + OutputValue::TokenV1(token_id, ask_amount), + Destination::AnyoneCanSpend, + )), + None, + ]; + + let outputs = vec![TxOutput::Transfer( + OutputValue::Coin((give_amount + Amount::from_atoms(1)).unwrap()), + Destination::AnyoneCanSpend, + )]; + + let inputs_accumulator = ConstrainedValueAccumulator::from_inputs( + &chain_config, + block_height, + &orders_db, + &pos_db, + &inputs, + &input_utxos, + ) + .unwrap(); + + let outputs_accumulator = + ConstrainedValueAccumulator::from_outputs(&chain_config, block_height, &outputs) + .unwrap(); + + let result = inputs_accumulator.satisfy_with(outputs_accumulator); + + assert_eq!( + result.unwrap_err(), + Error::AttemptToPrintMoneyOrViolateTimelockConstraints(CoinOrTokenId::Coin) + ); + } + + // try to print tokens in output + { + let inputs = vec![ + TxInput::Utxo(UtxoOutPoint::new( + OutPointSourceId::BlockReward(Id::new(H256::random_using(&mut rng))), + 0, + )), + TxInput::AccountCommand( + AccountNonce::new(0), + AccountCommand::FillOrder( + order_id, + OutputValue::TokenV1(token_id, ask_amount), + Destination::AnyoneCanSpend, + ), + ), + ]; + let input_utxos = vec![ + Some(TxOutput::Transfer( + OutputValue::TokenV1(token_id, ask_amount), + Destination::AnyoneCanSpend, + )), + None, + ]; + + let outputs = vec![ + TxOutput::Transfer(OutputValue::Coin(give_amount), Destination::AnyoneCanSpend), + TxOutput::Transfer( + OutputValue::TokenV1(token_id, Amount::from_atoms(1)), + Destination::AnyoneCanSpend, + ), + ]; + + let inputs_accumulator = ConstrainedValueAccumulator::from_inputs( + &chain_config, + block_height, + &orders_db, + &pos_db, + &inputs, + &input_utxos, + ) + .unwrap(); + + let outputs_accumulator = + ConstrainedValueAccumulator::from_outputs(&chain_config, block_height, &outputs) + .unwrap(); + + let result = inputs_accumulator.satisfy_with(outputs_accumulator); + + assert_eq!( + result.unwrap_err(), + Error::AttemptToPrintMoneyOrViolateTimelockConstraints(CoinOrTokenId::TokenId( + token_id + )) + ); + } + + { + // partially use input in command + let inputs = vec![ + TxInput::Utxo(UtxoOutPoint::new( + OutPointSourceId::BlockReward(Id::new(H256::random_using(&mut rng))), + 0, + )), + TxInput::AccountCommand( + AccountNonce::new(0), + AccountCommand::FillOrder( + order_id, + OutputValue::TokenV1(token_id, ask_amount), + Destination::AnyoneCanSpend, + ), + ), + ]; + let input_utxos = vec![ + Some(TxOutput::Transfer( + OutputValue::TokenV1(token_id, (ask_amount + Amount::from_atoms(1)).unwrap()), + Destination::AnyoneCanSpend, + )), + None, + ]; + + let outputs = vec![ + TxOutput::Transfer(OutputValue::Coin(give_amount), Destination::AnyoneCanSpend), + TxOutput::Transfer( + OutputValue::TokenV1(token_id, Amount::from_atoms(1)), + Destination::AnyoneCanSpend, + ), + ]; + + let inputs_accumulator = ConstrainedValueAccumulator::from_inputs( + &chain_config, + block_height, + &orders_db, + &pos_db, + &inputs, + &input_utxos, + ) + .unwrap(); + + let outputs_accumulator = + ConstrainedValueAccumulator::from_outputs(&chain_config, block_height, &outputs) + .unwrap(); + + let accumulated_fee = inputs_accumulator + .satisfy_with(outputs_accumulator) + .unwrap() + .map_into_block_fees(&chain_config, block_height) + .unwrap(); + + assert_eq!(accumulated_fee, Fee(Amount::ZERO)); + } + + // valid case + let inputs = vec![ + TxInput::Utxo(UtxoOutPoint::new( + OutPointSourceId::BlockReward(Id::new(H256::random_using(&mut rng))), + 0, + )), + TxInput::AccountCommand( + AccountNonce::new(0), + AccountCommand::FillOrder( + order_id, + OutputValue::TokenV1(token_id, ask_amount), + Destination::AnyoneCanSpend, + ), + ), + ]; + let input_utxos = vec![ + Some(TxOutput::Transfer( + OutputValue::TokenV1(token_id, ask_amount), + Destination::AnyoneCanSpend, + )), + None, + ]; + + let outputs = + vec![TxOutput::Transfer(OutputValue::Coin(give_amount), Destination::AnyoneCanSpend)]; + + let inputs_accumulator = ConstrainedValueAccumulator::from_inputs( + &chain_config, + block_height, + &orders_db, + &pos_db, + &inputs, + &input_utxos, + ) + .unwrap(); + + let outputs_accumulator = + ConstrainedValueAccumulator::from_outputs(&chain_config, block_height, &outputs).unwrap(); + + let accumulated_fee = inputs_accumulator + .satisfy_with(outputs_accumulator) + .unwrap() + .map_into_block_fees(&chain_config, block_height) + .unwrap(); + + assert_eq!(accumulated_fee, Fee(Amount::ZERO)); +} + +#[rstest] +#[trace] +#[case(Seed::from_entropy())] +fn conclude_order_constraints(#[case] seed: Seed) { + let mut rng = make_seedable_rng(seed); + + let chain_config = create_unit_test_config(); + let block_height = BlockHeight::one(); + + let pos_store = InMemoryPoSAccounting::new(); + let pos_db = PoSAccountingDB::new(&pos_store); + + let order_id = OrderId::random_using(&mut rng); + let give_amount = Amount::from_atoms(rng.gen_range(100..1000)); + let token_id = TokenId::random_using(&mut rng); + let ask_amount = Amount::from_atoms(rng.gen_range(100..1000)); + let order_data = OrderData::new( + Destination::AnyoneCanSpend, + OutputValue::TokenV1(token_id, ask_amount), + OutputValue::Coin(give_amount), + ); + + let orders_store = InMemoryOrdersAccounting::from_values( + BTreeMap::from_iter([(order_id, order_data.clone())]), + BTreeMap::from_iter([(order_id, ask_amount)]), + BTreeMap::from_iter([(order_id, give_amount)]), + ); + let orders_db = OrdersAccountingDB::new(&orders_store); + + // try to print coins in output + { + let inputs = vec![TxInput::AccountCommand( + AccountNonce::new(0), + AccountCommand::ConcludeOrder(order_id), + )]; + let input_utxos = vec![None]; + + let outputs = vec![TxOutput::Transfer( + OutputValue::Coin((give_amount + Amount::from_atoms(1)).unwrap()), + Destination::AnyoneCanSpend, + )]; + + let inputs_accumulator = ConstrainedValueAccumulator::from_inputs( + &chain_config, + block_height, + &orders_db, + &pos_db, + &inputs, + &input_utxos, + ) + .unwrap(); + + let outputs_accumulator = + ConstrainedValueAccumulator::from_outputs(&chain_config, block_height, &outputs) + .unwrap(); + + let result = inputs_accumulator.satisfy_with(outputs_accumulator); + + assert_eq!( + result.unwrap_err(), + Error::AttemptToPrintMoneyOrViolateTimelockConstraints(CoinOrTokenId::Coin) + ); + } + + // try to print tokens in output + { + let inputs = vec![TxInput::AccountCommand( + AccountNonce::new(0), + AccountCommand::ConcludeOrder(order_id), + )]; + let input_utxos = vec![None]; + + let outputs = vec![TxOutput::Transfer( + OutputValue::TokenV1(token_id, Amount::from_atoms(1)), + Destination::AnyoneCanSpend, + )]; + + let inputs_accumulator = ConstrainedValueAccumulator::from_inputs( + &chain_config, + block_height, + &orders_db, + &pos_db, + &inputs, + &input_utxos, + ) + .unwrap(); + + let outputs_accumulator = + ConstrainedValueAccumulator::from_outputs(&chain_config, block_height, &outputs) + .unwrap(); + + let result = inputs_accumulator.satisfy_with(outputs_accumulator); + + assert_eq!( + result.unwrap_err(), + Error::AttemptToPrintMoneyOrViolateTimelockConstraints(CoinOrTokenId::TokenId( + token_id + )) + ); + } + + { + // partially use input in command + let inputs = vec![TxInput::AccountCommand( + AccountNonce::new(0), + AccountCommand::ConcludeOrder(order_id), + )]; + let input_utxos = vec![None]; + + let outputs = vec![TxOutput::Transfer( + OutputValue::Coin((give_amount - Amount::from_atoms(1)).unwrap()), + Destination::AnyoneCanSpend, + )]; + + let inputs_accumulator = ConstrainedValueAccumulator::from_inputs( + &chain_config, + block_height, + &orders_db, + &pos_db, + &inputs, + &input_utxos, + ) + .unwrap(); + + let outputs_accumulator = + ConstrainedValueAccumulator::from_outputs(&chain_config, block_height, &outputs) + .unwrap(); + + let accumulated_fee = inputs_accumulator + .satisfy_with(outputs_accumulator) + .unwrap() + .map_into_block_fees(&chain_config, block_height) + .unwrap(); + + assert_eq!(accumulated_fee, Fee(Amount::from_atoms(1))); + } + + // valid case + let inputs = vec![TxInput::AccountCommand( + AccountNonce::new(0), + AccountCommand::ConcludeOrder(order_id), + )]; + let input_utxos = vec![None]; + + let outputs = + vec![TxOutput::Transfer(OutputValue::Coin(give_amount), Destination::AnyoneCanSpend)]; + + let inputs_accumulator = ConstrainedValueAccumulator::from_inputs( + &chain_config, + block_height, + &orders_db, + &pos_db, + &inputs, + &input_utxos, + ) + .unwrap(); + + let outputs_accumulator = + ConstrainedValueAccumulator::from_outputs(&chain_config, block_height, &outputs).unwrap(); + + let accumulated_fee = inputs_accumulator + .satisfy_with(outputs_accumulator) + .unwrap() + .map_into_block_fees(&chain_config, block_height) + .unwrap(); + + assert_eq!(accumulated_fee, Fee(Amount::ZERO)); +} diff --git a/chainstate/src/detail/ban_score.rs b/chainstate/src/detail/ban_score.rs index 0987e8b143..15b9245bb2 100644 --- a/chainstate/src/detail/ban_score.rs +++ b/chainstate/src/detail/ban_score.rs @@ -84,6 +84,7 @@ impl BanScore for BlockError { BlockError::UnexpectedHeightRange(_, _) => 0, BlockError::TokensAccountingError(err) => err.ban_score(), + BlockError::OrdersAccountingError(err) => err.ban_score(), } } } @@ -140,6 +141,8 @@ impl BanScore for ConnectTransactionError { ConnectTransactionError::RewardDistributionError(err) => err.ban_score(), ConnectTransactionError::CheckTransactionError(err) => err.ban_score(), ConnectTransactionError::InputCheck(e) => e.ban_score(), + ConnectTransactionError::OrdersAccountingError(err) => err.ban_score(), + ConnectTransactionError::AttemptToCreateOrderFromAccounts => 100, } } } @@ -169,11 +172,13 @@ impl BanScore for mintscript::translate::TranslationError { | Self::IllegalOutputSpend | Self::PoolNotFound(_) | Self::DelegationNotFound(_) - | Self::TokenNotFound(_) => 100, + | Self::TokenNotFound(_) + | Self::OrderNotFound(_) => 100, Self::SignatureError(_) => 100, Self::PoSAccounting(e) => e.ban_score(), Self::TokensAccounting(e) => e.ban_score(), + Self::OrdersAccounting(e) => e.ban_score(), } } } @@ -231,6 +236,8 @@ impl BanScore for SignatureDestinationGetterError { SignatureDestinationGetterError::UtxoViewError(_) => 100, SignatureDestinationGetterError::TokenDataNotFound(_) => 100, SignatureDestinationGetterError::TokensAccountingViewError(_) => 100, + SignatureDestinationGetterError::OrdersAccountingViewError(_) => 100, + SignatureDestinationGetterError::OrderDataNotFound(_) => 0, } } } @@ -248,6 +255,7 @@ impl BanScore for TransactionVerifierStorageError { TransactionVerifierStorageError::PoSAccountingError(err) => err.ban_score(), TransactionVerifierStorageError::AccountingBlockUndoError(_) => 100, TransactionVerifierStorageError::TokensAccountingError(err) => err.ban_score(), + TransactionVerifierStorageError::OrdersAccountingError(err) => err.ban_score(), } } } @@ -353,6 +361,8 @@ impl BanScore for CheckTransactionError { CheckTransactionError::TxSizeTooLarge(_, _, _) => 100, CheckTransactionError::DeprecatedTokenOperationVersion(_, _) => 100, CheckTransactionError::HtlcsAreNotActivated => 100, + CheckTransactionError::OrdersAreNotActivated(_) => 100, + CheckTransactionError::OrdersCurrenciesMustBeDifferent(_) => 100, } } } @@ -583,6 +593,8 @@ impl BanScore for constraints_value_accumulator::Error { constraints_value_accumulator::Error::DelegationBalanceNotFound(_) => 0, constraints_value_accumulator::Error::AccountBalanceNotFound(_) => 0, constraints_value_accumulator::Error::NegativeAccountBalance(_) => 100, + constraints_value_accumulator::Error::UnsupportedTokenVersion => 100, + constraints_value_accumulator::Error::OrdersAccountingError(err) => err.ban_score(), } } } @@ -644,4 +656,36 @@ impl BanScore for RewardDistributionError { } } +impl BanScore for orders_accounting::Error { + fn ban_score(&self) -> u32 { + use orders_accounting::Error; + match self { + Error::StorageError(_) => 0, + Error::AccountingError(_) => 100, + Error::OrderAlreadyExists(_) => 100, + Error::OrderDataNotFound(_) => 100, + Error::OrderAskBalanceNotFound(_) => 100, + Error::OrderGiveBalanceNotFound(_) => 100, + Error::OrderWithZeroValue(_) => 100, + Error::InvariantOrderDataNotFoundForUndo(_) => 100, + Error::InvariantOrderAskBalanceNotFoundForUndo(_) => 100, + Error::InvariantOrderAskBalanceChangedForUndo(_) => 100, + Error::InvariantOrderGiveBalanceNotFoundForUndo(_) => 100, + Error::InvariantOrderGiveBalanceChangedForUndo(_) => 100, + Error::InvariantOrderDataExistForConcludeUndo(_) => 100, + Error::InvariantOrderAskBalanceExistForConcludeUndo(_) => 100, + Error::InvariantOrderGiveBalanceExistForConcludeUndo(_) => 100, + Error::CurrencyMismatch => 100, + Error::OrderOverflow(_) => 100, + Error::OrderOverbid(_, _, _) => 100, + Error::AttemptedConcludeNonexistingOrderData(_) => 100, + Error::InvariantNonzeroAskBalanceForMissingOrder(_) => 100, + Error::InvariantNonzeroGiveBalanceForMissingOrder(_) => 100, + Error::UnsupportedTokenVersion => 100, + Error::ViewFail => 0, + Error::StorageWrite => 0, + } + } +} + // TODO: tests in which we simulate every possible case and test the score diff --git a/chainstate/src/detail/chainstateref/epoch_seal.rs b/chainstate/src/detail/chainstateref/epoch_seal.rs index c1d25c56c8..1ee2c7d353 100644 --- a/chainstate/src/detail/chainstateref/epoch_seal.rs +++ b/chainstate/src/detail/chainstateref/epoch_seal.rs @@ -175,7 +175,8 @@ where | TxOutput::IssueFungibleToken(_) | TxOutput::IssueNft(_, _, _) | TxOutput::DataDeposit(_) - | TxOutput::Htlc(_, _) => { + | TxOutput::Htlc(_, _) + | TxOutput::AnyoneCanTake(_) => { return Err(EpochSealError::SpendStakeError( SpendStakeError::InvalidBlockRewardOutputType, )); diff --git a/chainstate/src/detail/chainstateref/in_memory_reorg.rs b/chainstate/src/detail/chainstateref/in_memory_reorg.rs index 0fde9add2d..2aa6507e62 100644 --- a/chainstate/src/detail/chainstateref/in_memory_reorg.rs +++ b/chainstate/src/detail/chainstateref/in_memory_reorg.rs @@ -23,6 +23,7 @@ use common::{ chain::{Block, ChainConfig, GenBlock, GenBlockId}, primitives::{id::WithId, Id}, }; +use orders_accounting::OrdersAccountingDB; use thiserror::Error; use tokens_accounting::TokensAccountingDB; use tx_verifier::{ @@ -197,6 +198,7 @@ type TxVerifier<'a, 'b, S, V> = TransactionVerifier< UtxosDB<&'b ChainstateRef<'a, S, V>>, &'b ChainstateRef<'a, S, V>, TokensAccountingDB<&'b ChainstateRef<'a, S, V>>, + OrdersAccountingDB<&'b ChainstateRef<'a, S, V>>, >; #[derive(Error, Debug, PartialEq, Eq, Clone)] diff --git a/chainstate/src/detail/chainstateref/mod.rs b/chainstate/src/detail/chainstateref/mod.rs index f0fb4182d4..48224f1873 100644 --- a/chainstate/src/detail/chainstateref/mod.rs +++ b/chainstate/src/detail/chainstateref/mod.rs @@ -698,7 +698,8 @@ impl<'a, S: BlockchainStorageRead, V: TransactionVerificationStrategy> Chainstat | TxOutput::IssueFungibleToken(_) | TxOutput::IssueNft(_, _, _) | TxOutput::DataDeposit(_) - | TxOutput::Htlc(_, _) => Err( + | TxOutput::Htlc(_, _) + | TxOutput::AnyoneCanTake(_) => Err( CheckBlockError::InvalidBlockRewardOutputType(block.get_id()), ), }, @@ -715,7 +716,8 @@ impl<'a, S: BlockchainStorageRead, V: TransactionVerificationStrategy> Chainstat | TxOutput::IssueFungibleToken(_) | TxOutput::IssueNft(_, _, _) | TxOutput::DataDeposit(_) - | TxOutput::Htlc(_, _) => Err( + | TxOutput::Htlc(_, _) + | TxOutput::AnyoneCanTake(_) => Err( CheckBlockError::InvalidBlockRewardOutputType(block.get_id()), ), } diff --git a/chainstate/src/detail/chainstateref/tx_verifier_storage.rs b/chainstate/src/detail/chainstateref/tx_verifier_storage.rs index e560ea7d31..279645576a 100644 --- a/chainstate/src/detail/chainstateref/tx_verifier_storage.rs +++ b/chainstate/src/detail/chainstateref/tx_verifier_storage.rs @@ -28,11 +28,15 @@ use chainstate_types::{storage_result, GenBlockIndex}; use common::{ chain::{ tokens::{TokenAuxiliaryData, TokenId}, - AccountNonce, AccountType, ChainConfig, DelegationId, GenBlock, GenBlockId, PoolId, - Transaction, + AccountNonce, AccountType, ChainConfig, DelegationId, GenBlock, GenBlockId, OrderData, + OrderId, PoolId, Transaction, }, primitives::{Amount, Id}, }; +use orders_accounting::{ + FlushableOrdersAccountingView, OrdersAccountingDB, OrdersAccountingStorageRead, + OrdersAccountingUndo, +}; use pos_accounting::{ DelegationData, DeltaMergeUndo, FlushablePoSAccountingView, PoSAccountingDB, PoSAccountingDeltaData, PoSAccountingUndo, PoSAccountingView, PoolData, @@ -138,6 +142,26 @@ impl<'a, S: BlockchainStorageRead, V: TransactionVerificationStrategy> Transacti .get_account_nonce_count(account) .map_err(TransactionVerifierStorageError::from) } + + #[log_error] + fn get_orders_accounting_undo( + &self, + tx_source: TransactionSource, + ) -> Result>, TransactionVerifierStorageError> + { + match tx_source { + TransactionSource::Chain(id) => { + let undo = self + .db_tx + .get_orders_accounting_undo(id)? + .map(CachedBlockUndo::from_block_undo); + Ok(undo) + } + TransactionSource::Mempool => { + panic!("Mempool should not undo stuff in chainstate") + } + } + } } // TODO: this function is a duplicate of one in chainstate-types; the cause for this is that BlockchainStorageRead causes a circular dependencies @@ -388,6 +412,41 @@ impl<'a, S: BlockchainStorageWrite, V: TransactionVerificationStrategy> } } } + + #[log_error] + fn set_orders_accounting_undo_data( + &mut self, + tx_source: TransactionSource, + undo: &CachedBlockUndo, + ) -> Result<(), TransactionVerifierStorageError> { + // TODO: check tx_source at compile-time (mintlayer/mintlayer-core#633) + match tx_source { + TransactionSource::Chain(id) => self + .db_tx + .set_orders_accounting_undo_data(id, &undo.clone().consume()) + .map_err(TransactionVerifierStorageError::from), + TransactionSource::Mempool => { + panic!("Flushing mempool info into the storage is forbidden") + } + } + } + + #[log_error] + fn del_orders_accounting_undo_data( + &mut self, + tx_source: TransactionSource, + ) -> Result<(), TransactionVerifierStorageError> { + // TODO: check tx_source at compile-time (mintlayer/mintlayer-core#633) + match tx_source { + TransactionSource::Chain(id) => self + .db_tx + .del_orders_accounting_undo_data(id) + .map_err(TransactionVerifierStorageError::from), + TransactionSource::Mempool => { + panic!("Flushing mempool info into the storage is forbidden") + } + } + } } impl<'a, S: BlockchainStorageRead, V: TransactionVerificationStrategy> PoSAccountingView @@ -494,3 +553,38 @@ impl<'a, S: BlockchainStorageWrite, V: TransactionVerificationStrategy> db.batch_write_tokens_data(delta) } } + +impl<'a, S: BlockchainStorageRead, V: TransactionVerificationStrategy> OrdersAccountingStorageRead + for ChainstateRef<'a, S, V> +{ + type Error = storage_result::Error; + + #[log_error] + fn get_order_data(&self, id: &OrderId) -> Result, Self::Error> { + self.db_tx.get_order_data(id) + } + + #[log_error] + fn get_ask_balance(&self, id: &OrderId) -> Result, Self::Error> { + self.db_tx.get_ask_balance(id) + } + + #[log_error] + fn get_give_balance(&self, id: &OrderId) -> Result, Self::Error> { + self.db_tx.get_give_balance(id) + } +} + +impl<'a, S: BlockchainStorageWrite, V: TransactionVerificationStrategy> + FlushableOrdersAccountingView for ChainstateRef<'a, S, V> +{ + type Error = orders_accounting::Error; + + fn batch_write_orders_data( + &mut self, + delta: orders_accounting::OrdersAccountingDeltaData, + ) -> Result { + let mut db = OrdersAccountingDB::new(&mut self.db_tx); + db.batch_write_orders_data(delta) + } +} diff --git a/chainstate/src/detail/error.rs b/chainstate/src/detail/error.rs index 0ff1d466da..b2d5a5e5d7 100644 --- a/chainstate/src/detail/error.rs +++ b/chainstate/src/detail/error.rs @@ -74,6 +74,8 @@ pub enum BlockError { TokensAccountingError(#[from] tokens_accounting::Error), #[error("In-memory reorg failed: {0}")] InMemoryReorgFailed(#[from] InMemoryReorgError), + #[error("Orders accounting error: {0}")] + OrdersAccountingError(#[from] orders_accounting::Error), #[error("Failed to obtain best block id: {0}")] BestBlockIdQueryError(PropertyQueryError), diff --git a/chainstate/src/detail/error_classification.rs b/chainstate/src/detail/error_classification.rs index bbed9511cf..b047f299bd 100644 --- a/chainstate/src/detail/error_classification.rs +++ b/chainstate/src/detail/error_classification.rs @@ -130,6 +130,7 @@ impl BlockProcessingErrorClassification for BlockError { BlockError::BestChainCandidatesAccessorError(err) => err.classify(), BlockError::TokensAccountingError(err) => err.classify(), + BlockError::OrdersAccountingError(err) => err.classify(), BlockError::StorageError(err) => err.classify(), BlockError::OrphanCheckFailed(err) => err.classify(), BlockError::CheckBlockFailed(err) => err.classify(), @@ -296,6 +297,7 @@ impl BlockProcessingErrorClassification for ConnectTransactionError { | ConnectTransactionError::NotEnoughPledgeToCreateStakePool(_, _, _) | ConnectTransactionError::AttemptToCreateStakePoolFromAccounts | ConnectTransactionError::AttemptToCreateDelegationFromAccounts + | ConnectTransactionError::AttemptToCreateOrderFromAccounts | ConnectTransactionError::IOPolicyError(_, _) | ConnectTransactionError::TotalFeeRequiredOverflow | ConnectTransactionError::InsufficientCoinsFee(_, _) @@ -316,6 +318,7 @@ impl BlockProcessingErrorClassification for ConnectTransactionError { ConnectTransactionError::PoSAccountingError(err) => err.classify(), ConnectTransactionError::ConstrainedValueAccumulatorError(err, _) => err.classify(), ConnectTransactionError::InputCheck(err) => err.classify(), + ConnectTransactionError::OrdersAccountingError(err) => err.classify(), } } } @@ -345,10 +348,12 @@ impl BlockProcessingErrorClassification for mintscript::translate::TranslationEr | Self::IllegalOutputSpend | Self::PoolNotFound(_) | Self::DelegationNotFound(_) - | Self::TokenNotFound(_) => BlockProcessingErrorClass::BadBlock, + | Self::TokenNotFound(_) + | Self::OrderNotFound(_) => BlockProcessingErrorClass::BadBlock, Self::PoSAccounting(e) => e.classify(), Self::TokensAccounting(e) => e.classify(), + Self::OrdersAccounting(e) => e.classify(), Self::SignatureError(e) => e.classify(), } } @@ -489,6 +494,7 @@ impl BlockProcessingErrorClassification for TransactionVerifierStorageError { TransactionVerifierStorageError::PoSAccountingError(err) => err.classify(), TransactionVerifierStorageError::AccountingBlockUndoError(err) => err.classify(), TransactionVerifierStorageError::TokensAccountingError(err) => err.classify(), + TransactionVerifierStorageError::OrdersAccountingError(err) => err.classify(), } } } @@ -539,13 +545,15 @@ impl BlockProcessingErrorClassification for SignatureDestinationGetterError { | SignatureDestinationGetterError::PoolDataNotFound(_) | SignatureDestinationGetterError::DelegationDataNotFound(_) | SignatureDestinationGetterError::TokenDataNotFound(_) - | SignatureDestinationGetterError::UtxoOutputNotFound(_) => { + | SignatureDestinationGetterError::UtxoOutputNotFound(_) + | SignatureDestinationGetterError::OrderDataNotFound(_) => { BlockProcessingErrorClass::BadBlock } SignatureDestinationGetterError::UtxoViewError(err) => err.classify(), SignatureDestinationGetterError::PoSAccountingViewError(err) => err.classify(), SignatureDestinationGetterError::TokensAccountingViewError(err) => err.classify(), + SignatureDestinationGetterError::OrdersAccountingViewError(err) => err.classify(), } } } @@ -788,7 +796,11 @@ impl BlockProcessingErrorClassification for CheckTransactionError { | CheckTransactionError::DataDepositMaxSizeExceeded(_, _, _) | CheckTransactionError::TxSizeTooLarge(_, _, _) | CheckTransactionError::DeprecatedTokenOperationVersion(_, _) - | CheckTransactionError::HtlcsAreNotActivated => BlockProcessingErrorClass::BadBlock, + | CheckTransactionError::HtlcsAreNotActivated + | CheckTransactionError::OrdersAreNotActivated(_) + | CheckTransactionError::OrdersCurrenciesMustBeDifferent(_) => { + BlockProcessingErrorClass::BadBlock + } CheckTransactionError::PropertyQueryError(err) => err.classify(), CheckTransactionError::TokensError(err) => err.classify(), @@ -871,9 +883,46 @@ impl BlockProcessingErrorClassification for constraints_value_accumulator::Error | Error::MissingOutputOrSpent(_) | Error::PledgeAmountNotFound(_) | Error::SpendingNonSpendableOutput(_) - | Error::NegativeAccountBalance(_) => BlockProcessingErrorClass::BadBlock, + | Error::NegativeAccountBalance(_) + | Error::UnsupportedTokenVersion => BlockProcessingErrorClass::BadBlock, Error::PoSAccountingError(err) => err.classify(), + Error::OrdersAccountingError(err) => err.classify(), + } + } +} + +impl BlockProcessingErrorClassification for orders_accounting::Error { + fn classify(&self) -> BlockProcessingErrorClass { + use orders_accounting::Error; + match self { + Error::ViewFail | Error::StorageWrite => BlockProcessingErrorClass::General, + + Error::OrderAlreadyExists(_) + | Error::OrderDataNotFound(_) + | Error::OrderAskBalanceNotFound(_) + | Error::OrderGiveBalanceNotFound(_) + | Error::OrderWithZeroValue(_) + | Error::InvariantOrderDataNotFoundForUndo(_) + | Error::InvariantOrderAskBalanceNotFoundForUndo(_) + | Error::InvariantOrderAskBalanceChangedForUndo(_) + | Error::InvariantOrderGiveBalanceNotFoundForUndo(_) + | Error::InvariantOrderGiveBalanceChangedForUndo(_) + | Error::InvariantOrderDataExistForConcludeUndo(_) + | Error::InvariantOrderAskBalanceExistForConcludeUndo(_) + | Error::InvariantOrderGiveBalanceExistForConcludeUndo(_) + | Error::CurrencyMismatch + | Error::OrderOverflow(_) + | Error::OrderOverbid(_, _, _) + | Error::AttemptedConcludeNonexistingOrderData(_) + | Error::UnsupportedTokenVersion + | Error::InvariantNonzeroAskBalanceForMissingOrder(_) + | Error::InvariantNonzeroGiveBalanceForMissingOrder(_) => { + BlockProcessingErrorClass::BadBlock + } + + Error::StorageError(err) => err.classify(), + Error::AccountingError(err) => err.classify(), } } } diff --git a/chainstate/src/detail/mod.rs b/chainstate/src/detail/mod.rs index 75dad6467f..5790d06df9 100644 --- a/chainstate/src/detail/mod.rs +++ b/chainstate/src/detail/mod.rs @@ -724,7 +724,8 @@ impl Chainstate | TxOutput::IssueFungibleToken(_) | TxOutput::IssueNft(_, _, _) | TxOutput::DataDeposit(_) - | TxOutput::Htlc(_, _) => { /* do nothing */ } + | TxOutput::Htlc(_, _) + | TxOutput::AnyoneCanTake(_) => { /* do nothing */ } | TxOutput::CreateStakePool(pool_id, data) => { let _ = db .create_pool(*pool_id, data.as_ref().clone().into()) diff --git a/chainstate/src/detail/query.rs b/chainstate/src/detail/query.rs index 3338a15c20..a88367ed3f 100644 --- a/chainstate/src/detail/query.rs +++ b/chainstate/src/detail/query.rs @@ -24,10 +24,11 @@ use common::{ NftIssuance, RPCFungibleTokenInfo, RPCIsTokenFrozen, RPCNonFungibleTokenInfo, RPCTokenInfo, TokenAuxiliaryData, TokenId, }, - Block, GenBlock, Transaction, TxOutput, + Block, GenBlock, OrderData, OrderId, Transaction, TxOutput, }, primitives::{Amount, BlockDistance, BlockHeight, Id, Idable}, }; +use orders_accounting::OrdersAccountingStorageRead; use tokens_accounting::TokensAccountingStorageRead; use utils::ensure; @@ -349,7 +350,8 @@ impl<'a, S: BlockchainStorageRead, V: TransactionVerificationStrategy> Chainstat | TxOutput::CreateDelegationId(_, _) | TxOutput::DelegateStaking(_, _) | TxOutput::DataDeposit(_) - | TxOutput::Htlc(_, _) => None, + | TxOutput::Htlc(_, _) + | TxOutput::AnyoneCanTake(_) => None, TxOutput::IssueNft(_, issuance, _) => match issuance.as_ref() { NftIssuance::V0(nft) => { Some(RPCTokenInfo::new_nonfungible(RPCNonFungibleTokenInfo::new( @@ -401,4 +403,22 @@ impl<'a, S: BlockchainStorageRead, V: TransactionVerificationStrategy> Chainstat ) -> Result, PropertyQueryError> { self.chainstate_ref.get_circulating_supply(id).map_err(PropertyQueryError::from) } + + pub fn get_order_data(&self, id: &OrderId) -> Result, PropertyQueryError> { + self.chainstate_ref.get_order_data(id).map_err(PropertyQueryError::from) + } + + pub fn get_order_ask_balance( + &self, + id: &OrderId, + ) -> Result, PropertyQueryError> { + self.chainstate_ref.get_ask_balance(id).map_err(PropertyQueryError::from) + } + + pub fn get_order_give_balance( + &self, + id: &OrderId, + ) -> Result, PropertyQueryError> { + self.chainstate_ref.get_give_balance(id).map_err(PropertyQueryError::from) + } } diff --git a/chainstate/src/detail/tx_verification_strategy/default_strategy.rs b/chainstate/src/detail/tx_verification_strategy/default_strategy.rs index 08132b554b..3c3da64f65 100644 --- a/chainstate/src/detail/tx_verification_strategy/default_strategy.rs +++ b/chainstate/src/detail/tx_verification_strategy/default_strategy.rs @@ -21,6 +21,7 @@ use common::{ primitives::{id::WithId, Idable}, }; use constraints_value_accumulator::AccumulatedFee; +use orders_accounting::OrdersAccountingView; use pos_accounting::PoSAccountingView; use tokens_accounting::TokensAccountingView; use tx_verifier::{ @@ -48,7 +49,7 @@ impl Default for DefaultTransactionVerificationStrategy { } impl TransactionVerificationStrategy for DefaultTransactionVerificationStrategy { - fn connect_block( + fn connect_block( &self, tx_verifier_maker: M, storage_backend: S, @@ -56,14 +57,15 @@ impl TransactionVerificationStrategy for DefaultTransactionVerificationStrategy block_index: &BlockIndex, block: &WithId, median_time_past: BlockTimestamp, - ) -> Result, ConnectTransactionError> + ) -> Result, ConnectTransactionError> where C: AsRef + ShallowClone, S: TransactionVerifierStorageRef, U: UtxosView, A: PoSAccountingView, T: TokensAccountingView, - M: TransactionVerifierMakerFn, + O: OrdersAccountingView, + M: TransactionVerifierMakerFn, ::Error: From, { let mut tx_verifier = tx_verifier_maker(storage_backend, chain_config.shallow_clone()); @@ -113,20 +115,21 @@ impl TransactionVerificationStrategy for DefaultTransactionVerificationStrategy Ok(tx_verifier) } - fn disconnect_block( + fn disconnect_block( &self, tx_verifier_maker: M, storage_backend: S, chain_config: C, block: &WithId, - ) -> Result, ConnectTransactionError> + ) -> Result, ConnectTransactionError> where C: AsRef, S: TransactionVerifierStorageRef, U: UtxosView, A: PoSAccountingView, T: TokensAccountingView, - M: TransactionVerifierMakerFn, + O: OrdersAccountingView, + M: TransactionVerifierMakerFn, ::Error: From, { let mut tx_verifier = tx_verifier_maker(storage_backend, chain_config); diff --git a/chainstate/src/detail/tx_verification_strategy/mod.rs b/chainstate/src/detail/tx_verification_strategy/mod.rs index f2bcf4477a..c793a9dcfb 100644 --- a/chainstate/src/detail/tx_verification_strategy/mod.rs +++ b/chainstate/src/detail/tx_verification_strategy/mod.rs @@ -21,6 +21,7 @@ use common::{ chain::{block::timestamp::BlockTimestamp, Block, ChainConfig}, primitives::id::WithId, }; +use orders_accounting::OrdersAccountingView; use pos_accounting::PoSAccountingView; use tokens_accounting::TokensAccountingView; use tx_verifier::{ @@ -34,13 +35,13 @@ use utils::shallow_clone::ShallowClone; use utxo::UtxosView; // TODO: replace with trait_alias when stabilized -pub trait TransactionVerifierMakerFn: - Fn(S, C) -> TransactionVerifier +pub trait TransactionVerifierMakerFn: + Fn(S, C) -> TransactionVerifier { } -impl TransactionVerifierMakerFn for F where - F: Fn(S, C) -> TransactionVerifier +impl TransactionVerifierMakerFn for F where + F: Fn(S, C) -> TransactionVerifier { } @@ -53,7 +54,7 @@ pub trait TransactionVerificationStrategy: Sized + Send { /// state. It just returns a TransactionVerifier that can be /// used to update the database/storage state. #[allow(clippy::too_many_arguments)] - fn connect_block( + fn connect_block( &self, tx_verifier_maker: M, storage_backend: S, @@ -61,14 +62,15 @@ pub trait TransactionVerificationStrategy: Sized + Send { block_index: &BlockIndex, block: &WithId, median_time_past: BlockTimestamp, - ) -> Result, ConnectTransactionError> + ) -> Result, ConnectTransactionError> where S: TransactionVerifierStorageRef, U: UtxosView, C: AsRef + ShallowClone, A: PoSAccountingView, T: TokensAccountingView, - M: TransactionVerifierMakerFn, + O: OrdersAccountingView, + M: TransactionVerifierMakerFn, ::Error: From; /// Disconnect the transactions given by block and block_index, @@ -77,19 +79,20 @@ pub trait TransactionVerificationStrategy: Sized + Send { /// Notice that this doesn't modify the internal database/storage /// state. It just returns a TransactionVerifier that can be /// used to update the database/storage state. - fn disconnect_block( + fn disconnect_block( &self, tx_verifier_maker: M, storage_backend: S, chain_config: C, block: &WithId, - ) -> Result, ConnectTransactionError> + ) -> Result, ConnectTransactionError> where C: AsRef, S: TransactionVerifierStorageRef, U: UtxosView, A: PoSAccountingView, T: TokensAccountingView, - M: TransactionVerifierMakerFn, + O: OrdersAccountingView, + M: TransactionVerifierMakerFn, ::Error: From; } diff --git a/chainstate/src/interface/chainstate_interface.rs b/chainstate/src/interface/chainstate_interface.rs index abebc8b549..d50fc2bb5d 100644 --- a/chainstate/src/interface/chainstate_interface.rs +++ b/chainstate/src/interface/chainstate_interface.rs @@ -27,8 +27,8 @@ use common::{ GenBlock, }, tokens::{RPCTokenInfo, TokenAuxiliaryData, TokenId}, - AccountNonce, AccountType, ChainConfig, DelegationId, PoolId, Transaction, TxInput, - UtxoOutPoint, + AccountNonce, AccountType, ChainConfig, DelegationId, OrderData, OrderId, PoolId, + Transaction, TxInput, UtxoOutPoint, }, primitives::{Amount, BlockHeight, Id}, }; @@ -220,6 +220,10 @@ pub trait ChainstateInterface: Send + Sync { fn get_token_circulating_supply(&self, id: &TokenId) -> Result, ChainstateError>; + fn get_order_data(&self, id: &OrderId) -> Result, ChainstateError>; + fn get_order_ask_balance(&self, id: &OrderId) -> Result, ChainstateError>; + fn get_order_give_balance(&self, id: &OrderId) -> Result, ChainstateError>; + /// Returns the coin amounts of the outpoints spent by a transaction. /// If a utxo for an input was not found or contains tokens the result is `None`. fn get_inputs_outpoints_coin_amount( diff --git a/chainstate/src/interface/chainstate_interface_impl.rs b/chainstate/src/interface/chainstate_interface_impl.rs index f618300572..0823ec8bbf 100644 --- a/chainstate/src/interface/chainstate_interface_impl.rs +++ b/chainstate/src/interface/chainstate_interface_impl.rs @@ -35,8 +35,8 @@ use common::{ block::{signed_block_header::SignedBlockHeader, Block, BlockReward, GenBlock}, config::ChainConfig, tokens::{RPCTokenInfo, TokenAuxiliaryData, TokenId}, - AccountNonce, AccountType, DelegationId, PoolId, Transaction, TxInput, TxOutput, - UtxoOutPoint, + AccountNonce, AccountType, DelegationId, OrderData, OrderId, PoolId, Transaction, TxInput, + TxOutput, UtxoOutPoint, }, primitives::{id::WithId, Amount, BlockHeight, Id, Idable}, }; @@ -767,6 +767,33 @@ where .get_token_circulating_supply(id) .map_err(ChainstateError::from) } + + #[tracing::instrument(skip_all, fields(id = %id))] + fn get_order_data(&self, id: &OrderId) -> Result, ChainstateError> { + self.chainstate + .query() + .map_err(ChainstateError::from)? + .get_order_data(id) + .map_err(ChainstateError::from) + } + + #[tracing::instrument(skip_all, fields(id = %id))] + fn get_order_ask_balance(&self, id: &OrderId) -> Result, ChainstateError> { + self.chainstate + .query() + .map_err(ChainstateError::from)? + .get_order_ask_balance(id) + .map_err(ChainstateError::from) + } + + #[tracing::instrument(skip_all, fields(id = %id))] + fn get_order_give_balance(&self, id: &OrderId) -> Result, ChainstateError> { + self.chainstate + .query() + .map_err(ChainstateError::from)? + .get_order_give_balance(id) + .map_err(ChainstateError::from) + } } // TODO: remove this function. The value of an output cannot be generalized and exposed from ChainstateInterface in such way @@ -804,7 +831,8 @@ fn get_output_coin_amount( TxOutput::CreateDelegationId(_, _) | TxOutput::IssueFungibleToken(_) | TxOutput::IssueNft(_, _, _) - | TxOutput::DataDeposit(_) => None, + | TxOutput::DataDeposit(_) + | TxOutput::AnyoneCanTake(_) => None, }; Ok(amount) diff --git a/chainstate/src/interface/chainstate_interface_impl_delegation.rs b/chainstate/src/interface/chainstate_interface_impl_delegation.rs index 0eb6590368..409154137d 100644 --- a/chainstate/src/interface/chainstate_interface_impl_delegation.rs +++ b/chainstate/src/interface/chainstate_interface_impl_delegation.rs @@ -26,8 +26,8 @@ use common::{ block::{signed_block_header::SignedBlockHeader, timestamp::BlockTimestamp, BlockReward}, config::ChainConfig, tokens::{RPCTokenInfo, TokenAuxiliaryData, TokenId}, - AccountNonce, AccountType, Block, DelegationId, GenBlock, PoolId, Transaction, TxInput, - UtxoOutPoint, + AccountNonce, AccountType, Block, DelegationId, GenBlock, OrderData, OrderId, PoolId, + Transaction, TxInput, UtxoOutPoint, }, primitives::{Amount, BlockHeight, Id}, }; @@ -409,6 +409,18 @@ where ) -> Result, ChainstateError> { self.deref().get_token_circulating_supply(id) } + + fn get_order_data(&self, id: &OrderId) -> Result, ChainstateError> { + self.deref().get_order_data(id) + } + + fn get_order_ask_balance(&self, id: &OrderId) -> Result, ChainstateError> { + self.deref().get_order_ask_balance(id) + } + + fn get_order_give_balance(&self, id: &OrderId) -> Result, ChainstateError> { + self.deref().get_order_give_balance(id) + } } #[cfg(test)] diff --git a/chainstate/src/rpc/types/account.rs b/chainstate/src/rpc/types/account.rs index c4c138441d..021db073bc 100644 --- a/chainstate/src/rpc/types/account.rs +++ b/chainstate/src/rpc/types/account.rs @@ -17,11 +17,13 @@ use common::{ address::{AddressError, RpcAddress}, chain::{ tokens::{IsTokenUnfreezable, TokenId}, - AccountCommand, AccountSpending, ChainConfig, DelegationId, Destination, + AccountCommand, AccountSpending, ChainConfig, DelegationId, Destination, OrderId, }, primitives::amount::RpcAmountOut, }; +use super::output::RpcOutputValue; + #[derive(Debug, Clone, serde::Serialize, rpc_description::HasValueHint)] #[serde(tag = "type", content = "content")] pub enum RpcAccountSpending { @@ -72,6 +74,14 @@ pub enum RpcAccountCommand { token_id: RpcAddress, new_authority: RpcAddress, }, + ConcludeOrder { + order_id: RpcAddress, + }, + FillOrder { + order_id: RpcAddress, + fill_value: RpcOutputValue, + destination: RpcAddress, + }, } impl RpcAccountCommand { @@ -103,6 +113,14 @@ impl RpcAccountCommand { new_authority: RpcAddress::new(chain_config, destination.clone())?, } } + AccountCommand::ConcludeOrder(id) => RpcAccountCommand::ConcludeOrder { + order_id: RpcAddress::new(chain_config, *id)?, + }, + AccountCommand::FillOrder(id, fill, dest) => RpcAccountCommand::FillOrder { + order_id: RpcAddress::new(chain_config, *id)?, + fill_value: RpcOutputValue::new(chain_config, fill.clone())?, + destination: RpcAddress::new(chain_config, dest.clone())?, + }, }; Ok(result) } diff --git a/chainstate/src/rpc/types/output.rs b/chainstate/src/rpc/types/output.rs index fa0d7f0bed..2084087b2f 100644 --- a/chainstate/src/rpc/types/output.rs +++ b/chainstate/src/rpc/types/output.rs @@ -40,7 +40,7 @@ pub enum RpcOutputValue { } impl RpcOutputValue { - fn new(chain_config: &ChainConfig, value: OutputValue) -> Result { + pub fn new(chain_config: &ChainConfig, value: OutputValue) -> Result { let result = match value { OutputValue::Coin(amount) => RpcOutputValue::Coin { amount: RpcAmountOut::from_amount(amount, chain_config.coin_decimals()), @@ -151,6 +151,11 @@ pub enum RpcTxOutput { value: RpcOutputValue, htlc: RpcHashedTimelockContract, }, + AnyoneCanTake { + authority: RpcAddress, + ask_value: RpcOutputValue, + give_value: RpcOutputValue, + }, } impl RpcTxOutput { @@ -203,6 +208,11 @@ impl RpcTxOutput { TxOutput::DataDeposit(data) => RpcTxOutput::DataDeposit { data: RpcHexString::from_bytes(data), }, + TxOutput::AnyoneCanTake(data) => RpcTxOutput::AnyoneCanTake { + authority: RpcAddress::new(chain_config, data.conclude_key().clone())?, + ask_value: RpcOutputValue::new(chain_config, data.ask().clone())?, + give_value: RpcOutputValue::new(chain_config, data.give().clone())?, + }, }; Ok(result) } diff --git a/chainstate/storage/Cargo.toml b/chainstate/storage/Cargo.toml index d8f3205140..5a373cc21c 100644 --- a/chainstate/storage/Cargo.toml +++ b/chainstate/storage/Cargo.toml @@ -12,6 +12,7 @@ accounting = { path = "../../accounting" } chainstate-types = { path = "../types" } common = { path = "../../common" } logging = { path = "../../logging" } +orders-accounting = { path = "../../orders-accounting" } pos-accounting = { path = "../../pos-accounting" } randomness = { path = "../../randomness" } serialization = { path = "../../serialization" } diff --git a/chainstate/storage/src/internal/expensive.rs b/chainstate/storage/src/internal/expensive.rs index 012cbc811c..ab1ada3aa6 100644 --- a/chainstate/storage/src/internal/expensive.rs +++ b/chainstate/storage/src/internal/expensive.rs @@ -145,4 +145,33 @@ impl StoreTxRo<'_, B> { circulating_supply, }) } + + #[log_error] + pub fn read_orders_accounting_data( + &self, + ) -> crate::Result { + let order_data = self + .0 + .get::() + .prefix_iter_decoded(&())? + .collect::>(); + + let ask_balances = self + .0 + .get::() + .prefix_iter_decoded(&())? + .collect::>(); + + let give_balances = self + .0 + .get::() + .prefix_iter_decoded(&())? + .collect::>(); + + Ok(orders_accounting::OrdersAccountingData { + order_data, + ask_balances, + give_balances, + }) + } } diff --git a/chainstate/storage/src/internal/store_tx/read_impls.rs b/chainstate/storage/src/internal/store_tx/read_impls.rs index 3322e8d666..2975e04d97 100644 --- a/chainstate/storage/src/internal/store_tx/read_impls.rs +++ b/chainstate/storage/src/internal/store_tx/read_impls.rs @@ -22,11 +22,12 @@ use common::{ block::{signed_block_header::SignedBlockHeader, BlockReward}, config::{EpochIndex, MagicBytes}, tokens::{TokenAuxiliaryData, TokenId}, - AccountNonce, AccountType, Block, DelegationId, GenBlock, PoolId, Transaction, - UtxoOutPoint, + AccountNonce, AccountType, Block, DelegationId, GenBlock, OrderData, OrderId, PoolId, + Transaction, UtxoOutPoint, }, primitives::{Amount, BlockHeight, Id, H256}, }; +use orders_accounting::{OrdersAccountingStorageRead, OrdersAccountingUndo}; use pos_accounting::{ DelegationData, DeltaMergeUndo, PoSAccountingDeltaData, PoSAccountingStorageRead, PoSAccountingUndo, PoolData, @@ -168,6 +169,14 @@ impl<'st, B: storage::Backend> BlockchainStorageRead for super::StoreTxRo<'st, B self.read::(&id) } + #[log_error] + fn get_orders_accounting_undo( + &self, + id: Id, + ) -> crate::Result>> { + self.read::(id) + } + #[log_error] fn get_block_tree_by_height( &self, @@ -365,6 +374,25 @@ impl<'st, B: storage::Backend> TokensAccountingStorageRead for super::StoreTxRo< } } +impl<'st, B: storage::Backend> OrdersAccountingStorageRead for super::StoreTxRo<'st, B> { + type Error = crate::Error; + + #[log_error] + fn get_order_data(&self, id: &OrderId) -> Result, Self::Error> { + self.read::(id) + } + + #[log_error] + fn get_ask_balance(&self, id: &OrderId) -> Result, Self::Error> { + self.read::(id) + } + + #[log_error] + fn get_give_balance(&self, id: &OrderId) -> Result, Self::Error> { + self.read::(id) + } +} + /// Blockchain data storage transaction impl<'st, B: storage::Backend> BlockchainStorageRead for super::StoreTxRw<'st, B> { #[log_error] @@ -449,6 +477,14 @@ impl<'st, B: storage::Backend> BlockchainStorageRead for super::StoreTxRw<'st, B self.read::(&id) } + #[log_error] + fn get_orders_accounting_undo( + &self, + id: Id, + ) -> crate::Result>> { + self.read::(id) + } + #[log_error] fn get_block_tree_by_height( &self, @@ -650,3 +686,22 @@ impl<'st, B: storage::Backend> TokensAccountingStorageRead for super::StoreTxRw< self.read::(id) } } + +impl<'st, B: storage::Backend> OrdersAccountingStorageRead for super::StoreTxRw<'st, B> { + type Error = crate::Error; + + #[log_error] + fn get_order_data(&self, id: &OrderId) -> Result, Self::Error> { + self.read::(id) + } + + #[log_error] + fn get_ask_balance(&self, id: &OrderId) -> Result, Self::Error> { + self.read::(id) + } + + #[log_error] + fn get_give_balance(&self, id: &OrderId) -> Result, Self::Error> { + self.read::(id) + } +} diff --git a/chainstate/storage/src/internal/store_tx/write_impls.rs b/chainstate/storage/src/internal/store_tx/write_impls.rs index 70b1b045e8..73079d820e 100644 --- a/chainstate/storage/src/internal/store_tx/write_impls.rs +++ b/chainstate/storage/src/internal/store_tx/write_impls.rs @@ -20,11 +20,12 @@ use common::{ chain::{ config::{EpochIndex, MagicBytes}, tokens::{TokenAuxiliaryData, TokenId}, - AccountNonce, AccountType, Block, DelegationId, GenBlock, PoolId, Transaction, - UtxoOutPoint, + AccountNonce, AccountType, Block, DelegationId, GenBlock, OrderData, OrderId, PoolId, + Transaction, UtxoOutPoint, }, primitives::{Amount, BlockHeight, Id, Idable}, }; +use orders_accounting::{OrdersAccountingStorageWrite, OrdersAccountingUndo}; use pos_accounting::{ DelegationData, DeltaMergeUndo, PoSAccountingDeltaData, PoSAccountingStorageWrite, PoSAccountingUndo, PoolData, @@ -147,6 +148,20 @@ impl<'st, B: storage::Backend> BlockchainStorageWrite for StoreTxRw<'st, B> { self.del::(id) } + #[log_error] + fn set_orders_accounting_undo_data( + &mut self, + id: Id, + undo: &accounting::BlockUndo, + ) -> crate::Result<()> { + self.write::(id, undo) + } + + #[log_error] + fn del_orders_accounting_undo_data(&mut self, id: Id) -> crate::Result<()> { + self.del::(id) + } + #[log_error] fn set_pos_accounting_undo_data( &mut self, @@ -402,3 +417,35 @@ impl<'st, B: storage::Backend> TokensAccountingStorageWrite for StoreTxRw<'st, B self.del::(id) } } + +impl<'st, B: storage::Backend> OrdersAccountingStorageWrite for StoreTxRw<'st, B> { + #[log_error] + fn set_order_data(&mut self, id: &OrderId, data: &OrderData) -> crate::Result<()> { + self.write::(id, data) + } + + #[log_error] + fn del_order_data(&mut self, id: &OrderId) -> crate::Result<()> { + self.del::(id) + } + + #[log_error] + fn set_ask_balance(&mut self, id: &OrderId, balance: &Amount) -> crate::Result<()> { + self.write::(id, balance) + } + + #[log_error] + fn del_ask_balance(&mut self, id: &OrderId) -> crate::Result<()> { + self.del::(id) + } + + #[log_error] + fn set_give_balance(&mut self, id: &OrderId, balance: &Amount) -> crate::Result<()> { + self.write::(id, balance) + } + + #[log_error] + fn del_give_balance(&mut self, id: &OrderId) -> crate::Result<()> { + self.del::(id) + } +} diff --git a/chainstate/storage/src/lib.rs b/chainstate/storage/src/lib.rs index 91c61fa71e..29d35a7852 100644 --- a/chainstate/storage/src/lib.rs +++ b/chainstate/storage/src/lib.rs @@ -34,6 +34,9 @@ use common::{ }, primitives::{BlockHeight, Id}, }; +use orders_accounting::{ + OrdersAccountingStorageRead, OrdersAccountingStorageWrite, OrdersAccountingUndo, +}; use pos_accounting::{ DeltaMergeUndo, PoSAccountingDeltaData, PoSAccountingStorageRead, PoSAccountingStorageWrite, PoSAccountingUndo, @@ -66,6 +69,7 @@ pub trait BlockchainStorageRead: + PoSAccountingStorageRead + EpochStorageRead + TokensAccountingStorageRead + + OrdersAccountingStorageRead { // TODO: below (and in lots of other places too) Id is sometimes passes by ref and sometimes // by value. It's better to choose one "canonical" approach and use it everywhere. @@ -123,6 +127,12 @@ pub trait BlockchainStorageRead: id: Id, ) -> crate::Result>>; + /// Get tokens accounting undo for specific block + fn get_orders_accounting_undo( + &self, + id: Id, + ) -> crate::Result>>; + /// Get accounting undo for specific block fn get_pos_accounting_undo( &self, @@ -163,6 +173,7 @@ pub trait BlockchainStorageWrite: + PoSAccountingStorageWrite + EpochStorageWrite + TokensAccountingStorageWrite + + OrdersAccountingStorageWrite { /// Set storage version fn set_storage_version(&mut self, version: ChainstateStorageVersion) -> Result<()>; @@ -226,6 +237,16 @@ pub trait BlockchainStorageWrite: /// Remove tokens accounting undo data for specific block fn del_tokens_accounting_undo_data(&mut self, id: Id) -> Result<()>; + /// Set orders accounting undo data for specific block + fn set_orders_accounting_undo_data( + &mut self, + id: Id, + undo: &accounting::BlockUndo, + ) -> Result<()>; + + /// Remove orders accounting undo data for specific block + fn del_orders_accounting_undo_data(&mut self, id: Id) -> Result<()>; + /// Set accounting block undo data for specific block fn set_pos_accounting_undo_data( &mut self, diff --git a/chainstate/storage/src/mock/mock_impl.rs b/chainstate/storage/src/mock/mock_impl.rs index e1769e9e8b..4e7bfaa011 100644 --- a/chainstate/storage/src/mock/mock_impl.rs +++ b/chainstate/storage/src/mock/mock_impl.rs @@ -24,10 +24,14 @@ use common::{ config::{EpochIndex, MagicBytes}, tokens::{TokenAuxiliaryData, TokenId}, transaction::Transaction, - AccountNonce, AccountType, Block, DelegationId, GenBlock, PoolId, UtxoOutPoint, + AccountNonce, AccountType, Block, DelegationId, GenBlock, OrderData, OrderId, PoolId, + UtxoOutPoint, }, primitives::{Amount, BlockHeight, Id}, }; +use orders_accounting::{ + OrdersAccountingStorageRead, OrdersAccountingStorageWrite, OrdersAccountingUndo, +}; use pos_accounting::{ DelegationData, DeltaMergeUndo, PoSAccountingDeltaData, PoSAccountingUndo, PoolData, }; @@ -76,6 +80,11 @@ mockall::mock! { id: Id, ) -> crate::Result>>; + fn get_orders_accounting_undo( + &self, + id: Id, + ) -> crate::Result>>; + fn get_block_tree_by_height( &self, start_from: BlockHeight, @@ -160,6 +169,13 @@ mockall::mock! { fn get_circulating_supply(&self, id: &TokenId,) -> crate::Result >; } + impl OrdersAccountingStorageRead for Store { + type Error = crate::Error; + fn get_order_data(&self, id: &OrderId) -> crate::Result>; + fn get_ask_balance(&self, id: &OrderId) -> crate::Result>; + fn get_give_balance(&self, id: &OrderId) -> crate::Result>; + } + impl crate::BlockchainStorageWrite for Store { fn set_storage_version(&mut self, version: ChainstateStorageVersion) -> crate::Result<()>; fn set_magic_bytes(&mut self, bytes: &MagicBytes) -> crate::Result<()>; @@ -195,6 +211,13 @@ mockall::mock! { ) -> crate::Result<()>; fn del_tokens_accounting_undo_data(&mut self, id: Id) -> crate::Result<()>; + fn set_orders_accounting_undo_data( + &mut self, + id: Id, + undo: &accounting::BlockUndo, + ) -> crate::Result<()>; + fn del_orders_accounting_undo_data(&mut self, id: Id) -> crate::Result<()>; + fn set_pos_accounting_undo_data(&mut self, id: Id, undo: &accounting::BlockUndo) -> crate::Result<()>; fn del_pos_accounting_undo_data(&mut self, id: Id) -> crate::Result<()>; @@ -307,6 +330,17 @@ mockall::mock! { fn del_circulating_supply(&mut self, id: &TokenId) -> crate::Result<()>; } + impl OrdersAccountingStorageWrite for Store { + fn set_order_data(&mut self, id: &OrderId, data: &OrderData) -> crate::Result<()>; + fn del_order_data(&mut self, id: &OrderId) -> crate::Result<()>; + + fn set_ask_balance(&mut self, id: &OrderId, balance: &Amount) -> crate::Result<()>; + fn del_ask_balance(&mut self, id: &OrderId) -> crate::Result<()>; + + fn set_give_balance(&mut self, id: &OrderId, balance: &Amount) -> crate::Result<()>; + fn del_give_balance(&mut self, id: &OrderId) -> crate::Result<()>; + } + #[allow(clippy::extra_unused_lifetimes)] impl<'tx> crate::Transactional<'tx> for Store { type TransactionRo = MockStoreTxRo; @@ -351,6 +385,8 @@ mockall::mock! { fn get_tokens_accounting_undo(&self, id: Id) -> crate::Result>>; + fn get_orders_accounting_undo(&self, id: Id) -> crate::Result>>; + fn get_pos_accounting_undo(&self, id: Id) -> crate::Result>>; fn get_accounting_epoch_delta( @@ -430,6 +466,13 @@ mockall::mock! { fn get_circulating_supply(&self, id: &TokenId,) -> crate::Result >; } + impl OrdersAccountingStorageRead for StoreTxRo { + type Error = crate::Error; + fn get_order_data(&self, id: &OrderId) -> crate::Result>; + fn get_ask_balance(&self, id: &OrderId) -> crate::Result>; + fn get_give_balance(&self, id: &OrderId) -> crate::Result>; + } + impl crate::TransactionRo for StoreTxRo { fn close(self); } @@ -470,6 +513,8 @@ mockall::mock! { fn get_pos_accounting_undo(&self, id: Id) -> crate::Result>>; + fn get_orders_accounting_undo(&self, id: Id) -> crate::Result>>; + fn get_accounting_epoch_delta( &self, epoch_index: EpochIndex, @@ -547,6 +592,13 @@ mockall::mock! { fn get_circulating_supply(&self, id: &TokenId,) -> crate::Result >; } + impl OrdersAccountingStorageRead for StoreTxRw { + type Error = crate::Error; + fn get_order_data(&self, id: &OrderId) -> crate::Result>; + fn get_ask_balance(&self, id: &OrderId) -> crate::Result>; + fn get_give_balance(&self, id: &OrderId) -> crate::Result>; + } + impl crate::BlockchainStorageWrite for StoreTxRw { fn set_storage_version(&mut self, version: ChainstateStorageVersion) -> crate::Result<()>; fn set_magic_bytes(&mut self, bytes: &MagicBytes) -> crate::Result<()>; @@ -582,6 +634,13 @@ mockall::mock! { ) -> crate::Result<()>; fn del_tokens_accounting_undo_data(&mut self, id: Id) -> crate::Result<()>; + fn set_orders_accounting_undo_data( + &mut self, + id: Id, + undo: &accounting::BlockUndo, + ) -> crate::Result<()>; + fn del_orders_accounting_undo_data(&mut self, id: Id) -> crate::Result<()>; + fn set_pos_accounting_undo_data(&mut self, id: Id, undo: &accounting::BlockUndo) -> crate::Result<()>; fn del_pos_accounting_undo_data(&mut self, id: Id) -> crate::Result<()>; @@ -694,6 +753,18 @@ mockall::mock! { fn del_circulating_supply(&mut self, id: &TokenId) -> crate::Result<()>; } + + impl OrdersAccountingStorageWrite for StoreTxRw { + fn set_order_data(&mut self, id: &OrderId, data: &OrderData) -> crate::Result<()>; + fn del_order_data(&mut self, id: &OrderId) -> crate::Result<()>; + + fn set_ask_balance(&mut self, id: &OrderId, balance: &Amount) -> crate::Result<()>; + fn del_ask_balance(&mut self, id: &OrderId) -> crate::Result<()>; + + fn set_give_balance(&mut self, id: &OrderId, balance: &Amount) -> crate::Result<()>; + fn del_give_balance(&mut self, id: &OrderId) -> crate::Result<()>; + } + impl crate::TransactionRw for StoreTxRw { fn abort(self); fn commit(self) -> crate::Result<()>; diff --git a/chainstate/storage/src/schema.rs b/chainstate/storage/src/schema.rs index 69da5254d7..6ce20515ce 100644 --- a/chainstate/storage/src/schema.rs +++ b/chainstate/storage/src/schema.rs @@ -20,11 +20,12 @@ use common::{ chain::{ config::EpochIndex, tokens::{TokenAuxiliaryData, TokenId}, - AccountNonce, AccountType, Block, DelegationId, GenBlock, PoolId, Transaction, - UtxoOutPoint, + AccountNonce, AccountType, Block, DelegationId, GenBlock, OrderData, OrderId, PoolId, + Transaction, UtxoOutPoint, }, primitives::{Amount, BlockHeight, Id}, }; +use orders_accounting::OrdersAccountingUndo; use pos_accounting::{ DelegationData, DeltaMergeUndo, PoSAccountingDeltaData, PoSAccountingUndo, PoolData, }; @@ -59,6 +60,11 @@ storage::decl_schema! { pub DBTokensCirculatingSupply: Map, pub DBTokensAccountingBlockUndo: Map, accounting::BlockUndo>, + pub DBOrdersData: Map, + pub DBOrdersAskBalances: Map, + pub DBOrdersGiveBalances: Map, + pub DBOrdersAccountingBlockUndo: Map, accounting::BlockUndo>, + /// Store for accounting BlockUndo pub DBAccountingBlockUndo: Map, accounting::BlockUndo>, /// Store for accounting deltas per epoch diff --git a/chainstate/test-framework/Cargo.toml b/chainstate/test-framework/Cargo.toml index 7c37d8e8f5..a2b0bab55f 100644 --- a/chainstate/test-framework/Cargo.toml +++ b/chainstate/test-framework/Cargo.toml @@ -15,6 +15,7 @@ common = { path = "../../common" } consensus = { path = "../../consensus" } constraints-value-accumulator = { path = "../constraints-value-accumulator" } crypto = { path = "../../crypto" } +orders-accounting = { path = "../../orders-accounting" } pos-accounting = { path = "../../pos-accounting" } randomness = { path = "../../randomness" } serialization = { path = "../../serialization" } diff --git a/chainstate/test-framework/src/block_builder.rs b/chainstate/test-framework/src/block_builder.rs index bca00c0916..cfcd7b8027 100644 --- a/chainstate/test-framework/src/block_builder.rs +++ b/chainstate/test-framework/src/block_builder.rs @@ -37,6 +37,7 @@ use common::{ }; use crypto::key::PrivateKey; use itertools::Itertools; +use orders_accounting::{InMemoryOrdersAccounting, OrdersAccountingDB}; use pos_accounting::{InMemoryPoSAccounting, PoSAccountingDB}; use randomness::{CryptoRng, Rng}; use serialization::Encode; @@ -58,6 +59,7 @@ pub struct BlockBuilder<'f> { account_nonce_tracker: BTreeMap, tokens_accounting_store: InMemoryTokensAccounting, pos_accounting_store: InMemoryPoSAccounting, + orders_accounting_store: InMemoryOrdersAccounting, } impl<'f> BlockBuilder<'f> { @@ -91,6 +93,18 @@ impl<'f> BlockBuilder<'f> { .unwrap(); let pos_accounting_store = InMemoryPoSAccounting::from_data(all_pos_accounting_data); + let all_orders_data = framework + .storage + .transaction_ro() + .unwrap() + .read_orders_accounting_data() + .unwrap(); + let orders_accounting_store = InMemoryOrdersAccounting::from_values( + all_orders_data.order_data, + all_orders_data.ask_balances, + all_orders_data.give_balances, + ); + Self { framework, transactions, @@ -104,6 +118,7 @@ impl<'f> BlockBuilder<'f> { account_nonce_tracker, tokens_accounting_store, pos_accounting_store, + orders_accounting_store, } } @@ -124,6 +139,7 @@ impl<'f> BlockBuilder<'f> { mut self, rng: &mut (impl Rng + CryptoRng), support_htlc: bool, + support_orders: bool, ) -> Self { let utxo_set = self .framework @@ -144,15 +160,17 @@ impl<'f> BlockBuilder<'f> { }) }); - let (tx, new_tokens_delta, new_pos_accounting_delta) = + let (tx, new_tokens_delta, new_pos_accounting_delta, new_orders_accounting_delta) = super::random_tx_maker::RandomTxMaker::new( &self.framework.chainstate, &utxo_set, &self.tokens_accounting_store, &self.pos_accounting_store, + &self.orders_accounting_store, None, account_nonce_getter, support_htlc, + support_orders, ) .make( rng, @@ -165,8 +183,10 @@ impl<'f> BlockBuilder<'f> { // spending destinations could change let tokens_db = TokensAccountingDB::new(&self.tokens_accounting_store); let pos_db = PoSAccountingDB::new(&self.pos_accounting_store); - let destination_getter = - SignatureDestinationGetter::new_for_transaction(&tokens_db, &pos_db, &utxo_set); + let orders_db = OrdersAccountingDB::new(&self.orders_accounting_store); + let destination_getter = SignatureDestinationGetter::new_for_transaction( + &tokens_db, &pos_db, &orders_db, &utxo_set, + ); let witnesses = sign_witnesses( rng, &self.framework.key_manager, @@ -181,6 +201,10 @@ impl<'f> BlockBuilder<'f> { let mut tokens_db = TokensAccountingDB::new(&mut self.tokens_accounting_store); tokens_db.merge_with_delta(new_tokens_delta).unwrap(); + // flush new orders info to the in-memory store + let mut orders_db = OrdersAccountingDB::new(&mut self.orders_accounting_store); + orders_db.merge_with_delta(new_orders_accounting_delta).unwrap(); + // flush new pos accounting info to the in-memory store let mut pos_db = PoSAccountingDB::new(&mut self.pos_accounting_store); pos_db.merge_with_delta(new_pos_accounting_delta).unwrap(); diff --git a/chainstate/test-framework/src/key_manager.rs b/chainstate/test-framework/src/key_manager.rs index 3d449d6ebd..7e6392d721 100644 --- a/chainstate/test-framework/src/key_manager.rs +++ b/chainstate/test-framework/src/key_manager.rs @@ -253,7 +253,8 @@ fn is_htlc_output(output: &TxOutput) -> bool { | TxOutput::DelegateStaking(_, _) | TxOutput::IssueFungibleToken(_) | TxOutput::IssueNft(_, _, _) - | TxOutput::DataDeposit(_) => false, + | TxOutput::DataDeposit(_) + | TxOutput::AnyoneCanTake(_) => false, TxOutput::Htlc(_, _) => true, } } diff --git a/chainstate/test-framework/src/lib.rs b/chainstate/test-framework/src/lib.rs index d1e69b4a39..b0631153f0 100644 --- a/chainstate/test-framework/src/lib.rs +++ b/chainstate/test-framework/src/lib.rs @@ -40,7 +40,7 @@ pub use { anyonecanspend_address, create_chain_config_with_default_staking_pool, create_chain_config_with_staking_pool, create_custom_genesis_with_stake_pool, create_stake_pool_data_with_all_reward_to_staker, empty_witness, get_output_value, - pos_mine, produce_kernel_signature, + output_value_amount, pos_mine, produce_kernel_signature, }, block_builder::BlockBuilder, framework::TestFramework, diff --git a/chainstate/test-framework/src/pos_block_builder.rs b/chainstate/test-framework/src/pos_block_builder.rs index 80fd56b762..72ad0f7af0 100644 --- a/chainstate/test-framework/src/pos_block_builder.rs +++ b/chainstate/test-framework/src/pos_block_builder.rs @@ -44,6 +44,7 @@ use crypto::{ key::{PrivateKey, PublicKey}, vrf::VRFPrivateKey, }; +use orders_accounting::{InMemoryOrdersAccounting, OrdersAccountingDB}; use pos_accounting::{InMemoryPoSAccounting, PoSAccountingDB}; use randomness::{seq::IteratorRandom, CryptoRng, Rng}; use serialization::Encode; @@ -69,6 +70,7 @@ pub struct PoSBlockBuilder<'f> { account_nonce_tracker: BTreeMap, tokens_accounting_store: InMemoryTokensAccounting, pos_accounting_store: InMemoryPoSAccounting, + orders_accounting_store: InMemoryOrdersAccounting, } impl<'f> PoSBlockBuilder<'f> { @@ -97,6 +99,18 @@ impl<'f> PoSBlockBuilder<'f> { .unwrap(); let pos_accounting_store = InMemoryPoSAccounting::from_data(all_pos_accounting_data); + let all_orders_data = framework + .storage + .transaction_ro() + .unwrap() + .read_orders_accounting_data() + .unwrap(); + let orders_accounting_store = InMemoryOrdersAccounting::from_values( + all_orders_data.order_data, + all_orders_data.ask_balances, + all_orders_data.give_balances, + ); + Self { framework, transactions, @@ -112,6 +126,7 @@ impl<'f> PoSBlockBuilder<'f> { account_nonce_tracker: BTreeMap::new(), tokens_accounting_store, pos_accounting_store, + orders_accounting_store, } } @@ -364,6 +379,7 @@ impl<'f> PoSBlockBuilder<'f> { mut self, rng: &mut (impl Rng + CryptoRng), support_htlc: bool, + support_orders: bool, ) -> Self { let utxo_set = self .framework @@ -384,15 +400,17 @@ impl<'f> PoSBlockBuilder<'f> { }) }); - let (tx, new_tokens_delta, new_pos_accounting_delta) = + let (tx, new_tokens_delta, new_pos_accounting_delta, new_orders_accounting_delta) = super::random_tx_maker::RandomTxMaker::new( &self.framework.chainstate, &utxo_set, &self.tokens_accounting_store, &self.pos_accounting_store, + &self.orders_accounting_store, self.staking_pool, account_nonce_getter, support_htlc, + support_orders, ) .make( rng, @@ -405,8 +423,10 @@ impl<'f> PoSBlockBuilder<'f> { // spending destinations could change let tokens_db = TokensAccountingDB::new(&self.tokens_accounting_store); let pos_db = PoSAccountingDB::new(&self.pos_accounting_store); - let destination_getter = - SignatureDestinationGetter::new_for_transaction(&tokens_db, &pos_db, &utxo_set); + let orders_db = OrdersAccountingDB::new(&self.orders_accounting_store); + let destination_getter = SignatureDestinationGetter::new_for_transaction( + &tokens_db, &pos_db, &orders_db, &utxo_set, + ); let witnesses = sign_witnesses( rng, &self.framework.key_manager, @@ -421,6 +441,10 @@ impl<'f> PoSBlockBuilder<'f> { let mut tokens_db = TokensAccountingDB::new(&mut self.tokens_accounting_store); tokens_db.merge_with_delta(new_tokens_delta).unwrap(); + // flush new orders info to the in-memory store + let mut orders_db = OrdersAccountingDB::new(&mut self.orders_accounting_store); + orders_db.merge_with_delta(new_orders_accounting_delta).unwrap(); + // flush new pos accounting info to the in-memory store let mut pos_db = PoSAccountingDB::new(&mut self.pos_accounting_store); pos_db.merge_with_delta(new_pos_accounting_delta).unwrap(); diff --git a/chainstate/test-framework/src/random_tx_maker.rs b/chainstate/test-framework/src/random_tx_maker.rs index c482c4bd94..6825f0abbb 100644 --- a/chainstate/test-framework/src/random_tx_maker.rs +++ b/chainstate/test-framework/src/random_tx_maker.rs @@ -15,12 +15,17 @@ use std::collections::BTreeMap; -use crate::{key_manager::KeyManager, TestChainstate}; +use crate::{ + key_manager::KeyManager, + utils::{output_value_amount, output_value_with_amount}, + TestChainstate, +}; use chainstate::chainstate_interface::ChainstateInterface; use common::{ chain::{ htlc::{HashedTimelockContract, HtlcSecretHash}, + make_order_id, output_value::OutputValue, stakelock::StakePoolData, timelock::OutputTimeLock, @@ -29,15 +34,19 @@ use common::{ TokenTotalSupply, }, AccountCommand, AccountNonce, AccountOutPoint, AccountSpending, AccountType, DelegationId, - Destination, GenBlockId, OutPointSourceId, PoolId, Transaction, TxInput, TxOutput, - UtxoOutPoint, + Destination, GenBlockId, OrderData, OrderId, OutPointSourceId, PoolId, Transaction, + TxInput, TxOutput, UtxoOutPoint, }, - primitives::{per_thousand::PerThousand, Amount, BlockHeight, Id, Idable, H256}, + primitives::{per_thousand::PerThousand, Amount, BlockHeight, CoinOrTokenId, Id, Idable, H256}, }; use crypto::{ key::{KeyKind, PrivateKey}, vrf::{VRFKeyKind, VRFPrivateKey}, }; +use orders_accounting::{ + InMemoryOrdersAccounting, OrdersAccountingCache, OrdersAccountingDB, OrdersAccountingDeltaData, + OrdersAccountingOperations, OrdersAccountingView, +}; use pos_accounting::{ make_pool_id, DelegationData, InMemoryPoSAccounting, PoSAccountingDB, PoSAccountingDelta, PoSAccountingDeltaData, PoSAccountingOperations, PoSAccountingUndo, PoSAccountingView, @@ -59,9 +68,10 @@ fn get_random_pool_data<'a>( storage: &'a InMemoryPoSAccounting, tip_view: &impl PoSAccountingView, ) -> Option<(&'a PoolId, &'a PoolData)> { - let all_pool_data = storage.all_pool_data(); - (!all_pool_data.is_empty()) - .then(|| all_pool_data.iter().choose(rng).unwrap()) + storage + .all_pool_data() + .iter() + .choose(rng) .and_then(|(id, data)| tip_view.pool_exists(*id).unwrap().then_some((id, data))) } @@ -70,11 +80,48 @@ fn get_random_delegation_data<'a>( storage: &'a InMemoryPoSAccounting, tip_view: &impl PoSAccountingView, ) -> Option<(&'a DelegationId, &'a DelegationData)> { - let all_delegation_data = storage.all_delegation_data(); - (!all_delegation_data.is_empty()) - .then(|| all_delegation_data.iter().choose(rng).unwrap()) - .and_then(|(id, data)| { - tip_view.get_delegation_data(*id).unwrap().is_some().then_some((id, data)) + storage.all_delegation_data().iter().choose(rng).and_then(|(id, data)| { + tip_view.get_delegation_data(*id).unwrap().is_some().then_some((id, data)) + }) +} + +fn get_random_token<'a>( + rng: &mut impl Rng, + storage: &'a InMemoryTokensAccounting, + tip_view: &'a impl TokensAccountingView, +) -> Option<(TokenId, Amount)> { + storage + .tokens_data() + .iter() + .choose(rng) + .and_then(|(token_id, _)| { + tip_view.get_token_data(token_id).unwrap().is_some().then_some(*token_id) + }) + .and_then(|token_id| { + tip_view + .get_circulating_supply(&token_id) + .unwrap() + .map(|supply| (token_id, supply)) + }) +} + +fn get_random_order_to_fill<'a>( + storage: &'a InMemoryOrdersAccounting, + tip_view: &'a impl OrdersAccountingView, + value: &OutputValue, +) -> Option { + storage + .orders_data() + .iter() + .filter(|(id, _)| tip_view.get_order_data(id).unwrap().is_some()) + .find_map(|(order_id, data)| { + let same_currency = CoinOrTokenId::from_output_value(data.ask()) + == CoinOrTokenId::from_output_value(value); + let order_not_overbid = tip_view + .get_ask_balance(order_id) + .unwrap() + .is_some_and(|ask| ask >= output_value_amount(value)); + (same_currency && order_not_overbid).then_some(*order_id) }) } @@ -129,6 +176,7 @@ pub struct RandomTxMaker<'a> { utxo_set: &'a UtxosDBInMemoryImpl, tokens_store: &'a InMemoryTokensAccounting, pos_accounting_store: &'a InMemoryPoSAccounting, + orders_store: &'a InMemoryOrdersAccounting, // Pool used for staking cannot be spent staking_pool: Option, @@ -139,6 +187,7 @@ pub struct RandomTxMaker<'a> { // Transaction is composed of multiple inputs and outputs // but pools, delegations and tokens can be created only once per transaction token_can_be_issued: bool, + order_can_be_created: bool, stake_pool_can_be_created: bool, delegation_can_be_created: bool, @@ -156,24 +205,29 @@ pub struct RandomTxMaker<'a> { } impl<'a> RandomTxMaker<'a> { + #[allow(clippy::too_many_arguments)] pub fn new( chainstate: &'a TestChainstate, utxo_set: &'a UtxosDBInMemoryImpl, tokens_store: &'a InMemoryTokensAccounting, pos_accounting_store: &'a InMemoryPoSAccounting, + orders_store: &'a InMemoryOrdersAccounting, staking_pool: Option, account_nonce_getter: Box Option + 'a>, - support_htlc: bool, + support_htlc: bool, // TODO: remove this when api-server supports orders + support_orders: bool, // TODO: remove this when api-server supports orders ) -> Self { Self { chainstate, utxo_set, tokens_store, pos_accounting_store, + orders_store, staking_pool, account_nonce_getter, account_nonce_tracker: BTreeMap::new(), token_can_be_issued: true, + order_can_be_created: support_orders, stake_pool_can_be_created: true, delegation_can_be_created: true, account_command_used: false, @@ -193,6 +247,7 @@ impl<'a> RandomTxMaker<'a> { Transaction, TokensAccountingDeltaData, PoSAccountingDeltaData, + OrdersAccountingDeltaData, ) { let tokens_db = TokensAccountingDB::new(self.tokens_store); let mut tokens_cache = TokensAccountingCache::new(&tokens_db); @@ -200,6 +255,9 @@ impl<'a> RandomTxMaker<'a> { let pos_db = PoSAccountingDB::new(self.pos_accounting_store); let mut pos_delta = PoSAccountingDelta::new(&pos_db); + let orders_db = OrdersAccountingDB::new(self.orders_store); + let mut orders_cache = OrdersAccountingCache::new(&orders_db); + // Select random number of utxos to spend let inputs_with_utxos = { let mut inputs_with_utxos = self.select_utxos(rng); @@ -236,6 +294,7 @@ impl<'a> RandomTxMaker<'a> { staking_pools_observer, &mut tokens_cache, &mut pos_delta, + &mut orders_cache, key_manager, inputs_with_utxos, ); @@ -248,6 +307,7 @@ impl<'a> RandomTxMaker<'a> { &mut tokens_cache, &pos_db, &mut pos_delta, + &mut orders_cache, &account_inputs, key_manager, ); @@ -263,8 +323,14 @@ impl<'a> RandomTxMaker<'a> { outputs.shuffle(rng); // now that the inputs are in place calculate the ids and replace dummy values - let (outputs, new_staking_pools) = - Self::tx_outputs_post_process(rng, &mut pos_delta, &inputs, outputs); + let (outputs, new_staking_pools) = Self::tx_outputs_post_process( + rng, + &mut pos_delta, + &mut tokens_cache, + &mut orders_cache, + &inputs, + outputs, + ); let tx = Transaction::new(0, inputs, outputs).unwrap(); let tx_id = tx.get_id(); @@ -280,7 +346,8 @@ impl<'a> RandomTxMaker<'a> { | TxOutput::IssueFungibleToken(_) | TxOutput::IssueNft(_, _, _) | TxOutput::DataDeposit(_) - | TxOutput::Htlc(_, _) => { /* do nothing */ } + | TxOutput::Htlc(_, _) + | TxOutput::AnyoneCanTake(_) => { /* do nothing */ } TxOutput::CreateStakePool(pool_id, _) => { let (staker_sk, vrf_sk) = new_staking_pools.get(pool_id).unwrap(); staking_pools_observer.on_pool_created( @@ -292,7 +359,12 @@ impl<'a> RandomTxMaker<'a> { } }); - (tx, tokens_cache.consume(), pos_delta.consume()) + ( + tx, + tokens_cache.consume(), + pos_delta.consume(), + orders_cache.consume(), + ) } fn select_utxos(&self, rng: &mut impl Rng) -> Vec<(TxInput, TxOutput)> { @@ -302,7 +374,29 @@ impl<'a> RandomTxMaker<'a> { .iter() .choose_multiple(rng, number_of_inputs) .iter() - .map(|(outpoint, utxo)| (TxInput::Utxo((*outpoint).clone()), utxo.output().clone())) + .filter_map(|(outpoint, utxo)| { + let input = TxInput::Utxo((*outpoint).clone()); + let input_utxo = utxo.output().clone(); + match input_utxo { + TxOutput::LockThenTransfer(_, _, timelock) => { + self.check_timelock(&input, &timelock) + } + TxOutput::Htlc(_, ref htlc) => { + self.check_timelock(&input, &htlc.refund_timelock) + } + TxOutput::Transfer(_, _) + | TxOutput::Burn(_) + | TxOutput::CreateStakePool(_, _) + | TxOutput::ProduceBlockFromStake(_, _) + | TxOutput::CreateDelegationId(_, _) + | TxOutput::DelegateStaking(_, _) + | TxOutput::IssueFungibleToken(_) + | TxOutput::IssueNft(_, _, _) + | TxOutput::DataDeposit(_) + | TxOutput::AnyoneCanTake(_) => true, + } + .then_some((input, input_utxo)) + }) .collect() } @@ -318,7 +412,16 @@ impl<'a> RandomTxMaker<'a> { .map(|(token_id, _)| AccountType::Token(**token_id)) .collect::>(); - let mut delegations = self + let orders = self + .orders_store + .orders_data() + .iter() + .choose_multiple(rng, number_of_inputs) + .iter() + .map(|(id, _)| AccountType::Order(**id)) + .collect::>(); + + let mut result = self .pos_accounting_store .all_delegation_balances() .iter() @@ -327,10 +430,11 @@ impl<'a> RandomTxMaker<'a> { .map(|(id, _)| AccountType::Delegation(**id)) .collect::>(); - delegations.extend_from_slice(&tokens); - delegations.shuffle(rng); + result.extend_from_slice(&tokens); + result.extend_from_slice(&orders); + result.shuffle(rng); - delegations + result } fn get_next_nonce(&mut self, account: AccountType) -> AccountNonce { @@ -359,6 +463,7 @@ impl<'a> RandomTxMaker<'a> { pos_accounting_before_tx: &impl PoSAccountingView, pos_accounting_latest: &mut (impl PoSAccountingView + PoSAccountingOperations), + orders_cache: &mut (impl OrdersAccountingView + OrdersAccountingOperations), accounts: &[AccountType], key_manager: &mut KeyManager, ) -> (Vec, Vec) { @@ -409,6 +514,50 @@ impl<'a> RandomTxMaker<'a> { result_inputs.extend(inputs); result_outputs.extend(outputs); } + AccountType::Order(order_id) => { + if !self.account_command_used { + // conclude an order + let order_data = orders_cache.get_order_data(&order_id).unwrap().unwrap(); + if token_not_frozen(order_data.ask(), tokens_cache) + && token_not_frozen(order_data.give(), tokens_cache) + { + let new_nonce = self.get_next_nonce(AccountType::Order(order_id)); + result_inputs.push(TxInput::AccountCommand( + new_nonce, + AccountCommand::ConcludeOrder(order_id), + )); + + let available_give_balance = + orders_cache.get_give_balance(&order_id).unwrap().unwrap(); + let give_output = + output_value_with_amount(order_data.give(), available_give_balance); + + let current_ask_balance = + orders_cache.get_ask_balance(&order_id).unwrap().unwrap(); + let filled_amount = (output_value_amount(order_data.ask()) + - current_ask_balance) + .unwrap(); + let filled_output = + output_value_with_amount(order_data.ask(), filled_amount); + + let _ = orders_cache.conclude_order(order_id).unwrap(); + self.account_command_used = true; + + result_outputs.extend(vec![ + TxOutput::Transfer( + give_output, + key_manager + .new_destination(self.chainstate.get_chain_config(), rng), + ), + TxOutput::Transfer( + filled_output, + key_manager + .new_destination(self.chainstate.get_chain_config(), rng), + ), + ]); + } + } + } } } @@ -422,7 +571,7 @@ impl<'a> RandomTxMaker<'a> { token_id: TokenId, key_manager: &mut KeyManager, ) -> (Vec, Vec) { - if !self.account_command_used { + if self.account_command_used || self.fee_input.is_none() { return (Vec::new(), Vec::new()); } @@ -518,8 +667,14 @@ impl<'a> RandomTxMaker<'a> { Amount::from_atoms(i128::MAX as u128) } }; + let supply_left = (supply_limit - circulating_supply).unwrap(); - let to_mint = Amount::from_atoms(rng.gen_range(1..supply_left.into_atoms())); + if supply_left == Amount::ZERO { + return (Vec::new(), Vec::new()); + } + + let mint_limit = std::cmp::min(100_000, supply_left.into_atoms()); + let to_mint = Amount::from_atoms(rng.gen_range(1..=mint_limit)); let new_nonce = self.get_next_nonce(AccountType::Token(token_id)); let account_input = TxInput::AccountCommand( @@ -548,11 +703,13 @@ impl<'a> RandomTxMaker<'a> { (vec![account_input, fee_input], outputs) } else { - let is_locked = match tokens_cache.get_token_data(&token_id).unwrap().unwrap() { - tokens_accounting::TokenData::FungibleToken(data) => data.is_locked(), + let can_be_locked = match tokens_cache.get_token_data(&token_id).unwrap().unwrap() { + tokens_accounting::TokenData::FungibleToken(data) => { + !data.is_locked() && data.try_lock().is_ok() + } }; - if !is_locked { + if can_be_locked { let new_nonce = self.get_next_nonce(AccountType::Token(token_id)); let account_input = TxInput::AccountCommand( new_nonce, @@ -583,12 +740,14 @@ impl<'a> RandomTxMaker<'a> { } /// Given an output as in input creates multiple new random outputs. + #[allow(clippy::too_many_arguments)] fn create_utxo_spending( &mut self, rng: &mut (impl Rng + CryptoRng), staking_pools_observer: &mut impl StakingPoolsObserver, tokens_cache: &mut (impl TokensAccountingView + TokensAccountingOperations), pos_accounting_cache: &mut (impl PoSAccountingView + PoSAccountingOperations), + orders_cache: &mut (impl OrdersAccountingView + OrdersAccountingOperations), key_manager: &mut KeyManager, inputs: Vec<(TxInput, TxOutput)>, ) -> (Vec, Vec) { @@ -601,6 +760,7 @@ impl<'a> RandomTxMaker<'a> { rng, tokens_cache, pos_accounting_cache, + orders_cache, input, v, key_manager, @@ -613,6 +773,7 @@ impl<'a> RandomTxMaker<'a> { rng, tokens_cache, pos_accounting_cache, + orders_cache, input, v, key_manager, @@ -656,6 +817,7 @@ impl<'a> RandomTxMaker<'a> { let (mut new_inputs, new_outputs) = self.spend_tokens_v1( rng, tokens_cache, + orders_cache, *token_id, Amount::from_atoms(1), key_manager, @@ -672,6 +834,7 @@ impl<'a> RandomTxMaker<'a> { rng, tokens_cache, pos_accounting_cache, + orders_cache, input, v, key_manager, @@ -684,7 +847,8 @@ impl<'a> RandomTxMaker<'a> { | TxOutput::CreateDelegationId(_, _) | TxOutput::DelegateStaking(_, _) | TxOutput::IssueFungibleToken(_) - | TxOutput::DataDeposit(_) => unreachable!(), + | TxOutput::DataDeposit(_) + | TxOutput::AnyoneCanTake(_) => unreachable!(), }; result_inputs.extend(new_inputs); @@ -705,161 +869,256 @@ impl<'a> RandomTxMaker<'a> { &mut self, rng: &mut (impl Rng + CryptoRng), coins: Amount, + tokens_cache: &mut (impl TokensAccountingView + TokensAccountingOperations), pos_accounting_cache: &mut (impl PoSAccountingView + PoSAccountingOperations), + orders_cache: &mut (impl OrdersAccountingView + OrdersAccountingOperations), key_manager: &mut KeyManager, - ) -> Vec { - let num_outputs = rng.gen_range(1..5); - let switch = rng.gen_range(0..3); - if switch == 0 && self.token_can_be_issued { - // issue token v1 - let min_tx_fee = self.chainstate.get_chain_config().fungible_token_issuance_fee(); - if coins >= min_tx_fee { - self.token_can_be_issued = false; - let change = (coins - min_tx_fee).unwrap(); - // Coin output is created intentionally besides issuance output in order to not waste utxo - // (e.g. single genesis output on issuance) - vec![ - TxOutput::IssueFungibleToken(Box::new(TokenIssuance::V1( - random_token_issuance_v1( - self.chainstate.get_chain_config(), + ) -> (Vec, Vec) { + if coins == Amount::ZERO { + return (Vec::new(), Vec::new()); + } + + let atoms_vec = test_utils::split_value(rng, coins.into_atoms()) + .into_iter() + .filter(|v| *v > 0) + .collect::>(); + + let mut result_inputs = Vec::new(); + let mut result_outputs = Vec::new(); + + for atoms_to_spend in atoms_vec { + let switch = rng.gen_range(0..6); + let amount_to_spend = Amount::from_atoms(atoms_to_spend); + if switch == 0 && self.token_can_be_issued { + // issue token v1 + let min_tx_fee = self.chainstate.get_chain_config().fungible_token_issuance_fee(); + if amount_to_spend >= min_tx_fee { + self.token_can_be_issued = false; + let change = (amount_to_spend - min_tx_fee).unwrap(); + // Coin output is created intentionally besides issuance output in order to not waste utxo + // (e.g. single genesis output on issuance) + let outputs = vec![ + TxOutput::IssueFungibleToken(Box::new(TokenIssuance::V1( + random_token_issuance_v1( + self.chainstate.get_chain_config(), + key_manager + .new_destination(self.chainstate.get_chain_config(), rng), + rng, + ), + ))), + TxOutput::Transfer( + OutputValue::Coin(change), key_manager.new_destination(self.chainstate.get_chain_config(), rng), - rng, ), - ))), - TxOutput::Transfer( - OutputValue::Coin(change), - key_manager.new_destination(self.chainstate.get_chain_config(), rng), - ), - ] - } else { - Vec::new() - } - } else if switch == 1 && self.token_can_be_issued { - // issue nft v1 - let min_tx_fee = - self.chainstate.get_chain_config().nft_issuance_fee(BlockHeight::zero()); - if coins >= min_tx_fee { - self.token_can_be_issued = false; - let change = (coins - min_tx_fee).unwrap(); - - let dummy_inputs = vec![TxInput::from_utxo( - OutPointSourceId::Transaction(Id::::new(H256::zero())), - 0, - )]; - let dummy_token_id = make_token_id(&dummy_inputs).unwrap(); - // Coin output is created intentionally besides issuance output in order to not waste utxo - // (e.g. single genesis output on issuance) - vec![ - TxOutput::IssueNft( - dummy_token_id, - Box::new(NftIssuance::V0(random_nft_issuance( - self.chainstate.get_chain_config(), - rng, - ))), - key_manager.new_destination(self.chainstate.get_chain_config(), rng), - ), - TxOutput::Transfer( - OutputValue::Coin(change), - key_manager.new_destination(self.chainstate.get_chain_config(), rng), - ), - ] - } else { - Vec::new() - } - } else { - // transfer coins - let num_outputs = if num_outputs > coins.into_atoms() { - 1 - } else { - num_outputs - }; + ]; - let mut new_outputs = (0..num_outputs) - .map(|_| { - let new_value = Amount::from_atoms(coins.into_atoms() / num_outputs); - debug_assert!(new_value >= Amount::from_atoms(1)); + result_outputs.extend_from_slice(&outputs); + } + } else if switch == 1 && self.token_can_be_issued { + // issue nft v1 + let min_tx_fee = + self.chainstate.get_chain_config().nft_issuance_fee(BlockHeight::zero()); + if amount_to_spend >= min_tx_fee { + self.token_can_be_issued = false; + let change = (amount_to_spend - min_tx_fee).unwrap(); + + let dummy_inputs = vec![TxInput::from_utxo( + OutPointSourceId::Transaction(Id::::new(H256::zero())), + 0, + )]; + let dummy_token_id = make_token_id(&dummy_inputs).unwrap(); + // Coin output is created intentionally besides issuance output in order to not waste utxo + // (e.g. single genesis output on issuance) + let outputs = vec![ + TxOutput::IssueNft( + dummy_token_id, + Box::new(NftIssuance::V0(random_nft_issuance( + self.chainstate.get_chain_config(), + rng, + ))), + key_manager.new_destination(self.chainstate.get_chain_config(), rng), + ), + TxOutput::Transfer( + OutputValue::Coin(change), + key_manager.new_destination(self.chainstate.get_chain_config(), rng), + ), + ]; - if new_value >= self.chainstate.get_chain_config().min_stake_pool_pledge() - && self.stake_pool_can_be_created - { - // If enough coins for pledge - create a pool - let dummy_pool_id = PoolId::new(H256::zero()); - self.stake_pool_can_be_created = false; + result_outputs.extend_from_slice(&outputs); + } + } else if switch == 2 && self.order_can_be_created { + // create order to exchange part of available coins for tokens + if let Some((token_id, token_supply)) = + get_random_token(rng, self.tokens_store, tokens_cache) + { + let ask_amount = + Amount::from_atoms(rng.gen_range(1u128..=token_supply.into_atoms())); + let give_amount = Amount::from_atoms(rng.gen_range(1u128..=atoms_to_spend)); + let order_data = OrderData::new( + Destination::AnyoneCanSpend, + OutputValue::TokenV1(token_id, ask_amount), + OutputValue::Coin(give_amount), + ); + let change = (amount_to_spend - give_amount).unwrap(); + + // Transfer output is created intentionally besides order output to not waste utxo + // (e.g. single genesis output on issuance) + let outputs = vec![ + TxOutput::AnyoneCanTake(Box::new(order_data)), + TxOutput::Transfer( + OutputValue::Coin(change), + key_manager.new_destination(self.chainstate.get_chain_config(), rng), + ), + ]; - let pool_data = StakePoolData::new( - new_value, - Destination::AnyoneCanSpend, - VRFPrivateKey::new_from_rng(rng, VRFKeyKind::Schnorrkel).1, - Destination::AnyoneCanSpend, - PerThousand::new_from_rng(rng), - Amount::from_atoms(rng.gen_range(0..1000)), + self.order_can_be_created = false; + + result_outputs.extend_from_slice(&outputs); + } + } else if switch == 3 && !self.account_command_used { + // try fill order + let fill_value = OutputValue::Coin(amount_to_spend); + if let Some(order_id) = + get_random_order_to_fill(self.orders_store, &orders_cache, &fill_value) + { + let filled_value = + calculate_filled_order_value(&orders_cache, order_id, &fill_value); + + if token_not_frozen(&filled_value, tokens_cache) { + let new_nonce = self.get_next_nonce(AccountType::Order(order_id)); + let input = TxInput::AccountCommand( + new_nonce, + AccountCommand::FillOrder( + order_id, + fill_value.clone(), + key_manager + .new_destination(self.chainstate.get_chain_config(), rng), + ), ); - TxOutput::CreateStakePool(dummy_pool_id, Box::new(pool_data)) - } else { - if rng.gen_bool(0.3) { - // Send coins to random delegation - if let Some((delegation_id, _)) = get_random_delegation_data( - rng, - self.pos_accounting_store, - &pos_accounting_cache, - ) { - let _ = pos_accounting_cache - .delegate_staking(*delegation_id, new_value) - .unwrap(); - return TxOutput::DelegateStaking(new_value, *delegation_id); - } + + let output = TxOutput::Transfer( + filled_value, + key_manager.new_destination(self.chainstate.get_chain_config(), rng), + ); + + let _ = orders_cache.fill_order(order_id, fill_value).unwrap(); + self.account_command_used = true; + + result_inputs.push(input); + result_outputs.push(output); + } + } + } else if switch == 6 { + // data deposit + let min_tx_fee = self.chainstate.get_chain_config().data_deposit_fee(); + if amount_to_spend >= min_tx_fee { + let change = (amount_to_spend - min_tx_fee).unwrap(); + + let deposited_data_len = + self.chainstate.get_chain_config().data_deposit_max_size(); + let deposited_data_len = rng.gen_range(0..deposited_data_len); + let deposited_data = + (0..deposited_data_len).map(|_| rng.gen::()).collect::>(); + + let outputs = vec![ + TxOutput::DataDeposit(deposited_data), + TxOutput::Transfer( + OutputValue::Coin(change), + key_manager.new_destination(self.chainstate.get_chain_config(), rng), + ), + ]; + + result_outputs.extend_from_slice(&outputs); + } + } else { + // transfer coins + let output = if amount_to_spend + >= self.chainstate.get_chain_config().min_stake_pool_pledge() + && self.stake_pool_can_be_created + { + // If enough coins for pledge - create a pool + let dummy_pool_id = PoolId::new(H256::zero()); + self.stake_pool_can_be_created = false; + + let pool_data = StakePoolData::new( + amount_to_spend, + Destination::AnyoneCanSpend, + VRFPrivateKey::new_from_rng(rng, VRFKeyKind::Schnorrkel).1, + Destination::AnyoneCanSpend, + PerThousand::new_from_rng(rng), + Amount::from_atoms(rng.gen_range(0..1000)), + ); + + TxOutput::CreateStakePool(dummy_pool_id, Box::new(pool_data)) + } else { + if rng.gen_bool(0.3) { + // Send coins to random delegation + if let Some((delegation_id, _)) = get_random_delegation_data( + rng, + self.pos_accounting_store, + &pos_accounting_cache, + ) { + let _ = pos_accounting_cache + .delegate_staking(*delegation_id, amount_to_spend) + .unwrap(); + result_outputs + .push(TxOutput::DelegateStaking(amount_to_spend, *delegation_id)); + continue; } + } - let destination = - key_manager.new_destination(self.chainstate.get_chain_config(), rng); - let timelock = get_random_timelock(rng, self.chainstate); - match rng.gen_range(0..5) { - 0 => TxOutput::LockThenTransfer( - OutputValue::Coin(new_value), - destination, - timelock, - ), - 1 => { - if self.support_htlc { - TxOutput::Htlc( - OutputValue::Coin(new_value), - Box::new(HashedTimelockContract { - secret_hash: HtlcSecretHash::zero(), - spend_key: destination, - refund_timelock: timelock, - refund_key: key_manager - .new_2_of_2_multisig_destination( - self.chainstate.get_chain_config(), - rng, - ), - }), - ) - } else { - TxOutput::Transfer(OutputValue::Coin(new_value), destination) - } + let destination = + key_manager.new_destination(self.chainstate.get_chain_config(), rng); + let timelock = get_random_timelock(rng, self.chainstate); + match rng.gen_range(0..5) { + 0 => TxOutput::LockThenTransfer( + OutputValue::Coin(amount_to_spend), + destination, + timelock, + ), + 1 => { + if self.support_htlc { + TxOutput::Htlc( + OutputValue::Coin(amount_to_spend), + Box::new(HashedTimelockContract { + secret_hash: HtlcSecretHash::zero(), + spend_key: destination, + refund_timelock: timelock, + refund_key: key_manager.new_2_of_2_multisig_destination( + self.chainstate.get_chain_config(), + rng, + ), + }), + ) + } else { + TxOutput::Transfer(OutputValue::Coin(amount_to_spend), destination) } - 2..=4 => TxOutput::Transfer(OutputValue::Coin(new_value), destination), - _ => unreachable!(), } + 2..=4 => { + TxOutput::Transfer(OutputValue::Coin(amount_to_spend), destination) + } + _ => unreachable!(), } - }) - .collect::>(); + }; - // Occasionally create new delegation id - if rng.gen_bool(0.3) && self.delegation_can_be_created { - if let Some((pool_id, _)) = - get_random_pool_data(rng, self.pos_accounting_store, &pos_accounting_cache) - { - self.delegation_can_be_created = false; + result_outputs.push(output); - new_outputs.push(TxOutput::CreateDelegationId( - key_manager.new_destination(self.chainstate.get_chain_config(), rng), - *pool_id, - )); + // Occasionally create new delegation id + if rng.gen::() && self.delegation_can_be_created { + if let Some((pool_id, _)) = + get_random_pool_data(rng, self.pos_accounting_store, &pos_accounting_cache) + { + self.delegation_can_be_created = false; + + result_outputs.push(TxOutput::CreateDelegationId( + key_manager.new_destination(self.chainstate.get_chain_config(), rng), + *pool_id, + )); + } } } - new_outputs } + (result_inputs, result_outputs) } #[allow(clippy::too_many_arguments)] @@ -868,6 +1127,7 @@ impl<'a> RandomTxMaker<'a> { rng: &mut (impl Rng + CryptoRng), tokens_cache: &mut (impl TokensAccountingView + TokensAccountingOperations), pos_accounting_cache: &mut (impl PoSAccountingView + PoSAccountingOperations), + orders_cache: &mut (impl OrdersAccountingView + OrdersAccountingOperations), input: TxInput, input_utxo_value: &OutputValue, key_manager: &mut KeyManager, @@ -877,7 +1137,15 @@ impl<'a> RandomTxMaker<'a> { match input_utxo_value { OutputValue::Coin(coins) => { - let new_outputs = self.spend_coins(rng, *coins, pos_accounting_cache, key_manager); + let (new_inputs, new_outputs) = self.spend_coins( + rng, + *coins, + tokens_cache, + pos_accounting_cache, + orders_cache, + key_manager, + ); + result_inputs.extend(new_inputs); result_outputs.extend(new_outputs); } OutputValue::TokenV0(_) => { @@ -891,6 +1159,7 @@ impl<'a> RandomTxMaker<'a> { let (new_inputs, new_outputs) = self.spend_tokens_v1( rng, tokens_cache, + orders_cache, *token_id, *amount, key_manager, @@ -909,22 +1178,58 @@ impl<'a> RandomTxMaker<'a> { &mut self, rng: &mut (impl Rng + CryptoRng), tokens_cache: &mut (impl TokensAccountingView + TokensAccountingOperations), + orders_cache: &mut (impl OrdersAccountingView + OrdersAccountingOperations), token_id: TokenId, amount: Amount, key_manager: &mut KeyManager, ) -> (Vec, Vec) { + if amount == Amount::ZERO { + return (Vec::new(), Vec::new()); + } + let atoms_vec = test_utils::split_value(rng, amount.into_atoms()); let mut result_inputs = Vec::new(); let mut result_outputs = Vec::new(); for atoms in atoms_vec { if rng.gen::() { - // transfer - result_outputs.push(TxOutput::Transfer( - OutputValue::TokenV1(token_id, Amount::from_atoms(atoms)), - key_manager.new_destination(self.chainstate.get_chain_config(), rng), - )); - } else if rng.gen_bool(0.9) && !self.account_command_used { + let output_value = OutputValue::TokenV1(token_id, Amount::from_atoms(atoms)); + if rng.gen::() { + // transfer + result_outputs.push(TxOutput::Transfer( + output_value, + key_manager.new_destination(self.chainstate.get_chain_config(), rng), + )); + } else if !self.account_command_used { + // try fill order + if let Some(order_id) = + get_random_order_to_fill(self.orders_store, &orders_cache, &output_value) + { + let new_nonce = self.get_next_nonce(AccountType::Order(order_id)); + + let filled_value = + calculate_filled_order_value(&orders_cache, order_id, &output_value); + + result_outputs.push(TxOutput::Transfer( + filled_value, + key_manager.new_destination(self.chainstate.get_chain_config(), rng), + )); + + result_inputs.push(TxInput::AccountCommand( + new_nonce, + AccountCommand::FillOrder( + order_id, + output_value.clone(), + key_manager + .new_destination(self.chainstate.get_chain_config(), rng), + ), + )); + + let _ = orders_cache.fill_order(order_id, output_value).unwrap(); + self.account_command_used = true; + } + } + } else if rng.gen_bool(0.4) && !self.account_command_used { // unmint let token_data = tokens_cache.get_token_data(&token_id).unwrap(); @@ -970,6 +1275,20 @@ impl<'a> RandomTxMaker<'a> { self.account_command_used = true; } } + } else if rng.gen_bool(0.4) && self.order_can_be_created && atoms > 0 { + // create order to exchange part of available tokens for coins or other tokens + let random_token = get_random_token(rng, self.tokens_store, tokens_cache); + let ask_value = + if rng.gen::() && random_token.is_some_and(|(id, _)| id != token_id) { + OutputValue::TokenV1(random_token.unwrap().0, random_token.unwrap().1) + } else { + OutputValue::Coin(Amount::from_atoms(rng.gen_range(100..10000))) + }; + + let give_value = OutputValue::TokenV1(token_id, Amount::from_atoms(atoms)); + let order_data = OrderData::new(Destination::AnyoneCanSpend, ask_value, give_value); + result_outputs.push(TxOutput::AnyoneCanTake(Box::new(order_data))); + self.order_can_be_created = false; } else { // burn let to_burn = Amount::from_atoms(atoms); @@ -990,6 +1309,8 @@ impl<'a> RandomTxMaker<'a> { fn tx_outputs_post_process( rng: &mut (impl Rng + CryptoRng), pos_accounting_cache: &mut (impl PoSAccountingView + PoSAccountingOperations), + tokens_cache: &mut (impl TokensAccountingView + TokensAccountingOperations), + orders_cache: &mut (impl OrdersAccountingView + OrdersAccountingOperations), inputs: &[TxInput], outputs: Vec, ) -> (Vec, BTreeMap) { @@ -1003,7 +1324,6 @@ impl<'a> RandomTxMaker<'a> { | TxOutput::Burn(_) | TxOutput::ProduceBlockFromStake(_, _) | TxOutput::DelegateStaking(_, _) - | TxOutput::IssueFungibleToken(_) | TxOutput::DataDeposit(_) | TxOutput::Htlc(_, _) => Some(output), TxOutput::CreateStakePool(dummy_pool_id, pool_data) => { @@ -1043,10 +1363,23 @@ impl<'a> RandomTxMaker<'a> { None // if a pool was decommissioned in this tx then skip creating a delegation } } + TxOutput::IssueFungibleToken(issuance) => { + let token_id = make_token_id(inputs).unwrap(); + let data = tokens_accounting::TokenData::FungibleToken( + issuance.as_ref().clone().into(), + ); + let _ = tokens_cache.issue_token(token_id, data); + Some(output) + } TxOutput::IssueNft(dummy_token_id, _, _) => { *dummy_token_id = make_token_id(inputs).unwrap(); Some(output) } + TxOutput::AnyoneCanTake(data) => { + let order_id = make_order_id(inputs[0].utxo_outpoint().unwrap()); + let _ = orders_cache.create_order(order_id, *data.clone()).unwrap(); + Some(output) + } }) .collect(); @@ -1095,3 +1428,27 @@ impl<'a> RandomTxMaker<'a> { .is_ok() } } + +fn token_not_frozen(value: &OutputValue, view: &impl TokensAccountingView) -> bool { + match value { + OutputValue::Coin(_) | OutputValue::TokenV0(_) => true, + OutputValue::TokenV1(token_id, _) => { + view.get_token_data(token_id).unwrap().is_some_and(|data| match data { + tokens_accounting::TokenData::FungibleToken(data) => !data.is_frozen(), + }) + } + } +} + +fn calculate_filled_order_value( + view: &impl OrdersAccountingView, + order_id: OrderId, + fill_value: &OutputValue, +) -> OutputValue { + let order_data = view.get_order_data(&order_id).unwrap().unwrap(); + + let filled_amount = + orders_accounting::calculate_fill_order(view, order_id, fill_value).unwrap(); + + output_value_with_amount(order_data.give(), filled_amount) +} diff --git a/chainstate/test-framework/src/signature_destination_getter.rs b/chainstate/test-framework/src/signature_destination_getter.rs index 66400a6fe5..4b244b16fa 100644 --- a/chainstate/test-framework/src/signature_destination_getter.rs +++ b/chainstate/test-framework/src/signature_destination_getter.rs @@ -14,6 +14,7 @@ // limitations under the License. use common::chain::{AccountCommand, AccountSpending, Destination, TxInput, TxOutput}; +use orders_accounting::OrdersAccountingView; use pos_accounting::PoSAccountingView; use tokens_accounting::TokensAccountingView; use tx_verifier::error::SignatureDestinationGetterError; @@ -43,9 +44,15 @@ pub struct SignatureDestinationGetter<'a> { } impl<'a> SignatureDestinationGetter<'a> { - pub fn new_for_transaction( + pub fn new_for_transaction< + T: TokensAccountingView, + P: PoSAccountingView, + U: UtxosView, + O: OrdersAccountingView, + >( tokens_view: &'a T, accounting_view: &'a P, + orders_view: &'a O, utxos_view: &'a U, ) -> Self { let destination_getter = @@ -69,7 +76,8 @@ impl<'a> SignatureDestinationGetter<'a> { } TxOutput::CreateDelegationId(_, _) | TxOutput::Burn(_) - | TxOutput::DataDeposit(_) => { + | TxOutput::DataDeposit(_) + | TxOutput::AnyoneCanTake(_) => { // This error is emitted in other places for attempting to make this spend, // but this is just a double-check. Err(SignatureDestinationGetterError::SigVerifyOfNotSpendableOutput) @@ -160,6 +168,20 @@ impl<'a> SignatureDestinationGetter<'a> { }; Ok(destination) } + AccountCommand::ConcludeOrder(order_id) => { + let order_data = orders_view + .get_order_data(order_id) + .map_err(|_| { + SignatureDestinationGetterError::OrdersAccountingViewError( + orders_accounting::Error::ViewFail, + ) + })? + .ok_or(SignatureDestinationGetterError::OrderDataNotFound( + *order_id, + ))?; + Ok(order_data.conclude_key().clone()) + } + AccountCommand::FillOrder(_, _, d) => Ok(d.clone()), }, } }; diff --git a/chainstate/test-framework/src/tx_verification_strategy/disposable_strategy.rs b/chainstate/test-framework/src/tx_verification_strategy/disposable_strategy.rs index 6239a062eb..b6e1be89f9 100644 --- a/chainstate/test-framework/src/tx_verification_strategy/disposable_strategy.rs +++ b/chainstate/test-framework/src/tx_verification_strategy/disposable_strategy.rs @@ -22,6 +22,7 @@ use common::{ primitives::{id::WithId, Idable}, }; use constraints_value_accumulator::AccumulatedFee; +use orders_accounting::OrdersAccountingView; use pos_accounting::PoSAccountingView; use tokens_accounting::TokensAccountingView; use tx_verifier::{ @@ -52,7 +53,7 @@ impl Default for DisposableTransactionVerificationStrategy { } impl TransactionVerificationStrategy for DisposableTransactionVerificationStrategy { - fn connect_block( + fn connect_block( &self, tx_verifier_maker: M, storage_backend: S, @@ -60,14 +61,15 @@ impl TransactionVerificationStrategy for DisposableTransactionVerificationStrate block_index: &BlockIndex, block: &WithId, median_time_past: BlockTimestamp, - ) -> Result, ConnectTransactionError> + ) -> Result, ConnectTransactionError> where C: AsRef + ShallowClone, S: TransactionVerifierStorageRef, U: UtxosView, A: PoSAccountingView, T: TokensAccountingView, - M: TransactionVerifierMakerFn, + O: OrdersAccountingView, + M: TransactionVerifierMakerFn, ::Error: From, { let mut base_tx_verifier = tx_verifier_maker(storage_backend, chain_config.shallow_clone()); @@ -121,20 +123,21 @@ impl TransactionVerificationStrategy for DisposableTransactionVerificationStrate Ok(base_tx_verifier) } - fn disconnect_block( + fn disconnect_block( &self, tx_verifier_maker: M, storage_backend: S, chain_config: C, block: &WithId, - ) -> Result, ConnectTransactionError> + ) -> Result, ConnectTransactionError> where C: AsRef, S: TransactionVerifierStorageRef, U: UtxosView, A: PoSAccountingView, T: TokensAccountingView, - M: TransactionVerifierMakerFn, + O: OrdersAccountingView, + M: TransactionVerifierMakerFn, ::Error: From, { let mut base_tx_verifier = tx_verifier_maker(storage_backend, chain_config); diff --git a/chainstate/test-framework/src/tx_verification_strategy/randomized_strategy.rs b/chainstate/test-framework/src/tx_verification_strategy/randomized_strategy.rs index e175f9d65c..6c71cd0edf 100644 --- a/chainstate/test-framework/src/tx_verification_strategy/randomized_strategy.rs +++ b/chainstate/test-framework/src/tx_verification_strategy/randomized_strategy.rs @@ -22,6 +22,7 @@ use common::{ primitives::{id::WithId, Idable}, }; use constraints_value_accumulator::AccumulatedFee; +use orders_accounting::OrdersAccountingView; use pos_accounting::PoSAccountingView; use randomness::{Rng, RngCore}; use test_utils::random::{make_seedable_rng, Seed}; @@ -64,7 +65,7 @@ impl RandomizedTransactionVerificationStrategy { } impl TransactionVerificationStrategy for RandomizedTransactionVerificationStrategy { - fn connect_block( + fn connect_block( &self, tx_verifier_maker: M, storage_backend: S, @@ -72,14 +73,15 @@ impl TransactionVerificationStrategy for RandomizedTransactionVerificationStrate block_index: &BlockIndex, block: &WithId, median_time_past: BlockTimestamp, - ) -> Result, ConnectTransactionError> + ) -> Result, ConnectTransactionError> where C: AsRef + ShallowClone, S: TransactionVerifierStorageRef, U: UtxosView, A: PoSAccountingView, T: TokensAccountingView, - M: TransactionVerifierMakerFn, + O: OrdersAccountingView, + M: TransactionVerifierMakerFn, ::Error: From, { let mut tx_verifier = self @@ -98,20 +100,21 @@ impl TransactionVerificationStrategy for RandomizedTransactionVerificationStrate Ok(tx_verifier) } - fn disconnect_block( + fn disconnect_block( &self, tx_verifier_maker: M, storage_backend: S, chain_config: C, block: &WithId, - ) -> Result, ConnectTransactionError> + ) -> Result, ConnectTransactionError> where C: AsRef, S: TransactionVerifierStorageRef, U: UtxosView, A: PoSAccountingView, T: TokensAccountingView, - M: TransactionVerifierMakerFn, + O: OrdersAccountingView, + M: TransactionVerifierMakerFn, ::Error: From, { let mut tx_verifier = @@ -125,7 +128,7 @@ impl TransactionVerificationStrategy for RandomizedTransactionVerificationStrate impl RandomizedTransactionVerificationStrategy { #[allow(clippy::too_many_arguments, clippy::type_complexity)] - fn connect_with_base( + fn connect_with_base( &self, tx_verifier_maker: M, storage_backend: S, @@ -133,14 +136,15 @@ impl RandomizedTransactionVerificationStrategy { block_index: &BlockIndex, block: &WithId, median_time_past: &BlockTimestamp, - ) -> Result, ConnectTransactionError> + ) -> Result, ConnectTransactionError> where C: AsRef + ShallowClone, S: TransactionVerifierStorageRef, U: UtxosView, A: PoSAccountingView, T: TokensAccountingView, - M: TransactionVerifierMakerFn, + O: OrdersAccountingView, + M: TransactionVerifierMakerFn, ::Error: From, { let mut tx_verifier = tx_verifier_maker(storage_backend, chain_config.shallow_clone()); @@ -209,9 +213,9 @@ impl RandomizedTransactionVerificationStrategy { Ok(tx_verifier) } - fn connect_with_derived( + fn connect_with_derived( &self, - base_tx_verifier: &TransactionVerifier, + base_tx_verifier: &TransactionVerifier, block: &WithId, block_index: &BlockIndex, median_time_past: &BlockTimestamp, @@ -222,6 +226,7 @@ impl RandomizedTransactionVerificationStrategy { U: UtxosView, A: PoSAccountingView, T: TokensAccountingView, + O: OrdersAccountingView, S: TransactionVerifierStorageRef, ::Error: From, { @@ -251,20 +256,21 @@ impl RandomizedTransactionVerificationStrategy { Ok((cache, total_fees, tx_num)) } - fn disconnect_with_base( + fn disconnect_with_base( &self, tx_verifier_maker: M, storage_backend: S, chain_config: C, block: &WithId, - ) -> Result, ConnectTransactionError> + ) -> Result, ConnectTransactionError> where C: AsRef, S: TransactionVerifierStorageRef, U: UtxosView, A: PoSAccountingView, T: TokensAccountingView, - M: TransactionVerifierMakerFn, + O: OrdersAccountingView, + M: TransactionVerifierMakerFn, ::Error: From, { let mut tx_verifier = tx_verifier_maker(storage_backend, chain_config); @@ -296,9 +302,9 @@ impl RandomizedTransactionVerificationStrategy { Ok(tx_verifier) } - fn disconnect_with_derived( + fn disconnect_with_derived( &self, - base_tx_verifier: &TransactionVerifier, + base_tx_verifier: &TransactionVerifier, block: &WithId, mut tx_num: i32, ) -> Result<(TransactionVerifierDelta, i32), ConnectTransactionError> @@ -307,6 +313,7 @@ impl RandomizedTransactionVerificationStrategy { U: UtxosView, A: PoSAccountingView, T: TokensAccountingView, + O: OrdersAccountingView, S: TransactionVerifierStorageRef, ::Error: From, { diff --git a/chainstate/test-framework/src/utils.rs b/chainstate/test-framework/src/utils.rs index bbc85f952d..23ccc1cd39 100644 --- a/chainstate/test-framework/src/utils.rs +++ b/chainstate/test-framework/src/utils.rs @@ -68,13 +68,29 @@ pub fn get_output_value(output: &TxOutput) -> Option { | TxOutput::CreateDelegationId(_, _) | TxOutput::DelegateStaking(_, _) | TxOutput::IssueFungibleToken(_) - | TxOutput::DataDeposit(_) => None, + | TxOutput::DataDeposit(_) + | TxOutput::AnyoneCanTake(_) => None, TxOutput::IssueNft(token_id, _, _) => { Some(OutputValue::TokenV1(*token_id, Amount::from_atoms(1))) } } } +pub fn output_value_amount(value: &OutputValue) -> Amount { + match value { + OutputValue::Coin(amount) | OutputValue::TokenV1(_, amount) => *amount, + OutputValue::TokenV0(_) => panic!("deprecated token version"), + } +} + +pub fn output_value_with_amount(value: &OutputValue, amount: Amount) -> OutputValue { + match value { + OutputValue::Coin(_) => OutputValue::Coin(amount), + OutputValue::TokenV1(token_id, _) => OutputValue::TokenV1(*token_id, amount), + OutputValue::TokenV0(_) => panic!("deprecated token version"), + } +} + pub fn create_new_outputs( srcid: OutPointSourceId, outs: &[TxOutput], @@ -129,7 +145,8 @@ pub fn create_utxo_data( | TxOutput::IssueFungibleToken(_) | TxOutput::IssueNft(_, _, _) | TxOutput::DataDeposit(_) - | TxOutput::Htlc(_, _) => None, + | TxOutput::Htlc(_, _) + | TxOutput::AnyoneCanTake(_) => None, } } @@ -386,7 +403,8 @@ pub fn find_create_pool_tx_in_genesis(genesis: &Genesis, pool_id: &PoolId) -> Op | TxOutput::IssueFungibleToken(_) | TxOutput::IssueNft(_, _, _) | TxOutput::DataDeposit(_) - | TxOutput::Htlc(_, _) => false, + | TxOutput::Htlc(_, _) + | TxOutput::AnyoneCanTake(_) => false, TxOutput::CreateStakePool(genesis_pool_id, _) => genesis_pool_id == pool_id, }); diff --git a/chainstate/test-suite/Cargo.toml b/chainstate/test-suite/Cargo.toml index 1094f6154e..148bebc9ce 100644 --- a/chainstate/test-suite/Cargo.toml +++ b/chainstate/test-suite/Cargo.toml @@ -18,6 +18,7 @@ consensus = { path = "../../consensus" } constraints-value-accumulator = { path = "../constraints-value-accumulator" } crypto = { path = "../../crypto" } logging = { path = "../../logging" } +orders-accounting = { path = "../../orders-accounting" } pos-accounting = { path = "../../pos-accounting" } randomness = { path = "../../randomness" } serialization = { path = "../../serialization" } diff --git a/chainstate/test-suite/src/tests/chainstate_storage_tests.rs b/chainstate/test-suite/src/tests/chainstate_storage_tests.rs index 72101923cb..683a652cfa 100644 --- a/chainstate/test-suite/src/tests/chainstate_storage_tests.rs +++ b/chainstate/test-suite/src/tests/chainstate_storage_tests.rs @@ -24,9 +24,9 @@ use common::{ chain::{ output_value::OutputValue, tokens::{make_token_id, NftIssuance, TokenAuxiliaryData, TokenIssuanceV0}, - ChainstateUpgrade, Destination, HtlcActivated, NetUpgrades, OutPointSourceId, - RewardDistributionVersion, TokenIssuanceVersion, TokensFeeVersion, Transaction, TxInput, - TxOutput, UtxoOutPoint, + ChainstateUpgrade, Destination, HtlcActivated, NetUpgrades, OrdersActivated, + OutPointSourceId, RewardDistributionVersion, TokenIssuanceVersion, TokensFeeVersion, + Transaction, TxInput, TxOutput, UtxoOutPoint, }, primitives::{Amount, Id, Idable}, }; @@ -120,6 +120,7 @@ fn store_fungible_token_v0(#[case] seed: Seed) { RewardDistributionVersion::V1, TokensFeeVersion::V1, HtlcActivated::Yes, + OrdersActivated::Yes, ), )]) .unwrap(), @@ -199,6 +200,7 @@ fn store_nft_v0(#[case] seed: Seed) { RewardDistributionVersion::V1, TokensFeeVersion::V1, HtlcActivated::Yes, + OrdersActivated::Yes, ), )]) .unwrap(), @@ -509,6 +511,7 @@ fn store_aux_data_from_issue_nft(#[case] seed: Seed) { RewardDistributionVersion::V1, TokensFeeVersion::V1, HtlcActivated::Yes, + OrdersActivated::Yes, ), )]) .unwrap(), diff --git a/chainstate/test-suite/src/tests/fungible_tokens.rs b/chainstate/test-suite/src/tests/fungible_tokens.rs index 7e0ea18085..eb9423fbd6 100644 --- a/chainstate/test-suite/src/tests/fungible_tokens.rs +++ b/chainstate/test-suite/src/tests/fungible_tokens.rs @@ -28,8 +28,8 @@ use common::{ output_value::OutputValue, signature::inputsig::InputWitness, tokens::{make_token_id, TokenData, TokenId}, - ChainstateUpgrade, Destination, HtlcActivated, OutPointSourceId, TokenIssuanceVersion, - TokensFeeVersion, TxInput, TxOutput, + ChainstateUpgrade, Destination, HtlcActivated, OrdersActivated, OutPointSourceId, + TokenIssuanceVersion, TokensFeeVersion, TxInput, TxOutput, }, primitives::{Amount, Idable}, }; @@ -57,6 +57,7 @@ fn make_test_framework_with_v0(rng: &mut (impl Rng + CryptoRng)) -> TestFramewor RewardDistributionVersion::V1, TokensFeeVersion::V1, HtlcActivated::Yes, + OrdersActivated::Yes, ), )]) .unwrap(), @@ -961,6 +962,7 @@ fn no_v0_issuance_after_v1(#[case] seed: Seed) { RewardDistributionVersion::V1, TokensFeeVersion::V1, HtlcActivated::Yes, + OrdersActivated::Yes, ), )]) .unwrap(), @@ -1024,6 +1026,7 @@ fn no_v0_transfer_after_v1(#[case] seed: Seed) { RewardDistributionVersion::V1, TokensFeeVersion::V1, HtlcActivated::Yes, + OrdersActivated::Yes, ), ), ( @@ -1033,6 +1036,7 @@ fn no_v0_transfer_after_v1(#[case] seed: Seed) { RewardDistributionVersion::V1, TokensFeeVersion::V1, HtlcActivated::Yes, + OrdersActivated::Yes, ), ), ]) diff --git a/chainstate/test-suite/src/tests/fungible_tokens_v1.rs b/chainstate/test-suite/src/tests/fungible_tokens_v1.rs index cc8799e4ef..9777f32ccc 100644 --- a/chainstate/test-suite/src/tests/fungible_tokens_v1.rs +++ b/chainstate/test-suite/src/tests/fungible_tokens_v1.rs @@ -53,6 +53,8 @@ use tx_verifier::{ CheckTransactionError, }; +use crate::tests::helpers::{issue_token_from_block, mint_tokens_in_block}; + fn make_issuance( rng: &mut impl Rng, supply: TokenTotalSupply, @@ -68,43 +70,6 @@ fn make_issuance( }) } -fn issue_token_from_block( - rng: &mut (impl Rng + CryptoRng), - tf: &mut TestFramework, - parent_block_id: Id, - utxo_to_pay_fee: UtxoOutPoint, - issuance: TokenIssuance, -) -> (TokenId, Id, UtxoOutPoint) { - let token_issuance_fee = tf.chainstate.get_chain_config().fungible_token_issuance_fee(); - - let fee_utxo_coins = chainstate_test_framework::get_output_value( - tf.chainstate.utxo(&utxo_to_pay_fee).unwrap().unwrap().output(), - ) - .unwrap() - .coin_amount() - .unwrap(); - - let tx = TransactionBuilder::new() - .add_input(utxo_to_pay_fee.into(), InputWitness::NoSignature(None)) - .add_output(TxOutput::Transfer( - OutputValue::Coin((fee_utxo_coins - token_issuance_fee).unwrap()), - Destination::AnyoneCanSpend, - )) - .add_output(TxOutput::IssueFungibleToken(Box::new(issuance.clone()))) - .build(); - let token_id = make_token_id(tx.transaction().inputs()).unwrap(); - let tx_id = tx.transaction().get_id(); - let block = tf - .make_block_builder() - .add_transaction(tx) - .with_parent(parent_block_id) - .build(rng); - let block_id = block.get_id(); - tf.process_block(block, BlockSource::Local).unwrap(); - - (token_id, block_id, UtxoOutPoint::new(tx_id.into(), 0)) -} - // Returns created token id and outpoint with change fn issue_token_from_genesis( rng: &mut (impl Rng + CryptoRng), @@ -123,69 +88,6 @@ fn issue_token_from_genesis( ) } -fn mint_tokens_in_block( - rng: &mut (impl Rng + CryptoRng), - tf: &mut TestFramework, - parent_block_id: Id, - utxo_to_pay_fee: UtxoOutPoint, - token_id: TokenId, - amount_to_mint: Amount, - produce_change: bool, -) -> (Id, Id) { - let token_supply_change_fee = - tf.chainstate.get_chain_config().token_supply_change_fee(BlockHeight::zero()); - - let nonce = BlockchainStorageRead::get_account_nonce_count( - &tf.storage.transaction_ro().unwrap(), - AccountType::Token(token_id), - ) - .unwrap() - .map_or(AccountNonce::new(0), |n| n.increment().unwrap()); - - let tx_builder = TransactionBuilder::new() - .add_input( - TxInput::from_command(nonce, AccountCommand::MintTokens(token_id, amount_to_mint)), - InputWitness::NoSignature(None), - ) - .add_input( - utxo_to_pay_fee.clone().into(), - InputWitness::NoSignature(None), - ) - .add_output(TxOutput::Transfer( - OutputValue::TokenV1(token_id, amount_to_mint), - Destination::AnyoneCanSpend, - )); - - let tx_builder = if produce_change { - let fee_utxo_coins = chainstate_test_framework::get_output_value( - tf.chainstate.utxo(&utxo_to_pay_fee).unwrap().unwrap().output(), - ) - .unwrap() - .coin_amount() - .unwrap(); - - tx_builder.add_output(TxOutput::Transfer( - OutputValue::Coin((fee_utxo_coins - token_supply_change_fee).unwrap()), - Destination::AnyoneCanSpend, - )) - } else { - tx_builder - }; - - let tx = tx_builder.build(); - let tx_id = tx.transaction().get_id(); - - let block = tf - .make_block_builder() - .add_transaction(tx) - .with_parent(parent_block_id) - .build(rng); - let block_id = block.get_id(); - tf.process_block(block, BlockSource::Local).unwrap(); - - (block_id, tx_id) -} - fn unmint_tokens_in_block( rng: &mut (impl Rng + CryptoRng), tf: &mut TestFramework, diff --git a/chainstate/test-suite/src/tests/helpers/in_memory_storage_wrapper.rs b/chainstate/test-suite/src/tests/helpers/in_memory_storage_wrapper.rs index a7068cd180..68942b2f15 100644 --- a/chainstate/test-suite/src/tests/helpers/in_memory_storage_wrapper.rs +++ b/chainstate/test-suite/src/tests/helpers/in_memory_storage_wrapper.rs @@ -24,11 +24,12 @@ use chainstate_types::{storage_result, GenBlockIndex}; use common::{ chain::{ tokens::{TokenAuxiliaryData, TokenId}, - AccountNonce, AccountType, ChainConfig, DelegationId, GenBlock, GenBlockId, PoolId, - Transaction, + AccountNonce, AccountType, ChainConfig, DelegationId, GenBlock, GenBlockId, OrderData, + OrderId, PoolId, Transaction, }, primitives::{Amount, Id}, }; +use orders_accounting::{OrdersAccountingStorageRead, OrdersAccountingUndo}; use pos_accounting::{ DelegationData, PoSAccountingDB, PoSAccountingUndo, PoSAccountingView, PoolData, }; @@ -159,6 +160,25 @@ impl TransactionVerifierStorageRef for InMemoryStorageWrapper { TransactionSource::Mempool => Ok(None), } } + + fn get_orders_accounting_undo( + &self, + tx_source: TransactionSource, + ) -> Result>, TransactionVerifierStorageError> + { + match tx_source { + TransactionSource::Chain(id) => { + let undo = self + .storage + .transaction_ro() + .unwrap() + .get_orders_accounting_undo(id)? + .map(CachedBlockUndo::from_block_undo); + Ok(undo) + } + TransactionSource::Mempool => Ok(None), + } + } } impl UtxosStorageRead for InMemoryStorageWrapper { @@ -237,3 +257,19 @@ impl TokensAccountingStorageRead for InMemoryStorageWrapper { self.storage.transaction_ro().unwrap().get_circulating_supply(id) } } + +impl OrdersAccountingStorageRead for InMemoryStorageWrapper { + type Error = storage_result::Error; + + fn get_order_data(&self, id: &OrderId) -> Result, Self::Error> { + self.storage.transaction_ro().unwrap().get_order_data(id) + } + + fn get_ask_balance(&self, id: &OrderId) -> Result, Self::Error> { + self.storage.transaction_ro().unwrap().get_ask_balance(id) + } + + fn get_give_balance(&self, id: &OrderId) -> Result, Self::Error> { + self.storage.transaction_ro().unwrap().get_give_balance(id) + } +} diff --git a/chainstate/test-suite/src/tests/helpers/mod.rs b/chainstate/test-suite/src/tests/helpers/mod.rs index 176214192e..8dee793b5a 100644 --- a/chainstate/test-suite/src/tests/helpers/mod.rs +++ b/chainstate/test-suite/src/tests/helpers/mod.rs @@ -13,14 +13,20 @@ // See the License for the specific language governing permissions and // limitations under the License. +use chainstate::BlockSource; +use chainstate_storage::{BlockchainStorageRead, Transactional}; use chainstate_test_framework::{anyonecanspend_address, TestFramework, TransactionBuilder}; use common::{ chain::{ - block::timestamp::BlockTimestamp, output_value::OutputValue, - signature::inputsig::InputWitness, timelock::OutputTimeLock, Destination, Transaction, - TxInput, TxOutput, + block::timestamp::BlockTimestamp, + output_value::OutputValue, + signature::inputsig::InputWitness, + timelock::OutputTimeLock, + tokens::{make_token_id, TokenId, TokenIssuance}, + AccountCommand, AccountNonce, AccountType, Block, Destination, GenBlock, Transaction, + TxInput, TxOutput, UtxoOutPoint, }, - primitives::{Amount, BlockDistance, Id, Idable}, + primitives::{Amount, BlockDistance, BlockHeight, Id, Idable}, }; use crypto::key::{KeyKind, PrivateKey}; use randomness::{CryptoRng, Rng}; @@ -77,3 +83,103 @@ pub fn new_pub_key_destination(rng: &mut (impl Rng + CryptoRng)) -> Destination let (_, pub_key) = PrivateKey::new_from_rng(rng, KeyKind::Secp256k1Schnorr); Destination::PublicKey(pub_key) } + +pub fn issue_token_from_block( + rng: &mut (impl Rng + CryptoRng), + tf: &mut TestFramework, + parent_block_id: Id, + utxo_to_pay_fee: UtxoOutPoint, + issuance: TokenIssuance, +) -> (TokenId, Id, UtxoOutPoint) { + let token_issuance_fee = tf.chainstate.get_chain_config().fungible_token_issuance_fee(); + + let fee_utxo_coins = chainstate_test_framework::get_output_value( + tf.chainstate.utxo(&utxo_to_pay_fee).unwrap().unwrap().output(), + ) + .unwrap() + .coin_amount() + .unwrap(); + + let tx = TransactionBuilder::new() + .add_input(utxo_to_pay_fee.into(), InputWitness::NoSignature(None)) + .add_output(TxOutput::Transfer( + OutputValue::Coin((fee_utxo_coins - token_issuance_fee).unwrap()), + Destination::AnyoneCanSpend, + )) + .add_output(TxOutput::IssueFungibleToken(Box::new(issuance.clone()))) + .build(); + let token_id = make_token_id(tx.transaction().inputs()).unwrap(); + let tx_id = tx.transaction().get_id(); + let block = tf + .make_block_builder() + .add_transaction(tx) + .with_parent(parent_block_id) + .build(rng); + let block_id = block.get_id(); + tf.process_block(block, BlockSource::Local).unwrap(); + + (token_id, block_id, UtxoOutPoint::new(tx_id.into(), 0)) +} + +pub fn mint_tokens_in_block( + rng: &mut (impl Rng + CryptoRng), + tf: &mut TestFramework, + parent_block_id: Id, + utxo_to_pay_fee: UtxoOutPoint, + token_id: TokenId, + amount_to_mint: Amount, + produce_change: bool, +) -> (Id, Id) { + let token_supply_change_fee = + tf.chainstate.get_chain_config().token_supply_change_fee(BlockHeight::zero()); + + let nonce = BlockchainStorageRead::get_account_nonce_count( + &tf.storage.transaction_ro().unwrap(), + AccountType::Token(token_id), + ) + .unwrap() + .map_or(AccountNonce::new(0), |n| n.increment().unwrap()); + + let tx_builder = TransactionBuilder::new() + .add_input( + TxInput::from_command(nonce, AccountCommand::MintTokens(token_id, amount_to_mint)), + InputWitness::NoSignature(None), + ) + .add_input( + utxo_to_pay_fee.clone().into(), + InputWitness::NoSignature(None), + ) + .add_output(TxOutput::Transfer( + OutputValue::TokenV1(token_id, amount_to_mint), + Destination::AnyoneCanSpend, + )); + + let tx_builder = if produce_change { + let fee_utxo_coins = chainstate_test_framework::get_output_value( + tf.chainstate.utxo(&utxo_to_pay_fee).unwrap().unwrap().output(), + ) + .unwrap() + .coin_amount() + .unwrap(); + + tx_builder.add_output(TxOutput::Transfer( + OutputValue::Coin((fee_utxo_coins - token_supply_change_fee).unwrap()), + Destination::AnyoneCanSpend, + )) + } else { + tx_builder + }; + + let tx = tx_builder.build(); + let tx_id = tx.transaction().get_id(); + + let block = tf + .make_block_builder() + .add_transaction(tx) + .with_parent(parent_block_id) + .build(rng); + let block_id = block.get_id(); + tf.process_block(block, BlockSource::Local).unwrap(); + + (block_id, tx_id) +} diff --git a/chainstate/test-suite/src/tests/htlc.rs b/chainstate/test-suite/src/tests/htlc.rs index 4e3b604260..19c499f532 100644 --- a/chainstate/test-suite/src/tests/htlc.rs +++ b/chainstate/test-suite/src/tests/htlc.rs @@ -39,7 +39,8 @@ use common::{ timelock::OutputTimeLock, tokens::{make_token_id, TokenData, TokenIssuance, TokenTransfer}, AccountCommand, AccountNonce, ChainConfig, ChainstateUpgrade, Destination, HtlcActivated, - RewardDistributionVersion, TokenIssuanceVersion, TokensFeeVersion, TxInput, TxOutput, + OrdersActivated, RewardDistributionVersion, TokenIssuanceVersion, TokensFeeVersion, + TxInput, TxOutput, }, primitives::{Amount, Idable}, }; @@ -594,6 +595,7 @@ fn fork_activation(#[case] seed: Seed) { RewardDistributionVersion::V1, TokensFeeVersion::V1, HtlcActivated::No, + OrdersActivated::No, ), ), ( @@ -603,6 +605,7 @@ fn fork_activation(#[case] seed: Seed) { RewardDistributionVersion::V1, TokensFeeVersion::V1, HtlcActivated::Yes, + OrdersActivated::No, ), ), ]) @@ -691,6 +694,7 @@ fn spend_tokens(#[case] seed: Seed) { RewardDistributionVersion::V1, TokensFeeVersion::V1, HtlcActivated::Yes, + OrdersActivated::Yes, ), ), ( @@ -700,6 +704,7 @@ fn spend_tokens(#[case] seed: Seed) { RewardDistributionVersion::V1, TokensFeeVersion::V1, HtlcActivated::Yes, + OrdersActivated::Yes, ), ), ]) diff --git a/chainstate/test-suite/src/tests/mod.rs b/chainstate/test-suite/src/tests/mod.rs index c3051369bd..7a871ac641 100644 --- a/chainstate/test-suite/src/tests/mod.rs +++ b/chainstate/test-suite/src/tests/mod.rs @@ -49,6 +49,7 @@ mod nft_burn; mod nft_issuance; mod nft_reorgs; mod nft_transfer; +mod orders_tests; mod output_timelock; mod pos_accounting_reorg; mod pos_maturity_settings; diff --git a/chainstate/test-suite/src/tests/nft_burn.rs b/chainstate/test-suite/src/tests/nft_burn.rs index 57e7135c31..4564f8885a 100644 --- a/chainstate/test-suite/src/tests/nft_burn.rs +++ b/chainstate/test-suite/src/tests/nft_burn.rs @@ -17,8 +17,8 @@ use chainstate::{BlockError, ChainstateError, ConnectTransactionError}; use chainstate_test_framework::{TestFramework, TransactionBuilder}; use common::chain::{ output_value::OutputValue, signature::inputsig::InputWitness, tokens::make_token_id, - ChainstateUpgrade, Destination, HtlcActivated, RewardDistributionVersion, TokenIssuanceVersion, - TokensFeeVersion, TxInput, TxOutput, + ChainstateUpgrade, Destination, HtlcActivated, OrdersActivated, RewardDistributionVersion, + TokenIssuanceVersion, TokensFeeVersion, TxInput, TxOutput, }; use common::chain::{OutPointSourceId, UtxoOutPoint}; use common::primitives::{Amount, BlockHeight, CoinOrTokenId, Idable}; @@ -216,6 +216,7 @@ fn no_v0_issuance_after_v1(#[case] seed: Seed) { RewardDistributionVersion::V1, TokensFeeVersion::V1, HtlcActivated::Yes, + OrdersActivated::Yes, ), )]) .unwrap(), diff --git a/chainstate/test-suite/src/tests/nft_issuance.rs b/chainstate/test-suite/src/tests/nft_issuance.rs index 3810f9f6c7..8a559d2961 100644 --- a/chainstate/test-suite/src/tests/nft_issuance.rs +++ b/chainstate/test-suite/src/tests/nft_issuance.rs @@ -22,7 +22,7 @@ use common::chain::{ output_value::OutputValue, signature::inputsig::InputWitness, tokens::{is_rfc3986_valid_symbol, make_token_id, Metadata, NftIssuance, NftIssuanceV0}, - Block, ChainstateUpgrade, Destination, HtlcActivated, OutPointSourceId, + Block, ChainstateUpgrade, Destination, HtlcActivated, OrdersActivated, OutPointSourceId, RewardDistributionVersion, TokenIssuanceVersion, TokensFeeVersion, TxInput, TxOutput, }; use common::primitives::{BlockHeight, Idable}; @@ -1652,6 +1652,7 @@ fn no_v0_issuance_after_v1(#[case] seed: Seed) { RewardDistributionVersion::V1, TokensFeeVersion::V1, HtlcActivated::Yes, + OrdersActivated::Yes, ), )]) .unwrap(), @@ -1715,6 +1716,7 @@ fn only_ascii_alphanumeric_after_v1(#[case] seed: Seed) { RewardDistributionVersion::V1, TokensFeeVersion::V1, HtlcActivated::Yes, + OrdersActivated::Yes, ), )]) .unwrap(), diff --git a/chainstate/test-suite/src/tests/nft_transfer.rs b/chainstate/test-suite/src/tests/nft_transfer.rs index e0b1b4e9b7..fa8511ea5c 100644 --- a/chainstate/test-suite/src/tests/nft_transfer.rs +++ b/chainstate/test-suite/src/tests/nft_transfer.rs @@ -21,8 +21,9 @@ use common::{ output_value::OutputValue, signature::inputsig::InputWitness, tokens::{make_token_id, NftIssuance, TokenId}, - ChainstateUpgrade, Destination, HtlcActivated, NetUpgrades, OutPointSourceId, - RewardDistributionVersion, TokenIssuanceVersion, TokensFeeVersion, TxInput, TxOutput, + ChainstateUpgrade, Destination, HtlcActivated, NetUpgrades, OrdersActivated, + OutPointSourceId, RewardDistributionVersion, TokenIssuanceVersion, TokensFeeVersion, + TxInput, TxOutput, }, primitives::{Amount, BlockHeight, CoinOrTokenId}, }; @@ -370,6 +371,7 @@ fn ensure_nft_cannot_be_printed_from_tokens_op(#[case] seed: Seed) { RewardDistributionVersion::V1, TokensFeeVersion::V1, HtlcActivated::Yes, + OrdersActivated::Yes, ), )]) .unwrap(), diff --git a/chainstate/test-suite/src/tests/orders_tests.rs b/chainstate/test-suite/src/tests/orders_tests.rs new file mode 100644 index 0000000000..2efa95f2c8 --- /dev/null +++ b/chainstate/test-suite/src/tests/orders_tests.rs @@ -0,0 +1,1352 @@ +// Copyright (c) 2024 RBB S.r.l +// opensource@mintlayer.org +// SPDX-License-Identifier: MIT +// Licensed under the MIT License; +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://github.com/mintlayer/mintlayer-core/blob/master/LICENSE +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use chainstate::ConnectTransactionError; +use chainstate_storage::Transactional; +use chainstate_test_framework::{output_value_amount, TestFramework, TransactionBuilder}; +use common::{ + address::pubkeyhash::PublicKeyHash, + chain::{ + make_order_id, + output_value::OutputValue, + signature::{ + inputsig::{standard_signature::StandardInputSignature, InputWitness}, + DestinationSigError, + }, + tokens::{TokenId, TokenIssuance, TokenTotalSupply}, + AccountCommand, AccountNonce, ChainstateUpgrade, Destination, OrderData, SignedTransaction, + TxInput, TxOutput, UtxoOutPoint, + }, + primitives::{Amount, BlockHeight, CoinOrTokenId, Idable}, +}; +use crypto::key::{KeyKind, PrivateKey}; +use orders_accounting::OrdersAccountingDB; +use randomness::{CryptoRng, Rng}; +use rstest::rstest; +use test_utils::{ + nft_utils::random_token_issuance_v1, + random::{make_seedable_rng, Seed}, +}; +use tx_verifier::error::{InputCheckError, ScriptError}; + +use crate::tests::helpers::{issue_token_from_block, mint_tokens_in_block}; + +fn issue_and_mint_token_from_genesis( + rng: &mut (impl Rng + CryptoRng), + tf: &mut TestFramework, +) -> (TokenId, UtxoOutPoint, UtxoOutPoint) { + let genesis_block_id = tf.genesis().get_id(); + let utxo = UtxoOutPoint::new(genesis_block_id.into(), 0); + + issue_and_mint_token_from_best_block(rng, tf, utxo) +} + +fn issue_and_mint_token_from_best_block( + rng: &mut (impl Rng + CryptoRng), + tf: &mut TestFramework, + utxo_outpoint: UtxoOutPoint, +) -> (TokenId, UtxoOutPoint, UtxoOutPoint) { + let best_block_id = tf.best_block_id(); + let issuance = TokenIssuance::V1(random_token_issuance_v1( + tf.chain_config(), + Destination::AnyoneCanSpend, + rng, + )); + + let mint_limit = match &issuance { + TokenIssuance::V1(issuance) => match issuance.total_supply { + TokenTotalSupply::Fixed(supply) => supply, + TokenTotalSupply::Lockable | TokenTotalSupply::Unlimited => { + Amount::from_atoms(100_000_000) + } + }, + }; + + let (token_id, _, utxo_with_change) = + issue_token_from_block(rng, tf, best_block_id, utxo_outpoint, issuance); + + let to_mint = Amount::from_atoms(rng.gen_range(1..mint_limit.into_atoms())); + + let best_block_id = tf.best_block_id(); + let (_, mint_tx_id) = mint_tokens_in_block( + rng, + tf, + best_block_id, + utxo_with_change, + token_id, + to_mint, + true, + ); + + ( + token_id, + UtxoOutPoint::new(mint_tx_id.into(), 0), + UtxoOutPoint::new(mint_tx_id.into(), 1), + ) +} + +#[rstest] +#[trace] +#[case(Seed::from_entropy())] +fn create_order_check_storage(#[case] seed: Seed) { + utils::concurrency::model(move || { + let mut rng = make_seedable_rng(seed); + let mut tf = TestFramework::builder(&mut rng).build(); + + let (token_id, tokens_outpoint, _) = issue_and_mint_token_from_genesis(&mut rng, &mut tf); + + let ask_amount = Amount::from_atoms(rng.gen_range(1u128..1000)); + let give_amount = Amount::from_atoms(rng.gen_range(1u128..1000)); + let order_data = OrderData::new( + Destination::AnyoneCanSpend, + OutputValue::Coin(ask_amount), + OutputValue::TokenV1(token_id, give_amount), + ); + + let order_id = make_order_id(&tokens_outpoint); + let tx = TransactionBuilder::new() + .add_input(tokens_outpoint.into(), InputWitness::NoSignature(None)) + .add_output(TxOutput::AnyoneCanTake(Box::new(order_data.clone()))) + .build(); + tf.make_block_builder().add_transaction(tx).build_and_process(&mut rng).unwrap(); + + assert_eq!( + Some(order_data), + tf.chainstate.get_order_data(&order_id).unwrap() + ); + assert_eq!( + Some(ask_amount), + tf.chainstate.get_order_ask_balance(&order_id).unwrap() + ); + assert_eq!( + Some(give_amount), + tf.chainstate.get_order_give_balance(&order_id).unwrap() + ); + }); +} + +#[rstest] +#[trace] +#[case(Seed::from_entropy())] +fn create_two_same_orders_in_tx(#[case] seed: Seed) { + utils::concurrency::model(move || { + let mut rng = make_seedable_rng(seed); + let mut tf = TestFramework::builder(&mut rng).build(); + + let (token_id, tokens_outpoint, _) = issue_and_mint_token_from_genesis(&mut rng, &mut tf); + + let ask_amount = Amount::from_atoms(rng.gen_range(1u128..1000)); + let give_amount = Amount::from_atoms(rng.gen_range(1u128..1000)); + let order_data = Box::new(OrderData::new( + Destination::AnyoneCanSpend, + OutputValue::Coin(ask_amount), + OutputValue::TokenV1(token_id, give_amount), + )); + + let order_id = make_order_id(&tokens_outpoint); + let tx = TransactionBuilder::new() + .add_input(tokens_outpoint.into(), InputWitness::NoSignature(None)) + .add_output(TxOutput::AnyoneCanTake(order_data.clone())) + .add_output(TxOutput::AnyoneCanTake(order_data)) + .build(); + let result = tf.make_block_builder().add_transaction(tx).build_and_process(&mut rng); + + assert_eq!( + result.unwrap_err(), + chainstate::ChainstateError::ProcessBlockError( + chainstate::BlockError::StateUpdateFailed( + chainstate::ConnectTransactionError::OrdersAccountingError( + orders_accounting::Error::OrderAlreadyExists(order_id) + ) + ) + ) + ); + }); +} + +#[rstest] +#[trace] +#[case(Seed::from_entropy())] +fn create_two_orders_same_tx(#[case] seed: Seed) { + utils::concurrency::model(move || { + let mut rng = make_seedable_rng(seed); + let mut tf = TestFramework::builder(&mut rng).build(); + + let (token_id, tokens_outpoint, _) = issue_and_mint_token_from_genesis(&mut rng, &mut tf); + + let amount1 = Amount::from_atoms(rng.gen_range(1u128..1000)); + let amount2 = Amount::from_atoms(rng.gen_range(1u128..1000)); + let order_data_1 = OrderData::new( + Destination::AnyoneCanSpend, + OutputValue::Coin(amount1), + OutputValue::TokenV1(token_id, amount2), + ); + + let order_data_2 = OrderData::new( + Destination::PublicKeyHash(PublicKeyHash::random()), + OutputValue::Coin(amount2), + OutputValue::TokenV1(token_id, amount1), + ); + + let order_id = make_order_id(&tokens_outpoint); + let tx = TransactionBuilder::new() + .add_input(tokens_outpoint.into(), InputWitness::NoSignature(None)) + .add_output(TxOutput::AnyoneCanTake(Box::new(order_data_1))) + .add_output(TxOutput::AnyoneCanTake(Box::new(order_data_2))) + .build(); + let result = tf.make_block_builder().add_transaction(tx).build_and_process(&mut rng); + + assert_eq!( + result.unwrap_err(), + chainstate::ChainstateError::ProcessBlockError( + chainstate::BlockError::StateUpdateFailed( + chainstate::ConnectTransactionError::OrdersAccountingError( + orders_accounting::Error::OrderAlreadyExists(order_id) + ) + ) + ) + ); + }); +} + +#[rstest] +#[trace] +#[case(Seed::from_entropy())] +fn create_two_orders_same_block(#[case] seed: Seed) { + utils::concurrency::model(move || { + let mut rng = make_seedable_rng(seed); + let mut tf = TestFramework::builder(&mut rng).build(); + + let (token_id, tokens_outpoint, _) = issue_and_mint_token_from_genesis(&mut rng, &mut tf); + + let ask_amount = Amount::from_atoms(rng.gen_range(1u128..1000)); + let give_amount = Amount::from_atoms(rng.gen_range(1u128..1000)); + let order_data = Box::new(OrderData::new( + Destination::AnyoneCanSpend, + OutputValue::Coin(ask_amount), + OutputValue::TokenV1(token_id, give_amount), + )); + + let tx1 = TransactionBuilder::new() + .add_input( + tokens_outpoint.clone().into(), + InputWitness::NoSignature(None), + ) + .add_output(TxOutput::AnyoneCanTake(order_data.clone())) + .build(); + let tx2 = TransactionBuilder::new() + .add_input(tokens_outpoint.into(), InputWitness::NoSignature(None)) + .add_output(TxOutput::AnyoneCanTake(order_data)) + .build(); + let block = tf.make_block_builder().with_transactions(vec![tx1, tx2]).build(&mut rng); + let block_id = block.get_id(); + let result = tf.process_block(block, chainstate::BlockSource::Local); + + assert_eq!( + result.unwrap_err(), + chainstate::ChainstateError::ProcessBlockError( + chainstate::BlockError::CheckBlockFailed( + chainstate::CheckBlockError::CheckTransactionFailed( + chainstate::CheckBlockTransactionsError::DuplicateInputInBlock(block_id) + ) + ) + ) + ); + }); +} + +#[rstest] +#[trace] +#[case(Seed::from_entropy())] +fn create_order_check_currencies(#[case] seed: Seed) { + utils::concurrency::model(move || { + let mut rng = make_seedable_rng(seed); + let mut tf = TestFramework::builder(&mut rng).build(); + + let (token_id, tokens_outpoint, _) = issue_and_mint_token_from_genesis(&mut rng, &mut tf); + + let ask_amount = Amount::from_atoms(rng.gen_range(1u128..1000)); + let give_amount = Amount::from_atoms(rng.gen_range(1u128..1000)); + + // Check coins for coins trade + { + let order_data = OrderData::new( + Destination::AnyoneCanSpend, + OutputValue::Coin(ask_amount), + OutputValue::Coin(give_amount), + ); + + let tx = TransactionBuilder::new() + .add_input( + tokens_outpoint.clone().into(), + InputWitness::NoSignature(None), + ) + .add_output(TxOutput::AnyoneCanTake(Box::new(order_data))) + .build(); + let tx_id = tx.transaction().get_id(); + let result = tf.make_block_builder().add_transaction(tx).build_and_process(&mut rng); + assert_eq!( + result.unwrap_err(), + chainstate::ChainstateError::ProcessBlockError( + chainstate::BlockError::CheckBlockFailed( + chainstate::CheckBlockError::CheckTransactionFailed( + chainstate::CheckBlockTransactionsError::CheckTransactionError( + tx_verifier::CheckTransactionError::OrdersCurrenciesMustBeDifferent( + tx_id + ) + ) + ) + ) + ) + ); + } + + // Check tokens for tokens trade + { + let order_data = OrderData::new( + Destination::AnyoneCanSpend, + OutputValue::TokenV1(token_id, ask_amount), + OutputValue::TokenV1(token_id, give_amount), + ); + + let tx = TransactionBuilder::new() + .add_input( + tokens_outpoint.clone().into(), + InputWitness::NoSignature(None), + ) + .add_output(TxOutput::AnyoneCanTake(Box::new(order_data))) + .build(); + let tx_id = tx.transaction().get_id(); + let result = tf.make_block_builder().add_transaction(tx).build_and_process(&mut rng); + assert_eq!( + result.unwrap_err(), + chainstate::ChainstateError::ProcessBlockError( + chainstate::BlockError::CheckBlockFailed( + chainstate::CheckBlockError::CheckTransactionFailed( + chainstate::CheckBlockTransactionsError::CheckTransactionError( + tx_verifier::CheckTransactionError::OrdersCurrenciesMustBeDifferent( + tx_id + ) + ) + ) + ) + ) + ); + } + + // Trade tokens for coins + let order_data = OrderData::new( + Destination::AnyoneCanSpend, + OutputValue::Coin(ask_amount), + OutputValue::TokenV1(token_id, give_amount), + ); + + tf.make_block_builder() + .add_transaction( + TransactionBuilder::new() + .add_input(tokens_outpoint.into(), InputWitness::NoSignature(None)) + .add_output(TxOutput::AnyoneCanTake(Box::new(order_data))) + .build(), + ) + .build_and_process(&mut rng) + .unwrap(); + }); +} + +#[rstest] +#[trace] +#[case(Seed::from_entropy())] +fn create_order_tokens_for_tokens(#[case] seed: Seed) { + utils::concurrency::model(move || { + let mut rng = make_seedable_rng(seed); + let mut tf = TestFramework::builder(&mut rng).build(); + + let (token_id_1, _, coins_outpoint) = issue_and_mint_token_from_genesis(&mut rng, &mut tf); + + let (token_id_2, tokens_outpoint_2, _) = + issue_and_mint_token_from_best_block(&mut rng, &mut tf, coins_outpoint); + + let ask_amount = Amount::from_atoms(rng.gen_range(1u128..1000)); + let give_amount = Amount::from_atoms(rng.gen_range(1u128..1000)); + + // Trade tokens for coins + let order_data = OrderData::new( + Destination::AnyoneCanSpend, + OutputValue::TokenV1(token_id_1, ask_amount), + OutputValue::TokenV1(token_id_2, give_amount), + ); + + tf.make_block_builder() + .add_transaction( + TransactionBuilder::new() + .add_input(tokens_outpoint_2.into(), InputWitness::NoSignature(None)) + .add_output(TxOutput::AnyoneCanTake(Box::new(order_data))) + .build(), + ) + .build_and_process(&mut rng) + .unwrap(); + }); +} + +#[rstest] +#[trace] +#[case(Seed::from_entropy())] +fn conclude_order_check_storage(#[case] seed: Seed) { + utils::concurrency::model(move || { + let mut rng = make_seedable_rng(seed); + let mut tf = TestFramework::builder(&mut rng).build(); + + let (token_id, tokens_outpoint, _) = issue_and_mint_token_from_genesis(&mut rng, &mut tf); + + let ask_amount = Amount::from_atoms(rng.gen_range(1u128..1000)); + let give_amount = Amount::from_atoms(rng.gen_range(1u128..1000)); + let order_data = OrderData::new( + Destination::AnyoneCanSpend, + OutputValue::Coin(ask_amount), + OutputValue::TokenV1(token_id, give_amount), + ); + + let order_id = make_order_id(&tokens_outpoint); + let tx = TransactionBuilder::new() + .add_input(tokens_outpoint.into(), InputWitness::NoSignature(None)) + .add_output(TxOutput::AnyoneCanTake(Box::new(order_data))) + .build(); + tf.make_block_builder().add_transaction(tx).build_and_process(&mut rng).unwrap(); + + let tx = TransactionBuilder::new() + .add_input( + TxInput::AccountCommand( + AccountNonce::new(0), + AccountCommand::ConcludeOrder(order_id), + ), + InputWitness::NoSignature(None), + ) + .add_output(TxOutput::Transfer( + OutputValue::TokenV1(token_id, give_amount), + Destination::AnyoneCanSpend, + )) + .build(); + tf.make_block_builder().add_transaction(tx).build_and_process(&mut rng).unwrap(); + + assert_eq!(None, tf.chainstate.get_order_data(&order_id).unwrap()); + assert_eq!( + None, + tf.chainstate.get_order_ask_balance(&order_id).unwrap() + ); + assert_eq!( + None, + tf.chainstate.get_order_give_balance(&order_id).unwrap() + ); + }); +} + +#[rstest] +#[trace] +#[case(Seed::from_entropy())] +fn fill_order_check_storage(#[case] seed: Seed) { + utils::concurrency::model(move || { + let mut rng = make_seedable_rng(seed); + let mut tf = TestFramework::builder(&mut rng).build(); + + let (token_id, tokens_outpoint, coins_outpoint) = + issue_and_mint_token_from_genesis(&mut rng, &mut tf); + + let ask_amount = Amount::from_atoms(rng.gen_range(10u128..1000)); + let give_amount = Amount::from_atoms(rng.gen_range(10u128..1000)); + let order_data = OrderData::new( + Destination::AnyoneCanSpend, + OutputValue::Coin(ask_amount), + OutputValue::TokenV1(token_id, give_amount), + ); + + let order_id = make_order_id(&tokens_outpoint); + let tx = TransactionBuilder::new() + .add_input(tokens_outpoint.into(), InputWitness::NoSignature(None)) + .add_output(TxOutput::AnyoneCanTake(Box::new(order_data.clone()))) + .build(); + tf.make_block_builder().add_transaction(tx).build_and_process(&mut rng).unwrap(); + + // Fill the order partially + let fill_value = OutputValue::Coin(Amount::from_atoms( + rng.gen_range(1..ask_amount.into_atoms()), + )); + let filled_amount = { + let db_tx = tf.storage.transaction_ro().unwrap(); + let orders_db = OrdersAccountingDB::new(&db_tx); + orders_accounting::calculate_fill_order(&orders_db, order_id, &fill_value).unwrap() + }; + let left_to_fill = (ask_amount - output_value_amount(&fill_value)).unwrap(); + + let tx = TransactionBuilder::new() + .add_input(coins_outpoint.into(), InputWitness::NoSignature(None)) + .add_input( + TxInput::AccountCommand( + AccountNonce::new(0), + AccountCommand::FillOrder(order_id, fill_value, Destination::AnyoneCanSpend), + ), + InputWitness::NoSignature(None), + ) + .add_output(TxOutput::Transfer( + OutputValue::TokenV1(token_id, filled_amount), + Destination::AnyoneCanSpend, + )) + .add_output(TxOutput::Transfer( + OutputValue::Coin(left_to_fill), + Destination::AnyoneCanSpend, + )) + .build(); + let partial_fill_tx_id = tx.transaction().get_id(); + tf.make_block_builder().add_transaction(tx).build_and_process(&mut rng).unwrap(); + + assert_eq!( + Some(order_data.clone()), + tf.chainstate.get_order_data(&order_id).unwrap() + ); + assert_eq!( + Some(left_to_fill), + tf.chainstate.get_order_ask_balance(&order_id).unwrap() + ); + assert_eq!( + Some((give_amount - filled_amount).unwrap()), + tf.chainstate.get_order_give_balance(&order_id).unwrap() + ); + + // Fill the rest of the order + let fill_value = OutputValue::Coin(left_to_fill); + let filled_amount = { + let db_tx = tf.storage.transaction_ro().unwrap(); + let orders_db = OrdersAccountingDB::new(&db_tx); + orders_accounting::calculate_fill_order(&orders_db, order_id, &fill_value).unwrap() + }; + + let tx = TransactionBuilder::new() + .add_input( + UtxoOutPoint::new(partial_fill_tx_id.into(), 1).into(), + InputWitness::NoSignature(None), + ) + .add_input( + TxInput::AccountCommand( + AccountNonce::new(1), + AccountCommand::FillOrder(order_id, fill_value, Destination::AnyoneCanSpend), + ), + InputWitness::NoSignature(None), + ) + .add_output(TxOutput::Transfer( + OutputValue::TokenV1(token_id, filled_amount), + Destination::AnyoneCanSpend, + )) + .build(); + tf.make_block_builder().add_transaction(tx).build_and_process(&mut rng).unwrap(); + + assert_eq!( + Some(order_data), + tf.chainstate.get_order_data(&order_id).unwrap() + ); + assert_eq!( + None, + tf.chainstate.get_order_ask_balance(&order_id).unwrap() + ); + assert_eq!( + None, + tf.chainstate.get_order_give_balance(&order_id).unwrap() + ); + }); +} + +#[rstest] +#[trace] +#[case(Seed::from_entropy())] +fn fill_partially_then_conclude(#[case] seed: Seed) { + utils::concurrency::model(move || { + let mut rng = make_seedable_rng(seed); + let mut tf = TestFramework::builder(&mut rng).build(); + + let (token_id, tokens_outpoint, coins_outpoint) = + issue_and_mint_token_from_genesis(&mut rng, &mut tf); + + let ask_amount = Amount::from_atoms(rng.gen_range(1u128..1000)); + let give_amount = Amount::from_atoms(rng.gen_range(1u128..1000)); + let order_data = OrderData::new( + Destination::AnyoneCanSpend, + OutputValue::Coin(ask_amount), + OutputValue::TokenV1(token_id, give_amount), + ); + + let order_id = make_order_id(&tokens_outpoint); + let tx = TransactionBuilder::new() + .add_input(tokens_outpoint.into(), InputWitness::NoSignature(None)) + .add_output(TxOutput::AnyoneCanTake(Box::new(order_data))) + .build(); + tf.make_block_builder().add_transaction(tx).build_and_process(&mut rng).unwrap(); + + // Fill the order partially + let fill_value = OutputValue::Coin(Amount::from_atoms( + rng.gen_range(1..ask_amount.into_atoms()), + )); + let filled_amount = { + let db_tx = tf.storage.transaction_ro().unwrap(); + let orders_db = OrdersAccountingDB::new(&db_tx); + orders_accounting::calculate_fill_order(&orders_db, order_id, &fill_value).unwrap() + }; + + let tx = TransactionBuilder::new() + .add_input(coins_outpoint.into(), InputWitness::NoSignature(None)) + .add_input( + TxInput::AccountCommand( + AccountNonce::new(0), + AccountCommand::FillOrder( + order_id, + fill_value.clone(), + Destination::AnyoneCanSpend, + ), + ), + InputWitness::NoSignature(None), + ) + .add_output(TxOutput::Transfer( + OutputValue::TokenV1(token_id, filled_amount), + Destination::AnyoneCanSpend, + )) + .build(); + tf.make_block_builder().add_transaction(tx).build_and_process(&mut rng).unwrap(); + + { + // Try overspend give in conclude order + let tx = TransactionBuilder::new() + .add_input( + TxInput::AccountCommand( + AccountNonce::new(1), + AccountCommand::ConcludeOrder(order_id), + ), + InputWitness::NoSignature(None), + ) + .add_output(TxOutput::Transfer( + OutputValue::TokenV1( + token_id, + (give_amount - filled_amount) + .and_then(|v| v + Amount::from_atoms(1)) + .unwrap(), + ), + Destination::AnyoneCanSpend, + )) + .add_output(TxOutput::Transfer( + OutputValue::Coin(output_value_amount(&fill_value)), + Destination::AnyoneCanSpend, + )) + .build(); + let tx_id = tx.transaction().get_id(); + let res = tf.make_block_builder().add_transaction(tx).build_and_process(&mut rng); + assert_eq!(res.unwrap_err(), + chainstate::ChainstateError::ProcessBlockError(chainstate::BlockError::StateUpdateFailed( + ConnectTransactionError::ConstrainedValueAccumulatorError( + constraints_value_accumulator::Error::AttemptToPrintMoneyOrViolateTimelockConstraints( + CoinOrTokenId::TokenId(token_id) + ), + tx_id.into() + ) + )) + ); + } + + { + // Try overspend ask in conclude order + let tx = TransactionBuilder::new() + .add_input( + TxInput::AccountCommand( + AccountNonce::new(1), + AccountCommand::ConcludeOrder(order_id), + ), + InputWitness::NoSignature(None), + ) + .add_output(TxOutput::Transfer( + OutputValue::TokenV1(token_id, (give_amount - filled_amount).unwrap()), + Destination::AnyoneCanSpend, + )) + .add_output(TxOutput::Transfer( + OutputValue::Coin( + (output_value_amount(&fill_value) + Amount::from_atoms(1)).unwrap(), + ), + Destination::AnyoneCanSpend, + )) + .build(); + let tx_id = tx.transaction().get_id(); + let res = tf.make_block_builder().add_transaction(tx).build_and_process(&mut rng); + assert_eq!(res.unwrap_err(), + chainstate::ChainstateError::ProcessBlockError(chainstate::BlockError::StateUpdateFailed( + ConnectTransactionError::ConstrainedValueAccumulatorError( + constraints_value_accumulator::Error::AttemptToPrintMoneyOrViolateTimelockConstraints( + CoinOrTokenId::Coin + ), + tx_id.into() + ) + )) + ); + } + + // conclude the order + let tx = TransactionBuilder::new() + .add_input( + TxInput::AccountCommand( + AccountNonce::new(1), + AccountCommand::ConcludeOrder(order_id), + ), + InputWitness::NoSignature(None), + ) + .add_output(TxOutput::Transfer( + OutputValue::TokenV1(token_id, (give_amount - filled_amount).unwrap()), + Destination::AnyoneCanSpend, + )) + .add_output(TxOutput::Transfer( + OutputValue::Coin(output_value_amount(&fill_value)), + Destination::AnyoneCanSpend, + )) + .build(); + tf.make_block_builder().add_transaction(tx).build_and_process(&mut rng).unwrap(); + + assert_eq!(None, tf.chainstate.get_order_data(&order_id).unwrap()); + assert_eq!( + None, + tf.chainstate.get_order_ask_balance(&order_id).unwrap() + ); + assert_eq!( + None, + tf.chainstate.get_order_give_balance(&order_id).unwrap() + ); + }); +} + +#[rstest] +#[trace] +#[case(Seed::from_entropy())] +fn fill_completely_then_conclude(#[case] seed: Seed) { + utils::concurrency::model(move || { + let mut rng = make_seedable_rng(seed); + let mut tf = TestFramework::builder(&mut rng).build(); + + let (token_id, tokens_outpoint, coins_outpoint) = + issue_and_mint_token_from_genesis(&mut rng, &mut tf); + + let ask_amount = Amount::from_atoms(rng.gen_range(1u128..1000)); + let give_amount = Amount::from_atoms(rng.gen_range(1u128..1000)); + let order_data = OrderData::new( + Destination::AnyoneCanSpend, + OutputValue::Coin(ask_amount), + OutputValue::TokenV1(token_id, give_amount), + ); + + let order_id = make_order_id(&tokens_outpoint); + let tx = TransactionBuilder::new() + .add_input(tokens_outpoint.into(), InputWitness::NoSignature(None)) + .add_output(TxOutput::AnyoneCanTake(Box::new(order_data))) + .build(); + tf.make_block_builder().add_transaction(tx).build_and_process(&mut rng).unwrap(); + + { + // Try overspend complete fill order + let tx = TransactionBuilder::new() + .add_input( + coins_outpoint.clone().into(), + InputWitness::NoSignature(None), + ) + .add_input( + TxInput::AccountCommand( + AccountNonce::new(0), + AccountCommand::FillOrder( + order_id, + OutputValue::Coin(ask_amount), + Destination::AnyoneCanSpend, + ), + ), + InputWitness::NoSignature(None), + ) + .add_output(TxOutput::Transfer( + OutputValue::TokenV1(token_id, (give_amount + Amount::from_atoms(1)).unwrap()), + Destination::AnyoneCanSpend, + )) + .build(); + let tx_id = tx.transaction().get_id(); + let res = tf.make_block_builder().add_transaction(tx).build_and_process(&mut rng); + assert_eq!(res.unwrap_err(), + chainstate::ChainstateError::ProcessBlockError(chainstate::BlockError::StateUpdateFailed( + ConnectTransactionError::ConstrainedValueAccumulatorError( + constraints_value_accumulator::Error::AttemptToPrintMoneyOrViolateTimelockConstraints( + CoinOrTokenId::TokenId(token_id) + ), + tx_id.into() + ) + )) + ); + } + + { + // Try overbid complete fill order + let tx = TransactionBuilder::new() + .add_input( + coins_outpoint.clone().into(), + InputWitness::NoSignature(None), + ) + .add_input( + TxInput::AccountCommand( + AccountNonce::new(0), + AccountCommand::FillOrder( + order_id, + OutputValue::Coin((ask_amount + Amount::from_atoms(1)).unwrap()), + Destination::AnyoneCanSpend, + ), + ), + InputWitness::NoSignature(None), + ) + .add_output(TxOutput::Transfer( + OutputValue::TokenV1(token_id, give_amount), + Destination::AnyoneCanSpend, + )) + .build(); + let tx_id = tx.transaction().get_id(); + let res = tf.make_block_builder().add_transaction(tx).build_and_process(&mut rng); + assert_eq!( + res.unwrap_err(), + chainstate::ChainstateError::ProcessBlockError( + chainstate::BlockError::StateUpdateFailed( + ConnectTransactionError::ConstrainedValueAccumulatorError( + orders_accounting::Error::OrderOverbid( + order_id, + ask_amount, + (ask_amount + Amount::from_atoms(1)).unwrap() + ) + .into(), + tx_id.into() + ) + ) + ) + ); + } + + // Fill the order completely + let tx = TransactionBuilder::new() + .add_input(coins_outpoint.into(), InputWitness::NoSignature(None)) + .add_input( + TxInput::AccountCommand( + AccountNonce::new(0), + AccountCommand::FillOrder( + order_id, + OutputValue::Coin(ask_amount), + Destination::AnyoneCanSpend, + ), + ), + InputWitness::NoSignature(None), + ) + .add_output(TxOutput::Transfer( + OutputValue::TokenV1(token_id, give_amount), + Destination::AnyoneCanSpend, + )) + .build(); + tf.make_block_builder().add_transaction(tx).build_and_process(&mut rng).unwrap(); + + { + // Try overspend conclude order + let tx = TransactionBuilder::new() + .add_input( + TxInput::AccountCommand( + AccountNonce::new(1), + AccountCommand::ConcludeOrder(order_id), + ), + InputWitness::NoSignature(None), + ) + .add_output(TxOutput::Transfer( + OutputValue::Coin((ask_amount + Amount::from_atoms(1)).unwrap()), + Destination::AnyoneCanSpend, + )) + .build(); + let tx_id = tx.transaction().get_id(); + let res = tf.make_block_builder().add_transaction(tx).build_and_process(&mut rng); + assert_eq!(res.unwrap_err(), + chainstate::ChainstateError::ProcessBlockError(chainstate::BlockError::StateUpdateFailed( + ConnectTransactionError::ConstrainedValueAccumulatorError( + constraints_value_accumulator::Error::AttemptToPrintMoneyOrViolateTimelockConstraints( + CoinOrTokenId::Coin + ), + tx_id.into() + ) + )) + ); + } + + // conclude the order + let tx = TransactionBuilder::new() + .add_input( + TxInput::AccountCommand( + AccountNonce::new(1), + AccountCommand::ConcludeOrder(order_id), + ), + InputWitness::NoSignature(None), + ) + .add_output(TxOutput::Transfer( + OutputValue::Coin(ask_amount), + Destination::AnyoneCanSpend, + )) + .build(); + tf.make_block_builder().add_transaction(tx).build_and_process(&mut rng).unwrap(); + + assert_eq!(None, tf.chainstate.get_order_data(&order_id).unwrap()); + assert_eq!( + None, + tf.chainstate.get_order_ask_balance(&order_id).unwrap() + ); + assert_eq!( + None, + tf.chainstate.get_order_give_balance(&order_id).unwrap() + ); + }); +} + +#[rstest] +#[trace] +#[case(Seed::from_entropy())] +fn conclude_order_check_signature(#[case] seed: Seed) { + utils::concurrency::model(move || { + let mut rng = make_seedable_rng(seed); + let mut tf = TestFramework::builder(&mut rng).build(); + + let (order_sk, order_pk) = PrivateKey::new_from_rng(&mut rng, KeyKind::Secp256k1Schnorr); + + let (token_id, tokens_outpoint, _) = issue_and_mint_token_from_genesis(&mut rng, &mut tf); + + let ask_amount = Amount::from_atoms(rng.gen_range(1u128..1000)); + let give_amount = Amount::from_atoms(rng.gen_range(1u128..1000)); + let order_data = OrderData::new( + Destination::PublicKey(order_pk.clone()), + OutputValue::Coin(ask_amount), + OutputValue::TokenV1(token_id, give_amount), + ); + + let order_id = make_order_id(&tokens_outpoint); + let tx = TransactionBuilder::new() + .add_input(tokens_outpoint.into(), InputWitness::NoSignature(None)) + .add_output(TxOutput::AnyoneCanTake(Box::new(order_data))) + .build(); + tf.make_block_builder().add_transaction(tx).build_and_process(&mut rng).unwrap(); + + // try conclude without signature + { + let tx = TransactionBuilder::new() + .add_input( + TxInput::AccountCommand( + AccountNonce::new(0), + AccountCommand::ConcludeOrder(order_id), + ), + InputWitness::NoSignature(None), + ) + .add_output(TxOutput::Transfer( + OutputValue::TokenV1(token_id, give_amount), + Destination::AnyoneCanSpend, + )) + .build(); + let result = tf.make_block_builder().add_transaction(tx).build_and_process(&mut rng); + + assert_eq!( + result.unwrap_err(), + chainstate::ChainstateError::ProcessBlockError( + chainstate::BlockError::StateUpdateFailed(ConnectTransactionError::InputCheck( + InputCheckError::new( + 0, + ScriptError::Signature(DestinationSigError::SignatureNotFound) + ) + )) + ) + ) + } + + // try conclude with wrong signature + { + let tx = TransactionBuilder::new() + .add_input( + TxInput::AccountCommand( + AccountNonce::new(0), + AccountCommand::ConcludeOrder(order_id), + ), + InputWitness::NoSignature(None), + ) + .add_output(TxOutput::Transfer( + OutputValue::TokenV1(token_id, give_amount), + Destination::AnyoneCanSpend, + )) + .build(); + + let inputs_utxos: Vec> = vec![None]; + let inputs_utxos_refs = + inputs_utxos.iter().map(|utxo| utxo.as_ref()).collect::>(); + let (some_sk, some_pk) = PrivateKey::new_from_rng(&mut rng, KeyKind::Secp256k1Schnorr); + let account_sig = StandardInputSignature::produce_uniparty_signature_for_input( + &some_sk, + Default::default(), + Destination::PublicKey(some_pk), + &tx, + &inputs_utxos_refs, + 0, + &mut rng, + ) + .unwrap(); + + let tx = SignedTransaction::new( + tx.take_transaction(), + vec![InputWitness::Standard(account_sig)], + ) + .unwrap(); + let result = tf.make_block_builder().add_transaction(tx).build_and_process(&mut rng); + + assert_eq!( + result.unwrap_err(), + chainstate::ChainstateError::ProcessBlockError( + chainstate::BlockError::StateUpdateFailed(ConnectTransactionError::InputCheck( + InputCheckError::new( + 0, + ScriptError::Signature( + DestinationSigError::SignatureVerificationFailed + ) + ) + )) + ) + ) + } + + // valid case + let tx = TransactionBuilder::new() + .add_input( + TxInput::AccountCommand( + AccountNonce::new(0), + AccountCommand::ConcludeOrder(order_id), + ), + InputWitness::NoSignature(None), + ) + .add_output(TxOutput::Transfer( + OutputValue::TokenV1(token_id, give_amount), + Destination::AnyoneCanSpend, + )) + .build(); + + let inputs_utxos: Vec> = vec![None]; + let inputs_utxos_refs = inputs_utxos.iter().map(|utxo| utxo.as_ref()).collect::>(); + let account_sig = StandardInputSignature::produce_uniparty_signature_for_input( + &order_sk, + Default::default(), + Destination::PublicKey(order_pk), + &tx, + &inputs_utxos_refs, + 0, + &mut rng, + ) + .unwrap(); + + let tx = SignedTransaction::new( + tx.take_transaction(), + vec![InputWitness::Standard(account_sig)], + ) + .unwrap(); + tf.make_block_builder().add_transaction(tx).build_and_process(&mut rng).unwrap(); + }); +} + +// Create a chain with an order which is filled partially. +// Reorg from a point before the order was created, so that after reorg storage has no information on the order +#[rstest] +#[trace] +#[case(Seed::from_entropy())] +fn reorg_before_create(#[case] seed: Seed) { + utils::concurrency::model(move || { + let mut rng = make_seedable_rng(seed); + let mut tf = TestFramework::builder(&mut rng).build(); + + let (token_id, tokens_outpoint, coins_outpoint) = + issue_and_mint_token_from_genesis(&mut rng, &mut tf); + let reorg_common_ancestor = tf.best_block_id(); + + let ask_amount = Amount::from_atoms(rng.gen_range(10u128..1000)); + let give_amount = Amount::from_atoms(rng.gen_range(10u128..1000)); + let order_data = OrderData::new( + Destination::AnyoneCanSpend, + OutputValue::Coin(ask_amount), + OutputValue::TokenV1(token_id, give_amount), + ); + + let order_id = make_order_id(&tokens_outpoint); + let tx = TransactionBuilder::new() + .add_input(tokens_outpoint.into(), InputWitness::NoSignature(None)) + .add_output(TxOutput::AnyoneCanTake(Box::new(order_data.clone()))) + .build(); + tf.make_block_builder().add_transaction(tx).build_and_process(&mut rng).unwrap(); + + // Fill the order partially + let fill_value = OutputValue::Coin(Amount::from_atoms( + rng.gen_range(1..ask_amount.into_atoms()), + )); + let filled_amount = { + let db_tx = tf.storage.transaction_ro().unwrap(); + let orders_db = OrdersAccountingDB::new(&db_tx); + orders_accounting::calculate_fill_order(&orders_db, order_id, &fill_value).unwrap() + }; + let left_to_fill = (ask_amount - output_value_amount(&fill_value)).unwrap(); + + let tx = TransactionBuilder::new() + .add_input(coins_outpoint.into(), InputWitness::NoSignature(None)) + .add_input( + TxInput::AccountCommand( + AccountNonce::new(0), + AccountCommand::FillOrder(order_id, fill_value, Destination::AnyoneCanSpend), + ), + InputWitness::NoSignature(None), + ) + .add_output(TxOutput::Transfer( + OutputValue::TokenV1(token_id, filled_amount), + Destination::AnyoneCanSpend, + )) + .add_output(TxOutput::Transfer( + OutputValue::Coin(left_to_fill), + Destination::AnyoneCanSpend, + )) + .build(); + tf.make_block_builder().add_transaction(tx).build_and_process(&mut rng).unwrap(); + + assert_eq!( + Some(order_data), + tf.chainstate.get_order_data(&order_id).unwrap() + ); + assert_eq!( + Some(left_to_fill), + tf.chainstate.get_order_ask_balance(&order_id).unwrap() + ); + assert_eq!( + Some((give_amount - filled_amount).unwrap()), + tf.chainstate.get_order_give_balance(&order_id).unwrap() + ); + + // Create alternative chain and trigger the reorg + let new_best_block = tf.create_chain(&reorg_common_ancestor, 3, &mut rng).unwrap(); + assert_eq!(tf.best_block_id(), new_best_block); + + assert_eq!(None, tf.chainstate.get_order_data(&order_id).unwrap()); + assert_eq!( + None, + tf.chainstate.get_order_ask_balance(&order_id).unwrap() + ); + assert_eq!( + None, + tf.chainstate.get_order_give_balance(&order_id).unwrap() + ); + }); +} + +// Create a chain with an order which is filled partially and then concluded. +// Reorg from a point after the order was created, so that after reorg storage has original information on the order +#[rstest] +#[trace] +#[case(Seed::from_entropy())] +fn reorg_after_create(#[case] seed: Seed) { + utils::concurrency::model(move || { + let mut rng = make_seedable_rng(seed); + let mut tf = TestFramework::builder(&mut rng).build(); + + let (token_id, tokens_outpoint, coins_outpoint) = + issue_and_mint_token_from_genesis(&mut rng, &mut tf); + + let ask_amount = Amount::from_atoms(rng.gen_range(10u128..1000)); + let give_amount = Amount::from_atoms(rng.gen_range(10u128..1000)); + let order_data = OrderData::new( + Destination::AnyoneCanSpend, + OutputValue::Coin(ask_amount), + OutputValue::TokenV1(token_id, give_amount), + ); + + let order_id = make_order_id(&tokens_outpoint); + let tx = TransactionBuilder::new() + .add_input(tokens_outpoint.into(), InputWitness::NoSignature(None)) + .add_output(TxOutput::AnyoneCanTake(Box::new(order_data.clone()))) + // transfer output just to be able to spend something in alternative branch + .add_output(TxOutput::Transfer( + OutputValue::TokenV1(token_id, Amount::from_atoms(100)), + Destination::AnyoneCanSpend, + )) + .build(); + tf.make_block_builder().add_transaction(tx).build_and_process(&mut rng).unwrap(); + let reorg_common_ancestor = tf.best_block_id(); + + // Fill the order partially + let fill_value = OutputValue::Coin(Amount::from_atoms( + rng.gen_range(1..ask_amount.into_atoms()), + )); + let filled_amount = { + let db_tx = tf.storage.transaction_ro().unwrap(); + let orders_db = OrdersAccountingDB::new(&db_tx); + orders_accounting::calculate_fill_order(&orders_db, order_id, &fill_value).unwrap() + }; + let left_to_fill = (ask_amount - output_value_amount(&fill_value)).unwrap(); + + tf.make_block_builder() + .add_transaction( + TransactionBuilder::new() + .add_input(coins_outpoint.into(), InputWitness::NoSignature(None)) + .add_input( + TxInput::AccountCommand( + AccountNonce::new(0), + AccountCommand::FillOrder( + order_id, + fill_value, + Destination::AnyoneCanSpend, + ), + ), + InputWitness::NoSignature(None), + ) + .add_output(TxOutput::Transfer( + OutputValue::TokenV1(token_id, filled_amount), + Destination::AnyoneCanSpend, + )) + .add_output(TxOutput::Transfer( + OutputValue::Coin(left_to_fill), + Destination::AnyoneCanSpend, + )) + .build(), + ) + .build_and_process(&mut rng) + .unwrap(); + + tf.make_block_builder() + .add_transaction( + TransactionBuilder::new() + .add_input( + TxInput::AccountCommand( + AccountNonce::new(1), + AccountCommand::ConcludeOrder(order_id), + ), + InputWitness::NoSignature(None), + ) + .build(), + ) + .build_and_process(&mut rng) + .unwrap(); + + assert_eq!(None, tf.chainstate.get_order_data(&order_id).unwrap()); + assert_eq!( + None, + tf.chainstate.get_order_ask_balance(&order_id).unwrap() + ); + assert_eq!( + None, + tf.chainstate.get_order_give_balance(&order_id).unwrap() + ); + + // Create alternative chain and trigger the reorg + let new_best_block = tf.create_chain(&reorg_common_ancestor, 3, &mut rng).unwrap(); + assert_eq!(tf.best_block_id(), new_best_block); + + assert_eq!( + Some(order_data.clone()), + tf.chainstate.get_order_data(&order_id).unwrap() + ); + assert_eq!( + Some(output_value_amount(order_data.ask())), + tf.chainstate.get_order_ask_balance(&order_id).unwrap() + ); + assert_eq!( + Some(output_value_amount(order_data.give())), + tf.chainstate.get_order_give_balance(&order_id).unwrap() + ); + }); +} + +#[rstest] +#[trace] +#[case(Seed::from_entropy())] +fn test_activation(#[case] seed: Seed) { + utils::concurrency::model(move || { + let mut rng = test_utils::random::make_seedable_rng(seed); + // activate orders at height 4 (genesis + issue block + mint block + empty block) + let mut tf = TestFramework::builder(&mut rng) + .with_chain_config( + common::chain::config::Builder::test_chain() + .chainstate_upgrades( + common::chain::NetUpgrades::initialize(vec![ + ( + BlockHeight::zero(), + ChainstateUpgrade::new( + common::chain::TokenIssuanceVersion::V1, + common::chain::RewardDistributionVersion::V1, + common::chain::TokensFeeVersion::V1, + common::chain::HtlcActivated::No, + common::chain::OrdersActivated::No, + ), + ), + ( + BlockHeight::new(4), + ChainstateUpgrade::new( + common::chain::TokenIssuanceVersion::V1, + common::chain::RewardDistributionVersion::V1, + common::chain::TokensFeeVersion::V1, + common::chain::HtlcActivated::No, + common::chain::OrdersActivated::Yes, + ), + ), + ]) + .unwrap(), + ) + .genesis_unittest(Destination::AnyoneCanSpend) + .build(), + ) + .build(); + + let (token_id, tokens_outpoint, _) = issue_and_mint_token_from_genesis(&mut rng, &mut tf); + + let order_data = Box::new(OrderData::new( + Destination::AnyoneCanSpend, + OutputValue::Coin(Amount::from_atoms(rng.gen_range(1u128..1000))), + OutputValue::TokenV1(token_id, Amount::from_atoms(rng.gen_range(1u128..1000))), + )); + + // Try to produce order output before activation, check an error + let tx = TransactionBuilder::new() + .add_input( + tokens_outpoint.clone().into(), + InputWitness::NoSignature(None), + ) + .add_output(TxOutput::AnyoneCanTake(order_data.clone())) + .build(); + let tx_id = tx.transaction().get_id(); + let result = tf.make_block_builder().add_transaction(tx).build_and_process(&mut rng); + + assert_eq!( + result.unwrap_err(), + chainstate::ChainstateError::ProcessBlockError( + chainstate::BlockError::CheckBlockFailed( + chainstate::CheckBlockError::CheckTransactionFailed( + chainstate::CheckBlockTransactionsError::CheckTransactionError( + tx_verifier::CheckTransactionError::OrdersAreNotActivated(tx_id) + ) + ) + ) + ) + ); + + // produce an empty block + tf.make_block_builder().build_and_process(&mut rng).unwrap(); + + // now it should be possible to use order output + tf.make_block_builder() + .add_transaction( + TransactionBuilder::new() + .add_input(tokens_outpoint.into(), InputWitness::NoSignature(None)) + .add_output(TxOutput::AnyoneCanTake(order_data)) + .build(), + ) + .build_and_process(&mut rng) + .unwrap(); + }); +} diff --git a/chainstate/test-suite/src/tests/tx_fee.rs b/chainstate/test-suite/src/tests/tx_fee.rs index 5fbdfd2604..45b80da5f5 100644 --- a/chainstate/test-suite/src/tests/tx_fee.rs +++ b/chainstate/test-suite/src/tests/tx_fee.rs @@ -33,7 +33,7 @@ use common::{ make_token_id, IsTokenFreezable, TokenIssuance, TokenIssuanceV0, TokenIssuanceV1, TokenTotalSupply, }, - ChainConfig, ChainstateUpgrade, Destination, HtlcActivated, NetUpgrades, + ChainConfig, ChainstateUpgrade, Destination, HtlcActivated, NetUpgrades, OrdersActivated, TokenIssuanceVersion, TokensFeeVersion, TxInput, TxOutput, UtxoOutPoint, }, primitives::{Amount, Fee, Idable}, @@ -577,6 +577,7 @@ fn issue_fungible_token_v0(#[case] seed: Seed) { RewardDistributionVersion::V1, TokensFeeVersion::V1, HtlcActivated::Yes, + OrdersActivated::Yes, ), )]) .unwrap(), diff --git a/chainstate/test-suite/src/tests/tx_verification_simulation.rs b/chainstate/test-suite/src/tests/tx_verification_simulation.rs index e7fe110c3c..fa39b14c0d 100644 --- a/chainstate/test-suite/src/tests/tx_verification_simulation.rs +++ b/chainstate/test-suite/src/tests/tx_verification_simulation.rs @@ -115,7 +115,7 @@ fn simulation(#[case] seed: Seed, #[case] max_blocks: usize, #[case] max_tx_per_ let mut block_builder = tf.make_pos_block_builder().with_random_staking_pool(&mut rng); for _ in 0..rng.gen_range(10..max_tx_per_block) { - block_builder = block_builder.add_test_transaction(&mut rng, true); + block_builder = block_builder.add_test_transaction(&mut rng, true, true); } let block = block_builder.build(&mut rng); @@ -151,7 +151,7 @@ fn simulation(#[case] seed: Seed, #[case] max_blocks: usize, #[case] max_tx_per_ let mut block_builder = tf2.make_pos_block_builder().with_random_staking_pool(&mut rng); for _ in 0..rng.gen_range(10..max_tx_per_block) { - block_builder = block_builder.add_test_transaction(&mut rng, true); + block_builder = block_builder.add_test_transaction(&mut rng, true, true); } let block = block_builder.build(&mut rng); diff --git a/chainstate/test-suite/src/tests/tx_verifier_among_threads.rs b/chainstate/test-suite/src/tests/tx_verifier_among_threads.rs index 21d2bebefd..bae2eb8492 100644 --- a/chainstate/test-suite/src/tests/tx_verifier_among_threads.rs +++ b/chainstate/test-suite/src/tests/tx_verifier_among_threads.rs @@ -16,9 +16,10 @@ use chainstate_test_framework::{TestFramework, TestStore}; use common::chain::config::Builder as ConfigBuilder; use common::{ - chain::{DelegationId, PoolId, UtxoOutPoint}, + chain::{DelegationId, OrderData, OrderId, PoolId, UtxoOutPoint}, primitives::{Id, H256}, }; +use orders_accounting::OrdersAccountingView; use pos_accounting::PoSAccountingView; use rstest::rstest; use test_utils::random::{make_seedable_rng, Seed}; @@ -126,6 +127,30 @@ impl TokensAccountingView for EmptyTokensAccountingView { } } +struct EmptyOrdersAccountingView; + +impl OrdersAccountingView for EmptyOrdersAccountingView { + type Error = orders_accounting::Error; + + fn get_order_data(&self, _id: &OrderId) -> Result, Self::Error> { + Ok(None) + } + + fn get_ask_balance( + &self, + _id: &OrderId, + ) -> Result, Self::Error> { + Ok(None) + } + + fn get_give_balance( + &self, + _id: &OrderId, + ) -> Result, Self::Error> { + Ok(None) + } +} + /// This test proves that a transaction verifier with this structure can be moved among threads #[rstest] #[trace] @@ -142,6 +167,7 @@ fn transfer_tx_verifier_to_thread(#[case] seed: Seed) { let utxos = EmptyUtxosView {}; let accounting = EmptyAccountingView {}; let tokens_accounting = EmptyTokensAccountingView {}; + let orders_accounting = EmptyOrdersAccountingView {}; let verifier = TransactionVerifier::new_generic( &storage, @@ -149,6 +175,7 @@ fn transfer_tx_verifier_to_thread(#[case] seed: Seed) { utxos, accounting, tokens_accounting, + orders_accounting, ); std::thread::scope(|s| { diff --git a/chainstate/tx-verifier/Cargo.toml b/chainstate/tx-verifier/Cargo.toml index cecd9b41a4..9407bd7486 100644 --- a/chainstate/tx-verifier/Cargo.toml +++ b/chainstate/tx-verifier/Cargo.toml @@ -17,6 +17,7 @@ constraints-value-accumulator = { path = "../constraints-value-accumulator" } crypto = { path = "../../crypto" } logging = { path = "../../logging" } mintscript = { path = "../../mintscript" } +orders-accounting = { path = "../../orders-accounting" } pos-accounting = { path = "../../pos-accounting" } randomness = { path = "../../randomness" } serialization = { path = "../../serialization" } diff --git a/chainstate/tx-verifier/src/transaction_verifier/accounting_undo_cache/tests.rs b/chainstate/tx-verifier/src/transaction_verifier/accounting_undo_cache/tests.rs index 95b8340526..4e80612d76 100644 --- a/chainstate/tx-verifier/src/transaction_verifier/accounting_undo_cache/tests.rs +++ b/chainstate/tx-verifier/src/transaction_verifier/accounting_undo_cache/tests.rs @@ -24,6 +24,7 @@ use common::{ chain::{config::Builder as ConfigBuilder, Block}, primitives::H256, }; +use orders_accounting::OrdersAccountingDeltaUndoData; use pos_accounting::{DeltaMergeUndo, PoSAccountingUndo}; use test_utils::random::Seed; use tokens_accounting::TokensAccountingDeltaUndoData; @@ -89,6 +90,10 @@ fn connect_txs_in_hierarchy_default(#[case] seed: Seed) { .expect_batch_write_tokens_data() .times(1) .return_const(Ok(TokensAccountingDeltaUndoData::new())); + store + .expect_batch_write_orders_data() + .times(1) + .return_const(Ok(OrdersAccountingDeltaUndoData::new())); store .expect_batch_write_delta() .times(1) @@ -192,6 +197,10 @@ fn connect_txs_in_hierarchy_disposable(#[case] seed: Seed) { .expect_batch_write_tokens_data() .times(1) .return_const(Ok(TokensAccountingDeltaUndoData::new())); + store + .expect_batch_write_orders_data() + .times(1) + .return_const(Ok(OrdersAccountingDeltaUndoData::new())); store .expect_batch_write_delta() .times(1) @@ -273,6 +282,10 @@ fn connect_txs_in_hierarchy_twice(#[case] seed: Seed) { .expect_batch_write_tokens_data() .times(1) .return_const(Ok(TokensAccountingDeltaUndoData::new())); + store + .expect_batch_write_orders_data() + .times(1) + .return_const(Ok(OrdersAccountingDeltaUndoData::new())); store .expect_batch_write_delta() .times(1) @@ -355,6 +368,10 @@ fn connect_reward_in_hierarchy_twice(#[case] seed: Seed) { .expect_batch_write_tokens_data() .times(1) .return_const(Ok(TokensAccountingDeltaUndoData::new())); + store + .expect_batch_write_orders_data() + .times(1) + .return_const(Ok(OrdersAccountingDeltaUndoData::new())); store .expect_batch_write_delta() .times(1) @@ -465,6 +482,10 @@ fn disconnect_txs_in_hierarchy_default(#[case] seed: Seed) { .expect_batch_write_tokens_data() .times(1) .return_const(Ok(TokensAccountingDeltaUndoData::new())); + store + .expect_batch_write_orders_data() + .times(1) + .return_const(Ok(OrdersAccountingDeltaUndoData::new())); store .expect_batch_write_delta() .times(1) @@ -566,6 +587,10 @@ fn disconnect_txs_in_hierarchy_disposable(#[case] seed: Seed) { .expect_batch_write_tokens_data() .times(1) .return_const(Ok(TokensAccountingDeltaUndoData::new())); + store + .expect_batch_write_orders_data() + .times(1) + .return_const(Ok(OrdersAccountingDeltaUndoData::new())); store .expect_batch_write_delta() .times(1) diff --git a/chainstate/tx-verifier/src/transaction_verifier/check_transaction.rs b/chainstate/tx-verifier/src/transaction_verifier/check_transaction.rs index bd5e53e0ed..d16fcfc536 100644 --- a/chainstate/tx-verifier/src/transaction_verifier/check_transaction.rs +++ b/chainstate/tx-verifier/src/transaction_verifier/check_transaction.rs @@ -24,7 +24,7 @@ use common::{ ChainConfig, HtlcActivated, SignedTransaction, TokenIssuanceVersion, Transaction, TransactionSize, TxOutput, }, - primitives::{BlockHeight, Id, Idable}, + primitives::{BlockHeight, CoinOrTokenId, Id, Idable}, }; use thiserror::Error; use utils::ensure; @@ -58,6 +58,10 @@ pub enum CheckTransactionError { DeprecatedTokenOperationVersion(TokenIssuanceVersion, Id), #[error("Htlcs are not activated yet")] HtlcsAreNotActivated, + #[error("Orders from tx {0} are not yet activated")] + OrdersAreNotActivated(Id), + #[error("Orders currencies from tx {0} are the same")] + OrdersCurrenciesMustBeDifferent(Id), } pub fn check_transaction( @@ -72,6 +76,7 @@ pub fn check_transaction( check_no_signature_size(chain_config, tx)?; check_data_deposit_outputs(chain_config, tx)?; check_htlc_outputs(chain_config, block_height, tx)?; + check_order_outputs(chain_config, block_height, tx)?; Ok(()) } @@ -156,6 +161,17 @@ fn check_tokens_tx( OutputValue::Coin(_) | OutputValue::TokenV1(_, _) => false, OutputValue::TokenV0(_) => true, }, + TxOutput::AnyoneCanTake(data) => { + let ask_token_v0 = match data.ask() { + OutputValue::Coin(_) | OutputValue::TokenV1(_, _) => false, + OutputValue::TokenV0(_) => true, + }; + let give_token_v0 = match data.ask() { + OutputValue::Coin(_) | OutputValue::TokenV1(_, _) => false, + OutputValue::TokenV0(_) => true, + }; + ask_token_v0 || give_token_v0 + } TxOutput::CreateStakePool(_, _) | TxOutput::ProduceBlockFromStake(_, _) | TxOutput::CreateDelegationId(_, _) @@ -201,7 +217,8 @@ fn check_tokens_tx( | TxOutput::CreateDelegationId(_, _) | TxOutput::DelegateStaking(_, _) | TxOutput::DataDeposit(_) - | TxOutput::Htlc(_, _) => Ok(()), + | TxOutput::Htlc(_, _) + | TxOutput::AnyoneCanTake(_) => Ok(()), }) .map_err(CheckTransactionError::TokensError)?; @@ -254,7 +271,8 @@ fn check_data_deposit_outputs( | TxOutput::DelegateStaking(..) | TxOutput::IssueFungibleToken(..) | TxOutput::IssueNft(..) - | TxOutput::Htlc(_, _) => { /* Do nothing */ } + | TxOutput::Htlc(_, _) + | TxOutput::AnyoneCanTake(..) => { /* Do nothing */ } TxOutput::DataDeposit(v) => { // Ensure the size of the data doesn't exceed the max allowed if v.len() > chain_config.data_deposit_max_size() { @@ -295,7 +313,8 @@ fn check_htlc_outputs( | TxOutput::DelegateStaking(_, _) | TxOutput::IssueFungibleToken(_) | TxOutput::IssueNft(_, _, _) - | TxOutput::DataDeposit(_) => false, + | TxOutput::DataDeposit(_) + | TxOutput::AnyoneCanTake(_) => false, TxOutput::Htlc(_, _) => true, }); @@ -304,3 +323,50 @@ fn check_htlc_outputs( }; Ok(()) } + +fn check_order_outputs( + chain_config: &ChainConfig, + block_height: BlockHeight, + tx: &SignedTransaction, +) -> Result<(), CheckTransactionError> { + for output in tx.outputs() { + match output { + TxOutput::Transfer(..) + | TxOutput::LockThenTransfer(..) + | TxOutput::Burn(..) + | TxOutput::CreateStakePool(..) + | TxOutput::ProduceBlockFromStake(..) + | TxOutput::CreateDelegationId(..) + | TxOutput::DelegateStaking(..) + | TxOutput::IssueFungibleToken(..) + | TxOutput::IssueNft(..) + | TxOutput::DataDeposit(..) + | TxOutput::Htlc(..) => { /* Do nothing */ } + TxOutput::AnyoneCanTake(data) => { + let orders_activated = chain_config + .chainstate_upgrades() + .version_at_height(block_height) + .1 + .orders_activated(); + match orders_activated { + common::chain::OrdersActivated::Yes => { + ensure!( + CoinOrTokenId::from_output_value(data.ask()) + != CoinOrTokenId::from_output_value(data.give()), + CheckTransactionError::OrdersCurrenciesMustBeDifferent( + tx.transaction().get_id() + ) + ) + } + common::chain::OrdersActivated::No => { + return Err(CheckTransactionError::OrdersAreNotActivated( + tx.transaction().get_id(), + )) + } + } + } + } + } + + Ok(()) +} diff --git a/chainstate/tx-verifier/src/transaction_verifier/error.rs b/chainstate/tx-verifier/src/transaction_verifier/error.rs index ca398887b2..645cba5a8e 100644 --- a/chainstate/tx-verifier/src/transaction_verifier/error.rs +++ b/chainstate/tx-verifier/src/transaction_verifier/error.rs @@ -18,7 +18,7 @@ use common::{ chain::{ block::{Block, GenBlock}, tokens::TokenId, - AccountNonce, AccountType, DelegationId, OutPointSourceId, PoolId, Transaction, + AccountNonce, AccountType, DelegationId, OrderId, OutPointSourceId, PoolId, Transaction, UtxoOutPoint, }, primitives::{Amount, BlockHeight, CoinOrTokenId, Id}, @@ -99,6 +99,8 @@ pub enum ConnectTransactionError { AttemptToCreateStakePoolFromAccounts, #[error("Attempt to create delegation from accounting inputs")] AttemptToCreateDelegationFromAccounts, + #[error("Attempt to create order from accounting inputs")] + AttemptToCreateOrderFromAccounts, #[error("Failed to increment account nonce")] FailedToIncrementAccountNonce, #[error("Input output policy error: `{0}` in : `{1:?}`")] @@ -117,7 +119,8 @@ pub enum ConnectTransactionError { RewardDistributionError(#[from] reward_distribution::RewardDistributionError), #[error("Check transaction error: {0}")] CheckTransactionError(#[from] CheckTransactionError), - + #[error("Orders accounting error: {0}")] + OrdersAccountingError(#[from] orders_accounting::Error), #[error(transparent)] InputCheck(#[from] InputCheckError), } @@ -150,6 +153,8 @@ pub enum SignatureDestinationGetterError { DelegationDataNotFound(DelegationId), #[error("Token data not found for signature verification {0}")] TokenDataNotFound(TokenId), + #[error("Order data not found for signature verification {0}")] + OrderDataNotFound(OrderId), #[error("Utxo for the outpoint not fount: {0:?}")] UtxoOutputNotFound(UtxoOutPoint), #[error("Error accessing utxo set")] @@ -158,6 +163,8 @@ pub enum SignatureDestinationGetterError { PoSAccountingViewError(#[from] pos_accounting::Error), #[error("During destination getting for signature verification: Tokens accounting error {0}")] TokensAccountingViewError(#[from] tokens_accounting::Error), + #[error("During destination getting for signature verification: Orders accounting error {0}")] + OrdersAccountingViewError(#[from] orders_accounting::Error), } #[derive(Error, Debug, PartialEq, Eq, Clone)] diff --git a/chainstate/tx-verifier/src/transaction_verifier/flush.rs b/chainstate/tx-verifier/src/transaction_verifier/flush.rs index ad921f2746..2b77e1163a 100644 --- a/chainstate/tx-verifier/src/transaction_verifier/flush.rs +++ b/chainstate/tx-verifier/src/transaction_verifier/flush.rs @@ -18,6 +18,7 @@ use super::{ token_issuance_cache::{CachedAuxDataOp, CachedTokenIndexOp, ConsumedTokenIssuanceCache}, CachedOperation, TransactionVerifierDelta, }; +use orders_accounting::FlushableOrdersAccountingView; use tokens_accounting::FlushableTokensAccountingView; use utxo::FlushableUtxoView; @@ -66,6 +67,7 @@ pub fn flush_to_storage( where ::Error: From<::Error>, ::Error: From<::Error>, + ::Error: From<::Error>, ::Error: From, { flush_tokens(storage, &consumed.token_issuance_cache)?; @@ -122,5 +124,18 @@ where }; } + storage.batch_write_orders_data(consumed.orders_accounting_delta)?; + + // flush orders accounting block undo + for (tx_source, op) in consumed.orders_accounting_delta_undo { + match op { + CachedOperation::Write(undo) => { + storage.set_orders_accounting_undo_data(tx_source, &undo)? + } + CachedOperation::Read(_) => (), + CachedOperation::Erase => storage.del_orders_accounting_undo_data(tx_source)?, + } + } + Ok(()) } diff --git a/chainstate/tx-verifier/src/transaction_verifier/hierarchy.rs b/chainstate/tx-verifier/src/transaction_verifier/hierarchy.rs index 3d06877899..1dd5ede17c 100644 --- a/chainstate/tx-verifier/src/transaction_verifier/hierarchy.rs +++ b/chainstate/tx-verifier/src/transaction_verifier/hierarchy.rs @@ -29,10 +29,15 @@ use chainstate_types::{storage_result, GenBlockIndex}; use common::{ chain::{ tokens::{TokenAuxiliaryData, TokenId}, - AccountNonce, AccountType, DelegationId, GenBlock, PoolId, Transaction, UtxoOutPoint, + AccountNonce, AccountType, DelegationId, GenBlock, OrderData, OrderId, PoolId, Transaction, + UtxoOutPoint, }, primitives::{Amount, Id}, }; +use orders_accounting::{ + FlushableOrdersAccountingView, OrdersAccountingDeltaData, OrdersAccountingDeltaUndoData, + OrdersAccountingStorageRead, OrdersAccountingUndo, OrdersAccountingView, +}; use pos_accounting::{ DelegationData, DeltaMergeUndo, FlushablePoSAccountingView, PoSAccountingDeltaData, PoSAccountingUndo, PoSAccountingView, PoolData, @@ -49,7 +54,8 @@ impl< U: UtxosView, A: PoSAccountingView, T: TokensAccountingView, - > TransactionVerifierStorageRef for TransactionVerifier + O: OrdersAccountingView, + > TransactionVerifierStorageRef for TransactionVerifier where ::Error: From, { @@ -139,10 +145,23 @@ where None => self.storage.get_account_nonce_count(account), } } + + fn get_orders_accounting_undo( + &self, + tx_source: TransactionSource, + ) -> Result< + Option>, + ::Error, + > { + match self.orders_accounting_block_undo.data().get(&tx_source) { + Some(op) => Ok(op.get().cloned()), + None => self.storage.get_orders_accounting_undo(tx_source), + } + } } -impl UtxosStorageRead - for TransactionVerifier +impl UtxosStorageRead + for TransactionVerifier where ::Error: From, { @@ -157,7 +176,7 @@ where } } -impl TransactionVerifierStorageMut for TransactionVerifier +impl TransactionVerifierStorageMut for TransactionVerifier where S: TransactionVerifierStorageRef, ::Error: From, @@ -165,6 +184,7 @@ where U: UtxosView, A: PoSAccountingView, T: TokensAccountingView, + O: OrdersAccountingView, { fn set_token_aux_data( &mut self, @@ -288,11 +308,28 @@ where .del_undo_data(tx_source) .map_err(|e| TransactionVerifierStorageError::AccountingBlockUndoError(e).into()) } + + fn set_orders_accounting_undo_data( + &mut self, + tx_source: TransactionSource, + new_undo: &CachedBlockUndo, + ) -> Result<(), ::Error> { + self.orders_accounting_block_undo + .set_undo_data(tx_source, new_undo) + .map_err(|e| TransactionVerifierStorageError::AccountingBlockUndoError(e).into()) + } + + fn del_orders_accounting_undo_data( + &mut self, + tx_source: TransactionSource, + ) -> Result<(), ::Error> { + self.orders_accounting_block_undo + .del_undo_data(tx_source) + .map_err(|e| TransactionVerifierStorageError::AccountingBlockUndoError(e).into()) + } } -impl FlushableUtxoView - for TransactionVerifier -{ +impl FlushableUtxoView for TransactionVerifier { type Error = utxo::Error; fn batch_write(&mut self, utxos: ConsumedUtxoCache) -> Result<(), utxo::Error> { @@ -300,13 +337,8 @@ impl } } -impl< - C, - S: TransactionVerifierStorageRef, - U: UtxosView, - A: PoSAccountingView, - T: TokensAccountingView, - > PoSAccountingView for TransactionVerifier +impl PoSAccountingView + for TransactionVerifier { type Error = pos_accounting::Error; @@ -360,8 +392,8 @@ impl< } } -impl FlushablePoSAccountingView - for TransactionVerifier +impl FlushablePoSAccountingView + for TransactionVerifier { fn batch_write_delta( &mut self, @@ -371,13 +403,8 @@ impl FlushablePoSAccountingView } } -impl< - C, - S: TransactionVerifierStorageRef, - U: UtxosView, - A: PoSAccountingView, - T: TokensAccountingView, - > TokensAccountingStorageRead for TransactionVerifier +impl TokensAccountingStorageRead + for TransactionVerifier { type Error = tokens_accounting::Error; @@ -393,9 +420,7 @@ impl< } } -impl FlushableTokensAccountingView - for TransactionVerifier -{ +impl FlushableTokensAccountingView for TransactionVerifier { type Error = tokens_accounting::Error; fn batch_write_tokens_data( @@ -405,3 +430,32 @@ impl FlushableTokensAccountingView self.tokens_accounting_cache.batch_write_tokens_data(data) } } + +impl OrdersAccountingStorageRead + for TransactionVerifier +{ + type Error = orders_accounting::Error; + + fn get_order_data(&self, id: &OrderId) -> Result, Self::Error> { + self.orders_accounting_cache.get_order_data(id) + } + + fn get_ask_balance(&self, id: &OrderId) -> Result, Self::Error> { + self.orders_accounting_cache.get_ask_balance(id) + } + + fn get_give_balance(&self, id: &OrderId) -> Result, Self::Error> { + self.orders_accounting_cache.get_give_balance(id) + } +} + +impl FlushableOrdersAccountingView for TransactionVerifier { + type Error = orders_accounting::Error; + + fn batch_write_orders_data( + &mut self, + data: OrdersAccountingDeltaData, + ) -> Result { + self.orders_accounting_cache.batch_write_orders_data(data) + } +} diff --git a/chainstate/tx-verifier/src/transaction_verifier/input_check/mod.rs b/chainstate/tx-verifier/src/transaction_verifier/input_check/mod.rs index 2126a46af3..4880ff1f7e 100644 --- a/chainstate/tx-verifier/src/transaction_verifier/input_check/mod.rs +++ b/chainstate/tx-verifier/src/transaction_verifier/input_check/mod.rs @@ -132,26 +132,35 @@ impl mintscript::translate::InputInfoProvider for PerInputData<'_> { } } -pub struct TranslationContextFull<'a, AV, TV> { +pub struct TranslationContextFull<'a, AV, TV, OV> { // Sources of additional information, should it be required. pos_accounting: AV, tokens_accounting: TV, + orders_accounting: OV, // Information about the input input: &'a PerInputData<'a>, } -impl<'a, AV, TV> TranslationContextFull<'a, AV, TV> { - fn new(pos_accounting: AV, tokens_accounting: TV, input: &'a PerInputData<'a>) -> Self { +impl<'a, AV, TV, OV> TranslationContextFull<'a, AV, TV, OV> { + fn new( + pos_accounting: AV, + tokens_accounting: TV, + orders_accounting: OV, + input: &'a PerInputData<'a>, + ) -> Self { Self { pos_accounting, tokens_accounting, + orders_accounting, input, } } } -impl mintscript::translate::InputInfoProvider for TranslationContextFull<'_, AV, TV> { +impl mintscript::translate::InputInfoProvider + for TranslationContextFull<'_, AV, TV, OV> +{ fn input_info(&self) -> &InputInfo { self.input.input_info() } @@ -161,13 +170,16 @@ impl mintscript::translate::InputInfoProvider for TranslationContextFull } } -impl mintscript::translate::SignatureInfoProvider for TranslationContextFull<'_, AV, TV> +impl mintscript::translate::SignatureInfoProvider + for TranslationContextFull<'_, AV, TV, OV> where AV: pos_accounting::PoSAccountingView, TV: tokens_accounting::TokensAccountingView, + OV: orders_accounting::OrdersAccountingView, { type PoSAccounting = AV; type Tokens = TV; + type Orders = OV; fn pos_accounting(&self) -> &Self::PoSAccounting { &self.pos_accounting @@ -176,12 +188,17 @@ where fn tokens(&self) -> &Self::Tokens { &self.tokens_accounting } + + fn orders(&self) -> &Self::Orders { + &self.orders_accounting + } } -impl TranslationContextFull<'_, AV, TV> +impl TranslationContextFull<'_, AV, TV, OV> where AV: pos_accounting::PoSAccountingView, TV: tokens_accounting::TokensAccountingView, + OV: orders_accounting::OrdersAccountingView, { fn to_script>(&self) -> Result { Ok(T::translate_input(self)?) @@ -439,34 +456,36 @@ impl SignatureContext for InputVerifyContextFull<'_, T, S> { } } -pub trait FullyVerifiable: - Transactable + for<'a> TranslateInput> +pub trait FullyVerifiable: + Transactable + for<'a> TranslateInput> { } -impl FullyVerifiable for T where - T: Transactable + for<'a> TranslateInput> +impl FullyVerifiable for T where + T: Transactable + for<'a> TranslateInput> { } /// Perform full verification of given input. #[allow(clippy::too_many_arguments)] -pub fn verify_full( +pub fn verify_full( transaction: &T, chain_config: &ChainConfig, utxos_view: &UV, pos_accounting: &AV, tokens_accounting: &TV, + orders_accounting: &OV, storage: &S, tx_source: &TransactionSourceForConnect, spending_time: BlockTimestamp, ) -> Result<(), InputCheckError> where - T: FullyVerifiable, + T: FullyVerifiable, S: TransactionVerifierStorageRef, UV: utxo::UtxosView, AV: pos_accounting::PoSAccountingView, TV: tokens_accounting::TokensAccountingView, + OV: orders_accounting::OrdersAccountingView, { let core_ctx = CoreContext::new(utxos_view, transaction)?; let tl_ctx = VerifyContextTimelock::for_verifier( @@ -479,9 +498,10 @@ where let ctx = VerifyContextFull::new(transaction, &tl_ctx); for (n, inp) in core_ctx.inputs_iter() { - let script = TranslationContextFull::new(pos_accounting, tokens_accounting, inp) - .to_script::() - .map_err(|e| InputCheckError::new(n, e))?; + let script = + TranslationContextFull::new(pos_accounting, tokens_accounting, orders_accounting, inp) + .to_script::() + .map_err(|e| InputCheckError::new(n, e))?; let mut checker = mintscript::ScriptChecker::full(InputVerifyContextFull::new(&ctx, n)); script.verify(&mut checker).map_err(|e| InputCheckError::new(n, e))?; } diff --git a/chainstate/tx-verifier/src/transaction_verifier/input_output_policy/mod.rs b/chainstate/tx-verifier/src/transaction_verifier/input_output_policy/mod.rs index 76db7eea87..1e7a14a7e2 100644 --- a/chainstate/tx-verifier/src/transaction_verifier/input_output_policy/mod.rs +++ b/chainstate/tx-verifier/src/transaction_verifier/input_output_policy/mod.rs @@ -19,12 +19,12 @@ use common::{ output_value::OutputValue, signature::Signable, tokens::{get_tokens_issuance_count, TokenId}, - Block, ChainConfig, DelegationId, PoolId, TokenIssuanceVersion, Transaction, TxInput, - TxOutput, + Block, ChainConfig, TokenIssuanceVersion, Transaction, TxInput, TxOutput, }, primitives::{Amount, BlockHeight, Fee, Id, Idable, Subsidy}, }; use constraints_value_accumulator::{AccumulatedFee, ConstrainedValueAccumulator}; +use orders_accounting::OrdersAccountingView; use pos_accounting::PoSAccountingView; use thiserror::Error; @@ -75,7 +75,8 @@ pub fn calculate_tokens_burned_in_outputs( | TxOutput::IssueFungibleToken(_) | TxOutput::IssueNft(_, _, _) | TxOutput::DataDeposit(_) - | TxOutput::Htlc(_, _) => None, + | TxOutput::Htlc(_, _) + | TxOutput::AnyoneCanTake(_) => None, }) .sum::>() .ok_or(ConnectTransactionError::BurnAmountSumError(tx.get_id())) @@ -146,6 +147,7 @@ pub fn check_tx_inputs_outputs_policy( tx: &Transaction, chain_config: &ChainConfig, block_height: BlockHeight, + orders_accounting_view: &impl OrdersAccountingView, pos_accounting_view: &impl PoSAccountingView, utxo_view: &impl utxo::UtxosView, ) -> Result { @@ -181,26 +183,11 @@ pub fn check_tx_inputs_outputs_policy( }) .collect::, ConnectTransactionError>>()?; - let staker_balance_getter = |pool_id: PoolId| { - pos_accounting_view - .get_pool_data(pool_id) - .map_err(|_| pos_accounting::Error::ViewFail)? - .map(|pool_data| pool_data.staker_balance()) - .transpose() - .map_err(constraints_value_accumulator::Error::PoSAccountingError) - }; - - let delegation_balance_getter = |delegation_id: DelegationId| { - Ok(pos_accounting_view - .get_delegation_balance(delegation_id) - .map_err(|_| pos_accounting::Error::ViewFail)?) - }; - let inputs_accumulator = ConstrainedValueAccumulator::from_inputs( chain_config, block_height, - staker_balance_getter, - delegation_balance_getter, + orders_accounting_view, + pos_accounting_view, tx.inputs(), &inputs_utxos, ) @@ -243,7 +230,8 @@ fn check_issuance_fee_burn_v0( | TxOutput::IssueNft(_, _, _) | TxOutput::DataDeposit(_) | TxOutput::DelegateStaking(_, _) - | TxOutput::Htlc(_, _) => None, + | TxOutput::Htlc(_, _) + | TxOutput::AnyoneCanTake(_) => None, }) .sum::>() .ok_or_else(|| ConnectTransactionError::BurnAmountSumError(tx.get_id()))?; diff --git a/chainstate/tx-verifier/src/transaction_verifier/input_output_policy/purposes_check.rs b/chainstate/tx-verifier/src/transaction_verifier/input_output_policy/purposes_check.rs index eeb3a98f58..e3d96eb258 100644 --- a/chainstate/tx-verifier/src/transaction_verifier/input_output_policy/purposes_check.rs +++ b/chainstate/tx-verifier/src/transaction_verifier/input_output_policy/purposes_check.rs @@ -64,7 +64,8 @@ pub fn check_reward_inputs_outputs_purposes( | TxOutput::IssueFungibleToken(..) | TxOutput::IssueNft(..) | TxOutput::DataDeposit(..) - | TxOutput::Htlc(..) => Err(ConnectTransactionError::IOPolicyError( + | TxOutput::Htlc(..) + | TxOutput::AnyoneCanTake(..) => Err(ConnectTransactionError::IOPolicyError( IOPolicyError::InvalidInputTypeInReward, block_id.into(), )), @@ -88,7 +89,8 @@ pub fn check_reward_inputs_outputs_purposes( | TxOutput::IssueFungibleToken(..) | TxOutput::IssueNft(..) | TxOutput::DataDeposit(..) - | TxOutput::Htlc(..) => { + | TxOutput::Htlc(..) + | TxOutput::AnyoneCanTake(..) => { Err(ConnectTransactionError::IOPolicyError( IOPolicyError::InvalidOutputTypeInReward, block_id.into(), @@ -139,7 +141,8 @@ pub fn check_reward_inputs_outputs_purposes( | TxOutput::IssueFungibleToken(..) | TxOutput::IssueNft(..) | TxOutput::DataDeposit(..) - | TxOutput::Htlc(..) => false, + | TxOutput::Htlc(..) + | TxOutput::AnyoneCanTake(..) => false, }); ensure!( all_lock_then_transfer, @@ -170,7 +173,8 @@ pub fn check_tx_inputs_outputs_purposes( | TxOutput::CreateDelegationId(..) | TxOutput::DelegateStaking(..) | TxOutput::IssueFungibleToken(..) - | TxOutput::DataDeposit(..) => false, + | TxOutput::DataDeposit(..) + | TxOutput::AnyoneCanTake(..) => false, }); ensure!(are_inputs_valid, IOPolicyError::InvalidInputTypeInTx); @@ -202,7 +206,8 @@ pub fn check_tx_inputs_outputs_purposes( | TxOutput::IssueFungibleToken(..) | TxOutput::IssueNft(..) | TxOutput::DataDeposit(..) - | TxOutput::Htlc(..) => { /* do nothing */ } + | TxOutput::Htlc(..) + | TxOutput::AnyoneCanTake(..) => { /* do nothing */ } TxOutput::CreateStakePool(..) => { stake_pool_outputs_count += 1; } diff --git a/chainstate/tx-verifier/src/transaction_verifier/input_output_policy/tests/constraints_tests.rs b/chainstate/tx-verifier/src/transaction_verifier/input_output_policy/tests/constraints_tests.rs index 28d20fe0f8..6b7a003fc8 100644 --- a/chainstate/tx-verifier/src/transaction_verifier/input_output_policy/tests/constraints_tests.rs +++ b/chainstate/tx-verifier/src/transaction_verifier/input_output_policy/tests/constraints_tests.rs @@ -24,6 +24,7 @@ use common::{ primitives::{per_thousand::PerThousand, Amount, CoinOrTokenId, H256}, }; use crypto::vrf::{VRFKeyKind, VRFPrivateKey}; +use orders_accounting::{InMemoryOrdersAccounting, OrdersAccountingDB}; use randomness::{CryptoRng, Rng, SliceRandom}; use rstest::rstest; use test_utils::{ @@ -101,6 +102,9 @@ fn timelock_constraints_on_decommission_in_tx(#[case] seed: Seed) { ); let pos_db = pos_accounting::PoSAccountingDB::new(&pos_store); + let orders_store = InMemoryOrdersAccounting::new(); + let orders_db = OrdersAccountingDB::new(&orders_store); + let decommission_pool_utxo = if rng.gen::() { TxOutput::CreateStakePool(pool_id, Box::new(stake_pool_data)) } else { @@ -158,6 +162,7 @@ fn timelock_constraints_on_decommission_in_tx(#[case] seed: Seed) { &tx, &chain_config, BlockHeight::new(1), + &orders_db, &pos_db, &utxo_db, ) @@ -205,8 +210,15 @@ fn timelock_constraints_on_decommission_in_tx(#[case] seed: Seed) { let (utxo_db, tx) = prepare_utxos_and_tx(&mut rng, input_utxos, outputs); - check_tx_inputs_outputs_policy(&tx, &chain_config, BlockHeight::new(1), &pos_db, &utxo_db) - .unwrap(); + check_tx_inputs_outputs_policy( + &tx, + &chain_config, + BlockHeight::new(1), + &orders_db, + &pos_db, + &utxo_db, + ) + .unwrap(); } } @@ -240,6 +252,9 @@ fn timelock_constraints_on_spend_share_in_tx(#[case] seed: Seed) { let pos_db = pos_accounting::PoSAccountingDB::new(&pos_store); let utxo_db = UtxosDBInMemoryImpl::new(Id::::new(H256::zero()), BTreeMap::new()); + let orders_store = InMemoryOrdersAccounting::new(); + let orders_db = OrdersAccountingDB::new(&orders_store); + // make timelock outputs but total atoms that locked is less than required { let random_additional_value = rng.gen_range(1..=atoms_to_spend); @@ -284,6 +299,7 @@ fn timelock_constraints_on_spend_share_in_tx(#[case] seed: Seed) { &tx, &chain_config, BlockHeight::new(1), + &orders_db, &pos_db, &utxo_db, ) @@ -336,7 +352,14 @@ fn timelock_constraints_on_spend_share_in_tx(#[case] seed: Seed) { ) .unwrap(); - check_tx_inputs_outputs_policy(&tx, &chain_config, BlockHeight::new(1), &pos_db, &utxo_db) - .unwrap(); + check_tx_inputs_outputs_policy( + &tx, + &chain_config, + BlockHeight::new(1), + &orders_db, + &pos_db, + &utxo_db, + ) + .unwrap(); } } diff --git a/chainstate/tx-verifier/src/transaction_verifier/input_output_policy/tests/outputs_utils.rs b/chainstate/tx-verifier/src/transaction_verifier/input_output_policy/tests/outputs_utils.rs index 593415e41f..18916f2986 100644 --- a/chainstate/tx-verifier/src/transaction_verifier/input_output_policy/tests/outputs_utils.rs +++ b/chainstate/tx-verifier/src/transaction_verifier/input_output_policy/tests/outputs_utils.rs @@ -23,8 +23,8 @@ use common::{ IsTokenFreezable, IsTokenUnfreezable, Metadata, NftIssuance, NftIssuanceV0, TokenId, TokenIssuance, TokenIssuanceV1, TokenTotalSupply, }, - AccountCommand, AccountNonce, AccountSpending, DelegationId, Destination, PoolId, TxInput, - TxOutput, + AccountCommand, AccountNonce, AccountSpending, DelegationId, Destination, OrderData, + OrderId, PoolId, TxInput, TxOutput, }, primitives::{per_thousand::PerThousand, Amount, H256}, }; @@ -47,10 +47,11 @@ fn update_functions_below_if_new_outputs_were_added(output: TxOutput) { TxOutput::IssueNft(_, _, _) => unimplemented!(), TxOutput::DataDeposit(_) => unimplemented!(), TxOutput::Htlc(_, _) => unimplemented!(), + TxOutput::AnyoneCanTake(_) => unimplemented!(), } } -pub fn all_outputs() -> [TxOutput; 11] { +pub fn all_outputs() -> [TxOutput; 12] { [ transfer(), htlc(), @@ -63,10 +64,11 @@ pub fn all_outputs() -> [TxOutput; 11] { issue_tokens(), issue_nft(), data_deposit(), + create_order(), ] } -pub fn valid_tx_outputs() -> [TxOutput; 10] { +pub fn valid_tx_outputs() -> [TxOutput; 11] { [ transfer(), htlc(), @@ -78,6 +80,7 @@ pub fn valid_tx_outputs() -> [TxOutput; 10] { issue_tokens(), issue_nft(), data_deposit(), + create_order(), ] } @@ -92,11 +95,18 @@ pub fn valid_tx_inputs_utxos() -> [TxOutput; 6] { ] } -pub fn invalid_tx_inputs_utxos() -> [TxOutput; 5] { - [burn(), delegate_staking(), create_delegation(), issue_tokens(), data_deposit()] +pub fn invalid_tx_inputs_utxos() -> [TxOutput; 6] { + [ + burn(), + delegate_staking(), + create_delegation(), + issue_tokens(), + data_deposit(), + create_order(), + ] } -pub fn invalid_block_reward_for_pow() -> [TxOutput; 10] { +pub fn invalid_block_reward_for_pow() -> [TxOutput; 11] { [ transfer(), htlc(), @@ -108,10 +118,11 @@ pub fn invalid_block_reward_for_pow() -> [TxOutput; 10] { issue_nft(), issue_tokens(), data_deposit(), + create_order(), ] } -pub fn all_account_inputs() -> [TxInput; 7] { +pub fn all_account_inputs() -> [TxInput; 9] { [ TxInput::from_account( AccountNonce::new(0), @@ -141,6 +152,18 @@ pub fn all_account_inputs() -> [TxInput; 7] { AccountNonce::new(0), AccountCommand::ChangeTokenAuthority(TokenId::zero(), Destination::AnyoneCanSpend), ), + TxInput::from_command( + AccountNonce::new(0), + AccountCommand::ConcludeOrder(OrderId::zero()), + ), + TxInput::from_command( + AccountNonce::new(0), + AccountCommand::FillOrder( + OrderId::zero(), + OutputValue::Coin(Amount::ZERO), + Destination::AnyoneCanSpend, + ), + ), ] } @@ -214,6 +237,14 @@ pub fn data_deposit() -> TxOutput { TxOutput::DataDeposit(vec![]) } +pub fn create_order() -> TxOutput { + TxOutput::AnyoneCanTake(Box::new(OrderData::new( + Destination::AnyoneCanSpend, + OutputValue::Coin(Amount::ZERO), + OutputValue::Coin(Amount::ZERO), + ))) +} + pub fn issue_nft() -> TxOutput { TxOutput::IssueNft( TokenId::new(H256::zero()), @@ -244,7 +275,8 @@ pub fn is_stake_pool(output: &TxOutput) -> bool { | TxOutput::IssueFungibleToken(..) | TxOutput::IssueNft(..) | TxOutput::DataDeposit(..) - | TxOutput::Htlc(..) => false, + | TxOutput::Htlc(..) + | TxOutput::AnyoneCanTake(..) => false, TxOutput::CreateStakePool(..) => true, } } @@ -260,7 +292,8 @@ pub fn is_produce_block(output: &TxOutput) -> bool { | TxOutput::IssueFungibleToken(..) | TxOutput::IssueNft(..) | TxOutput::DataDeposit(..) - | TxOutput::Htlc(..) => false, + | TxOutput::Htlc(..) + | TxOutput::AnyoneCanTake(..) => false, TxOutput::ProduceBlockFromStake(..) => true, } } diff --git a/chainstate/tx-verifier/src/transaction_verifier/input_output_policy/tests/purpose_tests.rs b/chainstate/tx-verifier/src/transaction_verifier/input_output_policy/tests/purpose_tests.rs index dbd56ade49..26e54c31e5 100644 --- a/chainstate/tx-verifier/src/transaction_verifier/input_output_policy/tests/purpose_tests.rs +++ b/chainstate/tx-verifier/src/transaction_verifier/input_output_policy/tests/purpose_tests.rs @@ -119,6 +119,7 @@ fn tx_many_to_many_valid(#[case] seed: Seed) { issue_tokens(), issue_nft(), data_deposit(), + create_order(), ]; let (utxo_db, tx) = prepare_utxos_and_tx_with_random_combinations( @@ -133,6 +134,56 @@ fn tx_many_to_many_valid(#[case] seed: Seed) { check_tx_inputs_outputs_purposes(&tx, &inputs_utxos).unwrap(); } +#[rstest] +#[trace] +#[case(Seed::from_entropy())] +fn tx_many_to_many_valid_with_account_input(#[case] seed: Seed) { + let mut rng = make_seedable_rng(seed); + let number_of_inputs = rng.gen_range(1..10); + let number_of_outputs = rng.gen_range(1..10); + + let account_inputs = all_account_inputs(); + // stake pool and create delegation are skipped to avoid dealing with uniqueness + let valid_outputs = [ + lock_then_transfer(), + transfer(), + burn(), + delegate_staking(), + issue_tokens(), + issue_nft(), + data_deposit(), + create_order(), + ]; + + let inputs_utxos = get_random_outputs_combination( + &mut rng, + &super::outputs_utils::valid_tx_inputs_utxos(), + number_of_inputs, + ); + + let inputs = { + let mut inputs = inputs_utxos + .iter() + .enumerate() + .map(|(i, _)| { + TxInput::from_utxo( + OutPointSourceId::Transaction(Id::new(H256::zero())), + i as u32, + ) + }) + .chain(get_random_inputs_combination(&mut rng, &account_inputs, 1)) + .collect::>(); + inputs.shuffle(&mut rng); + inputs + }; + + let outputs = get_random_outputs_combination(&mut rng, &valid_outputs, number_of_outputs); + + let tx = Transaction::new(0, inputs, outputs).unwrap(); + + check_tx_inputs_outputs_purposes(&tx, &inputs_utxos).unwrap(); +} + #[rstest] #[trace] #[case(Seed::from_entropy())] diff --git a/chainstate/tx-verifier/src/transaction_verifier/mod.rs b/chainstate/tx-verifier/src/transaction_verifier/mod.rs index 2e8e8339ba..596ecac756 100644 --- a/chainstate/tx-verifier/src/transaction_verifier/mod.rs +++ b/chainstate/tx-verifier/src/transaction_verifier/mod.rs @@ -30,6 +30,10 @@ pub mod tokens_check; mod tx_source; use accounting::BlockRewardUndo; use constraints_value_accumulator::AccumulatedFee; +use orders_accounting::{ + OrdersAccountingCache, OrdersAccountingDB, OrdersAccountingDeltaData, + OrdersAccountingOperations, OrdersAccountingUndo, OrdersAccountingView, +}; use tokens_accounting::{ TokenAccountingUndo, TokensAccountingCache, TokensAccountingDB, TokensAccountingDeltaData, TokensAccountingOperations, TokensAccountingStorageRead, TokensAccountingView, @@ -64,6 +68,7 @@ use chainstate_types::BlockIndex; use common::{ chain::{ block::{timestamp::BlockTimestamp, BlockRewardTransactable, ConsensusData}, + make_order_id, output_value::OutputValue, signature::Signable, signed_transaction::SignedTransaction, @@ -92,6 +97,9 @@ pub struct TransactionVerifierDelta { tokens_accounting_delta: TokensAccountingDeltaData, tokens_accounting_delta_undo: BTreeMap>, + orders_accounting_delta: OrdersAccountingDeltaData, + orders_accounting_delta_undo: + BTreeMap>, } impl TransactionVerifierDelta { @@ -101,7 +109,7 @@ impl TransactionVerifierDelta { } /// The tool used to verify transactions and cache their updated states in memory -pub struct TransactionVerifier { +pub struct TransactionVerifier { chain_config: C, storage: S, best_block: Id, @@ -117,11 +125,14 @@ pub struct TransactionVerifier { tokens_accounting_cache: TokensAccountingCache, tokens_accounting_block_undo: AccountingBlockUndoCache, + orders_accounting_cache: OrdersAccountingCache, + orders_accounting_block_undo: AccountingBlockUndoCache, + account_nonce: BTreeMap>, } impl - TransactionVerifier, S, TokensAccountingDB> + TransactionVerifier, S, TokensAccountingDB, OrdersAccountingDB> { pub fn new(storage: S, chain_config: C) -> Self { let accounting_delta_adapter = PoSAccountingDeltaAdapter::new(storage.shallow_clone()); @@ -132,6 +143,8 @@ impl .expect("Database error while reading utxos best block"); let tokens_accounting_cache = TokensAccountingCache::new(TokensAccountingDB::new(storage.shallow_clone())); + let orders_accounting_cache = + OrdersAccountingCache::new(OrdersAccountingDB::new(storage.shallow_clone())); Self { storage, chain_config, @@ -143,17 +156,20 @@ impl pos_accounting_block_undo: AccountingBlockUndoCache::::new(), tokens_accounting_cache, tokens_accounting_block_undo: AccountingBlockUndoCache::::new(), + orders_accounting_cache, + orders_accounting_block_undo: AccountingBlockUndoCache::::new(), account_nonce: BTreeMap::new(), } } } -impl TransactionVerifier +impl TransactionVerifier where S: TransactionVerifierStorageRef, U: UtxosView + Send + Sync, A: PoSAccountingView + Send + Sync, T: TokensAccountingView + Send + Sync, + O: OrdersAccountingView + Send + Sync, { pub fn new_generic( storage: S, @@ -161,6 +177,7 @@ where utxos: U, accounting: A, tokens_accounting: T, + orders_accounting: O, ) -> Self { // TODO: both "expect"s in this function may fire when exiting the node-gui app; // get rid of them and return a proper Result. @@ -179,29 +196,33 @@ where pos_accounting_block_undo: AccountingBlockUndoCache::::new(), tokens_accounting_cache: TokensAccountingCache::new(tokens_accounting), tokens_accounting_block_undo: AccountingBlockUndoCache::::new(), + orders_accounting_cache: OrdersAccountingCache::new(orders_accounting), + orders_accounting_block_undo: AccountingBlockUndoCache::::new(), account_nonce: BTreeMap::new(), } } } -impl TransactionVerifier +type DerivedTxVerifier<'a, C, S, U, A, T, O> = TransactionVerifier< + &'a ChainConfig, + &'a TransactionVerifier, + &'a UtxosCache, + &'a PoSAccountingDelta, + &'a TokensAccountingCache, + &'a OrdersAccountingCache, +>; + +impl TransactionVerifier where C: AsRef, S: TransactionVerifierStorageRef, U: UtxosView, A: PoSAccountingView, T: TokensAccountingView, + O: OrdersAccountingView, ::Error: From, { - pub fn derive_child( - &self, - ) -> TransactionVerifier< - &ChainConfig, - &Self, - &UtxosCache, - &PoSAccountingDelta, - &TokensAccountingCache, - > { + pub fn derive_child(&self) -> DerivedTxVerifier { TransactionVerifier { storage: self, chain_config: self.chain_config.as_ref(), @@ -214,6 +235,8 @@ where pos_accounting_block_undo: AccountingBlockUndoCache::::new(), tokens_accounting_cache: TokensAccountingCache::new(&self.tokens_accounting_cache), tokens_accounting_block_undo: AccountingBlockUndoCache::::new(), + orders_accounting_cache: OrdersAccountingCache::new(&self.orders_accounting_cache), + orders_accounting_block_undo: AccountingBlockUndoCache::::new(), best_block: self.best_block, account_nonce: BTreeMap::new(), } @@ -303,7 +326,8 @@ where | TxOutput::IssueFungibleToken(_) | TxOutput::IssueNft(_, _, _) | TxOutput::DataDeposit(_) - | TxOutput::Htlc(_, _) => Ok(None), + | TxOutput::Htlc(_, _) + | TxOutput::AnyoneCanTake(_) => Ok(None), } } @@ -423,7 +447,8 @@ where | TxOutput::IssueFungibleToken(_) | TxOutput::IssueNft(_, _, _) | TxOutput::DataDeposit(_) - | TxOutput::Htlc(_, _) => None, + | TxOutput::Htlc(_, _) + | TxOutput::AnyoneCanTake(_) => None, }) .collect::, _>>()?; @@ -473,6 +498,8 @@ where self.disconnect_tokens_accounting_outputs(tx_source, tx)?; + self.disconnect_orders_accounting_outputs(tx_source, tx)?; + Ok(()) } @@ -583,6 +610,7 @@ where }); Some(res) } + AccountCommand::ConcludeOrder(_) | AccountCommand::FillOrder(_, _, _) => None, }, }) .collect::, _>>()?; @@ -600,7 +628,8 @@ where | TxOutput::LockThenTransfer(_, _, _) | TxOutput::IssueNft(_, _, _) | TxOutput::DataDeposit(_) - | TxOutput::Htlc(_, _) => None, + | TxOutput::Htlc(_, _) + | TxOutput::AnyoneCanTake(_) => None, TxOutput::IssueFungibleToken(issuance_data) => { let result = make_token_id(tx.inputs()) .ok_or(ConnectTransactionError::TokensError( @@ -672,7 +701,8 @@ where | TxOutput::IssueFungibleToken(_) | TxOutput::IssueNft(_, _, _) | TxOutput::DataDeposit(_) - | TxOutput::Htlc(_, _) => Ok(()), + | TxOutput::Htlc(_, _) + | TxOutput::AnyoneCanTake(_) => Ok(()), } }) } @@ -704,6 +734,119 @@ where Ok(()) } + fn connect_orders_outputs( + &mut self, + tx_source: &TransactionSourceForConnect, + tx: &Transaction, + ) -> Result<(), ConnectTransactionError> { + let input_undos = tx + .inputs() + .iter() + .filter_map(|input| match input { + TxInput::Utxo(_) | TxInput::Account(_) => None, + TxInput::AccountCommand(nonce, account_op) => match account_op { + AccountCommand::MintTokens(..) + | AccountCommand::UnmintTokens(..) + | AccountCommand::LockTokenSupply(..) + | AccountCommand::FreezeToken(..) + | AccountCommand::UnfreezeToken(..) + | AccountCommand::ChangeTokenAuthority(..) => None, + AccountCommand::ConcludeOrder(order_id) => { + let res = self + .spend_input_from_account(*nonce, account_op.clone().into()) + .and_then(|_| { + self.orders_accounting_cache + .conclude_order(*order_id) + .map_err(ConnectTransactionError::OrdersAccountingError) + }); + Some(res) + } + AccountCommand::FillOrder(order_id, fill, _) => { + let res = self + .spend_input_from_account(*nonce, account_op.clone().into()) + .and_then(|_| { + self.orders_accounting_cache + .fill_order(*order_id, fill.clone()) + .map_err(ConnectTransactionError::OrdersAccountingError) + }); + Some(res) + } + }, + }) + .collect::, _>>()?; + + let input_utxo_outpoint = tx.inputs().iter().find_map(|input| input.utxo_outpoint()); + let output_undos = tx + .outputs() + .iter() + .filter_map(|output| match output { + TxOutput::Transfer(..) + | TxOutput::Burn(..) + | TxOutput::CreateStakePool(..) + | TxOutput::ProduceBlockFromStake(..) + | TxOutput::CreateDelegationId(..) + | TxOutput::DelegateStaking(..) + | TxOutput::LockThenTransfer(..) + | TxOutput::IssueNft(..) + | TxOutput::DataDeposit(..) + | TxOutput::IssueFungibleToken(..) + | TxOutput::Htlc(_, _) => None, + TxOutput::AnyoneCanTake(order_data) => match input_utxo_outpoint { + Some(input_utxo_outpoint) => { + let order_id = make_order_id(input_utxo_outpoint); + let result = self + .orders_accounting_cache + .create_order(order_id, *order_data.clone()) + .map_err(ConnectTransactionError::OrdersAccountingError); + Some(result) + } + None => Some(Err( + ConnectTransactionError::AttemptToCreateOrderFromAccounts, + )), + }, + }) + .collect::, _>>()?; + + // Store accounting operations undos + if !input_undos.is_empty() || !output_undos.is_empty() { + let tx_undos = input_undos.into_iter().chain(output_undos).collect(); + self.orders_accounting_block_undo.add_tx_undo( + TransactionSource::from(tx_source), + tx.get_id(), + accounting::TxUndo::new(tx_undos), + )?; + } + + Ok(()) + } + + fn disconnect_orders_accounting_outputs( + &mut self, + tx_source: TransactionSource, + tx: &Transaction, + ) -> Result<(), ConnectTransactionError> { + // apply undos to accounting + let block_undo_fetcher = |tx_source: TransactionSource| { + self.storage + .get_orders_accounting_undo(tx_source) + .map_err(|_| ConnectTransactionError::TxVerifierStorage) + }; + let undos = self.orders_accounting_block_undo.take_tx_undo( + &tx_source, + &tx.get_id(), + block_undo_fetcher, + )?; + if let Some(undos) = undos { + undos + .into_inner() + .into_iter() + .rev() + .try_for_each(|undo| self.orders_accounting_cache.undo(undo))?; + } + + Ok(()) + } + pub fn connect_transaction( &mut self, tx_source: &TransactionSourceForConnect, @@ -730,6 +873,7 @@ where tx.transaction(), self.chain_config.as_ref(), tx_source.expected_block_height(), + &self.orders_accounting_cache, &self.pos_accounting_adapter.accounting_delta(), &self.utxo_cache, )?; @@ -740,6 +884,8 @@ where self.connect_tokens_outputs(tx_source, tx.transaction())?; + self.connect_orders_outputs(tx_source, tx.transaction())?; + // spend utxos let tx_undo = self .utxo_cache @@ -957,7 +1103,11 @@ where median_time_past: BlockTimestamp, ) -> Result<(), input_check::InputCheckError> where - Tx: input_check::FullyVerifiable, TokensAccountingCache>, + Tx: input_check::FullyVerifiable< + PoSAccountingDelta, + TokensAccountingCache, + OrdersAccountingCache, + >, { input_check::verify_full( tx, @@ -965,6 +1115,7 @@ where &self.utxo_cache, self.pos_accounting_adapter.accounting_delta(), &self.tokens_accounting_cache, + &self.orders_accounting_cache, &self.storage, tx_source, median_time_past, @@ -983,6 +1134,8 @@ where account_nonce: self.account_nonce, tokens_accounting_delta: self.tokens_accounting_cache.consume(), tokens_accounting_delta_undo: self.tokens_accounting_block_undo.consume(), + orders_accounting_delta: self.orders_accounting_cache.consume(), + orders_accounting_delta_undo: self.orders_accounting_block_undo.consume(), }) } } diff --git a/chainstate/tx-verifier/src/transaction_verifier/storage.rs b/chainstate/tx-verifier/src/transaction_verifier/storage.rs index b480c9126e..0e1b1d264d 100644 --- a/chainstate/tx-verifier/src/transaction_verifier/storage.rs +++ b/chainstate/tx-verifier/src/transaction_verifier/storage.rs @@ -23,6 +23,9 @@ use common::{ }, primitives::Id, }; +use orders_accounting::{ + FlushableOrdersAccountingView, OrdersAccountingStorageRead, OrdersAccountingUndo, +}; use pos_accounting::{ FlushablePoSAccountingView, PoSAccountingDeltaData, PoSAccountingUndo, PoSAccountingView, }; @@ -59,12 +62,14 @@ pub enum TransactionVerifierStorageError { AccountingBlockUndoError(#[from] accounting::BlockUndoError), #[error("Tokens accounting error: {0}")] TokensAccountingError(#[from] tokens_accounting::Error), + #[error("Orders accounting error: {0}")] + OrdersAccountingError(#[from] orders_accounting::Error), } // TODO(Gosha): PoSAccountingView should be replaced with PoSAccountingStorageRead in which the // return error type can handle both storage_result::Error and pos_accounting::Error pub trait TransactionVerifierStorageRef: - UtxosStorageRead + PoSAccountingView + TokensAccountingStorageRead + UtxosStorageRead + PoSAccountingView + TokensAccountingStorageRead + OrdersAccountingStorageRead { type Error: std::error::Error; @@ -113,6 +118,14 @@ pub trait TransactionVerifierStorageRef: &self, account: AccountType, ) -> Result, ::Error>; + + fn get_orders_accounting_undo( + &self, + tx_source: TransactionSource, + ) -> Result< + Option>, + ::Error, + >; } pub trait TransactionVerifierStorageMut: @@ -120,6 +133,7 @@ pub trait TransactionVerifierStorageMut: + FlushableUtxoView + FlushablePoSAccountingView + FlushableTokensAccountingView + + FlushableOrdersAccountingView { fn set_token_aux_data( &mut self, @@ -191,6 +205,17 @@ pub trait TransactionVerifierStorageMut: &mut self, tx_source: TransactionSource, ) -> Result<(), ::Error>; + + fn set_orders_accounting_undo_data( + &mut self, + tx_source: TransactionSource, + undo: &CachedBlockUndo, + ) -> Result<(), ::Error>; + + fn del_orders_accounting_undo_data( + &mut self, + tx_source: TransactionSource, + ) -> Result<(), ::Error>; } impl TransactionVerifierStorageRef for T @@ -253,4 +278,14 @@ where ) -> Result, ::Error> { self.deref().get_account_nonce_count(account) } + + fn get_orders_accounting_undo( + &self, + tx_source: TransactionSource, + ) -> Result< + Option>, + ::Error, + > { + self.deref().get_orders_accounting_undo(tx_source) + } } diff --git a/chainstate/tx-verifier/src/transaction_verifier/tests/hierarchy_write.rs b/chainstate/tx-verifier/src/transaction_verifier/tests/hierarchy_write.rs index 7a50f83bd2..29fe1558d7 100644 --- a/chainstate/tx-verifier/src/transaction_verifier/tests/hierarchy_write.rs +++ b/chainstate/tx-verifier/src/transaction_verifier/tests/hierarchy_write.rs @@ -26,6 +26,7 @@ use common::chain::{ DelegationId, }; use mockall::predicate::eq; +use orders_accounting::OrdersAccountingDeltaUndoData; use pos_accounting::DeltaMergeUndo; use rstest::rstest; use test_utils::random::Seed; @@ -80,6 +81,10 @@ fn utxo_set_from_chain_hierarchy(#[case] seed: Seed) { .expect_batch_write_tokens_data() .times(1) .return_const(Ok(TokensAccountingDeltaUndoData::new())); + store + .expect_batch_write_orders_data() + .times(1) + .return_const(Ok(OrdersAccountingDeltaUndoData::new())); store .expect_batch_write_delta() .times(1) @@ -159,6 +164,10 @@ fn tokens_set_hierarchy(#[case] seed: Seed) { .expect_batch_write_tokens_data() .times(1) .return_const(Ok(TokensAccountingDeltaUndoData::new())); + store + .expect_batch_write_orders_data() + .times(1) + .return_const(Ok(OrdersAccountingDeltaUndoData::new())); store .expect_batch_write_delta() .times(1) @@ -261,6 +270,10 @@ fn utxo_del_from_chain_hierarchy(#[case] seed: Seed) { .expect_batch_write_tokens_data() .times(1) .return_const(Ok(TokensAccountingDeltaUndoData::new())); + store + .expect_batch_write_orders_data() + .times(1) + .return_const(Ok(OrdersAccountingDeltaUndoData::new())); store .expect_batch_write_delta() .times(1) @@ -317,6 +330,10 @@ fn tokens_del_hierarchy(#[case] seed: Seed) { .expect_batch_write_tokens_data() .times(1) .return_const(Ok(TokensAccountingDeltaUndoData::new())); + store + .expect_batch_write_orders_data() + .times(1) + .return_const(Ok(OrdersAccountingDeltaUndoData::new())); store .expect_batch_write_delta() .times(1) @@ -381,6 +398,10 @@ fn utxo_conflict_hierarchy(#[case] seed: Seed) { .expect_batch_write_tokens_data() .times(1) .return_const(Ok(TokensAccountingDeltaUndoData::new())); + store + .expect_batch_write_orders_data() + .times(1) + .return_const(Ok(OrdersAccountingDeltaUndoData::new())); store .expect_batch_write_delta() .times(1) @@ -471,6 +492,10 @@ fn block_undo_from_chain_conflict_hierarchy(#[case] seed: Seed) { .expect_batch_write_tokens_data() .times(1) .return_const(Ok(TokensAccountingDeltaUndoData::new())); + store + .expect_batch_write_orders_data() + .times(1) + .return_const(Ok(OrdersAccountingDeltaUndoData::new())); store .expect_batch_write_delta() .times(1) @@ -582,6 +607,10 @@ fn tokens_conflict_hierarchy(#[case] seed: Seed) { .expect_batch_write_tokens_data() .times(1) .return_const(Ok(TokensAccountingDeltaUndoData::new())); + store + .expect_batch_write_orders_data() + .times(1) + .return_const(Ok(OrdersAccountingDeltaUndoData::new())); store .expect_batch_write_delta() .times(1) @@ -659,6 +688,10 @@ fn pos_accounting_stake_pool_set_hierarchy(#[case] seed: Seed) { .expect_batch_write_tokens_data() .times(1) .return_const(Ok(TokensAccountingDeltaUndoData::new())); + store + .expect_batch_write_orders_data() + .times(1) + .return_const(Ok(OrdersAccountingDeltaUndoData::new())); store .expect_batch_write_delta() .times(1) @@ -730,6 +763,10 @@ fn pos_accounting_stake_pool_undo_set_hierarchy(#[case] seed: Seed) { .expect_batch_write_tokens_data() .times(1) .return_const(Ok(TokensAccountingDeltaUndoData::new())); + store + .expect_batch_write_orders_data() + .times(1) + .return_const(Ok(OrdersAccountingDeltaUndoData::new())); store .expect_batch_write_delta() .times(1) @@ -837,6 +874,10 @@ fn pos_accounting_stake_pool_and_delegation_undo_set_hierarchy(#[case] seed: See .expect_batch_write_tokens_data() .times(1) .return_const(Ok(TokensAccountingDeltaUndoData::new())); + store + .expect_batch_write_orders_data() + .times(1) + .return_const(Ok(OrdersAccountingDeltaUndoData::new())); store .expect_batch_write_delta() .times(1) @@ -951,6 +992,10 @@ fn pos_accounting_stake_pool_undo_del_hierarchy(#[case] seed: Seed) { .expect_batch_write_tokens_data() .times(1) .return_const(Ok(TokensAccountingDeltaUndoData::new())); + store + .expect_batch_write_orders_data() + .times(1) + .return_const(Ok(OrdersAccountingDeltaUndoData::new())); store .expect_batch_write_delta() .times(1) @@ -1027,6 +1072,10 @@ fn nonce_set_hierarchy(#[case] seed: Seed) { .expect_batch_write_tokens_data() .times(1) .return_const(Ok(TokensAccountingDeltaUndoData::new())); + store + .expect_batch_write_orders_data() + .times(1) + .return_const(Ok(OrdersAccountingDeltaUndoData::new())); store .expect_batch_write_delta() .times(1) @@ -1083,6 +1132,10 @@ fn nonce_del_hierarchy(#[case] seed: Seed) { .expect_batch_write_tokens_data() .times(1) .return_const(Ok(TokensAccountingDeltaUndoData::new())); + store + .expect_batch_write_orders_data() + .times(1) + .return_const(Ok(OrdersAccountingDeltaUndoData::new())); store .expect_batch_write_delta() .times(1) @@ -1164,6 +1217,10 @@ fn tokens_v1_set_hierarchy(#[case] seed: Seed) { .expect_batch_write_tokens_data() .times(1) .return_const(Ok(TokensAccountingDeltaUndoData::new())); + store + .expect_batch_write_orders_data() + .times(1) + .return_const(Ok(OrdersAccountingDeltaUndoData::new())); store .expect_batch_write_delta() .times(1) @@ -1280,6 +1337,10 @@ fn tokens_v1_set_issue_and_lock_undo_hierarchy(#[case] seed: Seed) { .expect_batch_write_tokens_data() .times(1) .return_const(Ok(TokensAccountingDeltaUndoData::new())); + store + .expect_batch_write_orders_data() + .times(1) + .return_const(Ok(OrdersAccountingDeltaUndoData::new())); store .expect_batch_write_delta() .times(1) @@ -1385,6 +1446,10 @@ fn tokens_v1_del_undo_hierarchy(#[case] seed: Seed) { .expect_batch_write_tokens_data() .times(1) .return_const(Ok(TokensAccountingDeltaUndoData::new())); + store + .expect_batch_write_orders_data() + .times(1) + .return_const(Ok(OrdersAccountingDeltaUndoData::new())); store .expect_batch_write_delta() .times(1) @@ -1465,6 +1530,10 @@ fn utxo_set_from_chain_hierarchy_with_derived(#[case] seed: Seed) { .expect_batch_write_tokens_data() .times(1) .return_const(Ok(TokensAccountingDeltaUndoData::new())); + store + .expect_batch_write_orders_data() + .times(1) + .return_const(Ok(OrdersAccountingDeltaUndoData::new())); store .expect_batch_write_delta() .times(1) @@ -1536,6 +1605,10 @@ fn utxo_del_from_chain_hierarchy_with_derived(#[case] seed: Seed) { .expect_batch_write_tokens_data() .times(1) .return_const(Ok(TokensAccountingDeltaUndoData::new())); + store + .expect_batch_write_orders_data() + .times(1) + .return_const(Ok(OrdersAccountingDeltaUndoData::new())); store .expect_batch_write_delta() .times(1) diff --git a/chainstate/tx-verifier/src/transaction_verifier/tests/mock.rs b/chainstate/tx-verifier/src/transaction_verifier/tests/mock.rs index 6640bb5829..12ef4ad600 100644 --- a/chainstate/tx-verifier/src/transaction_verifier/tests/mock.rs +++ b/chainstate/tx-verifier/src/transaction_verifier/tests/mock.rs @@ -29,10 +29,15 @@ use chainstate_types::{storage_result, GenBlockIndex}; use common::{ chain::{ tokens::{TokenAuxiliaryData, TokenId}, - AccountNonce, AccountType, DelegationId, GenBlock, PoolId, Transaction, UtxoOutPoint, + AccountNonce, AccountType, DelegationId, GenBlock, OrderData, OrderId, PoolId, Transaction, + UtxoOutPoint, }, primitives::{Amount, Id}, }; +use orders_accounting::{ + FlushableOrdersAccountingView, OrdersAccountingDeltaData, OrdersAccountingDeltaUndoData, + OrdersAccountingStorageRead, OrdersAccountingUndo, +}; use pos_accounting::{ DelegationData, DeltaMergeUndo, FlushablePoSAccountingView, PoSAccountingDeltaData, PoSAccountingUndo, PoSAccountingView, PoolData, @@ -80,6 +85,11 @@ mockall::mock! { &self, account: AccountType, ) -> Result, TransactionVerifierStorageError>; + + fn get_orders_accounting_undo( + &self, + tx_source: TransactionSource, + ) -> Result>, TransactionVerifierStorageError>; } impl TransactionVerifierStorageMut for Store { @@ -145,6 +155,17 @@ mockall::mock! { &mut self, tx_source: TransactionSource, ) -> Result<(), TransactionVerifierStorageError>; + + fn set_orders_accounting_undo_data( + &mut self, + tx_source: TransactionSource, + undo: &CachedBlockUndo, + ) -> Result<(), TransactionVerifierStorageError>; + + fn del_orders_accounting_undo_data( + &mut self, + tx_source: TransactionSource, + ) -> Result<(), TransactionVerifierStorageError>; } impl UtxosStorageRead for Store { @@ -196,4 +217,19 @@ mockall::mock! { type Error = tokens_accounting::Error; fn batch_write_tokens_data(&mut self, delta: TokensAccountingDeltaData) -> Result; } + + impl OrdersAccountingStorageRead for Store { + type Error = orders_accounting::Error; + fn get_order_data(&self, id: &OrderId) -> Result, orders_accounting::Error>; + fn get_ask_balance(&self, id: &OrderId) -> Result, orders_accounting::Error>; + fn get_give_balance(&self, id: &OrderId) -> Result, orders_accounting::Error>; + } + + impl FlushableOrdersAccountingView for Store { + type Error = orders_accounting::Error; + fn batch_write_orders_data( + &mut self, + data: OrdersAccountingDeltaData, + ) -> Result; + } } diff --git a/chainstate/tx-verifier/src/transaction_verifier/token_issuance_cache.rs b/chainstate/tx-verifier/src/transaction_verifier/token_issuance_cache.rs index 53fd6a1eb6..cda00723db 100644 --- a/chainstate/tx-verifier/src/transaction_verifier/token_issuance_cache.rs +++ b/chainstate/tx-verifier/src/transaction_verifier/token_issuance_cache.rs @@ -240,7 +240,8 @@ fn has_tokens_issuance_to_cache(outputs: &[TxOutput]) -> Option { | TxOutput::DelegateStaking(_, _) | TxOutput::IssueFungibleToken(_) | TxOutput::DataDeposit(_) - | TxOutput::Htlc(_, _) => None, + | TxOutput::Htlc(_, _) + | TxOutput::AnyoneCanTake(_) => None, TxOutput::IssueNft(id, _, _) => Some(*id), }) } diff --git a/chainstate/tx-verifier/src/transaction_verifier/utxos_undo_cache/tests.rs b/chainstate/tx-verifier/src/transaction_verifier/utxos_undo_cache/tests.rs index f0ff6e9dfb..6473fefa66 100644 --- a/chainstate/tx-verifier/src/transaction_verifier/utxos_undo_cache/tests.rs +++ b/chainstate/tx-verifier/src/transaction_verifier/utxos_undo_cache/tests.rs @@ -28,6 +28,7 @@ use common::{ chain::{config::Builder as ConfigBuilder, Block}, primitives::H256, }; +use orders_accounting::OrdersAccountingDeltaUndoData; use pos_accounting::DeltaMergeUndo; use test_utils::random::Seed; use tokens_accounting::TokensAccountingDeltaUndoData; @@ -103,6 +104,10 @@ fn connect_txs_in_hierarchy_default(#[case] seed: Seed) { .expect_batch_write_tokens_data() .times(1) .return_const(Ok(TokensAccountingDeltaUndoData::new())); + store + .expect_batch_write_orders_data() + .times(1) + .return_const(Ok(OrdersAccountingDeltaUndoData::new())); store .expect_batch_write_delta() .times(1) @@ -215,6 +220,10 @@ fn connect_txs_in_hierarchy_disposable(#[case] seed: Seed) { .expect_batch_write_tokens_data() .times(1) .return_const(Ok(TokensAccountingDeltaUndoData::new())); + store + .expect_batch_write_orders_data() + .times(1) + .return_const(Ok(OrdersAccountingDeltaUndoData::new())); store .expect_batch_write_delta() .times(1) @@ -299,6 +308,10 @@ fn connect_txs_in_hierarchy_twice(#[case] seed: Seed) { .expect_batch_write_tokens_data() .times(1) .return_const(Ok(TokensAccountingDeltaUndoData::new())); + store + .expect_batch_write_orders_data() + .times(1) + .return_const(Ok(OrdersAccountingDeltaUndoData::new())); store .expect_batch_write_delta() .times(1) @@ -381,6 +394,10 @@ fn connect_reward_in_hierarchy_twice(#[case] seed: Seed) { .expect_batch_write_tokens_data() .times(1) .return_const(Ok(TokensAccountingDeltaUndoData::new())); + store + .expect_batch_write_orders_data() + .times(1) + .return_const(Ok(OrdersAccountingDeltaUndoData::new())); store .expect_batch_write_delta() .times(1) @@ -506,6 +523,10 @@ fn disconnect_txs_in_hierarchy_default(#[case] seed: Seed) { .expect_batch_write_tokens_data() .times(1) .return_const(Ok(TokensAccountingDeltaUndoData::new())); + store + .expect_batch_write_orders_data() + .times(1) + .return_const(Ok(OrdersAccountingDeltaUndoData::new())); store .expect_batch_write_delta() .times(1) @@ -664,6 +685,10 @@ fn disconnect_txs_in_hierarchy_disposable(#[case] seed: Seed) { .expect_batch_write_tokens_data() .times(1) .return_const(Ok(TokensAccountingDeltaUndoData::new())); + store + .expect_batch_write_orders_data() + .times(1) + .return_const(Ok(OrdersAccountingDeltaUndoData::new())); store .expect_batch_write_delta() .times(1) diff --git a/common/src/chain/config/builder.rs b/common/src/chain/config/builder.rs index 02b13c6d59..a8ff8014c4 100644 --- a/common/src/chain/config/builder.rs +++ b/common/src/chain/config/builder.rs @@ -29,8 +29,8 @@ use crate::{ pos_initial_difficulty, pow::PoWChainConfigBuilder, ChainstateUpgrade, CoinUnit, ConsensusUpgrade, Destination, GenBlock, Genesis, - HtlcActivated, NetUpgrades, PoSChainConfig, PoSConsensusVersion, PoWChainConfig, - RewardDistributionVersion, TokenIssuanceVersion, TokensFeeVersion, + HtlcActivated, NetUpgrades, OrdersActivated, PoSChainConfig, PoSConsensusVersion, + PoWChainConfig, RewardDistributionVersion, TokenIssuanceVersion, TokensFeeVersion, }, primitives::{ id::WithId, per_thousand::PerThousand, semver::SemVer, Amount, BlockCount, BlockDistance, @@ -51,8 +51,8 @@ const TESTNET_TOKEN_FORK_HEIGHT: BlockHeight = BlockHeight::new(78440); // and change various tokens fees const TESTNET_STAKER_REWARD_AND_TOKENS_FEE_FORK_HEIGHT: BlockHeight = BlockHeight::new(138244); // The fork, at which txs with htlc and orders outputs become valid -const TESTNET_HTLC_AND_ORDERS_FORK_HEIGHT: BlockHeight = BlockHeight::new(99999999); -const MAINNET_HTLC_AND_ORDERS_FORK_HEIGHT: BlockHeight = BlockHeight::new(99999999); +const TESTNET_HTLC_AND_ORDERS_FORK_HEIGHT: BlockHeight = BlockHeight::new(99_999_999); +const MAINNET_HTLC_AND_ORDERS_FORK_HEIGHT: BlockHeight = BlockHeight::new(99_999_999); impl ChainType { fn default_genesis_init(&self) -> GenesisBlockInit { @@ -167,6 +167,7 @@ impl ChainType { RewardDistributionVersion::V1, TokensFeeVersion::V1, HtlcActivated::No, + OrdersActivated::No, ), ), ( @@ -176,6 +177,7 @@ impl ChainType { RewardDistributionVersion::V1, TokensFeeVersion::V1, HtlcActivated::Yes, + OrdersActivated::Yes, ), ), ]; @@ -189,6 +191,7 @@ impl ChainType { RewardDistributionVersion::V1, TokensFeeVersion::V1, HtlcActivated::Yes, + OrdersActivated::Yes, ), )]; NetUpgrades::initialize(upgrades).expect("net upgrades") @@ -202,6 +205,7 @@ impl ChainType { RewardDistributionVersion::V0, TokensFeeVersion::V0, HtlcActivated::No, + OrdersActivated::No, ), ), ( @@ -211,6 +215,7 @@ impl ChainType { RewardDistributionVersion::V0, TokensFeeVersion::V0, HtlcActivated::No, + OrdersActivated::No, ), ), ( @@ -220,6 +225,7 @@ impl ChainType { RewardDistributionVersion::V1, TokensFeeVersion::V1, HtlcActivated::No, + OrdersActivated::No, ), ), ( @@ -229,6 +235,7 @@ impl ChainType { RewardDistributionVersion::V1, TokensFeeVersion::V1, HtlcActivated::Yes, + OrdersActivated::Yes, ), ), ]; diff --git a/common/src/chain/config/mod.rs b/common/src/chain/config/mod.rs index e6bf7ff89e..0b948da7ef 100644 --- a/common/src/chain/config/mod.rs +++ b/common/src/chain/config/mod.rs @@ -51,8 +51,10 @@ use self::checkpoints::Checkpoints; use self::emission_schedule::DEFAULT_INITIAL_MINT; use super::output_value::OutputValue; use super::{stakelock::StakePoolData, RequiredConsensus}; -use super::{ChainstateUpgrade, ConsensusUpgrade, HtlcActivated}; -use super::{RewardDistributionVersion, TokenIssuanceVersion, TokensFeeVersion}; +use super::{ + ChainstateUpgrade, ConsensusUpgrade, HtlcActivated, OrdersActivated, RewardDistributionVersion, + TokenIssuanceVersion, TokensFeeVersion, +}; const DEFAULT_MAX_FUTURE_BLOCK_TIME_OFFSET: Duration = Duration::from_secs(120); const DEFAULT_TARGET_BLOCK_SPACING: Duration = Duration::from_secs(120); @@ -329,6 +331,16 @@ impl ChainConfig { } } + #[must_use] + pub fn order_id_address_prefix(&self) -> &'static str { + match self.chain_type { + ChainType::Mainnet => "mordr", + ChainType::Testnet => "tordr", + ChainType::Regtest => "rordr", + ChainType::Signet => "sordr", + } + } + #[must_use] pub fn vrf_public_key_address_prefix(&self) -> &'static str { match self.chain_type { @@ -865,6 +877,7 @@ pub fn create_unit_test_config_builder() -> Builder { RewardDistributionVersion::V1, TokensFeeVersion::V1, HtlcActivated::Yes, + OrdersActivated::Yes, ), )]) .expect("cannot fail"), diff --git a/common/src/chain/mod.rs b/common/src/chain/mod.rs index 15e4c6a82d..2e4a712917 100644 --- a/common/src/chain/mod.rs +++ b/common/src/chain/mod.rs @@ -22,6 +22,7 @@ pub mod tokens; pub mod transaction; mod coin_unit; +mod order; mod pos; mod pow; mod upgrades; @@ -34,6 +35,7 @@ pub use coin_unit::CoinUnit; pub use config::ChainConfig; pub use gen_block::{GenBlock, GenBlockId}; pub use genesis::Genesis; +pub use order::{make_order_id, OrderData, OrderId}; pub use pos::{ config::PoSChainConfig, config_builder::PoSChainConfigBuilder, get_initial_randomness, pos_initial_difficulty, DelegationId, PoSConsensusVersion, PoolId, diff --git a/common/src/chain/order.rs b/common/src/chain/order.rs new file mode 100644 index 0000000000..2fd22c536c --- /dev/null +++ b/common/src/chain/order.rs @@ -0,0 +1,114 @@ +// Copyright (c) 2024 RBB S.r.l +// opensource@mintlayer.org +// SPDX-License-Identifier: MIT +// Licensed under the MIT License; +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://github.com/mintlayer/mintlayer-core/blob/master/LICENSE +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::{ + address::{hexified::HexifiedAddress, traits::Addressable, AddressError}, + chain::ChainConfig, + primitives::{id::hash_encoded, Id, H256}, +}; +use randomness::{CryptoRng, Rng}; +use serialization::{Decode, DecodeAll, Encode}; +use typename::TypeName; + +use super::{output_value::OutputValue, Destination, UtxoOutPoint}; + +#[derive(Eq, PartialEq, TypeName)] +pub enum Order {} +pub type OrderId = Id; + +impl OrderId { + pub fn random_using(rng: &mut R) -> Self { + Self::new(H256::random_using(rng)) + } + + pub const fn zero() -> Self { + Self::new(H256::zero()) + } +} + +impl Addressable for OrderId { + type Error = AddressError; + + fn address_prefix(&self, chain_config: &ChainConfig) -> &str { + chain_config.order_id_address_prefix() + } + + fn encode_to_bytes_for_address(&self) -> Vec { + self.encode() + } + + fn decode_from_bytes_from_address>(address_bytes: T) -> Result + where + Self: Sized, + { + Self::decode_all(&mut address_bytes.as_ref()) + .map_err(|e| AddressError::DecodingError(e.to_string())) + } + + fn json_wrapper_prefix() -> &'static str { + "HexifiedOrderId" + } +} + +impl serde::Serialize for OrderId { + fn serialize(&self, serializer: S) -> Result { + HexifiedAddress::serde_serialize(self, serializer) + } +} + +impl<'de> serde::Deserialize<'de> for OrderId { + fn deserialize>(deserializer: D) -> Result { + HexifiedAddress::::serde_deserialize(deserializer) + } +} + +pub fn make_order_id(input0_outpoint: &UtxoOutPoint) -> OrderId { + OrderId::new(hash_encoded(input0_outpoint)) +} + +/// Order data provides unified data structure to represent an order. +/// There are no buy or sell types of orders per se but rather exchanges. +/// The fields represent currencies and amounts to be exchanged and the trading pair can be deducted from it. +#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode, serde::Serialize, serde::Deserialize)] +pub struct OrderData { + /// The key that can authorize conclusion of an order + conclude_key: Destination, + /// `Ask` and `give` fields represent amounts of currencies + /// that an order maker wants to exchange, e.g. 5 coins for 10 tokens + ask: OutputValue, + give: OutputValue, +} + +impl OrderData { + pub fn new(conclude_key: Destination, ask: OutputValue, give: OutputValue) -> Self { + Self { + conclude_key, + ask, + give, + } + } + + pub fn conclude_key(&self) -> &Destination { + &self.conclude_key + } + + pub fn ask(&self) -> &OutputValue { + &self.ask + } + + pub fn give(&self) -> &OutputValue { + &self.give + } +} diff --git a/common/src/chain/tokens/issuance.rs b/common/src/chain/tokens/issuance.rs index 05c4b42e44..73c1699a70 100644 --- a/common/src/chain/tokens/issuance.rs +++ b/common/src/chain/tokens/issuance.rs @@ -39,7 +39,7 @@ pub enum TokenTotalSupply { Unlimited, // limited only by the Amount data type } -// Indicates whether a token an be frozen +/// Indicates whether a token can be frozen #[derive( Debug, Copy, @@ -69,7 +69,7 @@ impl IsTokenFreezable { } } -// Indicates whether a token an be unfrozen after being frozen +/// Indicates whether a token can be unfrozen after being frozen #[derive( Debug, Copy, @@ -99,9 +99,9 @@ impl IsTokenUnfreezable { } } -// Indicates whether a token is frozen at the moment or not. If it is then no operations wish this token can be performed. -// Meaning transfers, burns, minting, unminting, supply locks etc. Frozen token can only be unfrozen -// is such an option was provided while freezing. +/// Indicates whether a token is frozen at the moment or not. If it is then no operations with this token can be performed. +/// Meaning transfers, burns, minting, unminting, supply locks etc. Frozen token can only be unfrozen +/// if such an option was provided while freezing. #[derive( Debug, Copy, diff --git a/common/src/chain/tokens/mod.rs b/common/src/chain/tokens/mod.rs index b8b64177be..6ed2c549e9 100644 --- a/common/src/chain/tokens/mod.rs +++ b/common/src/chain/tokens/mod.rs @@ -94,7 +94,18 @@ pub struct TokenIssuanceV0 { pub metadata_uri: Vec, } -#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode, serde::Serialize, serde::Deserialize)] +#[derive( + Debug, + Clone, + PartialEq, + Eq, + PartialOrd, + Ord, + Encode, + Decode, + serde::Serialize, + serde::Deserialize, +)] pub enum TokenData { /// TokenTransfer data to another user. If it is a token, then the token data must also be transferred to the recipient. #[codec(index = 1)] diff --git a/common/src/chain/tokens/nft.rs b/common/src/chain/tokens/nft.rs index 687a8e67d1..5c96c6444c 100644 --- a/common/src/chain/tokens/nft.rs +++ b/common/src/chain/tokens/nft.rs @@ -22,7 +22,18 @@ pub enum NftIssuance { V0(NftIssuanceV0), } -#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode, serde::Serialize, serde::Deserialize)] +#[derive( + Debug, + Clone, + PartialEq, + Eq, + PartialOrd, + Ord, + Encode, + Decode, + serde::Serialize, + serde::Deserialize, +)] pub struct NftIssuanceV0 { pub metadata: Metadata, // TODO: Implement after additional research payout, royalty and refund. @@ -57,7 +68,18 @@ impl From for TokenCreator { } } -#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode, serde::Serialize, serde::Deserialize)] +#[derive( + Debug, + Clone, + PartialEq, + Eq, + Encode, + PartialOrd, + Ord, + Decode, + serde::Serialize, + serde::Deserialize, +)] pub struct Metadata { pub creator: Option, pub name: Vec, diff --git a/common/src/chain/tokens/token_id.rs b/common/src/chain/tokens/token_id.rs index c070625a3b..939a079927 100644 --- a/common/src/chain/tokens/token_id.rs +++ b/common/src/chain/tokens/token_id.rs @@ -18,7 +18,7 @@ use crate::{ chain::ChainConfig, primitives::{Id, H256}, }; -use randomness::{CryptoRng, Rng}; +use randomness::Rng; use serialization::{DecodeAll, Encode}; use typename::TypeName; @@ -27,7 +27,7 @@ pub enum Token {} pub type TokenId = Id; impl TokenId { - pub fn random_using(rng: &mut R) -> Self { + pub fn random_using(rng: &mut R) -> Self { Self::new(H256::random_using(rng)) } diff --git a/common/src/chain/tokens/tokens_utils.rs b/common/src/chain/tokens/tokens_utils.rs index 4b266714fb..47ac24a832 100644 --- a/common/src/chain/tokens/tokens_utils.rs +++ b/common/src/chain/tokens/tokens_utils.rs @@ -40,7 +40,8 @@ pub fn get_issuance_count_via_tokens_op(outputs: &[TxOutput]) -> usize { | TxOutput::CreateDelegationId(_, _) | TxOutput::DelegateStaking(_, _) | TxOutput::DataDeposit(_) - | TxOutput::Htlc(_, _) => false, + | TxOutput::Htlc(_, _) + | TxOutput::AnyoneCanTake(_) => false, TxOutput::IssueFungibleToken(_) | TxOutput::IssueNft(_, _, _) => true, }) .count() @@ -54,7 +55,9 @@ pub fn get_token_supply_change_count(inputs: &[TxInput]) -> usize { TxInput::AccountCommand(_, op) => match op { AccountCommand::FreezeToken(_, _) | AccountCommand::UnfreezeToken(_) - | AccountCommand::ChangeTokenAuthority(_, _) => false, + | AccountCommand::ChangeTokenAuthority(_, _) + | AccountCommand::ConcludeOrder(_) + | AccountCommand::FillOrder(_, _, _) => false, AccountCommand::MintTokens(_, _) | AccountCommand::UnmintTokens(_) | AccountCommand::LockTokenSupply(_) => true, @@ -79,7 +82,8 @@ pub fn is_token_or_nft_issuance(output: &TxOutput) -> bool { | TxOutput::CreateDelegationId(_, _) | TxOutput::DelegateStaking(_, _) | TxOutput::DataDeposit(_) - | TxOutput::Htlc(_, _) => false, + | TxOutput::Htlc(_, _) + | TxOutput::AnyoneCanTake(_) => false, TxOutput::IssueFungibleToken(_) | TxOutput::IssueNft(_, _, _) => true, } } diff --git a/common/src/chain/transaction/account_outpoint.rs b/common/src/chain/transaction/account_outpoint.rs index 79a3b6751c..9ad087363a 100644 --- a/common/src/chain/transaction/account_outpoint.rs +++ b/common/src/chain/transaction/account_outpoint.rs @@ -16,13 +16,13 @@ use crate::{ chain::{ tokens::{IsTokenUnfreezable, TokenId}, - AccountNonce, DelegationId, + AccountNonce, DelegationId, OrderId, }, primitives::Amount, }; use serialization::{Decode, Encode}; -use super::Destination; +use super::{output_value::OutputValue, Destination}; /// Type of an account that can be used to identify series of spending from an account #[derive(Debug, Clone, Copy, PartialEq, Eq, Ord, PartialOrd, Encode, Decode)] @@ -32,6 +32,8 @@ pub enum AccountType { /// Token account type is used to authorize changes in token data. #[codec(index = 1)] Token(TokenId), + #[codec(index = 2)] + Order(OrderId), } impl From for AccountType { @@ -51,6 +53,9 @@ impl From for AccountType { | AccountCommand::FreezeToken(id, _) | AccountCommand::UnfreezeToken(id) | AccountCommand::ChangeTokenAuthority(id, _) => AccountType::Token(id), + AccountCommand::ConcludeOrder(id) | AccountCommand::FillOrder(id, _, _) => { + AccountType::Order(id) + } } } } @@ -110,6 +115,10 @@ pub enum AccountCommand { // Change the authority who can authorize operations for a token #[codec(index = 5)] ChangeTokenAuthority(TokenId, Destination), + #[codec(index = 6)] + ConcludeOrder(OrderId), + #[codec(index = 7)] + FillOrder(OrderId, OutputValue, Destination), } /// Type of OutPoint that represents spending from an account diff --git a/common/src/chain/transaction/output/mod.rs b/common/src/chain/transaction/output/mod.rs index c075e2c6a6..9ae34c9ecf 100644 --- a/common/src/chain/transaction/output/mod.rs +++ b/common/src/chain/transaction/output/mod.rs @@ -22,6 +22,7 @@ use crate::{ AddressError, }, chain::{ + order::OrderData, output_value::OutputValue, tokens::{IsTokenFreezable, NftIssuance, TokenId, TokenIssuance, TokenTotalSupply}, ChainConfig, DelegationId, PoolId, @@ -140,6 +141,8 @@ pub enum TxOutput { /// Transfer an output under Hashed TimeLock Contract. #[codec(index = 10)] Htlc(OutputValue, Box), + #[codec(index = 11)] + AnyoneCanTake(Box), } impl TxOutput { @@ -154,7 +157,8 @@ impl TxOutput { | TxOutput::IssueFungibleToken(_) | TxOutput::IssueNft(_, _, _) | TxOutput::DataDeposit(_) - | TxOutput::Htlc(_, _) => None, + | TxOutput::Htlc(_, _) + | TxOutput::AnyoneCanTake(_) => None, TxOutput::LockThenTransfer(_, _, tl) => Some(tl), } } @@ -326,6 +330,12 @@ impl TextSummary for TxOutput { fmt_dest(&htlc.refund_key) ) } + TxOutput::AnyoneCanTake(order) => format!( + "AnyoneCanTake(ConcludeKey({}), AskValue({}), GiveValue({}))", + fmt_dest(order.conclude_key()), + fmt_val(order.ask()), + fmt_val(order.give()), + ), } } } diff --git a/common/src/chain/transaction/output/output_value.rs b/common/src/chain/transaction/output/output_value.rs index 2dd5a5e277..44e739978a 100644 --- a/common/src/chain/transaction/output/output_value.rs +++ b/common/src/chain/transaction/output/output_value.rs @@ -20,7 +20,18 @@ use crate::{ primitives::Amount, }; -#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode, serde::Serialize, serde::Deserialize)] +#[derive( + Debug, + Clone, + PartialEq, + Eq, + Ord, + PartialOrd, + Encode, + Decode, + serde::Serialize, + serde::Deserialize, +)] pub enum OutputValue { #[codec(index = 0)] Coin(Amount), diff --git a/common/src/chain/transaction/signature/tests/mod.rs b/common/src/chain/transaction/signature/tests/mod.rs index efdc1908eb..9d4cd4c555 100644 --- a/common/src/chain/transaction/signature/tests/mod.rs +++ b/common/src/chain/transaction/signature/tests/mod.rs @@ -768,6 +768,7 @@ fn check_mutate_output( TxOutput::IssueNft(_, _, _) => unreachable!(), TxOutput::DataDeposit(_) => unreachable!(), TxOutput::Htlc(_, _) => unreachable!(), + TxOutput::AnyoneCanTake(_) => unreachable!(), }; let tx = tx_updater.generate_tx().unwrap(); diff --git a/common/src/chain/transaction/signature/tests/sign_and_mutate.rs b/common/src/chain/transaction/signature/tests/sign_and_mutate.rs index 86c26282f5..7e9b354344 100644 --- a/common/src/chain/transaction/signature/tests/sign_and_mutate.rs +++ b/common/src/chain/transaction/signature/tests/sign_and_mutate.rs @@ -1138,6 +1138,7 @@ fn mutate_output(_rng: &mut impl Rng, tx: &SignedTransactionWithUtxo) -> SignedT TxOutput::IssueNft(_, _, _) => unreachable!(), // TODO: come back to this later TxOutput::DataDeposit(_) => unreachable!(), TxOutput::Htlc(_, _) => unreachable!(), + TxOutput::AnyoneCanTake(_) => unreachable!(), }; SignedTransactionWithUtxo { tx: updater.generate_tx().unwrap(), diff --git a/common/src/chain/upgrades/chainstate_upgrade.rs b/common/src/chain/upgrades/chainstate_upgrade.rs index fa44312988..479e813948 100644 --- a/common/src/chain/upgrades/chainstate_upgrade.rs +++ b/common/src/chain/upgrades/chainstate_upgrade.rs @@ -45,12 +45,19 @@ pub enum HtlcActivated { No, } +#[derive(Debug, Clone, Copy, PartialEq, Eq, Ord, PartialOrd)] +pub enum OrdersActivated { + Yes, + No, +} + #[derive(Debug, Clone, PartialEq, Eq, Ord, PartialOrd)] pub struct ChainstateUpgrade { token_issuance_version: TokenIssuanceVersion, reward_distribution_version: RewardDistributionVersion, tokens_fee_version: TokensFeeVersion, htlc_activated: HtlcActivated, + orders_activated: OrdersActivated, } impl ChainstateUpgrade { @@ -59,12 +66,14 @@ impl ChainstateUpgrade { reward_distribution_version: RewardDistributionVersion, tokens_fee_version: TokensFeeVersion, htlc_activated: HtlcActivated, + orders_activated: OrdersActivated, ) -> Self { Self { token_issuance_version, reward_distribution_version, tokens_fee_version, htlc_activated, + orders_activated, } } @@ -83,6 +92,10 @@ impl ChainstateUpgrade { pub fn htlc_activated(&self) -> HtlcActivated { self.htlc_activated } + + pub fn orders_activated(&self) -> OrdersActivated { + self.orders_activated + } } impl Activate for ChainstateUpgrade {} diff --git a/common/src/chain/upgrades/mod.rs b/common/src/chain/upgrades/mod.rs index f1feae3efb..229918d79e 100644 --- a/common/src/chain/upgrades/mod.rs +++ b/common/src/chain/upgrades/mod.rs @@ -18,8 +18,8 @@ mod consensus_upgrade; mod netupgrade; pub use chainstate_upgrade::{ - ChainstateUpgrade, HtlcActivated, RewardDistributionVersion, TokenIssuanceVersion, - TokensFeeVersion, + ChainstateUpgrade, HtlcActivated, OrdersActivated, RewardDistributionVersion, + TokenIssuanceVersion, TokensFeeVersion, }; pub use consensus_upgrade::{ConsensusUpgrade, PoSStatus, PoWStatus, RequiredConsensus}; pub use netupgrade::{Activate, NetUpgrades}; diff --git a/common/src/primitives/mod.rs b/common/src/primitives/mod.rs index e3756a8fb3..9226778544 100644 --- a/common/src/primitives/mod.rs +++ b/common/src/primitives/mod.rs @@ -35,7 +35,7 @@ pub use height::{BlockCount, BlockDistance, BlockHeight}; pub use id::{Id, Idable, H256}; pub use version_tag::VersionTag; -use crate::chain::tokens::TokenId; +use crate::chain::{output_value::OutputValue, tokens::TokenId}; use serialization::{Decode, Encode}; #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)] @@ -49,3 +49,13 @@ pub enum CoinOrTokenId { Coin, TokenId(TokenId), } + +impl CoinOrTokenId { + pub fn from_output_value(value: &OutputValue) -> Option { + match value { + OutputValue::Coin(_) => Some(Self::Coin), + OutputValue::TokenV0(_) => None, + OutputValue::TokenV1(id, _) => Some(Self::TokenId(*id)), + } + } +} diff --git a/common/src/size_estimation/mod.rs b/common/src/size_estimation/mod.rs index 189d877161..8645649619 100644 --- a/common/src/size_estimation/mod.rs +++ b/common/src/size_estimation/mod.rs @@ -109,6 +109,7 @@ fn get_tx_output_destination(txo: &TxOutput) -> Option<&Destination> { TxOutput::IssueFungibleToken(_) | TxOutput::Burn(_) | TxOutput::DelegateStaking(_, _) - | TxOutput::DataDeposit(_) => None, + | TxOutput::DataDeposit(_) + | TxOutput::AnyoneCanTake(_) => None, } } diff --git a/consensus/src/pos/block_sig.rs b/consensus/src/pos/block_sig.rs index da359baf39..ec24bf3971 100644 --- a/consensus/src/pos/block_sig.rs +++ b/consensus/src/pos/block_sig.rs @@ -51,7 +51,8 @@ fn get_staking_kernel_destination( | TxOutput::IssueFungibleToken(_) | TxOutput::IssueNft(_, _, _) | TxOutput::DataDeposit(_) - | TxOutput::Htlc(_, _) => { + | TxOutput::Htlc(_, _) + | TxOutput::AnyoneCanTake(_) => { return Err(BlockSignatureError::WrongOutputType(header.get_id())) } TxOutput::CreateStakePool(_, stake_pool) => stake_pool.staker(), diff --git a/consensus/src/pos/mod.rs b/consensus/src/pos/mod.rs index 97988adcf3..f14e8a9179 100644 --- a/consensus/src/pos/mod.rs +++ b/consensus/src/pos/mod.rs @@ -165,7 +165,8 @@ where | TxOutput::IssueFungibleToken(_) | TxOutput::IssueNft(_, _, _) | TxOutput::DataDeposit(_) - | TxOutput::Htlc(_, _) => { + | TxOutput::Htlc(_, _) + | TxOutput::AnyoneCanTake(_) => { // only pool outputs can be staked return Err(ConsensusPoSError::RandomnessError( PoSRandomnessError::InvalidOutputTypeInStakeKernel(header.get_id()), diff --git a/mempool/Cargo.toml b/mempool/Cargo.toml index 1fa5b6d975..ba0b5012ba 100644 --- a/mempool/Cargo.toml +++ b/mempool/Cargo.toml @@ -16,6 +16,7 @@ crypto = { path = "../crypto" } logging = { path = "../logging" } mempool-types = { path = "types" } mintscript = { path = "../mintscript" } +orders-accounting = { path = "../orders-accounting" } p2p-types = { path = "../p2p/types" } pos-accounting = { path = "../pos-accounting" } rpc = { path = "../rpc" } diff --git a/mempool/src/error/ban_score.rs b/mempool/src/error/ban_score.rs index e2713e1cba..f973144a54 100644 --- a/mempool/src/error/ban_score.rs +++ b/mempool/src/error/ban_score.rs @@ -130,6 +130,7 @@ impl MempoolBanScore for ConnectTransactionError { ConnectTransactionError::RewardDistributionError(err) => err.mempool_ban_score(), ConnectTransactionError::CheckTransactionError(err) => err.mempool_ban_score(), ConnectTransactionError::InputCheck(err) => err.mempool_ban_score(), + ConnectTransactionError::OrdersAccountingError(err) => err.mempool_ban_score(), // Transaction definitely invalid, ban peer ConnectTransactionError::RewardAdditionError(_) => 100, @@ -140,6 +141,7 @@ impl MempoolBanScore for ConnectTransactionError { ConnectTransactionError::NotEnoughPledgeToCreateStakePool(_, _, _) => 100, ConnectTransactionError::AttemptToCreateStakePoolFromAccounts => 100, ConnectTransactionError::AttemptToCreateDelegationFromAccounts => 100, + ConnectTransactionError::AttemptToCreateOrderFromAccounts => 100, ConnectTransactionError::TotalFeeRequiredOverflow => 100, ConnectTransactionError::InsufficientCoinsFee(_, _) => 100, @@ -194,11 +196,13 @@ impl MempoolBanScore for mintscript::translate::TranslationError { | Self::IllegalOutputSpend | Self::PoolNotFound(_) | Self::DelegationNotFound(_) - | Self::TokenNotFound(_) => 100, + | Self::TokenNotFound(_) + | Self::OrderNotFound(_) => 100, Self::SignatureError(_) => 100, Self::PoSAccounting(e) => e.ban_score(), Self::TokensAccounting(e) => e.ban_score(), + Self::OrdersAccounting(e) => e.ban_score(), } } } @@ -249,6 +253,8 @@ impl MempoolBanScore for SignatureDestinationGetterError { SignatureDestinationGetterError::UtxoViewError(_) => 0, SignatureDestinationGetterError::TokenDataNotFound(_) => 0, SignatureDestinationGetterError::TokensAccountingViewError(_) => 100, + SignatureDestinationGetterError::OrdersAccountingViewError(_) => 100, + SignatureDestinationGetterError::OrderDataNotFound(_) => 0, } } } @@ -261,6 +267,7 @@ impl MempoolBanScore for TransactionVerifierStorageError { TransactionVerifierStorageError::UtxoError(err) => err.mempool_ban_score(), TransactionVerifierStorageError::PoSAccountingError(err) => err.mempool_ban_score(), TransactionVerifierStorageError::TokensAccountingError(err) => err.mempool_ban_score(), + TransactionVerifierStorageError::OrdersAccountingError(err) => err.mempool_ban_score(), // Should not happen in mempool (no undos, no block processing, internal errors) TransactionVerifierStorageError::GetAncestorError(_) => 0, @@ -480,6 +487,41 @@ impl MempoolBanScore for CheckTransactionError { CheckTransactionError::TxSizeTooLarge(_, _, _) => 100, CheckTransactionError::DeprecatedTokenOperationVersion(_, _) => 100, CheckTransactionError::HtlcsAreNotActivated => 100, + CheckTransactionError::OrdersAreNotActivated(_) => 100, + CheckTransactionError::OrdersCurrenciesMustBeDifferent(_) => 100, + } + } +} + +impl MempoolBanScore for orders_accounting::Error { + fn mempool_ban_score(&self) -> u32 { + use orders_accounting::Error; + + match self { + Error::StorageError(_) => 0, + Error::AccountingError(_) => 100, + Error::OrderAlreadyExists(_) => 100, + Error::OrderDataNotFound(_) => 0, + Error::OrderAskBalanceNotFound(_) => 0, + Error::OrderGiveBalanceNotFound(_) => 0, + Error::OrderWithZeroValue(_) => 100, + Error::InvariantOrderDataNotFoundForUndo(_) => 100, + Error::InvariantOrderAskBalanceNotFoundForUndo(_) => 100, + Error::InvariantOrderAskBalanceChangedForUndo(_) => 100, + Error::InvariantOrderGiveBalanceNotFoundForUndo(_) => 100, + Error::InvariantOrderGiveBalanceChangedForUndo(_) => 100, + Error::InvariantOrderDataExistForConcludeUndo(_) => 100, + Error::InvariantOrderAskBalanceExistForConcludeUndo(_) => 100, + Error::InvariantOrderGiveBalanceExistForConcludeUndo(_) => 100, + Error::InvariantNonzeroAskBalanceForMissingOrder(_) => 100, + Error::InvariantNonzeroGiveBalanceForMissingOrder(_) => 100, + Error::CurrencyMismatch => 100, + Error::OrderOverflow(_) => 100, + Error::OrderOverbid(_, _, _) => 100, + Error::AttemptedConcludeNonexistingOrderData(_) => 0, + Error::UnsupportedTokenVersion => 100, + Error::ViewFail => 0, + Error::StorageWrite => 0, } } } diff --git a/mempool/src/pool/entry.rs b/mempool/src/pool/entry.rs index 0c0e0d3d5e..ab69831eeb 100644 --- a/mempool/src/pool/entry.rs +++ b/mempool/src/pool/entry.rs @@ -44,6 +44,7 @@ impl TxAccountDependency { pub enum TxDependency { DelegationAccount(TxAccountDependency), TokenSupplyAccount(TxAccountDependency), + OrderAccount(TxAccountDependency), TxOutput(Id, u32), // TODO: Block reward? } @@ -74,6 +75,9 @@ impl TxDependency { | AccountCommand::ChangeTokenAuthority(_, _) => { Self::TokenSupplyAccount(TxAccountDependency::new(acct.clone().into(), nonce)) } + AccountCommand::ConcludeOrder(_) | AccountCommand::FillOrder(_, _, _) => { + Self::OrderAccount(TxAccountDependency::new(acct.clone().into(), nonce)) + } } } diff --git a/mempool/src/pool/orphans/detect.rs b/mempool/src/pool/orphans/detect.rs index 4ea39725fa..b426ba3e27 100644 --- a/mempool/src/pool/orphans/detect.rs +++ b/mempool/src/pool/orphans/detect.rs @@ -76,6 +76,8 @@ impl OrphanType { | CTE::ConstrainedValueAccumulatorError(_, _) | CTE::RewardDistributionError(_) | CTE::CheckTransactionError(_) + | CTE::OrdersAccountingError(_) + | CTE::AttemptToCreateOrderFromAccounts | CTE::IOPolicyError(_, _) => Err(err), } } diff --git a/mempool/src/pool/orphans/mod.rs b/mempool/src/pool/orphans/mod.rs index 1931b95fb8..b6eebbf3e3 100644 --- a/mempool/src/pool/orphans/mod.rs +++ b/mempool/src/pool/orphans/mod.rs @@ -304,7 +304,9 @@ impl<'p> PoolEntry<'p> { let entry = self.get(); !entry.requires().any(|dep| match dep { // Always consider account deps. TODO: can be optimized in the future - TxDependency::DelegationAccount(_) | TxDependency::TokenSupplyAccount(_) => false, + TxDependency::DelegationAccount(_) + | TxDependency::TokenSupplyAccount(_) + | TxDependency::OrderAccount(_) => false, TxDependency::TxOutput(tx_id, _) => self.pool.maps.by_tx_id.contains_key(&tx_id), }) } diff --git a/mempool/src/pool/tx_pool/store/mem_usage.rs b/mempool/src/pool/tx_pool/store/mem_usage.rs index 0010c702e7..5399b53564 100644 --- a/mempool/src/pool/tx_pool/store/mem_usage.rs +++ b/mempool/src/pool/tx_pool/store/mem_usage.rs @@ -348,6 +348,7 @@ impl MemoryUsage for TxOutput { TxOutput::IssueNft(_, issuance, _) => issuance.indirect_memory_usage(), TxOutput::DataDeposit(v) => v.indirect_memory_usage(), TxOutput::Htlc(_, htlc) => htlc.indirect_memory_usage(), + TxOutput::AnyoneCanTake(_) => 0, } } } diff --git a/mempool/src/pool/tx_pool/tx_verifier/chainstate_handle.rs b/mempool/src/pool/tx_pool/tx_verifier/chainstate_handle.rs index 3813037f5b..76602b4c1c 100644 --- a/mempool/src/pool/tx_pool/tx_verifier/chainstate_handle.rs +++ b/mempool/src/pool/tx_pool/tx_verifier/chainstate_handle.rs @@ -27,10 +27,12 @@ use chainstate_types::storage_result; use common::{ chain::{ tokens::{TokenAuxiliaryData, TokenId}, - AccountNonce, AccountType, DelegationId, GenBlock, PoolId, Transaction, UtxoOutPoint, + AccountNonce, AccountType, DelegationId, GenBlock, OrderData, OrderId, PoolId, Transaction, + UtxoOutPoint, }, primitives::{Amount, Id}, }; +use orders_accounting::{OrdersAccountingStorageRead, OrdersAccountingUndo, OrdersAccountingView}; use pos_accounting::{DelegationData, PoSAccountingUndo, PoSAccountingView, PoolData}; use subsystem::blocking::BlockingHandle; use tokens_accounting::{TokenAccountingUndo, TokensAccountingStorageRead, TokensAccountingView}; @@ -59,6 +61,12 @@ impl From for Error { } } +impl From for Error { + fn from(e: orders_accounting::Error) -> Self { + Error::from(ChainstateError::from(chainstate::BlockError::from(e))) + } +} + impl From for Error { fn from(e: utxo::Error) -> Self { chainstate::tx_verifier::TransactionVerifierStorageError::from(e).into() @@ -236,6 +244,13 @@ impl TransactionVerifierStorageRef for ChainstateHandle { ) -> Result>, Error> { Ok(None) } + + fn get_orders_accounting_undo( + &self, + _source: TransactionSource, + ) -> Result>, Error> { + Ok(None) + } } impl UtxosView for ChainstateHandle { @@ -292,3 +307,41 @@ impl TokensAccountingStorageRead for ChainstateHandle { self.call(move |c| c.get_token_circulating_supply(&id)) } } + +impl OrdersAccountingView for ChainstateHandle { + type Error = Error; + + fn get_order_data(&self, id: &OrderId) -> Result, Self::Error> { + let id = *id; + self.call(move |c| c.get_order_data(&id)) + } + + fn get_ask_balance(&self, id: &OrderId) -> Result, Self::Error> { + let id = *id; + self.call(move |c| c.get_order_ask_balance(&id)) + } + + fn get_give_balance(&self, id: &OrderId) -> Result, Self::Error> { + let id = *id; + self.call(move |c| c.get_order_give_balance(&id)) + } +} + +impl OrdersAccountingStorageRead for ChainstateHandle { + type Error = Error; + + fn get_order_data(&self, id: &OrderId) -> Result, Self::Error> { + let id = *id; + self.call(move |c| c.get_order_data(&id)) + } + + fn get_ask_balance(&self, id: &OrderId) -> Result, Self::Error> { + let id = *id; + self.call(move |c| c.get_order_ask_balance(&id)) + } + + fn get_give_balance(&self, id: &OrderId) -> Result, Self::Error> { + let id = *id; + self.call(move |c| c.get_order_give_balance(&id)) + } +} diff --git a/mempool/src/pool/tx_pool/tx_verifier/mod.rs b/mempool/src/pool/tx_pool/tx_verifier/mod.rs index d606f713d8..23eb0b5688 100644 --- a/mempool/src/pool/tx_pool/tx_verifier/mod.rs +++ b/mempool/src/pool/tx_pool/tx_verifier/mod.rs @@ -36,6 +36,7 @@ pub type TransactionVerifier = chainstate::tx_verifier::TransactionVerifier< ChainstateHandle, ChainstateHandle, ChainstateHandle, + ChainstateHandle, >; /// Make a new transaction verifier @@ -49,6 +50,7 @@ pub fn create( chain_config, chainstate.shallow_clone(), chainstate.shallow_clone(), + chainstate.shallow_clone(), chainstate, ) } diff --git a/mintscript/Cargo.toml b/mintscript/Cargo.toml index 2d3b1c0abe..2b3f315905 100644 --- a/mintscript/Cargo.toml +++ b/mintscript/Cargo.toml @@ -10,6 +10,7 @@ license.workspace = true [dependencies] common = { path = "../common" } crypto = { path = "../crypto" } +orders-accounting = { path = "../orders-accounting" } pos-accounting = { path = "../pos-accounting" } serialization = { path = "../serialization" } tokens-accounting = { path = "../tokens-accounting" } diff --git a/mintscript/src/tests/translate/mocks.rs b/mintscript/src/tests/translate/mocks.rs index 2ee11b4098..80be2f48b0 100644 --- a/mintscript/src/tests/translate/mocks.rs +++ b/mintscript/src/tests/translate/mocks.rs @@ -16,6 +16,7 @@ use super::super::*; use crate::translate::InputInfo; +use common::chain::{OrderData, OrderId}; use pos_accounting::{DelegationData, PoolData}; use tokens_accounting::TokenData; @@ -27,6 +28,7 @@ pub struct MockSigInfoProvider<'a> { tokens: BTreeMap, pools: BTreeMap, delegations: BTreeMap, + orders: BTreeMap, } impl<'a> MockSigInfoProvider<'a> { @@ -36,6 +38,7 @@ impl<'a> MockSigInfoProvider<'a> { tokens: impl IntoIterator, pools: impl IntoIterator, delegations: impl IntoIterator, + orders: impl IntoIterator, ) -> Self { Self { input_info, @@ -43,6 +46,7 @@ impl<'a> MockSigInfoProvider<'a> { tokens: tokens.into_iter().collect(), pools: pools.into_iter().collect(), delegations: delegations.into_iter().collect(), + orders: orders.into_iter().collect(), } } } @@ -60,6 +64,7 @@ impl crate::translate::InputInfoProvider for MockSigInfoProvider<'_> { impl crate::translate::SignatureInfoProvider for MockSigInfoProvider<'_> { type PoSAccounting = Self; type Tokens = Self; + type Orders = Self; fn pos_accounting(&self) -> &Self::PoSAccounting { self @@ -68,6 +73,10 @@ impl crate::translate::SignatureInfoProvider for MockSigInfoProvider<'_> { fn tokens(&self) -> &Self::Tokens { self } + + fn orders(&self) -> &Self::Orders { + self + } } impl pos_accounting::PoSAccountingView for MockSigInfoProvider<'_> { @@ -120,3 +129,19 @@ impl tokens_accounting::TokensAccountingView for MockSigInfoProvider<'_> { unreachable!("not used in these tests") } } + +impl orders_accounting::OrdersAccountingView for MockSigInfoProvider<'_> { + type Error = orders_accounting::Error; + + fn get_order_data(&self, id: &OrderId) -> Result, Self::Error> { + Ok(self.orders.get(id).cloned()) + } + + fn get_ask_balance(&self, _id: &OrderId) -> Result, Self::Error> { + unreachable!() + } + + fn get_give_balance(&self, _id: &OrderId) -> Result, Self::Error> { + unreachable!() + } +} diff --git a/mintscript/src/tests/translate/mod.rs b/mintscript/src/tests/translate/mod.rs index e80d5ac5c6..d8b70c093f 100644 --- a/mintscript/src/tests/translate/mod.rs +++ b/mintscript/src/tests/translate/mod.rs @@ -22,7 +22,7 @@ use common::{ htlc::{HashedTimelockContract, HtlcSecret, HtlcSecretHash}, signature::inputsig::authorize_hashed_timelock_contract_spend::AuthorizedHashedTimelockContractSpend, stakelock::StakePoolData, - tokens, AccountNonce, AccountSpending, + tokens, AccountNonce, AccountSpending, OrderData, OrderId, }, primitives::per_thousand::PerThousand, }; @@ -210,6 +210,19 @@ fn pool0() -> (PoolId, PoolData) { (fake_id(0xbc), data) } +fn order0() -> (OrderId, OrderData) { + let data = OrderData::new( + dest_pk(0x33), + OutputValue::Coin(Amount::from_atoms(100)), + OutputValue::Coin(Amount::from_atoms(200)), + ); + (fake_id(0x44), data) +} + +fn anyonecantake(data: OrderData) -> TestInputInfo { + tii(TxOutput::AnyoneCanTake(Box::new(data))) +} + fn account_spend(deleg: DelegationId, amount: u128) -> TestInputInfo { let spend = AccountSpending::DelegationBalance(deleg, Amount::from_atoms(amount)); let outpoint = AccountOutPoint::new(AccountNonce::new(7), spend); @@ -235,6 +248,17 @@ fn mint(id: TokenId, amount: u128) -> TestInputInfo { TestInputInfo::AccountCommand { command } } +fn conclude_order(id: OrderId) -> TestInputInfo { + let command = AccountCommand::ConcludeOrder(id); + TestInputInfo::AccountCommand { command } +} + +fn fill_order(id: OrderId) -> TestInputInfo { + let command = + AccountCommand::FillOrder(id, OutputValue::Coin(Amount::from_atoms(1)), dest_pk(0x4)); + TestInputInfo::AccountCommand { command } +} + // A hack to specify all the modes in the parametrized test below. The mode specification ought to // be simplified in the actual implementation and then this may be dropped. @@ -327,6 +351,15 @@ fn mode_name<'a, T: TranslationMode<'a>>(_: &T) -> &'static str { #[case("htlc_02", htlc(15, 16, tl_until_time(99)), htlc_stdsig(0x53))] #[case("htlc_03", htlc(17, 18, tl_for_secs(124)), htlc_multisig(0x54))] #[case("htlc_04", htlc(19, 20, tl_for_blocks(1000)), htlc_multisig(0x55))] +#[case("anyonecantake_00", anyonecantake(order0().1), nosig())] +#[case("anyonecantake_01", anyonecantake(order0().1), stdsig(0x57))] +#[case("concludeorder_00", conclude_order(order0().0), nosig())] +#[case("concludeorder_01", conclude_order(fake_id(0x88)), nosig())] +#[case("concludeorder_02", conclude_order(order0().0), stdsig(0x44))] +#[case("concludeorder_03", conclude_order(order0().0), stdsig(0x45))] +#[case("fillorder_00", fill_order(order0().0), nosig())] +#[case("fillorder_01", fill_order(fake_id(0x77)), nosig())] +#[case("fillorder_00", fill_order(order0().0), stdsig(0x45))] fn translate_snap( #[values(TxnMode, RewardMode, TimelockOnly)] mode: impl for<'a> TranslationMode<'a>, #[case] name: &str, @@ -337,7 +370,9 @@ fn translate_snap( let tokens = [token0()]; let delegs = [deleg0()]; let pools = [pool0()]; - let sig_info = mocks::MockSigInfoProvider::new(input_info, witness, tokens, pools, delegs); + let orders = [order0()]; + let sig_info = + mocks::MockSigInfoProvider::new(input_info, witness, tokens, pools, delegs, orders); let mode_str = mode_name(&mode); let result = match mode.translate_input_and_witness(&sig_info) { diff --git a/mintscript/src/tests/translate/snap.translate.reward.anyonecantake_00.txt b/mintscript/src/tests/translate/snap.translate.reward.anyonecantake_00.txt new file mode 100644 index 0000000000..776077671b --- /dev/null +++ b/mintscript/src/tests/translate/snap.translate.reward.anyonecantake_00.txt @@ -0,0 +1 @@ +ERROR: Attempt to spend an unspendable output \ No newline at end of file diff --git a/mintscript/src/tests/translate/snap.translate.reward.anyonecantake_01.txt b/mintscript/src/tests/translate/snap.translate.reward.anyonecantake_01.txt new file mode 100644 index 0000000000..776077671b --- /dev/null +++ b/mintscript/src/tests/translate/snap.translate.reward.anyonecantake_01.txt @@ -0,0 +1 @@ +ERROR: Attempt to spend an unspendable output \ No newline at end of file diff --git a/mintscript/src/tests/translate/snap.translate.reward.concludeorder_00.txt b/mintscript/src/tests/translate/snap.translate.reward.concludeorder_00.txt new file mode 100644 index 0000000000..d6ff8f8ae1 --- /dev/null +++ b/mintscript/src/tests/translate/snap.translate.reward.concludeorder_00.txt @@ -0,0 +1 @@ +ERROR: Illegal account spend \ No newline at end of file diff --git a/mintscript/src/tests/translate/snap.translate.reward.concludeorder_01.txt b/mintscript/src/tests/translate/snap.translate.reward.concludeorder_01.txt new file mode 100644 index 0000000000..d6ff8f8ae1 --- /dev/null +++ b/mintscript/src/tests/translate/snap.translate.reward.concludeorder_01.txt @@ -0,0 +1 @@ +ERROR: Illegal account spend \ No newline at end of file diff --git a/mintscript/src/tests/translate/snap.translate.reward.concludeorder_02.txt b/mintscript/src/tests/translate/snap.translate.reward.concludeorder_02.txt new file mode 100644 index 0000000000..d6ff8f8ae1 --- /dev/null +++ b/mintscript/src/tests/translate/snap.translate.reward.concludeorder_02.txt @@ -0,0 +1 @@ +ERROR: Illegal account spend \ No newline at end of file diff --git a/mintscript/src/tests/translate/snap.translate.reward.concludeorder_03.txt b/mintscript/src/tests/translate/snap.translate.reward.concludeorder_03.txt new file mode 100644 index 0000000000..d6ff8f8ae1 --- /dev/null +++ b/mintscript/src/tests/translate/snap.translate.reward.concludeorder_03.txt @@ -0,0 +1 @@ +ERROR: Illegal account spend \ No newline at end of file diff --git a/mintscript/src/tests/translate/snap.translate.reward.fillorder_00.txt b/mintscript/src/tests/translate/snap.translate.reward.fillorder_00.txt new file mode 100644 index 0000000000..d6ff8f8ae1 --- /dev/null +++ b/mintscript/src/tests/translate/snap.translate.reward.fillorder_00.txt @@ -0,0 +1 @@ +ERROR: Illegal account spend \ No newline at end of file diff --git a/mintscript/src/tests/translate/snap.translate.reward.fillorder_01.txt b/mintscript/src/tests/translate/snap.translate.reward.fillorder_01.txt new file mode 100644 index 0000000000..d6ff8f8ae1 --- /dev/null +++ b/mintscript/src/tests/translate/snap.translate.reward.fillorder_01.txt @@ -0,0 +1 @@ +ERROR: Illegal account spend \ No newline at end of file diff --git a/mintscript/src/tests/translate/snap.translate.reward.fillorder_02.txt b/mintscript/src/tests/translate/snap.translate.reward.fillorder_02.txt new file mode 100644 index 0000000000..d6ff8f8ae1 --- /dev/null +++ b/mintscript/src/tests/translate/snap.translate.reward.fillorder_02.txt @@ -0,0 +1 @@ +ERROR: Illegal account spend \ No newline at end of file diff --git a/mintscript/src/tests/translate/snap.translate.tlockonly.anyonecantake_00.txt b/mintscript/src/tests/translate/snap.translate.tlockonly.anyonecantake_00.txt new file mode 100644 index 0000000000..776077671b --- /dev/null +++ b/mintscript/src/tests/translate/snap.translate.tlockonly.anyonecantake_00.txt @@ -0,0 +1 @@ +ERROR: Attempt to spend an unspendable output \ No newline at end of file diff --git a/mintscript/src/tests/translate/snap.translate.tlockonly.anyonecantake_01.txt b/mintscript/src/tests/translate/snap.translate.tlockonly.anyonecantake_01.txt new file mode 100644 index 0000000000..776077671b --- /dev/null +++ b/mintscript/src/tests/translate/snap.translate.tlockonly.anyonecantake_01.txt @@ -0,0 +1 @@ +ERROR: Attempt to spend an unspendable output \ No newline at end of file diff --git a/mintscript/src/tests/translate/snap.translate.tlockonly.concludeorder_00.txt b/mintscript/src/tests/translate/snap.translate.tlockonly.concludeorder_00.txt new file mode 100644 index 0000000000..27ba77ddaf --- /dev/null +++ b/mintscript/src/tests/translate/snap.translate.tlockonly.concludeorder_00.txt @@ -0,0 +1 @@ +true diff --git a/mintscript/src/tests/translate/snap.translate.tlockonly.concludeorder_01.txt b/mintscript/src/tests/translate/snap.translate.tlockonly.concludeorder_01.txt new file mode 100644 index 0000000000..27ba77ddaf --- /dev/null +++ b/mintscript/src/tests/translate/snap.translate.tlockonly.concludeorder_01.txt @@ -0,0 +1 @@ +true diff --git a/mintscript/src/tests/translate/snap.translate.tlockonly.concludeorder_02.txt b/mintscript/src/tests/translate/snap.translate.tlockonly.concludeorder_02.txt new file mode 100644 index 0000000000..27ba77ddaf --- /dev/null +++ b/mintscript/src/tests/translate/snap.translate.tlockonly.concludeorder_02.txt @@ -0,0 +1 @@ +true diff --git a/mintscript/src/tests/translate/snap.translate.tlockonly.concludeorder_03.txt b/mintscript/src/tests/translate/snap.translate.tlockonly.concludeorder_03.txt new file mode 100644 index 0000000000..27ba77ddaf --- /dev/null +++ b/mintscript/src/tests/translate/snap.translate.tlockonly.concludeorder_03.txt @@ -0,0 +1 @@ +true diff --git a/mintscript/src/tests/translate/snap.translate.tlockonly.fillorder_00.txt b/mintscript/src/tests/translate/snap.translate.tlockonly.fillorder_00.txt new file mode 100644 index 0000000000..27ba77ddaf --- /dev/null +++ b/mintscript/src/tests/translate/snap.translate.tlockonly.fillorder_00.txt @@ -0,0 +1 @@ +true diff --git a/mintscript/src/tests/translate/snap.translate.tlockonly.fillorder_01.txt b/mintscript/src/tests/translate/snap.translate.tlockonly.fillorder_01.txt new file mode 100644 index 0000000000..27ba77ddaf --- /dev/null +++ b/mintscript/src/tests/translate/snap.translate.tlockonly.fillorder_01.txt @@ -0,0 +1 @@ +true diff --git a/mintscript/src/tests/translate/snap.translate.tlockonly.fillorder_02.txt b/mintscript/src/tests/translate/snap.translate.tlockonly.fillorder_02.txt new file mode 100644 index 0000000000..27ba77ddaf --- /dev/null +++ b/mintscript/src/tests/translate/snap.translate.tlockonly.fillorder_02.txt @@ -0,0 +1 @@ +true diff --git a/mintscript/src/tests/translate/snap.translate.txn.anyonecantake_00.txt b/mintscript/src/tests/translate/snap.translate.txn.anyonecantake_00.txt new file mode 100644 index 0000000000..776077671b --- /dev/null +++ b/mintscript/src/tests/translate/snap.translate.txn.anyonecantake_00.txt @@ -0,0 +1 @@ +ERROR: Attempt to spend an unspendable output \ No newline at end of file diff --git a/mintscript/src/tests/translate/snap.translate.txn.anyonecantake_01.txt b/mintscript/src/tests/translate/snap.translate.txn.anyonecantake_01.txt new file mode 100644 index 0000000000..776077671b --- /dev/null +++ b/mintscript/src/tests/translate/snap.translate.txn.anyonecantake_01.txt @@ -0,0 +1 @@ +ERROR: Attempt to spend an unspendable output \ No newline at end of file diff --git a/mintscript/src/tests/translate/snap.translate.txn.concludeorder_00.txt b/mintscript/src/tests/translate/snap.translate.txn.concludeorder_00.txt new file mode 100644 index 0000000000..ecd4b9ce80 --- /dev/null +++ b/mintscript/src/tests/translate/snap.translate.txn.concludeorder_00.txt @@ -0,0 +1 @@ +signature(0x02000236d8c927b785e27385737e82cdde2e06dc510ab8545d6eab0ca05c36040a437c, 0x0000) diff --git a/mintscript/src/tests/translate/snap.translate.txn.concludeorder_01.txt b/mintscript/src/tests/translate/snap.translate.txn.concludeorder_01.txt new file mode 100644 index 0000000000..44099c2e1b --- /dev/null +++ b/mintscript/src/tests/translate/snap.translate.txn.concludeorder_01.txt @@ -0,0 +1 @@ +ERROR: Order with id 8888…8888 does not exist \ No newline at end of file diff --git a/mintscript/src/tests/translate/snap.translate.txn.concludeorder_02.txt b/mintscript/src/tests/translate/snap.translate.txn.concludeorder_02.txt new file mode 100644 index 0000000000..ef0b468b80 --- /dev/null +++ b/mintscript/src/tests/translate/snap.translate.txn.concludeorder_02.txt @@ -0,0 +1 @@ +signature(0x02000236d8c927b785e27385737e82cdde2e06dc510ab8545d6eab0ca05c36040a437c, 0x0101084444) diff --git a/mintscript/src/tests/translate/snap.translate.txn.concludeorder_03.txt b/mintscript/src/tests/translate/snap.translate.txn.concludeorder_03.txt new file mode 100644 index 0000000000..976babc21a --- /dev/null +++ b/mintscript/src/tests/translate/snap.translate.txn.concludeorder_03.txt @@ -0,0 +1 @@ +signature(0x02000236d8c927b785e27385737e82cdde2e06dc510ab8545d6eab0ca05c36040a437c, 0x0101084545) diff --git a/mintscript/src/tests/translate/snap.translate.txn.fillorder_00.txt b/mintscript/src/tests/translate/snap.translate.txn.fillorder_00.txt new file mode 100644 index 0000000000..27ba77ddaf --- /dev/null +++ b/mintscript/src/tests/translate/snap.translate.txn.fillorder_00.txt @@ -0,0 +1 @@ +true diff --git a/mintscript/src/tests/translate/snap.translate.txn.fillorder_01.txt b/mintscript/src/tests/translate/snap.translate.txn.fillorder_01.txt new file mode 100644 index 0000000000..27ba77ddaf --- /dev/null +++ b/mintscript/src/tests/translate/snap.translate.txn.fillorder_01.txt @@ -0,0 +1 @@ +true diff --git a/mintscript/src/tests/translate/snap.translate.txn.fillorder_02.txt b/mintscript/src/tests/translate/snap.translate.txn.fillorder_02.txt new file mode 100644 index 0000000000..27ba77ddaf --- /dev/null +++ b/mintscript/src/tests/translate/snap.translate.txn.fillorder_02.txt @@ -0,0 +1 @@ +true diff --git a/mintscript/src/translate.rs b/mintscript/src/translate.rs index 7bdaa1e0ca..ca590096be 100644 --- a/mintscript/src/translate.rs +++ b/mintscript/src/translate.rs @@ -23,9 +23,10 @@ use common::chain::{ DestinationSigError, }, tokens::TokenId, - AccountCommand, AccountOutPoint, AccountSpending, DelegationId, Destination, PoolId, + AccountCommand, AccountOutPoint, AccountSpending, DelegationId, Destination, OrderId, PoolId, SignedTransaction, TxOutput, UtxoOutPoint, }; +use orders_accounting::OrdersAccountingView; use pos_accounting::PoSAccountingView; use tokens_accounting::TokensAccountingView; use utxo::Utxo; @@ -50,6 +51,9 @@ pub enum TranslationError { #[error(transparent)] TokensAccounting(#[from] tokens_accounting::Error), + #[error(transparent)] + OrdersAccounting(#[from] orders_accounting::Error), + #[error(transparent)] SignatureError(#[from] DestinationSigError), @@ -61,6 +65,9 @@ pub enum TranslationError { #[error("Token with id {0} does not exist")] TokenNotFound(TokenId), + + #[error("Order with id {0} does not exist")] + OrderNotFound(OrderId), } /// Contextual information about given input @@ -95,9 +102,11 @@ pub trait InputInfoProvider { pub trait SignatureInfoProvider: InputInfoProvider { type PoSAccounting: PoSAccountingView; type Tokens: TokensAccountingView; + type Orders: OrdersAccountingView; fn pos_accounting(&self) -> &Self::PoSAccounting; fn tokens(&self) -> &Self::Tokens; + fn orders(&self) -> &Self::Orders; } pub trait TranslateInput { @@ -180,6 +189,7 @@ impl TranslateInput for SignedTransaction { TxOutput::IssueFungibleToken(_issuance) => Err(TranslationError::Unspendable), TxOutput::Burn(_val) => Err(TranslationError::Unspendable), TxOutput::DataDeposit(_data) => Err(TranslationError::Unspendable), + TxOutput::AnyoneCanTake(_) => Err(TranslationError::Unspendable), }, InputInfo::Account { outpoint } => match outpoint.account() { AccountSpending::DelegationBalance(delegation_id, _amount) => { @@ -205,6 +215,14 @@ impl TranslateInput for SignedTransaction { }; Ok(checksig(dest)) } + AccountCommand::ConcludeOrder(order_id) => { + let order_data = ctx + .orders() + .get_order_data(order_id)? + .ok_or(TranslationError::OrderNotFound(*order_id))?; + Ok(checksig(order_data.conclude_key())) + } + AccountCommand::FillOrder(_, _, _) => Ok(WitnessScript::TRUE), }, } } @@ -225,7 +243,8 @@ impl TranslateInput for BlockRewardTransactable<'_> | TxOutput::Burn(_) | TxOutput::DataDeposit(_) | TxOutput::DelegateStaking(_, _) - | TxOutput::IssueFungibleToken(_) => Err(TranslationError::Unspendable), + | TxOutput::IssueFungibleToken(_) + | TxOutput::AnyoneCanTake(_) => Err(TranslationError::Unspendable), TxOutput::ProduceBlockFromStake(d, _) => { // Spending an output of a block creation output is only allowed to @@ -282,7 +301,8 @@ impl TranslateInput for TimelockOnly { | TxOutput::IssueFungibleToken(_) | TxOutput::DelegateStaking(_, _) | TxOutput::Burn(_) - | TxOutput::DataDeposit(_) => Err(TranslationError::Unspendable), + | TxOutput::DataDeposit(_) + | TxOutput::AnyoneCanTake(_) => Err(TranslationError::Unspendable), }, InputInfo::Account { outpoint } => match outpoint.account() { AccountSpending::DelegationBalance(_deleg_id, _amt) => Ok(WitnessScript::TRUE), @@ -294,6 +314,9 @@ impl TranslateInput for TimelockOnly { | AccountCommand::FreezeToken(_token_id, _) | AccountCommand::UnfreezeToken(_token_id) | AccountCommand::ChangeTokenAuthority(_token_id, _) => Ok(WitnessScript::TRUE), + AccountCommand::ConcludeOrder(_) | AccountCommand::FillOrder(_, _, _) => { + Ok(WitnessScript::TRUE) + } }, } } diff --git a/mocks/src/chainstate.rs b/mocks/src/chainstate.rs index 44f47a5e61..ecb7aa654b 100644 --- a/mocks/src/chainstate.rs +++ b/mocks/src/chainstate.rs @@ -26,7 +26,8 @@ use common::{ GenBlock, }, tokens::{RPCTokenInfo, TokenAuxiliaryData, TokenId}, - AccountNonce, AccountType, ChainConfig, DelegationId, PoolId, TxInput, UtxoOutPoint, + AccountNonce, AccountType, ChainConfig, DelegationId, OrderData, OrderId, PoolId, TxInput, + UtxoOutPoint, }, primitives::{Amount, BlockHeight, Id}, }; @@ -198,6 +199,10 @@ mockall::mock! { &self, account: AccountType, ) -> Result, ChainstateError>; + + fn get_order_data(&self, id: &OrderId) -> Result, ChainstateError>; + fn get_order_ask_balance(&self, id: &OrderId) -> Result, ChainstateError>; + fn get_order_give_balance(&self, id: &OrderId) -> Result, ChainstateError>; } } diff --git a/orders-accounting/Cargo.toml b/orders-accounting/Cargo.toml new file mode 100644 index 0000000000..d4e314e08c --- /dev/null +++ b/orders-accounting/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "orders-accounting" +license.workspace = true +version.workspace = true +edition.workspace = true +rust-version.workspace = true + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +accounting = { path = "../accounting" } +chainstate-types = { path = "../chainstate/types" } +common = { path = "../common" } +crypto = { path = "../crypto" } +logging = { path = "../logging" } +randomness = { path = "../randomness" } +serialization = { path = "../serialization" } +utils = { path = "../utils" } + +thiserror.workspace = true +parity-scale-codec.workspace = true +variant_count.workspace = true + +[dev-dependencies] +test-utils = { path = "../test-utils" } + +rstest.workspace = true diff --git a/orders-accounting/src/cache.rs b/orders-accounting/src/cache.rs new file mode 100644 index 0000000000..426c3bc001 --- /dev/null +++ b/orders-accounting/src/cache.rs @@ -0,0 +1,274 @@ +// Copyright (c) 2024 RBB S.r.l +// opensource@mintlayer.org +// SPDX-License-Identifier: MIT +// Licensed under the MIT License; +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://github.com/mintlayer/mintlayer-core/blob/master/LICENSE +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use accounting::combine_amount_delta; +use common::{ + chain::{output_value::OutputValue, OrderData, OrderId}, + primitives::Amount, +}; +use logging::log; +use utils::ensure; + +use crate::{ + calculate_fill_order, + data::OrdersAccountingDeltaData, + error::{Error, Result}, + operations::{ + ConcludeOrderUndo, CreateOrderUndo, FillOrderUndo, OrdersAccountingOperations, + OrdersAccountingUndo, + }, + view::OrdersAccountingView, + FlushableOrdersAccountingView, OrdersAccountingDeltaUndoData, +}; + +fn output_value_amount(value: &OutputValue) -> Result { + match value { + OutputValue::Coin(amount) | OutputValue::TokenV1(_, amount) => Ok(*amount), + OutputValue::TokenV0(_) => Err(Error::UnsupportedTokenVersion), + } +} + +pub struct OrdersAccountingCache

{ + parent: P, + data: OrdersAccountingDeltaData, +} + +impl OrdersAccountingCache

{ + pub fn new(parent: P) -> Self { + Self { + parent, + data: OrdersAccountingDeltaData::new(), + } + } + + pub fn consume(self) -> OrdersAccountingDeltaData { + self.data + } + + pub fn data(&self) -> &OrdersAccountingDeltaData { + &self.data + } + + fn undo_create_order(&mut self, undo: CreateOrderUndo) -> Result<()> { + match self.get_ask_balance(&undo.id)? { + Some(balance) => { + if balance != undo.ask_balance { + return Err(Error::InvariantOrderAskBalanceChangedForUndo(undo.id)); + } + } + None => return Err(Error::InvariantOrderAskBalanceNotFoundForUndo(undo.id)), + } + self.data.ask_balances.sub_unsigned(undo.id, undo.ask_balance)?; + + match self.get_give_balance(&undo.id)? { + Some(balance) => { + if balance != undo.give_balance { + return Err(Error::InvariantOrderGiveBalanceChangedForUndo(undo.id)); + } + } + None => return Err(Error::InvariantOrderGiveBalanceNotFoundForUndo(undo.id)), + } + self.data.give_balances.sub_unsigned(undo.id, undo.give_balance)?; + + ensure!( + self.get_order_data(&undo.id)?.is_some(), + Error::InvariantOrderDataNotFoundForUndo(undo.id) + ); + self.data.order_data.undo_merge_delta_data_element(undo.id, undo.undo_data)?; + + Ok(()) + } + + fn undo_conclude_order(&mut self, undo: ConcludeOrderUndo) -> Result<()> { + ensure!( + self.get_order_data(&undo.id)?.is_none(), + Error::InvariantOrderDataExistForConcludeUndo(undo.id) + ); + self.data.order_data.undo_merge_delta_data_element(undo.id, undo.undo_data)?; + + ensure!( + self.get_ask_balance(&undo.id)?.unwrap_or(Amount::ZERO) == Amount::ZERO, + Error::InvariantOrderAskBalanceExistForConcludeUndo(undo.id) + ); + self.data.ask_balances.add_unsigned(undo.id, undo.ask_balance)?; + + ensure!( + self.get_give_balance(&undo.id)?.unwrap_or(Amount::ZERO) == Amount::ZERO, + Error::InvariantOrderGiveBalanceExistForConcludeUndo(undo.id) + ); + self.data.give_balances.add_unsigned(undo.id, undo.give_balance)?; + + Ok(()) + } + + fn undo_fill_order(&mut self, undo: FillOrderUndo) -> Result<()> { + self.data.ask_balances.add_unsigned(undo.id, undo.ask_balance)?; + self.data.give_balances.add_unsigned(undo.id, undo.give_balance)?; + + Ok(()) + } +} + +impl OrdersAccountingView for OrdersAccountingCache

{ + type Error = Error; + + fn get_order_data(&self, id: &OrderId) -> Result> { + match self.data.order_data.get_data(id) { + accounting::GetDataResult::Present(d) => Ok(Some(d.clone())), + accounting::GetDataResult::Deleted => Ok(None), + accounting::GetDataResult::Missing => { + Ok(self.parent.get_order_data(id).map_err(|_| Error::ViewFail)?) + } + } + } + + fn get_ask_balance(&self, id: &OrderId) -> Result> { + let parent_supply = self.parent.get_ask_balance(id).map_err(|_| Error::ViewFail)?; + let local_delta = self.data.ask_balances.data().get(id).cloned(); + let balance = + combine_amount_delta(&parent_supply, &local_delta).map_err(Error::AccountingError)?; + + // When combining deltas with amounts it's impossible to distinguish None from Some(Amount::ZERO). + // Use information from DeltaData to make the decision. + if self.get_order_data(id)?.is_some() { + Ok(balance) + } else { + utils::ensure!( + balance.unwrap_or(Amount::ZERO) == Amount::ZERO, + Error::InvariantNonzeroAskBalanceForMissingOrder(*id) + ); + Ok(None) + } + } + + fn get_give_balance(&self, id: &OrderId) -> Result> { + let parent_supply = self.parent.get_give_balance(id).map_err(|_| Error::ViewFail)?; + let local_delta = self.data.give_balances.data().get(id).cloned(); + let balance = + combine_amount_delta(&parent_supply, &local_delta).map_err(Error::AccountingError)?; + + // When combining deltas with amounts it's impossible to distinguish None from Some(Amount::ZERO). + // Use information from DeltaData to make the decision. + if self.get_order_data(id)?.is_some() { + Ok(balance) + } else { + utils::ensure!( + balance.unwrap_or(Amount::ZERO) == Amount::ZERO, + Error::InvariantNonzeroGiveBalanceForMissingOrder(*id) + ); + Ok(None) + } + } +} + +impl OrdersAccountingOperations for OrdersAccountingCache

{ + fn create_order(&mut self, id: OrderId, data: OrderData) -> Result { + log::debug!("Creating an order: {:?} {:?}", id, data); + + ensure!( + self.get_order_data(&id)?.is_none(), + Error::OrderAlreadyExists(id) + ); + + ensure!( + self.get_ask_balance(&id)?.is_none(), + Error::OrderAlreadyExists(id) + ); + + let ask_amount = output_value_amount(data.ask())?; + let give_amount = output_value_amount(data.give())?; + + ensure!( + ask_amount > Amount::ZERO && give_amount > Amount::ZERO, + Error::OrderWithZeroValue(id) + ); + + let undo_data = self + .data + .order_data + .merge_delta_data_element(id, accounting::DataDelta::new(None, Some(data)))?; + + self.data.ask_balances.add_unsigned(id, ask_amount)?; + self.data.give_balances.add_unsigned(id, give_amount)?; + + Ok(OrdersAccountingUndo::CreateOrder(CreateOrderUndo { + id, + undo_data, + ask_balance: ask_amount, + give_balance: give_amount, + })) + } + + fn conclude_order(&mut self, id: OrderId) -> Result { + log::debug!("Concluding an order: {:?}", id); + + let order_data = self + .get_order_data(&id)? + .ok_or(Error::AttemptedConcludeNonexistingOrderData(id))?; + let ask_balance = self.get_ask_balance(&id)?.unwrap_or(Amount::ZERO); + let give_balance = self.get_give_balance(&id)?.unwrap_or(Amount::ZERO); + + let undo_data = self + .data + .order_data + .merge_delta_data_element(id, accounting::DataDelta::new(Some(order_data), None))?; + + self.data.ask_balances.sub_unsigned(id, ask_balance)?; + self.data.give_balances.sub_unsigned(id, give_balance)?; + + Ok(OrdersAccountingUndo::ConcludeOrder(ConcludeOrderUndo { + id, + undo_data, + ask_balance, + give_balance, + })) + } + + fn fill_order(&mut self, id: OrderId, fill_value: OutputValue) -> Result { + log::debug!("Filling an order: {:?} {:?}", id, fill_value); + + let fill_amount = output_value_amount(&fill_value)?; + let filled_amount = calculate_fill_order(self, id, &fill_value)?; + + self.data.give_balances.sub_unsigned(id, filled_amount)?; + self.data.ask_balances.sub_unsigned(id, fill_amount)?; + + Ok(OrdersAccountingUndo::FillOrder(FillOrderUndo { + id, + ask_balance: fill_amount, + give_balance: filled_amount, + })) + } + + fn undo(&mut self, undo_data: OrdersAccountingUndo) -> Result<()> { + log::debug!("Undo an order: {:?}", undo_data); + match undo_data { + OrdersAccountingUndo::CreateOrder(undo) => self.undo_create_order(undo), + OrdersAccountingUndo::ConcludeOrder(undo) => self.undo_conclude_order(undo), + OrdersAccountingUndo::FillOrder(undo) => self.undo_fill_order(undo), + } + } +} + +impl

FlushableOrdersAccountingView for OrdersAccountingCache

{ + type Error = Error; + + fn batch_write_orders_data( + &mut self, + delta: OrdersAccountingDeltaData, + ) -> Result { + self.data.merge_with_delta(delta) + } +} diff --git a/orders-accounting/src/data.rs b/orders-accounting/src/data.rs new file mode 100644 index 0000000000..2e0ba0c3d3 --- /dev/null +++ b/orders-accounting/src/data.rs @@ -0,0 +1,97 @@ +// Copyright (c) 2024 RBB S.r.l +// opensource@mintlayer.org +// SPDX-License-Identifier: MIT +// Licensed under the MIT License; +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://github.com/mintlayer/mintlayer-core/blob/master/LICENSE +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::collections::BTreeMap; + +use accounting::{DeltaAmountCollection, DeltaDataCollection, DeltaDataUndoCollection}; +use common::{ + chain::{OrderData, OrderId}, + primitives::Amount, +}; +use serialization::{Decode, Encode}; + +use crate::error::Result; + +#[derive(Clone, Encode, Decode, Debug, PartialEq, Eq)] +pub struct OrdersAccountingData { + pub order_data: BTreeMap, + pub ask_balances: BTreeMap, + pub give_balances: BTreeMap, +} + +impl OrdersAccountingData { + pub fn new() -> Self { + Self { + order_data: BTreeMap::new(), + ask_balances: BTreeMap::new(), + give_balances: BTreeMap::new(), + } + } +} + +#[derive(Clone, Encode, Decode, Debug, PartialEq, Eq)] +pub struct OrdersAccountingDeltaData { + pub(crate) order_data: DeltaDataCollection, + pub(crate) ask_balances: DeltaAmountCollection, + pub(crate) give_balances: DeltaAmountCollection, +} + +impl OrdersAccountingDeltaData { + pub fn merge_with_delta( + &mut self, + other: OrdersAccountingDeltaData, + ) -> Result { + let order_data_undo = self.order_data.merge_delta_data(other.order_data)?; + + let ask_balance_undo = other.ask_balances.clone(); + self.ask_balances.merge_delta_amounts(other.ask_balances)?; + + let give_balance_undo = other.give_balances.clone(); + self.give_balances.merge_delta_amounts(other.give_balances)?; + + Ok(OrdersAccountingDeltaUndoData { + order_data: order_data_undo, + ask_balances: ask_balance_undo, + give_balances: give_balance_undo, + }) + } +} + +impl OrdersAccountingDeltaData { + pub fn new() -> Self { + Self { + order_data: DeltaDataCollection::new(), + ask_balances: DeltaAmountCollection::new(), + give_balances: DeltaAmountCollection::new(), + } + } +} + +#[derive(Clone, Encode, Decode, Debug, PartialEq, Eq)] +pub struct OrdersAccountingDeltaUndoData { + pub(crate) order_data: DeltaDataUndoCollection, + pub(crate) ask_balances: DeltaAmountCollection, + pub(crate) give_balances: DeltaAmountCollection, +} + +impl OrdersAccountingDeltaUndoData { + pub fn new() -> Self { + Self { + order_data: DeltaDataUndoCollection::new(), + ask_balances: DeltaAmountCollection::new(), + give_balances: DeltaAmountCollection::new(), + } + } +} diff --git a/orders-accounting/src/error.rs b/orders-accounting/src/error.rs new file mode 100644 index 0000000000..71c73c9f85 --- /dev/null +++ b/orders-accounting/src/error.rs @@ -0,0 +1,73 @@ +// Copyright (c) 2024 RBB S.r.l +// opensource@mintlayer.org +// SPDX-License-Identifier: MIT +// Licensed under the MIT License; +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://github.com/mintlayer/mintlayer-core/blob/master/LICENSE +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use common::{chain::OrderId, primitives::Amount}; + +#[derive(thiserror::Error, Debug, PartialEq, Eq, Clone)] +pub enum Error { + #[error("Accounting storage error")] + StorageError(#[from] chainstate_types::storage_result::Error), + #[error("Base accounting error: `{0}`")] + AccountingError(#[from] accounting::Error), + #[error("Order already exists: `{0}`")] + OrderAlreadyExists(OrderId), + #[error("Data for order `{0}` not found")] + OrderDataNotFound(OrderId), + #[error("Ask balance for order `{0}` not found")] + OrderAskBalanceNotFound(OrderId), + #[error("Give balance for order `{0}` not found")] + OrderGiveBalanceNotFound(OrderId), + #[error("Attempt to create an order with zero exchange value `{0}`")] + OrderWithZeroValue(OrderId), + #[error("Data for order `{0}` not found for undo")] + InvariantOrderDataNotFoundForUndo(OrderId), + #[error("Ask balance for order `{0}` not found for undo")] + InvariantOrderAskBalanceNotFoundForUndo(OrderId), + #[error("Ask balance for order `{0}` changed for undo")] + InvariantOrderAskBalanceChangedForUndo(OrderId), + #[error("Give balance for order `{0}` not found for undo")] + InvariantOrderGiveBalanceNotFoundForUndo(OrderId), + #[error("Give balance for order `{0}` changed for undo")] + InvariantOrderGiveBalanceChangedForUndo(OrderId), + #[error("Data for order `{0}` still exist on conclude undo")] + InvariantOrderDataExistForConcludeUndo(OrderId), + #[error("Ask balance for order `{0}` still exist on conclude undo")] + InvariantOrderAskBalanceExistForConcludeUndo(OrderId), + #[error("Give balance for order `{0}` still exist on conclude undo")] + InvariantOrderGiveBalanceExistForConcludeUndo(OrderId), + #[error("Ask balance for non-existing order `{0}` is not zero")] + InvariantNonzeroAskBalanceForMissingOrder(OrderId), + #[error("Give balance for non-existing order `{0}` is not zero")] + InvariantNonzeroGiveBalanceForMissingOrder(OrderId), + #[error("Coin type mismatch")] + CurrencyMismatch, + #[error("Order overflow: `{0}`")] + OrderOverflow(OrderId), + #[error("Order `{0}` can provide `{1:?}` amount; but attempted to fill `{2:?}`")] + OrderOverbid(OrderId, Amount, Amount), + #[error("Attempt to conclude non-existing order data `{0}`")] + AttemptedConcludeNonexistingOrderData(OrderId), + #[error("Unsupported token version")] + UnsupportedTokenVersion, + + // TODO Need a more granular error reporting in the following + // https://github.com/mintlayer/mintlayer-core/issues/811 + #[error("Orders accounting view query failed")] + ViewFail, + #[error("Orders accounting storage write failed")] + StorageWrite, +} + +pub type Result = core::result::Result; diff --git a/orders-accounting/src/lib.rs b/orders-accounting/src/lib.rs new file mode 100644 index 0000000000..7f0849e952 --- /dev/null +++ b/orders-accounting/src/lib.rs @@ -0,0 +1,38 @@ +// Copyright (c) 2024 RBB S.r.l +// opensource@mintlayer.org +// SPDX-License-Identifier: MIT +// Licensed under the MIT License; +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://github.com/mintlayer/mintlayer-core/blob/master/LICENSE +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +mod cache; +mod data; +mod error; +mod operations; +mod price_calculation; +mod storage; +mod view; + +pub use { + cache::OrdersAccountingCache, + data::{OrdersAccountingData, OrdersAccountingDeltaData, OrdersAccountingDeltaUndoData}, + error::Error, + operations::{OrdersAccountingOperations, OrdersAccountingUndo}, + price_calculation::calculate_fill_order, + storage::{ + db::OrdersAccountingDB, in_memory::InMemoryOrdersAccounting, OrdersAccountingStorageRead, + OrdersAccountingStorageWrite, + }, + view::{FlushableOrdersAccountingView, OrdersAccountingView}, +}; + +#[cfg(test)] +mod tests; diff --git a/orders-accounting/src/operations.rs b/orders-accounting/src/operations.rs new file mode 100644 index 0000000000..42be5efcca --- /dev/null +++ b/orders-accounting/src/operations.rs @@ -0,0 +1,63 @@ +// Copyright (c) 2024 RBB S.r.l +// opensource@mintlayer.org +// SPDX-License-Identifier: MIT +// Licensed under the MIT License; +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://github.com/mintlayer/mintlayer-core/blob/master/LICENSE +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use accounting::DataDeltaUndo; +use common::{ + chain::{output_value::OutputValue, OrderData, OrderId}, + primitives::Amount, +}; +use serialization::{Decode, Encode}; +use variant_count::VariantCount; + +use crate::error::Result; + +#[derive(Clone, Debug, PartialEq, Eq, Encode, Decode)] +pub struct CreateOrderUndo { + pub(crate) id: OrderId, + pub(crate) undo_data: DataDeltaUndo, + pub(crate) ask_balance: Amount, + pub(crate) give_balance: Amount, +} + +#[derive(Clone, Debug, PartialEq, Eq, Encode, Decode)] +pub struct ConcludeOrderUndo { + pub(crate) id: OrderId, + pub(crate) undo_data: DataDeltaUndo, + pub(crate) ask_balance: Amount, + pub(crate) give_balance: Amount, +} + +#[derive(Clone, Debug, PartialEq, Eq, Encode, Decode)] +pub struct FillOrderUndo { + pub(crate) id: OrderId, + pub(crate) ask_balance: Amount, + pub(crate) give_balance: Amount, +} + +#[must_use] +#[derive(Clone, Debug, PartialEq, Eq, Encode, Decode, VariantCount)] +pub enum OrdersAccountingUndo { + CreateOrder(CreateOrderUndo), + ConcludeOrder(ConcludeOrderUndo), + FillOrder(FillOrderUndo), +} + +pub trait OrdersAccountingOperations { + fn create_order(&mut self, id: OrderId, data: OrderData) -> Result; + fn conclude_order(&mut self, id: OrderId) -> Result; + fn fill_order(&mut self, id: OrderId, value: OutputValue) -> Result; + + fn undo(&mut self, undo_data: OrdersAccountingUndo) -> Result<()>; +} diff --git a/orders-accounting/src/price_calculation.rs b/orders-accounting/src/price_calculation.rs new file mode 100644 index 0000000000..becfc98368 --- /dev/null +++ b/orders-accounting/src/price_calculation.rs @@ -0,0 +1,250 @@ +// Copyright (c) 2024 RBB S.r.l +// opensource@mintlayer.org +// SPDX-License-Identifier: MIT +// Licensed under the MIT License; +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://github.com/mintlayer/mintlayer-core/blob/master/LICENSE +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use common::{ + chain::{output_value::OutputValue, OrderId}, + primitives::Amount, + Uint256, +}; +use utils::ensure; + +use crate::{error::Result, Error, OrdersAccountingView}; + +pub fn calculate_fill_order( + view: &impl OrdersAccountingView, + order_id: OrderId, + fill_value: &OutputValue, +) -> Result { + let order_data = view + .get_order_data(&order_id) + .map_err(|_| crate::Error::ViewFail)? + .ok_or(Error::OrderDataNotFound(order_id))?; + let ask_balance = view + .get_ask_balance(&order_id) + .map_err(|_| crate::Error::ViewFail)? + .ok_or(Error::OrderAskBalanceNotFound(order_id))?; + let give_balance = view + .get_give_balance(&order_id) + .map_err(|_| crate::Error::ViewFail)? + .ok_or(Error::OrderGiveBalanceNotFound(order_id))?; + + { + let ask_balance = match order_data.ask() { + OutputValue::Coin(_) => OutputValue::Coin(ask_balance), + OutputValue::TokenV0(_) => return Err(Error::UnsupportedTokenVersion), + OutputValue::TokenV1(token_id, _) => OutputValue::TokenV1(*token_id, ask_balance), + }; + + ensure_currencies_and_amounts_match(order_id, &ask_balance, fill_value)?; + } + + let fill_amount = match fill_value { + OutputValue::Coin(amount) | OutputValue::TokenV1(_, amount) => *amount, + OutputValue::TokenV0(_) => return Err(Error::UnsupportedTokenVersion), + }; + + calculate_filled_amount_impl(ask_balance, give_balance, fill_amount) + .ok_or(Error::OrderOverflow(order_id)) +} + +fn calculate_filled_amount_impl(ask: Amount, give: Amount, fill: Amount) -> Option { + let give = Uint256::from_u128(give.into_atoms()); + let fill = Uint256::from_u128(fill.into_atoms()); + let ask = Uint256::from_u128(ask.into_atoms()); + + let result = ((give * fill).expect("cannot overflow") / ask)?; + let result: u128 = result.try_into().ok()?; + + Some(Amount::from_atoms(result)) +} + +fn ensure_currencies_and_amounts_match( + order_id: OrderId, + ask: &OutputValue, + fill: &OutputValue, +) -> Result<()> { + match (ask, fill) { + (OutputValue::Coin(ask), OutputValue::Coin(fill)) => { + ensure!(ask >= fill, Error::OrderOverbid(order_id, *ask, *fill)); + Ok(()) + } + (OutputValue::TokenV1(id1, ask), OutputValue::TokenV1(id2, fill)) => { + ensure!(ask >= fill, Error::OrderOverbid(order_id, *ask, *fill)); + ensure!(id1 == id2, Error::CurrencyMismatch); + Ok(()) + } + (OutputValue::Coin(_), OutputValue::TokenV1(_, _)) + | (OutputValue::TokenV1(_, _), OutputValue::Coin(_)) => Err(Error::CurrencyMismatch), + (OutputValue::TokenV0(_), _) | (_, OutputValue::TokenV0(_)) => { + Err(Error::UnsupportedTokenVersion) + } + } +} + +#[cfg(test)] +mod tests { + use std::collections::BTreeMap; + + use super::*; + use common::{ + chain::{tokens::TokenId, Destination, OrderData}, + primitives::H256, + }; + use rstest::rstest; + + use crate::{InMemoryOrdersAccounting, OrdersAccountingDB}; + + macro_rules! coin { + ($value:expr) => { + OutputValue::Coin(Amount::from_atoms($value)) + }; + } + + macro_rules! token { + ($value:expr) => { + OutputValue::TokenV1(TokenId::zero(), Amount::from_atoms($value)) + }; + } + + macro_rules! token2 { + ($value:expr) => { + OutputValue::TokenV1(H256::from_low_u64_be(1).into(), Amount::from_atoms($value)) + }; + } + + fn output_value_amount(value: &OutputValue) -> Amount { + match value { + OutputValue::Coin(amount) | OutputValue::TokenV1(_, amount) => *amount, + OutputValue::TokenV0(_) => panic!("unsupported token"), + } + } + + #[rstest] + #[case(0, 0, 0, None)] + #[case(0, 1, 1, None)] + #[case(0, u128::MAX, 1, None)] + #[case(2, u128::MAX, 2, Some(u128::MAX))] + #[case(1, 0, 0, Some(0))] + #[case(1, 0, 1, Some(0))] + #[case(1, 1, 1, Some(1))] + #[case(1, 2, 1, Some(2))] + #[case(2, 100, 0, Some(0))] + #[case(2, 100, 1, Some(50))] + #[case(2, 100, 2, Some(100))] + #[case(3, 100, 0, Some(0))] + #[case(3, 100, 1, Some(33))] + #[case(3, 100, 2, Some(66))] + #[case(3, 100, 3, Some(100))] + fn calculate_filled_amount_impl_test( + #[case] ask: u128, + #[case] give: u128, + #[case] fill: u128, + #[case] result: Option, + ) { + assert_eq!( + result.map(Amount::from_atoms), + calculate_filled_amount_impl( + Amount::from_atoms(ask), + Amount::from_atoms(give), + Amount::from_atoms(fill) + ) + ); + } + + #[rstest] + #[case(token!(1), coin!(0), token!(0), 0)] + #[case(token!(1), coin!(0), token!(1), 0)] + #[case(token!(3), coin!(100), token!(0), 0)] + #[case(token!(3), coin!(100), token!(1), 33)] + #[case(token!(3), coin!(100), token!(2), 66)] + #[case(token!(3), coin!(100), token!(3), 100)] + #[case(token!(5), coin!(100), token!(0), 0)] + #[case(token!(5), coin!(100), token!(1), 20)] + #[case(token!(5), coin!(100), token!(2), 40)] + #[case(token!(5), coin!(100), token!(3), 60)] + #[case(token!(5), coin!(100), token!(4), 80)] + #[case(token!(5), coin!(100), token!(5), 100)] + #[case(coin!(100), token!(3), coin!(0), 0)] + #[case(coin!(100), token!(3), coin!(1), 0)] + #[case(coin!(100), token!(3), coin!(33), 0)] + #[case(coin!(100), token!(3), coin!(34), 1)] + #[case(coin!(100), token!(3), coin!(66), 1)] + #[case(coin!(100), token!(3), coin!(67), 2)] + #[case(coin!(100), token!(3), coin!(99), 2)] + #[case(coin!(100), token!(3), coin!(100), 3)] + #[case(token!(3), token2!(100), token!(0), 0)] + #[case(token!(3), token2!(100), token!(1), 33)] + #[case(token!(3), token2!(100), token!(2), 66)] + #[case(token!(3), token2!(100), token!(3), 100)] + #[case(coin!(3), coin!(100), coin!(0), 0)] + #[case(coin!(3), coin!(100), coin!(1), 33)] + #[case(coin!(3), coin!(100), coin!(2), 66)] + #[case(coin!(3), coin!(100), coin!(3), 100)] + #[case(coin!(1), token!(u128::MAX), coin!(1), u128::MAX)] + #[case(coin!(2), token!(u128::MAX), coin!(2), u128::MAX)] + fn fill_order_valid_values( + #[case] ask: OutputValue, + #[case] give: OutputValue, + #[case] fill: OutputValue, + #[case] result: u128, + ) { + let order_id = OrderId::zero(); + let orders_store = InMemoryOrdersAccounting::from_values( + BTreeMap::from_iter([( + order_id, + OrderData::new(Destination::AnyoneCanSpend, ask.clone(), give.clone()), + )]), + BTreeMap::from_iter([(order_id, output_value_amount(&ask))]), + BTreeMap::from_iter([(order_id, output_value_amount(&give))]), + ); + let orders_db = OrdersAccountingDB::new(&orders_store); + + assert_eq!( + calculate_fill_order(&orders_db, order_id, &fill), + Ok(Amount::from_atoms(result)) + ); + } + + #[rstest] + #[case(token!(0), coin!(1), token!(0), Error::OrderOverflow(OrderId::zero()))] + #[case(token!(0), coin!(1), token!(1), Error::OrderOverbid(OrderId::zero(), Amount::from_atoms(0), Amount::from_atoms(1)))] + #[case(coin!(1), token!(1), coin!(2), Error::OrderOverbid(OrderId::zero(), Amount::from_atoms(1), Amount::from_atoms(2)))] + #[case(coin!(1), token!(u128::MAX), coin!(2), Error::OrderOverbid(OrderId::zero(), Amount::from_atoms(1), Amount::from_atoms(2)))] + #[case(coin!(1), token!(1), token!(1), Error::CurrencyMismatch)] + #[case(coin!(1), token!(1), token!(1), Error::CurrencyMismatch)] + #[case(token!(1), token2!(1), token2!(1), Error::CurrencyMismatch)] + fn fill_order_invalid_values( + #[case] ask: OutputValue, + #[case] give: OutputValue, + #[case] fill: OutputValue, + #[case] error: Error, + ) { + let order_id = OrderId::zero(); + let orders_store = InMemoryOrdersAccounting::from_values( + BTreeMap::from_iter([( + order_id, + OrderData::new(Destination::AnyoneCanSpend, ask.clone(), give.clone()), + )]), + BTreeMap::from_iter([(order_id, output_value_amount(&ask))]), + BTreeMap::from_iter([(order_id, output_value_amount(&give))]), + ); + let orders_db = OrdersAccountingDB::new(&orders_store); + + assert_eq!( + calculate_fill_order(&orders_db, order_id, &fill), + Err(error) + ); + } +} diff --git a/orders-accounting/src/storage/db.rs b/orders-accounting/src/storage/db.rs new file mode 100644 index 0000000000..657ac5b88c --- /dev/null +++ b/orders-accounting/src/storage/db.rs @@ -0,0 +1,204 @@ +// Copyright (c) 2024 RBB S.r.l +// opensource@mintlayer.org +// SPDX-License-Identifier: MIT +// Licensed under the MIT License; +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://github.com/mintlayer/mintlayer-core/blob/master/LICENSE +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::{collections::BTreeMap, ops::Neg}; + +use accounting::{ + combine_amount_delta, combine_data_with_delta, DeltaAmountCollection, DeltaDataUndoCollection, +}; +use common::{ + chain::{OrderData, OrderId}, + primitives::Amount, +}; +use utils::tap_log::TapLog; + +use crate::{ + data::{OrdersAccountingDeltaData, OrdersAccountingDeltaUndoData}, + error::Error, + view::{FlushableOrdersAccountingView, OrdersAccountingView}, +}; + +use super::{OrdersAccountingStorageRead, OrdersAccountingStorageWrite}; + +#[must_use] +pub struct OrdersAccountingDB(S); + +impl OrdersAccountingDB { + pub fn new(store: S) -> Self { + Self(store) + } +} + +impl OrdersAccountingDB { + pub fn merge_with_delta( + &mut self, + other: OrdersAccountingDeltaData, + ) -> Result { + let data_undo = other + .order_data + .consume() + .into_iter() + .map(|(id, delta)| -> Result<_, Error> { + let undo = delta.clone().invert(); + let old_data = self.0.get_order_data(&id).log_err().map_err(|_| Error::ViewFail)?; + match combine_data_with_delta(old_data, Some(delta))? { + Some(result) => self + .0 + .set_order_data(&id, &result) + .log_err() + .map_err(|_| Error::StorageWrite)?, + None => { + self.0.del_order_data(&id).log_err().map_err(|_| Error::StorageWrite)? + } + }; + Ok((id, undo)) + }) + .collect::, _>>()?; + + let ask_balance_undo = other + .ask_balances + .consume() + .into_iter() + .map(|(id, delta)| -> Result<_, Error> { + let balance = self.0.get_ask_balance(&id).log_err().map_err(|_| Error::ViewFail)?; + match combine_amount_delta(&balance, &Some(delta))? { + Some(result) => { + if result > Amount::ZERO { + self.0 + .set_ask_balance(&id, &result) + .log_err() + .map_err(|_| Error::StorageWrite)? + } else { + self.0 + .del_ask_balance(&id) + .log_err() + .map_err(|_| Error::StorageWrite)? + } + } + None => { + self.0.del_ask_balance(&id).log_err().map_err(|_| Error::StorageWrite)? + } + }; + let balance_undo = delta.neg().expect("amount negation some"); + Ok((id, balance_undo)) + }) + .collect::, _>>()?; + + let give_balance_undo = other + .give_balances + .consume() + .into_iter() + .map(|(id, delta)| -> Result<_, Error> { + let balance = + self.0.get_give_balance(&id).log_err().map_err(|_| Error::ViewFail)?; + match combine_amount_delta(&balance, &Some(delta))? { + Some(result) => { + if result > Amount::ZERO { + self.0 + .set_give_balance(&id, &result) + .log_err() + .map_err(|_| Error::StorageWrite)? + } else { + self.0 + .del_give_balance(&id) + .log_err() + .map_err(|_| Error::StorageWrite)? + } + } + None => { + self.0.del_give_balance(&id).log_err().map_err(|_| Error::StorageWrite)? + } + }; + let balance_undo = delta.neg().expect("amount negation some"); + Ok((id, balance_undo)) + }) + .collect::, _>>()?; + + Ok(OrdersAccountingDeltaUndoData { + order_data: DeltaDataUndoCollection::from_data(data_undo), + ask_balances: DeltaAmountCollection::from_iter(ask_balance_undo), + give_balances: DeltaAmountCollection::from_iter(give_balance_undo), + }) + } +} + +impl OrdersAccountingView for OrdersAccountingDB { + type Error = S::Error; + + fn get_order_data(&self, id: &OrderId) -> Result, Self::Error> { + self.0.get_order_data(id) + } + + fn get_ask_balance(&self, id: &OrderId) -> Result, Self::Error> { + self.0.get_ask_balance(id) + } + + fn get_give_balance(&self, id: &OrderId) -> Result, Self::Error> { + self.0.get_give_balance(id) + } +} + +impl OrdersAccountingStorageRead for OrdersAccountingDB { + type Error = S::Error; + + fn get_order_data(&self, id: &OrderId) -> Result, Self::Error> { + self.0.get_order_data(id) + } + + fn get_ask_balance(&self, id: &OrderId) -> Result, Self::Error> { + self.0.get_ask_balance(id) + } + + fn get_give_balance(&self, id: &OrderId) -> Result, Self::Error> { + self.0.get_give_balance(id) + } +} + +impl OrdersAccountingStorageWrite for OrdersAccountingDB { + fn set_order_data(&mut self, id: &OrderId, data: &OrderData) -> Result<(), Self::Error> { + self.0.set_order_data(id, data) + } + + fn del_order_data(&mut self, id: &OrderId) -> Result<(), Self::Error> { + self.0.del_order_data(id) + } + + fn set_ask_balance(&mut self, id: &OrderId, balance: &Amount) -> Result<(), Self::Error> { + self.0.set_ask_balance(id, balance) + } + + fn del_ask_balance(&mut self, id: &OrderId) -> Result<(), Self::Error> { + self.0.del_ask_balance(id) + } + + fn set_give_balance(&mut self, id: &OrderId, balance: &Amount) -> Result<(), Self::Error> { + self.0.set_give_balance(id, balance) + } + + fn del_give_balance(&mut self, id: &OrderId) -> Result<(), Self::Error> { + self.0.del_give_balance(id) + } +} + +impl FlushableOrdersAccountingView for OrdersAccountingDB { + type Error = Error; + + fn batch_write_orders_data( + &mut self, + delta: OrdersAccountingDeltaData, + ) -> Result { + self.merge_with_delta(delta).log_err().map_err(|_| Error::StorageWrite) + } +} diff --git a/orders-accounting/src/storage/in_memory.rs b/orders-accounting/src/storage/in_memory.rs new file mode 100644 index 0000000000..fda2a778b7 --- /dev/null +++ b/orders-accounting/src/storage/in_memory.rs @@ -0,0 +1,113 @@ +// Copyright (c) 2024 RBB S.r.l +// opensource@mintlayer.org +// SPDX-License-Identifier: MIT +// Licensed under the MIT License; +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://github.com/mintlayer/mintlayer-core/blob/master/LICENSE +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::collections::BTreeMap; + +use common::{ + chain::{OrderData, OrderId}, + primitives::Amount, +}; + +use super::{OrdersAccountingStorageRead, OrdersAccountingStorageWrite}; + +#[must_use] +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct InMemoryOrdersAccounting { + orders_data: BTreeMap, + ask_balances: BTreeMap, + give_balances: BTreeMap, +} + +impl InMemoryOrdersAccounting { + pub fn new() -> Self { + Self { + orders_data: Default::default(), + ask_balances: Default::default(), + give_balances: Default::default(), + } + } + + pub fn from_values( + orders_data: BTreeMap, + ask_balances: BTreeMap, + give_balances: BTreeMap, + ) -> Self { + Self { + orders_data, + ask_balances, + give_balances, + } + } + + pub fn orders_data(&self) -> &BTreeMap { + &self.orders_data + } + + pub fn ask_balances(&self) -> &BTreeMap { + &self.ask_balances + } + + pub fn give_balances(&self) -> &BTreeMap { + &self.give_balances + } +} + +impl OrdersAccountingStorageRead for InMemoryOrdersAccounting { + type Error = chainstate_types::storage_result::Error; + + fn get_order_data(&self, id: &OrderId) -> Result, Self::Error> { + Ok(self.orders_data.get(id).cloned()) + } + + fn get_ask_balance(&self, id: &OrderId) -> Result, Self::Error> { + Ok(self.ask_balances.get(id).cloned()) + } + + fn get_give_balance(&self, id: &OrderId) -> Result, Self::Error> { + Ok(self.give_balances.get(id).cloned()) + } +} + +impl OrdersAccountingStorageWrite for InMemoryOrdersAccounting { + fn set_order_data(&mut self, id: &OrderId, data: &OrderData) -> Result<(), Self::Error> { + self.orders_data.insert(*id, data.clone()); + Ok(()) + } + + fn del_order_data(&mut self, id: &OrderId) -> Result<(), Self::Error> { + self.orders_data.remove(id); + Ok(()) + } + + fn set_ask_balance(&mut self, id: &OrderId, balance: &Amount) -> Result<(), Self::Error> { + self.ask_balances.insert(*id, *balance); + Ok(()) + } + + fn del_ask_balance(&mut self, id: &OrderId) -> Result<(), Self::Error> { + self.ask_balances.remove(id); + Ok(()) + } + + fn set_give_balance(&mut self, id: &OrderId, balance: &Amount) -> Result<(), Self::Error> { + self.give_balances.insert(*id, *balance); + Ok(()) + } + + fn del_give_balance(&mut self, id: &OrderId) -> Result<(), Self::Error> { + self.give_balances.remove(id); + Ok(()) + } +} diff --git a/orders-accounting/src/storage/mod.rs b/orders-accounting/src/storage/mod.rs new file mode 100644 index 0000000000..0f38a533d6 --- /dev/null +++ b/orders-accounting/src/storage/mod.rs @@ -0,0 +1,109 @@ +// Copyright (c) 2024 RBB S.r.l +// opensource@mintlayer.org +// SPDX-License-Identifier: MIT +// Licensed under the MIT License; +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://github.com/mintlayer/mintlayer-core/blob/master/LICENSE +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use common::{ + chain::{OrderData, OrderId}, + primitives::Amount, +}; +use std::ops::{Deref, DerefMut}; + +pub mod db; +pub mod in_memory; + +pub trait OrdersAccountingStorageRead { + type Error: std::error::Error; + + /// Provides access to auxiliary data of an order. + fn get_order_data(&self, id: &OrderId) -> Result, Self::Error>; + + /// Provides access to current ask balance. The data represents the remaining amount + /// that is left to satisfy for an order and can be filled by a taker. + /// + /// For example, if an order give 10 coins for 5 tokens this method would return 5. If the order is partially + /// filled and 2 tokens were bought this method would return 3. + /// + /// It's represented by `Amount` to simplify accounting math and the currency can be enquired from OrderData. + fn get_ask_balance(&self, id: &OrderId) -> Result, Self::Error>; + + /// Provides access to current give balance. The data represents the remaining amount + /// that can be taken from an order if filled by a taker. + /// + /// For example, if an order gives 10 coins for 5 tokens this method would return 10. If the order is partially + /// filled and 2 tokens were bought this method would return 6. + /// + /// It's represented by `Amount` to simplify accounting math and the currency can be enquired from OrderData. + fn get_give_balance(&self, id: &OrderId) -> Result, Self::Error>; +} + +pub trait OrdersAccountingStorageWrite: OrdersAccountingStorageRead { + fn set_order_data(&mut self, id: &OrderId, data: &OrderData) -> Result<(), Self::Error>; + fn del_order_data(&mut self, id: &OrderId) -> Result<(), Self::Error>; + + fn set_ask_balance(&mut self, id: &OrderId, balance: &Amount) -> Result<(), Self::Error>; + fn del_ask_balance(&mut self, id: &OrderId) -> Result<(), Self::Error>; + + fn set_give_balance(&mut self, id: &OrderId, balance: &Amount) -> Result<(), Self::Error>; + fn del_give_balance(&mut self, id: &OrderId) -> Result<(), Self::Error>; +} + +impl OrdersAccountingStorageRead for T +where + T: Deref, + ::Target: OrdersAccountingStorageRead, +{ + type Error = ::Error; + + fn get_order_data(&self, id: &OrderId) -> Result, Self::Error> { + self.deref().get_order_data(id) + } + + fn get_ask_balance(&self, id: &OrderId) -> Result, Self::Error> { + self.deref().get_ask_balance(id) + } + + fn get_give_balance(&self, id: &OrderId) -> Result, Self::Error> { + self.deref().get_give_balance(id) + } +} + +impl OrdersAccountingStorageWrite for T +where + T: DerefMut, + ::Target: OrdersAccountingStorageWrite, +{ + fn set_order_data(&mut self, id: &OrderId, data: &OrderData) -> Result<(), Self::Error> { + self.deref_mut().set_order_data(id, data) + } + + fn del_order_data(&mut self, id: &OrderId) -> Result<(), Self::Error> { + self.deref_mut().del_order_data(id) + } + + fn set_ask_balance(&mut self, id: &OrderId, balance: &Amount) -> Result<(), Self::Error> { + self.deref_mut().set_ask_balance(id, balance) + } + + fn del_ask_balance(&mut self, id: &OrderId) -> Result<(), Self::Error> { + self.deref_mut().del_ask_balance(id) + } + + fn set_give_balance(&mut self, id: &OrderId, balance: &Amount) -> Result<(), Self::Error> { + self.deref_mut().set_give_balance(id, balance) + } + + fn del_give_balance(&mut self, id: &OrderId) -> Result<(), Self::Error> { + self.deref_mut().del_give_balance(id) + } +} diff --git a/orders-accounting/src/tests/mod.rs b/orders-accounting/src/tests/mod.rs new file mode 100644 index 0000000000..180c12f7f1 --- /dev/null +++ b/orders-accounting/src/tests/mod.rs @@ -0,0 +1,16 @@ +// Copyright (c) 2024 RBB S.r.l +// opensource@mintlayer.org +// SPDX-License-Identifier: MIT +// Licensed under the MIT License; +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://github.com/mintlayer/mintlayer-core/blob/master/LICENSE +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +mod operations; diff --git a/orders-accounting/src/tests/operations.rs b/orders-accounting/src/tests/operations.rs new file mode 100644 index 0000000000..b03210fef2 --- /dev/null +++ b/orders-accounting/src/tests/operations.rs @@ -0,0 +1,616 @@ +// Copyright (c) 2024 RBB S.r.l +// opensource@mintlayer.org +// SPDX-License-Identifier: MIT +// Licensed under the MIT License; +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://github.com/mintlayer/mintlayer-core/blob/master/LICENSE +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::collections::BTreeMap; + +use common::{ + chain::{output_value::OutputValue, tokens::TokenId, Destination, OrderData, OrderId}, + primitives::Amount, +}; +use randomness::Rng; +use rstest::rstest; +use test_utils::random::{make_seedable_rng, Seed}; + +use crate::{ + cache::OrdersAccountingCache, operations::OrdersAccountingOperations, + view::FlushableOrdersAccountingView, Error, InMemoryOrdersAccounting, OrdersAccountingDB, + OrdersAccountingView, +}; + +fn make_order_data(rng: &mut impl Rng) -> OrderData { + let token_id = TokenId::random_using(rng); + OrderData::new( + Destination::AnyoneCanSpend, + OutputValue::Coin(Amount::from_atoms(rng.gen_range(1u128..1000))), + OutputValue::TokenV1(token_id, Amount::from_atoms(rng.gen_range(1u128..1000))), + ) +} + +fn output_value_amount(value: &OutputValue) -> Amount { + match value { + OutputValue::Coin(amount) | OutputValue::TokenV1(_, amount) => *amount, + OutputValue::TokenV0(_) => panic!("unsupported token"), + } +} + +#[rstest] +#[trace] +#[case(Seed::from_entropy())] +fn create_order_and_flush(#[case] seed: Seed) { + let mut rng = make_seedable_rng(seed); + + let order_id = OrderId::random_using(&mut rng); + let order_data = make_order_data(&mut rng); + + let mut storage = InMemoryOrdersAccounting::new(); + let mut db = OrdersAccountingDB::new(&mut storage); + let mut cache = OrdersAccountingCache::new(&db); + + let _ = cache.create_order(order_id, order_data.clone()).unwrap(); + + db.batch_write_orders_data(cache.consume()).unwrap(); + + let expected_storage = InMemoryOrdersAccounting::from_values( + BTreeMap::from_iter([(order_id, order_data.clone())]), + BTreeMap::from_iter([(order_id, output_value_amount(order_data.ask()))]), + BTreeMap::from_iter([(order_id, output_value_amount(order_data.give()))]), + ); + + assert_eq!(expected_storage, storage); +} + +#[rstest] +#[trace] +#[case(Seed::from_entropy())] +fn create_order_twice(#[case] seed: Seed) { + let mut rng = make_seedable_rng(seed); + + let order_id = OrderId::random_using(&mut rng); + let order_data = make_order_data(&mut rng); + + { + let storage = InMemoryOrdersAccounting::new(); + let db = OrdersAccountingDB::new(&storage); + let mut cache = OrdersAccountingCache::new(&db); + + let _ = cache.create_order(order_id, order_data.clone()).unwrap(); + + assert_eq!( + cache.create_order(order_id, order_data.clone()), + Err(Error::OrderAlreadyExists(order_id)) + ); + } + + { + let storage = InMemoryOrdersAccounting::from_values( + BTreeMap::from_iter([(order_id, order_data.clone())]), + BTreeMap::from_iter([(order_id, output_value_amount(order_data.ask()))]), + BTreeMap::from_iter([(order_id, output_value_amount(order_data.give()))]), + ); + let db = OrdersAccountingDB::new(&storage); + let mut cache = OrdersAccountingCache::new(&db); + + assert_eq!( + cache.create_order(order_id, order_data.clone()), + Err(Error::OrderAlreadyExists(order_id)) + ); + } +} + +#[rstest] +#[trace] +#[case(Seed::from_entropy())] +fn create_order_and_undo(#[case] seed: Seed) { + let mut rng = make_seedable_rng(seed); + + let order_id = OrderId::random_using(&mut rng); + let order_data = make_order_data(&mut rng); + + let mut storage = InMemoryOrdersAccounting::new(); + let mut db = OrdersAccountingDB::new(&mut storage); + let mut cache = OrdersAccountingCache::new(&db); + + let undo = cache.create_order(order_id, order_data.clone()).unwrap(); + + assert_eq!( + Some(&order_data), + cache.get_order_data(&order_id).unwrap().as_ref() + ); + assert_eq!( + Some(output_value_amount(order_data.ask())), + cache.get_ask_balance(&order_id).unwrap() + ); + assert_eq!( + Some(output_value_amount(order_data.give())), + cache.get_give_balance(&order_id).unwrap() + ); + + cache.undo(undo).unwrap(); + + assert_eq!(None, cache.get_order_data(&order_id).unwrap().as_ref()); + assert_eq!(None, cache.get_ask_balance(&order_id).unwrap()); + assert_eq!(None, cache.get_give_balance(&order_id).unwrap()); + + db.batch_write_orders_data(cache.consume()).unwrap(); + + assert_eq!(InMemoryOrdersAccounting::new(), storage); +} + +#[rstest] +#[trace] +#[case(Seed::from_entropy())] +fn conclude_order_and_flush(#[case] seed: Seed) { + let mut rng = make_seedable_rng(seed); + + let order_id = OrderId::random_using(&mut rng); + let order_data = make_order_data(&mut rng); + + let mut storage = InMemoryOrdersAccounting::from_values( + BTreeMap::from_iter([(order_id, order_data.clone())]), + BTreeMap::from_iter([(order_id, output_value_amount(order_data.ask()))]), + BTreeMap::from_iter([(order_id, output_value_amount(order_data.give()))]), + ); + let mut db = OrdersAccountingDB::new(&mut storage); + let mut cache = OrdersAccountingCache::new(&db); + + // try to conclude non-existing order + { + let random_order = OrderId::random_using(&mut rng); + let result = cache.conclude_order(random_order); + assert_eq!( + result.unwrap_err(), + Error::AttemptedConcludeNonexistingOrderData(random_order) + ); + } + + let _ = cache.conclude_order(order_id).unwrap(); + + db.batch_write_orders_data(cache.consume()).unwrap(); + + assert_eq!(InMemoryOrdersAccounting::new(), storage); +} + +#[rstest] +#[trace] +#[case(Seed::from_entropy())] +fn conclude_order_twice(#[case] seed: Seed) { + let mut rng = make_seedable_rng(seed); + + let order_id = OrderId::random_using(&mut rng); + let order_data = make_order_data(&mut rng); + + let storage = InMemoryOrdersAccounting::from_values( + BTreeMap::from_iter([(order_id, order_data.clone())]), + BTreeMap::from_iter([(order_id, output_value_amount(order_data.ask()))]), + BTreeMap::from_iter([(order_id, output_value_amount(order_data.give()))]), + ); + let db = OrdersAccountingDB::new(&storage); + let mut cache = OrdersAccountingCache::new(&db); + + let _ = cache.conclude_order(order_id).unwrap(); + + assert_eq!( + cache.conclude_order(order_id,), + Err(Error::AttemptedConcludeNonexistingOrderData(order_id)) + ); +} + +#[rstest] +#[trace] +#[case(Seed::from_entropy())] +fn conclude_order_and_undo(#[case] seed: Seed) { + let mut rng = make_seedable_rng(seed); + + let order_id = OrderId::random_using(&mut rng); + let order_data = make_order_data(&mut rng); + + let mut storage = InMemoryOrdersAccounting::from_values( + BTreeMap::from_iter([(order_id, order_data.clone())]), + BTreeMap::from_iter([(order_id, output_value_amount(order_data.ask()))]), + BTreeMap::from_iter([(order_id, output_value_amount(order_data.give()))]), + ); + let original_storage = storage.clone(); + let mut db = OrdersAccountingDB::new(&mut storage); + let mut cache = OrdersAccountingCache::new(&db); + + let undo = cache.conclude_order(order_id).unwrap(); + + assert_eq!(None, cache.get_order_data(&order_id).unwrap().as_ref()); + assert_eq!(None, cache.get_ask_balance(&order_id).unwrap()); + assert_eq!(None, cache.get_give_balance(&order_id).unwrap()); + + cache.undo(undo).unwrap(); + + assert_eq!( + Some(&order_data), + cache.get_order_data(&order_id).unwrap().as_ref() + ); + assert_eq!( + Some(output_value_amount(order_data.ask())), + cache.get_ask_balance(&order_id).unwrap() + ); + assert_eq!( + Some(output_value_amount(order_data.give())), + cache.get_give_balance(&order_id).unwrap() + ); + + db.batch_write_orders_data(cache.consume()).unwrap(); + + assert_eq!(original_storage, storage); +} + +#[rstest] +#[trace] +#[case(Seed::from_entropy())] +fn fill_order_wrong_currency(#[case] seed: Seed) { + let mut rng = make_seedable_rng(seed); + + let order_id = OrderId::random_using(&mut rng); + let order_data = make_order_data(&mut rng); + + let mut storage = InMemoryOrdersAccounting::from_values( + BTreeMap::from_iter([(order_id, order_data.clone())]), + BTreeMap::from_iter([(order_id, output_value_amount(order_data.ask()))]), + BTreeMap::from_iter([(order_id, output_value_amount(order_data.give()))]), + ); + let mut db = OrdersAccountingDB::new(&mut storage); + let mut cache = OrdersAccountingCache::new(&db); + + // try to fill with random token instead of a coin + { + let random_token_id = TokenId::random_using(&mut rng); + let result = cache.fill_order( + order_id, + OutputValue::TokenV1(random_token_id, Amount::from_atoms(1)), + ); + assert_eq!(result.unwrap_err(), Error::CurrencyMismatch); + } + + let _ = cache.fill_order(order_id, order_data.ask().clone()).unwrap(); + + db.batch_write_orders_data(cache.consume()).unwrap(); + + let expected_storage = InMemoryOrdersAccounting::from_values( + BTreeMap::from_iter([(order_id, order_data)]), + BTreeMap::new(), + BTreeMap::new(), + ); + assert_eq!(expected_storage, storage); +} + +#[rstest] +#[trace] +#[case(Seed::from_entropy())] +fn fill_entire_order_and_flush(#[case] seed: Seed) { + let mut rng = make_seedable_rng(seed); + + let order_id = OrderId::random_using(&mut rng); + let order_data = make_order_data(&mut rng); + + let mut storage = InMemoryOrdersAccounting::from_values( + BTreeMap::from_iter([(order_id, order_data.clone())]), + BTreeMap::from_iter([(order_id, output_value_amount(order_data.ask()))]), + BTreeMap::from_iter([(order_id, output_value_amount(order_data.give()))]), + ); + let mut db = OrdersAccountingDB::new(&mut storage); + let mut cache = OrdersAccountingCache::new(&db); + + // try to fill non-existing order + { + let random_order = OrderId::random_using(&mut rng); + let result = cache.fill_order(random_order, order_data.ask().clone()); + assert_eq!(result.unwrap_err(), Error::OrderDataNotFound(random_order)); + } + + // try to overbid + { + let ask_amount = output_value_amount(order_data.ask()); + let fill = OutputValue::Coin((ask_amount + Amount::from_atoms(1)).unwrap()); + let result = cache.fill_order(order_id, fill); + assert_eq!( + result.unwrap_err(), + Error::OrderOverbid( + order_id, + ask_amount, + (ask_amount + Amount::from_atoms(1)).unwrap() + ) + ); + } + + let _ = cache.fill_order(order_id, order_data.ask().clone()).unwrap(); + + db.batch_write_orders_data(cache.consume()).unwrap(); + + let expected_storage = InMemoryOrdersAccounting::from_values( + BTreeMap::from_iter([(order_id, order_data)]), + BTreeMap::new(), + BTreeMap::new(), + ); + assert_eq!(expected_storage, storage); +} + +#[rstest] +#[trace] +#[case(Seed::from_entropy())] +fn fill_order_partially_and_flush(#[case] seed: Seed) { + let mut rng = make_seedable_rng(seed); + + let order_id = OrderId::random_using(&mut rng); + let token_id = TokenId::random_using(&mut rng); + let order_data = OrderData::new( + Destination::AnyoneCanSpend, + OutputValue::TokenV1(token_id, Amount::from_atoms(3)), + OutputValue::Coin(Amount::from_atoms(10)), + ); + + let mut storage = InMemoryOrdersAccounting::from_values( + BTreeMap::from_iter([(order_id, order_data.clone())]), + BTreeMap::from_iter([(order_id, output_value_amount(order_data.ask()))]), + BTreeMap::from_iter([(order_id, output_value_amount(order_data.give()))]), + ); + let mut db = OrdersAccountingDB::new(&mut storage); + let mut cache = OrdersAccountingCache::new(&db); + + let _ = cache + .fill_order( + order_id, + OutputValue::TokenV1(token_id, Amount::from_atoms(1)), + ) + .unwrap(); + + assert_eq!( + Some(&order_data), + cache.get_order_data(&order_id).unwrap().as_ref() + ); + assert_eq!( + Some(Amount::from_atoms(2)), + cache.get_ask_balance(&order_id).unwrap() + ); + assert_eq!( + Some(Amount::from_atoms(7)), + cache.get_give_balance(&order_id).unwrap() + ); + + let _ = cache + .fill_order( + order_id, + OutputValue::TokenV1(token_id, Amount::from_atoms(1)), + ) + .unwrap(); + + assert_eq!( + Some(&order_data), + cache.get_order_data(&order_id).unwrap().as_ref() + ); + assert_eq!( + Some(Amount::from_atoms(1)), + cache.get_ask_balance(&order_id).unwrap() + ); + assert_eq!( + Some(Amount::from_atoms(4)), + cache.get_give_balance(&order_id).unwrap() + ); + + let _ = cache + .fill_order( + order_id, + OutputValue::TokenV1(token_id, Amount::from_atoms(1)), + ) + .unwrap(); + + db.batch_write_orders_data(cache.consume()).unwrap(); + + let expected_storage = InMemoryOrdersAccounting::from_values( + BTreeMap::from_iter([(order_id, order_data)]), + BTreeMap::new(), + BTreeMap::new(), + ); + assert_eq!(expected_storage, storage); +} + +#[rstest] +#[trace] +#[case(Seed::from_entropy())] +fn fill_order_partially_and_undo(#[case] seed: Seed) { + let mut rng = make_seedable_rng(seed); + + let order_id = OrderId::random_using(&mut rng); + let token_id = TokenId::random_using(&mut rng); + let order_data = OrderData::new( + Destination::AnyoneCanSpend, + OutputValue::TokenV1(token_id, Amount::from_atoms(3)), + OutputValue::Coin(Amount::from_atoms(10)), + ); + + let mut storage = InMemoryOrdersAccounting::from_values( + BTreeMap::from_iter([(order_id, order_data.clone())]), + BTreeMap::from_iter([(order_id, output_value_amount(order_data.ask()))]), + BTreeMap::from_iter([(order_id, output_value_amount(order_data.give()))]), + ); + let original_storage = storage.clone(); + let mut db = OrdersAccountingDB::new(&mut storage); + let mut cache = OrdersAccountingCache::new(&db); + + let undo1 = cache + .fill_order( + order_id, + OutputValue::TokenV1(token_id, Amount::from_atoms(1)), + ) + .unwrap(); + + let undo2 = cache + .fill_order( + order_id, + OutputValue::TokenV1(token_id, Amount::from_atoms(1)), + ) + .unwrap(); + + let undo3 = cache + .fill_order( + order_id, + OutputValue::TokenV1(token_id, Amount::from_atoms(1)), + ) + .unwrap(); + + assert_eq!( + Some(&order_data), + cache.get_order_data(&order_id).unwrap().as_ref() + ); + assert_eq!( + Some(Amount::ZERO), + cache.get_ask_balance(&order_id).unwrap() + ); + assert_eq!( + Some(Amount::ZERO), + cache.get_give_balance(&order_id).unwrap() + ); + + cache.undo(undo3).unwrap(); + + assert_eq!( + Some(&order_data), + cache.get_order_data(&order_id).unwrap().as_ref() + ); + assert_eq!( + Some(Amount::from_atoms(1)), + cache.get_ask_balance(&order_id).unwrap() + ); + assert_eq!( + Some(Amount::from_atoms(4)), + cache.get_give_balance(&order_id).unwrap() + ); + + cache.undo(undo2).unwrap(); + + assert_eq!( + Some(&order_data), + cache.get_order_data(&order_id).unwrap().as_ref() + ); + assert_eq!( + Some(Amount::from_atoms(2)), + cache.get_ask_balance(&order_id).unwrap() + ); + assert_eq!( + Some(Amount::from_atoms(7)), + cache.get_give_balance(&order_id).unwrap() + ); + + cache.undo(undo1).unwrap(); + + assert_eq!( + Some(&order_data), + cache.get_order_data(&order_id).unwrap().as_ref() + ); + assert_eq!( + Some(Amount::from_atoms(3)), + cache.get_ask_balance(&order_id).unwrap() + ); + assert_eq!( + Some(Amount::from_atoms(10)), + cache.get_give_balance(&order_id).unwrap() + ); + + db.batch_write_orders_data(cache.consume()).unwrap(); + + assert_eq!(original_storage, storage); +} + +#[rstest] +#[trace] +#[case(Seed::from_entropy())] +fn fill_order_partially_and_conclude(#[case] seed: Seed) { + let mut rng = make_seedable_rng(seed); + + let order_id = OrderId::random_using(&mut rng); + let token_id = TokenId::random_using(&mut rng); + let order_data = OrderData::new( + Destination::AnyoneCanSpend, + OutputValue::TokenV1(token_id, Amount::from_atoms(3)), + OutputValue::Coin(Amount::from_atoms(10)), + ); + + let mut storage = InMemoryOrdersAccounting::from_values( + BTreeMap::from_iter([(order_id, order_data.clone())]), + BTreeMap::from_iter([(order_id, output_value_amount(order_data.ask()))]), + BTreeMap::from_iter([(order_id, output_value_amount(order_data.give()))]), + ); + let mut db = OrdersAccountingDB::new(&mut storage); + let mut cache = OrdersAccountingCache::new(&db); + + let _ = cache + .fill_order( + order_id, + OutputValue::TokenV1(token_id, Amount::from_atoms(1)), + ) + .unwrap(); + + assert_eq!( + Some(&order_data), + cache.get_order_data(&order_id).unwrap().as_ref() + ); + assert_eq!( + Some(Amount::from_atoms(2)), + cache.get_ask_balance(&order_id).unwrap() + ); + assert_eq!( + Some(Amount::from_atoms(7)), + cache.get_give_balance(&order_id).unwrap() + ); + + let _ = cache.conclude_order(order_id).unwrap(); + + db.batch_write_orders_data(cache.consume()).unwrap(); + + assert_eq!(InMemoryOrdersAccounting::new(), storage); +} + +// If total give balance of an order is split into a random number of fill operations +// they must exhaust the order entirely without any change left. +#[rstest] +#[trace] +#[case(Seed::from_entropy())] +fn fill_order_must_converge(#[case] seed: Seed) { + let mut rng = make_seedable_rng(seed); + + let ask_amount = Amount::from_atoms(rng.gen_range(1u128..1000)); + let give_amount = Amount::from_atoms(rng.gen_range(1u128..1000)); + let fill_orders = test_utils::split_value(&mut rng, ask_amount.into_atoms()); + + let ask = OutputValue::Coin(ask_amount); + let give = OutputValue::Coin(give_amount); + + let order_id = OrderId::random_using(&mut rng); + let order_data = OrderData::new(Destination::AnyoneCanSpend, ask.clone(), give.clone()); + let mut storage = InMemoryOrdersAccounting::from_values( + BTreeMap::from_iter([(order_id, order_data.clone())]), + BTreeMap::from_iter([(order_id, ask_amount)]), + BTreeMap::from_iter([(order_id, give_amount)]), + ); + let mut db = OrdersAccountingDB::new(&mut storage); + let mut cache = OrdersAccountingCache::new(&db); + + for fill in fill_orders { + let _ = cache.fill_order(order_id, OutputValue::Coin(Amount::from_atoms(fill))).unwrap(); + } + + db.batch_write_orders_data(cache.consume()).unwrap(); + + let expected_storage = InMemoryOrdersAccounting::from_values( + BTreeMap::from_iter([(order_id, order_data)]), + BTreeMap::new(), + BTreeMap::new(), + ); + assert_eq!(expected_storage, storage); +} diff --git a/orders-accounting/src/view.rs b/orders-accounting/src/view.rs new file mode 100644 index 0000000000..20b62ae672 --- /dev/null +++ b/orders-accounting/src/view.rs @@ -0,0 +1,63 @@ +// Copyright (c) 2024 RBB S.r.l +// opensource@mintlayer.org +// SPDX-License-Identifier: MIT +// Licensed under the MIT License; +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://github.com/mintlayer/mintlayer-core/blob/master/LICENSE +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::ops::Deref; + +use common::{ + chain::{OrderData, OrderId}, + primitives::Amount, +}; + +use crate::data::{OrdersAccountingDeltaData, OrdersAccountingDeltaUndoData}; + +pub trait OrdersAccountingView { + /// Error that can occur during queries + type Error: std::error::Error; + + fn get_order_data(&self, id: &OrderId) -> Result, Self::Error>; + fn get_ask_balance(&self, id: &OrderId) -> Result, Self::Error>; + fn get_give_balance(&self, id: &OrderId) -> Result, Self::Error>; +} + +pub trait FlushableOrdersAccountingView { + /// Errors potentially triggered by flushing the view + type Error: std::error::Error; + + /// Performs bulk modification + fn batch_write_orders_data( + &mut self, + delta: OrdersAccountingDeltaData, + ) -> Result; +} + +impl OrdersAccountingView for T +where + T: Deref, + ::Target: OrdersAccountingView, +{ + type Error = ::Error; + + fn get_order_data(&self, id: &OrderId) -> Result, Self::Error> { + self.deref().get_order_data(id) + } + + fn get_ask_balance(&self, id: &OrderId) -> Result, Self::Error> { + self.deref().get_ask_balance(id) + } + + fn get_give_balance(&self, id: &OrderId) -> Result, Self::Error> { + self.deref().get_give_balance(id) + } +} diff --git a/serialization/src/extras/non_empty_vec.rs b/serialization/src/extras/non_empty_vec.rs index 847040bfde..c053a6f1e3 100644 --- a/serialization/src/extras/non_empty_vec.rs +++ b/serialization/src/extras/non_empty_vec.rs @@ -21,7 +21,7 @@ use serialization_core::{Decode, Encode}; /// - If the Vec has data, it encodes to just the Vec, the Option is omitted /// - If the Vec has no data, it encodes to None /// - Some(vec![]) and None are equivalent when encoded, but when decoded result in None -#[derive(Debug, PartialEq, Eq, Clone, serde::Serialize, serde::Deserialize)] +#[derive(Debug, PartialEq, Eq, Clone, PartialOrd, Ord, serde::Serialize, serde::Deserialize)] pub struct DataOrNoVec(Option>); impl DataOrNoVec { diff --git a/test-utils/src/nft_utils.rs b/test-utils/src/nft_utils.rs index c54d8e099e..7e3f8ee58d 100644 --- a/test-utils/src/nft_utils.rs +++ b/test-utils/src/nft_utils.rs @@ -56,12 +56,34 @@ pub fn random_token_issuance_v1( let max_dec_count = chain_config.token_max_dec_count(); let max_uri_len = chain_config.token_max_uri_len(); + let _fix_code_below_if_enum_changes = |supply: TokenTotalSupply| match supply { + TokenTotalSupply::Fixed(_) => unreachable!(), + TokenTotalSupply::Lockable => unreachable!(), + TokenTotalSupply::Unlimited => unreachable!(), + }; + + let supply = match rng.gen_range(0..3) { + 0 => { + let supply = Amount::from_atoms(rng.gen_range(1..1_000_000)); + TokenTotalSupply::Fixed(supply) + } + 1 => TokenTotalSupply::Lockable, + 2 => TokenTotalSupply::Unlimited, + _ => unreachable!(), + }; + + let is_freezable = if rng.gen::() { + IsTokenFreezable::Yes + } else { + IsTokenFreezable::No + }; + TokenIssuanceV1 { token_ticker: random_ascii_alphanumeric_string(rng, 1..max_ticker_len).as_bytes().to_vec(), number_of_decimals: rng.gen_range(1..max_dec_count), metadata_uri: random_ascii_alphanumeric_string(rng, 1..max_uri_len).as_bytes().to_vec(), - total_supply: TokenTotalSupply::Lockable, - is_freezable: IsTokenFreezable::Yes, + total_supply: supply, + is_freezable, authority, } } diff --git a/tokens-accounting/Cargo.toml b/tokens-accounting/Cargo.toml index 4b6d39e6ae..e55f989956 100644 --- a/tokens-accounting/Cargo.toml +++ b/tokens-accounting/Cargo.toml @@ -12,6 +12,7 @@ accounting = { path = "../accounting" } chainstate-types = { path = "../chainstate/types" } common = { path = "../common" } crypto = { path = "../crypto" } +logging = { path = "../logging" } randomness = { path = "../randomness" } serialization = { path = "../serialization" } utils = { path = "../utils" } diff --git a/tokens-accounting/src/cache.rs b/tokens-accounting/src/cache.rs index 057962acba..f0d32ac3f9 100644 --- a/tokens-accounting/src/cache.rs +++ b/tokens-accounting/src/cache.rs @@ -21,6 +21,7 @@ use common::{ }, primitives::Amount, }; +use logging::log; use crate::{ data::{TokenData, TokensAccountingDeltaData}, @@ -101,6 +102,8 @@ impl

FlushableTokensAccountingView for TokensAccountingCache

{ impl TokensAccountingOperations for TokensAccountingCache

{ fn issue_token(&mut self, id: TokenId, data: TokenData) -> Result { + log::debug!("Issuing a token: {:?} {:?}", id, data); + if self.get_token_data(&id)?.is_some() { return Err(Error::TokenAlreadyExists(id)); } @@ -125,6 +128,8 @@ impl TokensAccountingOperations for TokensAccountingCac id: TokenId, amount_to_add: Amount, ) -> Result { + log::debug!("Minting tokens: {:?} {:?}", id, amount_to_add); + let token_data = self.get_token_data(&id)?.ok_or(Error::TokenDataNotFound(id))?; let circulating_supply = self.get_circulating_supply(&id)?.unwrap_or(Amount::ZERO); @@ -165,6 +170,8 @@ impl TokensAccountingOperations for TokensAccountingCac id: TokenId, amount_to_burn: Amount, ) -> Result { + log::debug!("Unminting tokens: {:?} {:?}", id, amount_to_burn); + let token_data = self.get_token_data(&id)?.ok_or(Error::TokenDataNotFound(id))?; let circulating_supply = self.get_circulating_supply(&id)?.ok_or(Error::CirculatingSupplyNotFound(id))?; @@ -199,6 +206,7 @@ impl TokensAccountingOperations for TokensAccountingCac &mut self, id: TokenId, ) -> crate::error::Result { + log::debug!("Locking token supply: {:?}", id); let token_data = self.get_token_data(&id)?.ok_or(Error::TokenDataNotFound(id))?; let undo_data = match token_data { @@ -231,6 +239,7 @@ impl TokensAccountingOperations for TokensAccountingCac id: TokenId, is_unfreezable: IsTokenUnfreezable, ) -> Result { + log::debug!("Freezing token: {:?} {:?}", id, is_unfreezable); let token_data = self.get_token_data(&id)?.ok_or(Error::TokenDataNotFound(id))?; let undo_data = match token_data { @@ -258,6 +267,7 @@ impl TokensAccountingOperations for TokensAccountingCac } fn unfreeze_token(&mut self, id: TokenId) -> Result { + log::debug!("Unfreezing token supply: {:?}", id); let token_data = self.get_token_data(&id)?.ok_or(Error::TokenDataNotFound(id))?; let undo_data = match token_data { @@ -289,6 +299,7 @@ impl TokensAccountingOperations for TokensAccountingCac id: TokenId, new_authority: Destination, ) -> Result { + log::debug!("Changing token authority: {:?}", id); let token_data = self.get_token_data(&id)?.ok_or(Error::TokenDataNotFound(id))?; let undo_data = match token_data { @@ -313,6 +324,7 @@ impl TokensAccountingOperations for TokensAccountingCac } fn undo(&mut self, undo_data: TokenAccountingUndo) -> Result<(), Error> { + log::debug!("Undo in tokens: {:?}", undo_data); match undo_data { TokenAccountingUndo::IssueToken(undo) => { let _ = self diff --git a/utxo/src/cache.rs b/utxo/src/cache.rs index b2d83df015..c8db33e97f 100644 --- a/utxo/src/cache.rs +++ b/utxo/src/cache.rs @@ -504,7 +504,8 @@ fn should_include_in_utxo_set(output: &TxOutput) -> bool { | TxOutput::DelegateStaking(..) | TxOutput::Burn(..) | TxOutput::IssueFungibleToken(..) - | TxOutput::DataDeposit(..) => false, + | TxOutput::DataDeposit(..) + | TxOutput::AnyoneCanTake(..) => false, } } diff --git a/wallet/src/account/currency_grouper/mod.rs b/wallet/src/account/currency_grouper/mod.rs index 236ed2897f..4c3208c356 100644 --- a/wallet/src/account/currency_grouper/mod.rs +++ b/wallet/src/account/currency_grouper/mod.rs @@ -58,6 +58,8 @@ pub(crate) fn group_outputs( get_tx_output(&output).clone(), ))) } + // TODO(orders) + TxOutput::AnyoneCanTake(_) => unimplemented!(), }; match output_value { @@ -112,6 +114,8 @@ pub fn group_outputs_with_issuance_fee( get_tx_output(&output).clone(), ))) } + // TODO(orders) + TxOutput::AnyoneCanTake(_) => unimplemented!(), }; match output_value { @@ -155,7 +159,8 @@ fn output_spendable_value(output: &TxOutput) -> Result<(Currency, Amount), UtxoS | TxOutput::CreateDelegationId(_, _) | TxOutput::DelegateStaking(_, _) | TxOutput::IssueFungibleToken(_) - | TxOutput::DataDeposit(_) => { + | TxOutput::DataDeposit(_) + | TxOutput::AnyoneCanTake(_) => { return Err(UtxoSelectorError::UnsupportedTransactionOutput(Box::new( output.clone(), ))) diff --git a/wallet/src/account/mod.rs b/wallet/src/account/mod.rs index 28e42b250f..ebead6cb95 100644 --- a/wallet/src/account/mod.rs +++ b/wallet/src/account/mod.rs @@ -1018,7 +1018,8 @@ impl Account { | TxOutput::IssueFungibleToken(_) | TxOutput::IssueNft(_, _, _) | TxOutput::DataDeposit(_) - | TxOutput::Htlc(_, _) => None, + | TxOutput::Htlc(_, _) + | TxOutput::AnyoneCanTake(_) => None, }) .expect("find output with dummy_pool_id"); *old_pool_id = new_pool_id; @@ -1073,7 +1074,8 @@ impl Account { | TxOutput::ProduceBlockFromStake(_, _) | TxOutput::IssueFungibleToken(_) | TxOutput::DataDeposit(_) - | TxOutput::Htlc(_, _) => None, + | TxOutput::Htlc(_, _) + | TxOutput::AnyoneCanTake(_) => None, TxOutput::IssueNft(token_id, _, _) => { (*token_id == dummy_token_id).then_some(token_id) } @@ -1345,6 +1347,9 @@ impl Account { .token_data(token_id) .map(|data| (None, Some(data.authority.clone()))) .ok_or(WalletError::UnknownTokenId(*token_id)), + // TODO(orders) + AccountCommand::ConcludeOrder(_) => unimplemented!(), + AccountCommand::FillOrder(_, _, _) => unimplemented!(), } } }) @@ -1545,7 +1550,8 @@ impl Account { TxOutput::IssueFungibleToken(_) | TxOutput::Burn(_) | TxOutput::DelegateStaking(_, _) - | TxOutput::DataDeposit(_) => Vec::new(), + | TxOutput::DataDeposit(_) + | TxOutput::AnyoneCanTake(_) => Vec::new(), } } @@ -1822,6 +1828,9 @@ impl Account { self.find_token(token_id).is_ok() || self.is_destination_mine_or_watched(address) } + // TODO(orders) + AccountCommand::ConcludeOrder(_) => unimplemented!(), + AccountCommand::FillOrder(_, _, _) => unimplemented!(), }, }); let relevant_outputs = self.mark_outputs_as_seen(db_tx, tx.outputs())?; @@ -2185,6 +2194,8 @@ fn group_preselected_inputs( output.clone(), ))) } + // TODO(orders) + TxOutput::AnyoneCanTake(_) => unimplemented!(), }; update_preselected_inputs(currency, value, *fee)?; } @@ -2228,6 +2239,9 @@ fn group_preselected_inputs( .ok_or(WalletError::OutputAmountOverflow)?, )?; } + // TODO(orders) + AccountCommand::ConcludeOrder(_) => unimplemented!(), + AccountCommand::FillOrder(_, _, _) => unimplemented!(), }, } } diff --git a/wallet/src/account/output_cache/mod.rs b/wallet/src/account/output_cache/mod.rs index 314745b096..7484a344c4 100644 --- a/wallet/src/account/output_cache/mod.rs +++ b/wallet/src/account/output_cache/mod.rs @@ -558,7 +558,8 @@ impl OutputCache { | TxOutput::LockThenTransfer(_, _, _) | TxOutput::CreateDelegationId(_, _) | TxOutput::IssueFungibleToken(_) - | TxOutput::Htlc(_, _) => false, + | TxOutput::Htlc(_, _) + | TxOutput::AnyoneCanTake(_) => false, } } @@ -668,7 +669,9 @@ impl OutputCache { | AccountCommand::UnmintTokens(_) | AccountCommand::LockTokenSupply(_) | AccountCommand::ChangeTokenAuthority(_, _) - | AccountCommand::UnfreezeToken(_) => None, + | AccountCommand::UnfreezeToken(_) + | AccountCommand::ConcludeOrder(_) + | AccountCommand::FillOrder(_, _, _) => None, AccountCommand::FreezeToken(frozen_token_id, _) => Some(frozen_token_id), }, }); @@ -718,6 +721,8 @@ impl OutputCache { | TxOutput::CreateDelegationId(_, _) | TxOutput::IssueFungibleToken(_) | TxOutput::ProduceBlockFromStake(_, _) => false, + // TODO(orders) + TxOutput::AnyoneCanTake(_) => unimplemented!(), } }), TxInput::AccountCommand(_, cmd) => match cmd { @@ -727,6 +732,9 @@ impl OutputCache { | AccountCommand::UnfreezeToken(token_id) | AccountCommand::ChangeTokenAuthority(token_id, _) | AccountCommand::UnmintTokens(token_id) => frozen_token_id == token_id, + // TODO(orders) + AccountCommand::ConcludeOrder(_) => unimplemented!(), + AccountCommand::FillOrder(_, _, _) => unimplemented!(), }, TxInput::Account(_) => false, }) @@ -829,6 +837,8 @@ impl OutputCache { } } TxOutput::IssueNft(_, _, _) => {} + // TODO(orders) + TxOutput::AnyoneCanTake(_) => unimplemented!(), }; } Ok(()) @@ -917,6 +927,9 @@ impl OutputCache { self.token_issuance.insert(*token_id, data); } } + // TODO(orders) + AccountCommand::ConcludeOrder(_) => unimplemented!(), + AccountCommand::FillOrder(_, _, _) => unimplemented!(), }, } } @@ -1017,6 +1030,9 @@ impl OutputCache { data.unconfirmed_txs.remove(tx_id); } } + // TODO(orders) + AccountCommand::ConcludeOrder(_) => unimplemented!(), + AccountCommand::FillOrder(_, _, _) => unimplemented!(), }, } } @@ -1042,6 +1058,8 @@ impl OutputCache { | TxOutput::CreateDelegationId(_, _) | TxOutput::IssueFungibleToken(_) | TxOutput::Htlc(_, _) => {} + // TODO(orders) + TxOutput::AnyoneCanTake(_) => unimplemented!(), } } } @@ -1305,6 +1323,9 @@ impl OutputCache { data.unconfirmed_txs.remove(&tx_id.into()); } } + // TODO(orders) + AccountCommand::ConcludeOrder(_) => unimplemented!(), + AccountCommand::FillOrder(_, _, _) => unimplemented!(), }, } } @@ -1364,7 +1385,8 @@ impl OutputCache { | TxOutput::Burn(_) | TxOutput::Transfer(_, _) | TxOutput::LockThenTransfer(_, _, _) - | TxOutput::Htlc(_, _) => None, + | TxOutput::Htlc(_, _) + | TxOutput::AnyoneCanTake(_) => None, TxOutput::ProduceBlockFromStake(_, pool_id) | TxOutput::CreateStakePool(pool_id, _) => { self.pools.get(pool_id).and_then(|pool_data| { @@ -1407,7 +1429,8 @@ fn is_v0_token_output(output: &TxOutput) -> bool { | TxOutput::IssueNft(_, _, _) | TxOutput::IssueFungibleToken(_) | TxOutput::DataDeposit(_) - | TxOutput::ProduceBlockFromStake(_, _) => false, + | TxOutput::ProduceBlockFromStake(_, _) + | TxOutput::AnyoneCanTake(_) => false, } } @@ -1509,6 +1532,9 @@ fn apply_freeze_mutations_from_tx( | AccountCommand::UnmintTokens(_) | AccountCommand::LockTokenSupply(_) | AccountCommand::ChangeTokenAuthority(_, _) => {} + // TODO(orders) + AccountCommand::ConcludeOrder(_) => unimplemented!(), + AccountCommand::FillOrder(_, _, _) => unimplemented!(), }, } } @@ -1548,6 +1574,9 @@ fn apply_total_supply_mutations_from_tx( AccountCommand::FreezeToken(_, _) | AccountCommand::UnfreezeToken(_) | AccountCommand::ChangeTokenAuthority(_, _) => {} + // TODO(orders) + AccountCommand::ConcludeOrder(_) => unimplemented!(), + AccountCommand::FillOrder(_, _, _) => unimplemented!(), }, } } diff --git a/wallet/src/account/transaction_list/mod.rs b/wallet/src/account/transaction_list/mod.rs index 5cf323367d..0b46e91f8c 100644 --- a/wallet/src/account/transaction_list/mod.rs +++ b/wallet/src/account/transaction_list/mod.rs @@ -121,7 +121,8 @@ fn own_output(key_chain: &AccountKeyChainImpl, output: &TxOutput) -> bool { | TxOutput::DelegateStaking(_, _) | TxOutput::IssueFungibleToken(_) | TxOutput::IssueNft(_, _, _) - | TxOutput::DataDeposit(_) => false, + | TxOutput::DataDeposit(_) + | TxOutput::AnyoneCanTake(_) => false, } } diff --git a/wallet/src/account/utxo_selector/output_group.rs b/wallet/src/account/utxo_selector/output_group.rs index 017fd545ff..061c5c0bd5 100644 --- a/wallet/src/account/utxo_selector/output_group.rs +++ b/wallet/src/account/utxo_selector/output_group.rs @@ -67,7 +67,8 @@ impl OutputGroup { | TxOutput::CreateDelegationId(_, _) | TxOutput::DelegateStaking(_, _) | TxOutput::IssueFungibleToken(_) - | TxOutput::DataDeposit(_) => { + | TxOutput::DataDeposit(_) + | TxOutput::AnyoneCanTake(_) => { return Err(UtxoSelectorError::UnsupportedTransactionOutput(Box::new( output.1.clone(), ))) diff --git a/wallet/src/send_request/mod.rs b/wallet/src/send_request/mod.rs index ab727178db..2ec6174ab9 100644 --- a/wallet/src/send_request/mod.rs +++ b/wallet/src/send_request/mod.rs @@ -306,7 +306,8 @@ where TxOutput::IssueFungibleToken(_) | TxOutput::Burn(_) | TxOutput::DelegateStaking(_, _) - | TxOutput::DataDeposit(_) => None, + | TxOutput::DataDeposit(_) + | TxOutput::AnyoneCanTake(_) => None, TxOutput::Htlc(_, _) => None, // TODO(HTLC) } } diff --git a/wallet/types/src/utxo_types.rs b/wallet/types/src/utxo_types.rs index 64555fe2ad..47ffa3d548 100644 --- a/wallet/types/src/utxo_types.rs +++ b/wallet/types/src/utxo_types.rs @@ -52,7 +52,8 @@ pub fn get_utxo_type(output: &TxOutput) -> Option { | TxOutput::CreateDelegationId(_, _) | TxOutput::DelegateStaking(_, _) | TxOutput::IssueFungibleToken(_) - | TxOutput::DataDeposit(_) => None, + | TxOutput::DataDeposit(_) + | TxOutput::AnyoneCanTake(_) => None, TxOutput::Htlc(_, _) => None, // TODO(HTLC) } } diff --git a/wallet/wallet-controller/src/lib.rs b/wallet/wallet-controller/src/lib.rs index 91e1205399..0926f49ff6 100644 --- a/wallet/wallet-controller/src/lib.rs +++ b/wallet/wallet-controller/src/lib.rs @@ -626,7 +626,8 @@ impl Controll | TxOutput::IssueFungibleToken(_) | TxOutput::IssueNft(_, _, _) | TxOutput::DataDeposit(_) - | TxOutput::Htlc(_, _) => None, + | TxOutput::Htlc(_, _) + | TxOutput::AnyoneCanTake(_) => None, }); let mut balances = BTreeMap::new(); for pool_id in pool_ids {