From 68d0ea9572549f4de8ed546143be57856e37bd2e Mon Sep 17 00:00:00 2001 From: Omar Date: Wed, 13 Sep 2023 22:48:09 +0300 Subject: [PATCH] Rework staking transaction type to allow multiple accounts --- .../src/functions/execution.rs | 13 +- .../transaction_type/stake_visitor.rs | 125 ++++++--- radix-engine-toolkit-core/src/lib.rs | 1 + radix-engine-toolkit-core/src/traits.rs | 12 + radix-engine-toolkit-core/tests/execution.rs | 255 +++++++++++++----- 5 files changed, 302 insertions(+), 104 deletions(-) create mode 100644 radix-engine-toolkit-core/src/traits.rs diff --git a/radix-engine-toolkit-core/src/functions/execution.rs b/radix-engine-toolkit-core/src/functions/execution.rs index 73c877ad..c5ecff83 100644 --- a/radix-engine-toolkit-core/src/functions/execution.rs +++ b/radix-engine-toolkit-core/src/functions/execution.rs @@ -98,9 +98,15 @@ pub fn analyze( }, ))) } - if let Some((account, stakes)) = stake_transaction_visitor.output() { + if let Some((accounts_withdrawn_from, accounts_deposited_into, stakes)) = + stake_transaction_visitor.output() + { transaction_types.push(TransactionType::StakeTransaction(Box::new( - StakeTransactionType { account, stakes }, + StakeTransactionType { + accounts_withdrawn_from, + accounts_deposited_into, + stakes, + }, ))) } if let Some((account_withdraws, account_deposits)) = general_transaction_visitor.output() { @@ -277,7 +283,8 @@ pub struct GeneralTransactionType { #[derive(Clone, Debug, PartialEq, Eq)] pub struct StakeTransactionType { - pub account: ComponentAddress, + pub accounts_withdrawn_from: HashMap, + pub accounts_deposited_into: HashMap>, pub stakes: HashMap, } diff --git a/radix-engine-toolkit-core/src/instruction_visitor/visitors/transaction_type/stake_visitor.rs b/radix-engine-toolkit-core/src/instruction_visitor/visitors/transaction_type/stake_visitor.rs index 8d67a155..19dfc94f 100644 --- a/radix-engine-toolkit-core/src/instruction_visitor/visitors/transaction_type/stake_visitor.rs +++ b/radix-engine-toolkit-core/src/instruction_visitor/visitors/transaction_type/stake_visitor.rs @@ -18,6 +18,7 @@ use crate::instruction_visitor::core::error::InstructionVisitorError; use crate::instruction_visitor::core::traits::InstructionVisitor; use crate::sbor::indexed_manifest_value::IndexedManifestValue; +use crate::traits::CheckedAddAssign; use crate::utils::{is_account, is_validator}; use radix_engine::system::system_modules::execution_trace::{ResourceSpecifier, WorktopChange}; @@ -29,12 +30,16 @@ use scrypto::prelude::*; use transaction::prelude::*; use transaction::validation::ManifestIdAllocator; +/// An instruction visitor to detect if a transaction is a stake transaction or not. pub struct StakeVisitor<'r> { /// The execution trace from the preview receipt execution_trace: &'r TransactionExecutionTrace, - /// The account withdrawn from - tracked to ensure that we deposit into the same account. - account_withdrawn_from: Option, + /// The accounts withdrawn from. + accounts_withdrawn_from: HashMap, + + /// The accounts deposited into. + accounts_deposited_into: HashMap>, /// Maps the validator component address to a map of the LSU's resource address and amount /// obtained as part of staking. @@ -57,7 +62,8 @@ impl<'r> StakeVisitor<'r> { pub fn new(execution_trace: &'r TransactionExecutionTrace) -> Self { Self { execution_trace, - account_withdrawn_from: Default::default(), + accounts_withdrawn_from: Default::default(), + accounts_deposited_into: Default::default(), validator_stake_mapping: Default::default(), is_illegal_state: Default::default(), id_allocator: Default::default(), @@ -70,19 +76,29 @@ impl<'r> StakeVisitor<'r> { *resource_address == XRD || self.validator_stake_mapping.values().any( |Stake { - liquid_stake_units_resource_address, + stake_unit_resource_address: liquid_stake_units_resource_address, .. }| liquid_stake_units_resource_address == resource_address, ) } - pub fn output(self) -> Option<(ComponentAddress, HashMap)> { + pub fn output( + self, + ) -> Option<( + HashMap, /* Accounts withdrawn from */ + HashMap>, /* Accounts deposited into */ + HashMap, /* Validator stakes */ + )> { match ( self.is_illegal_state, self.validator_stake_mapping.is_empty(), - self.account_withdrawn_from, + self.accounts_withdrawn_from.is_empty(), ) { - (false, false, Some(account)) => Some((account, self.validator_stake_mapping)), + (false, false, false) => Some(( + self.accounts_withdrawn_from, + self.accounts_deposited_into, + self.validator_stake_mapping, + )), _ => None, } } @@ -91,8 +107,8 @@ impl<'r> StakeVisitor<'r> { #[derive(Clone, Debug, PartialEq, Eq)] pub struct Stake { pub staked_xrd: Decimal, - pub liquid_stake_units_resource_address: ResourceAddress, - pub liquid_stake_units_amount: Decimal, + pub stake_unit_resource_address: ResourceAddress, + pub stake_unit_amount: Decimal, } impl<'r> InstructionVisitor for StakeVisitor<'r> { @@ -128,7 +144,7 @@ impl<'r> InstructionVisitor for StakeVisitor<'r> { // Ensure arguments are valid and that the resource withdrawn is XRD. let Some(AccountWithdrawInput { resource_address: XRD, - .. + amount, }) = manifest_encode(&args) .ok() .and_then(|encoded| manifest_decode(&encoded).ok()) @@ -136,22 +152,18 @@ impl<'r> InstructionVisitor for StakeVisitor<'r> { self.is_illegal_state = true; return Ok(()); }; - // Ensure that this is either the first time we withdraw or that this is the - // account we withdraw from all throughout the manifest. let account_address = ComponentAddress::try_from(*global_address) .expect("We have checked that it's a component address"); - if let Some(previous_withdraw_component_address) = self.account_withdrawn_from { - if previous_withdraw_component_address != account_address { - self.is_illegal_state = true; - return Ok(()); - } - } else { - self.account_withdrawn_from = Some( - (*global_address) - .try_into() - .expect("We have checked that it's a component address"), - ); - } + + let Some(()) = self + .accounts_withdrawn_from + .entry(account_address) + .or_default() + .checked_add_assign(&amount) + else { + self.is_illegal_state = true; + return Ok(()); + }; } /* Only permit account deposits to the same account withdrawn from and only with authed @@ -161,21 +173,60 @@ impl<'r> InstructionVisitor for StakeVisitor<'r> { && (method_name == ACCOUNT_DEPOSIT_IDENT || method_name == ACCOUNT_DEPOSIT_BATCH_IDENT) { - match self.account_withdrawn_from { - Some(withdraw_account) - if withdraw_account.into_node_id() == global_address.into_node_id() => { - } - Some(..) | None => { + let account_address = ComponentAddress::try_from(*global_address) + .expect("We have checked that it's a component address"); + + let indexed_manifest_value = IndexedManifestValue::from_manifest_value(args); + for bucket in indexed_manifest_value.buckets() { + let Some((resource_address, amount)) = self.bucket_tracker.remove(bucket) + else { self.is_illegal_state = true; return Ok(()); - } + }; + + let Some(()) = self + .accounts_deposited_into + .entry(account_address) + .or_default() + .entry(resource_address) + .or_default() + .checked_add_assign(&amount) + else { + self.is_illegal_state = true; + return Ok(()); + }; } - let indexed_manifest_value = IndexedManifestValue::from_manifest_value(args); - for bucket in indexed_manifest_value.buckets() { - if self.bucket_tracker.remove(bucket).is_none() { + + let Some(worktop_changes) = self + .execution_trace + .worktop_changes() + .get(&self.instruction_index) + .cloned() + else { + return Ok(()); + }; + + for worktop_change in worktop_changes { + let WorktopChange::Take(ResourceSpecifier::Amount( + resource_address, + amount, + )) = worktop_change + else { self.is_illegal_state = true; return Ok(()); - } + }; + + let Some(()) = self + .accounts_deposited_into + .entry(account_address) + .or_default() + .entry(resource_address) + .or_default() + .checked_add_assign(&amount) + else { + self.is_illegal_state = true; + return Ok(()); + }; } } /* Staking to a validator */ @@ -221,11 +272,11 @@ impl<'r> InstructionVisitor for StakeVisitor<'r> { .validator_stake_mapping .entry(validator_address) .or_insert(Stake { - liquid_stake_units_resource_address, - liquid_stake_units_amount: Default::default(), + stake_unit_resource_address: liquid_stake_units_resource_address, + stake_unit_amount: Default::default(), staked_xrd: Default::default(), }); - entry.liquid_stake_units_amount += liquid_stake_units_amount; + entry.stake_unit_amount += liquid_stake_units_amount; entry.staked_xrd += xrd_staked_amount; } } diff --git a/radix-engine-toolkit-core/src/lib.rs b/radix-engine-toolkit-core/src/lib.rs index 2a95db14..8772d53d 100644 --- a/radix-engine-toolkit-core/src/lib.rs +++ b/radix-engine-toolkit-core/src/lib.rs @@ -26,4 +26,5 @@ pub mod models; pub mod sbor; pub mod schema_visitor; pub mod statics; +pub mod traits; pub mod utils; diff --git a/radix-engine-toolkit-core/src/traits.rs b/radix-engine-toolkit-core/src/traits.rs new file mode 100644 index 00000000..627012ac --- /dev/null +++ b/radix-engine-toolkit-core/src/traits.rs @@ -0,0 +1,12 @@ +use radix_engine_common::prelude::*; + +pub trait CheckedAddAssign { + fn checked_add_assign(&mut self, other: &Self) -> Option<()>; +} + +impl CheckedAddAssign for Decimal { + fn checked_add_assign(&mut self, other: &Self) -> Option<()> { + *self = self.checked_add(*other)?; + Some(()) + } +} diff --git a/radix-engine-toolkit-core/tests/execution.rs b/radix-engine-toolkit-core/tests/execution.rs index d768ab7c..4daa685a 100644 --- a/radix-engine-toolkit-core/tests/execution.rs +++ b/radix-engine-toolkit-core/tests/execution.rs @@ -393,7 +393,8 @@ fn simple_stake_transaction_can_be_caught_by_ret() { // Assert let Some(StakeTransactionType { - account: stake_account, + accounts_withdrawn_from, + accounts_deposited_into, stakes, }) = transaction_types.iter().find_map(|tx_type| match tx_type { TransactionType::StakeTransaction(stake) => Some(stake.as_ref()), @@ -402,19 +403,28 @@ fn simple_stake_transaction_can_be_caught_by_ret() { else { panic!("Stake transaction type not found!") }; - assert_eq!(*stake_account, account); + assert_eq!( + accounts_withdrawn_from.clone(), + hashmap! { account => dec!("100") } + ); + assert_eq!( + accounts_deposited_into.clone(), + hashmap! { account => hashmap! { + stake_unit_resource => dec!("100") + }} + ); assert_eq!( stakes.get(&validator), Some(&Stake { - liquid_stake_units_amount: dec!("100"), + stake_unit_amount: dec!("100"), staked_xrd: dec!("100"), - liquid_stake_units_resource_address: stake_unit_resource + stake_unit_resource_address: stake_unit_resource }) ); } #[test] -fn staking_of_zero_xrd_is_considered_staking() { +fn staking_of_zero_xrd_is_not_considered_staking() { // Arrange let mut test_runner = TestRunnerBuilder::new().without_trace().build(); let (pk, _, account) = test_runner.new_account(false); @@ -475,8 +485,8 @@ fn staking_of_zero_xrd_is_considered_staking() { // Assert let Some(StakeTransactionType { - account: stake_account, - stakes, + accounts_withdrawn_from, + stakes, .. }) = transaction_types.iter().find_map(|tx_type| match tx_type { TransactionType::StakeTransaction(stake) => Some(stake.as_ref()), _ => None, @@ -484,13 +494,16 @@ fn staking_of_zero_xrd_is_considered_staking() { else { panic!("Stake transaction type not found!") }; - assert_eq!(*stake_account, account); + assert_eq!( + accounts_withdrawn_from.clone(), + hashmap! { account => dec!("0") } + ); assert_eq!( stakes.get(&validator), Some(&Stake { - liquid_stake_units_amount: dec!("0"), + stake_unit_amount: dec!("0"), staked_xrd: dec!("0"), - liquid_stake_units_resource_address: stake_unit_resource + stake_unit_resource_address: stake_unit_resource }) ); } @@ -560,7 +573,8 @@ fn additional_stake_of_zero_has_no_effect_on_detection() { // Assert let Some(StakeTransactionType { - account: stake_account, + accounts_withdrawn_from, + accounts_deposited_into, stakes, }) = transaction_types.iter().find_map(|tx_type| match tx_type { TransactionType::StakeTransaction(stake) => Some(stake.as_ref()), @@ -569,24 +583,38 @@ fn additional_stake_of_zero_has_no_effect_on_detection() { else { panic!("Stake transaction type not found!") }; - assert_eq!(*stake_account, account); + assert_eq!( + accounts_withdrawn_from.clone(), + hashmap! { account => dec!("100") } + ); + assert_eq!( + accounts_deposited_into.clone(), + hashmap! { account => hashmap! { + stake_unit_resource => dec!("100") + }} + ); assert_eq!( stakes.get(&validator), Some(&Stake { - liquid_stake_units_amount: dec!("100"), + stake_unit_amount: dec!("100"), staked_xrd: dec!("100"), - liquid_stake_units_resource_address: stake_unit_resource + stake_unit_resource_address: stake_unit_resource }) ); } #[test] -fn withdraws_from_multiple_accounts_invalidate_stake_rules() { +fn withdraw_from_multiple_accounts_and_stake_is_a_valid_stake_transaction() { // Arrange let mut test_runner = TestRunnerBuilder::new().without_trace().build(); let (pk, _, account1) = test_runner.new_account(false); let (_, _, account2) = test_runner.new_account(false); let validator = test_runner.new_validator_with_pub_key(pk, account1); + let ValidatorSubstate { + stake_unit_resource, + claim_nft: _, + .. + } = test_runner.get_validator_info(validator); test_runner .execute_manifest( @@ -617,7 +645,7 @@ fn withdraws_from_multiple_accounts_invalidate_stake_rules() { let manifest = ManifestBuilder::new() .withdraw_from_account(account1, XRD, 100) .withdraw_from_account(account2, XRD, 50) - .take_from_worktop(XRD, 100, "XRD") + .take_from_worktop(XRD, 150, "XRD") .stake_validator(validator, "XRD") .deposit_batch(account1) .build(); @@ -638,24 +666,52 @@ fn withdraws_from_multiple_accounts_invalidate_stake_rules() { ); // Assert - assert!(transaction_types - .iter() - .find_map(|tx_type| { - match tx_type { - TransactionType::StakeTransaction(stake) => Some(stake.as_ref()), - _ => None, - } + let Some(StakeTransactionType { + accounts_withdrawn_from, + accounts_deposited_into, + stakes, + }) = transaction_types.iter().find_map(|tx_type| match tx_type { + TransactionType::StakeTransaction(stake) => Some(stake.as_ref()), + _ => None, + }) + else { + panic!("Stake transaction type not found!") + }; + assert_eq!( + accounts_withdrawn_from.clone(), + hashmap! { + account1 => dec!("100"), + account2 => dec!("50"), + } + ); + assert_eq!( + accounts_deposited_into.clone(), + hashmap! { account1 => hashmap! { + stake_unit_resource => dec!("150") + }} + ); + assert_eq!( + stakes.get(&validator), + Some(&Stake { + stake_unit_amount: dec!("150"), + staked_xrd: dec!("150"), + stake_unit_resource_address: stake_unit_resource }) - .is_none()) + ); } #[test] -fn deposits_into_a_different_account_invalidate_stake_rules() { +fn staking_then_depositing_into_multiple_accounts_is_allowed() { // Arrange let mut test_runner = TestRunnerBuilder::new().without_trace().build(); let (pk, _, account1) = test_runner.new_account(false); let (_, _, account2) = test_runner.new_account(false); let validator = test_runner.new_validator_with_pub_key(pk, account1); + let ValidatorSubstate { + stake_unit_resource, + claim_nft: _, + .. + } = test_runner.get_validator_info(validator); test_runner .execute_manifest( @@ -687,7 +743,10 @@ fn deposits_into_a_different_account_invalidate_stake_rules() { .withdraw_from_account(account1, XRD, 100) .take_from_worktop(XRD, 100, "XRD") .stake_validator(validator, "XRD") - .deposit_batch(account2) + .take_from_worktop(stake_unit_resource, 50, "SU1") + .deposit(account1, "SU1") + .take_from_worktop(stake_unit_resource, 50, "SU2") + .deposit(account2, "SU2") .build(); let receipt = test_runner.preview_manifest( manifest.clone(), @@ -706,25 +765,52 @@ fn deposits_into_a_different_account_invalidate_stake_rules() { ); // Assert - assert!(transaction_types - .iter() - .find_map(|tx_type| { - match tx_type { - TransactionType::StakeTransaction(stake) => Some(stake.as_ref()), - _ => None, + let Some(StakeTransactionType { + accounts_withdrawn_from, + accounts_deposited_into, + stakes, + }) = transaction_types.iter().find_map(|tx_type| match tx_type { + TransactionType::StakeTransaction(stake) => Some(stake.as_ref()), + _ => None, + }) + else { + panic!("Stake transaction type not found!") + }; + assert_eq!( + accounts_withdrawn_from.clone(), + hashmap! { account1 => dec!("100") } + ); + assert_eq!( + accounts_deposited_into.clone(), + hashmap! { + account1 => hashmap! { + stake_unit_resource => dec!("50") + }, + account2 => hashmap! { + stake_unit_resource => dec!("50") } + } + ); + assert_eq!( + stakes.get(&validator), + Some(&Stake { + stake_unit_amount: dec!("100"), + staked_xrd: dec!("100"), + stake_unit_resource_address: stake_unit_resource }) - .is_none()) + ); } #[test] -fn depositing_through_deposit_method_is_allowed() { +fn staking_then_depositing_and_depositing_batch_succeeds() { // Arrange let mut test_runner = TestRunnerBuilder::new().without_trace().build(); - let (pk, _, account) = test_runner.new_account(false); - let validator = test_runner.new_validator_with_pub_key(pk, account); + let (pk, _, account1) = test_runner.new_account(false); + let (_, _, account2) = test_runner.new_account(false); + let validator = test_runner.new_validator_with_pub_key(pk, account1); let ValidatorSubstate { stake_unit_resource, + claim_nft: _, .. } = test_runner.get_validator_info(validator); @@ -733,7 +819,7 @@ fn depositing_through_deposit_method_is_allowed() { ManifestBuilder::new() .lock_fee_from_faucet() .create_proof_from_account_of_non_fungible( - account, + account1, NonFungibleGlobalId::new( VALIDATOR_OWNER_BADGE, NonFungibleLocalId::bytes(validator.as_node_id().0).unwrap(), @@ -755,11 +841,12 @@ fn depositing_through_deposit_method_is_allowed() { // Act let manifest = ManifestBuilder::new() - .withdraw_from_account(account, XRD, 100) + .withdraw_from_account(account1, XRD, 100) .take_from_worktop(XRD, 100, "XRD") .stake_validator(validator, "XRD") - .take_from_worktop(stake_unit_resource, 100, "StakeUnits") - .deposit(account, "StakeUnits") + .take_from_worktop(stake_unit_resource, 50, "SU1") + .deposit(account1, "SU1") + .deposit_batch(account2) .build(); let receipt = test_runner.preview_manifest( manifest.clone(), @@ -779,7 +866,8 @@ fn depositing_through_deposit_method_is_allowed() { // Assert let Some(StakeTransactionType { - account: stake_account, + accounts_withdrawn_from, + accounts_deposited_into, stakes, }) = transaction_types.iter().find_map(|tx_type| match tx_type { TransactionType::StakeTransaction(stake) => Some(stake.as_ref()), @@ -788,19 +876,33 @@ fn depositing_through_deposit_method_is_allowed() { else { panic!("Stake transaction type not found!") }; - assert_eq!(*stake_account, account); + assert_eq!( + accounts_withdrawn_from.clone(), + hashmap! { account1 => dec!("100") } + ); + assert_eq!( + accounts_deposited_into.clone(), + hashmap! { + account1 => hashmap! { + stake_unit_resource => dec!("50") + }, + account2 => hashmap! { + stake_unit_resource => dec!("50") + } + } + ); assert_eq!( stakes.get(&validator), Some(&Stake { - liquid_stake_units_amount: dec!("100"), + stake_unit_amount: dec!("100"), staked_xrd: dec!("100"), - liquid_stake_units_resource_address: stake_unit_resource + stake_unit_resource_address: stake_unit_resource }) ); } #[test] -fn aggregation_of_stake_works_as_expected() { +fn staking_aggregation_is_done_as_expected() { // Arrange let mut test_runner = TestRunnerBuilder::new().without_trace().build(); let (pk, _, account) = test_runner.new_account(false); @@ -864,7 +966,8 @@ fn aggregation_of_stake_works_as_expected() { // Assert let Some(StakeTransactionType { - account: stake_account, + accounts_withdrawn_from, + accounts_deposited_into, stakes, }) = transaction_types.iter().find_map(|tx_type| match tx_type { TransactionType::StakeTransaction(stake) => Some(stake.as_ref()), @@ -873,19 +976,30 @@ fn aggregation_of_stake_works_as_expected() { else { panic!("Stake transaction type not found!") }; - assert_eq!(*stake_account, account); + assert_eq!( + accounts_withdrawn_from.clone(), + hashmap! { account => dec!("140") } + ); + assert_eq!( + accounts_deposited_into.clone(), + hashmap! { + account => hashmap! { + stake_unit_resource => dec!("140") + }, + } + ); assert_eq!( stakes.get(&validator), Some(&Stake { - liquid_stake_units_amount: dec!("140"), + stake_unit_amount: dec!("140"), staked_xrd: dec!("140"), - liquid_stake_units_resource_address: stake_unit_resource + stake_unit_resource_address: stake_unit_resource }) ); } #[test] -fn multiple_validators_works_as_expected() { +fn complex_staking_transaction_is_detected_and_summarized_as_expected() { // Arrange let mut test_runner = TestRunnerBuilder::new().without_trace().build(); let (pk, _, account) = test_runner.new_account(false); @@ -952,12 +1066,11 @@ fn multiple_validators_works_as_expected() { // Act let manifest = ManifestBuilder::new() - .withdraw_from_account(account, XRD, 100) - .take_from_worktop(XRD, 100, "XRD") - .stake_validator(validator1, "XRD") - .withdraw_from_account(account, XRD, 40) - .take_from_worktop(XRD, 40, "XRD1") - .stake_validator(validator2, "XRD1") + .withdraw_from_account(account, XRD, 1000) + .take_from_worktop(XRD, 600, "XRD1") + .stake_validator(validator1, "XRD1") + .take_from_worktop(XRD, 200, "XRD2") + .stake_validator(validator2, "XRD2") .deposit_batch(account) .build(); let receipt = test_runner.preview_manifest( @@ -978,7 +1091,8 @@ fn multiple_validators_works_as_expected() { // Assert let Some(StakeTransactionType { - account: stake_account, + accounts_withdrawn_from, + accounts_deposited_into, stakes, }) = transaction_types.iter().find_map(|tx_type| match tx_type { TransactionType::StakeTransaction(stake) => Some(stake.as_ref()), @@ -987,21 +1101,34 @@ fn multiple_validators_works_as_expected() { else { panic!("Stake transaction type not found!") }; - assert_eq!(*stake_account, account); + assert_eq!( + accounts_withdrawn_from.clone(), + hashmap! { account => dec!("1000") } + ); + assert_eq!( + accounts_deposited_into.clone(), + hashmap! { + account => hashmap! { + stake_unit_resource1 => dec!("600"), + stake_unit_resource2 => dec!("200"), + XRD => dec!("200"), + }, + } + ); assert_eq!( stakes.get(&validator1), Some(&Stake { - liquid_stake_units_amount: dec!("100"), - staked_xrd: dec!("100"), - liquid_stake_units_resource_address: stake_unit_resource1 + stake_unit_amount: dec!("600"), + staked_xrd: dec!("600"), + stake_unit_resource_address: stake_unit_resource1 }) ); assert_eq!( stakes.get(&validator2), Some(&Stake { - liquid_stake_units_amount: dec!("40"), - staked_xrd: dec!("40"), - liquid_stake_units_resource_address: stake_unit_resource2 + stake_unit_amount: dec!("200"), + staked_xrd: dec!("200"), + stake_unit_resource_address: stake_unit_resource2 }) ); }