Skip to content

Commit

Permalink
Add detection of staking transaction type to core toolkit
Browse files Browse the repository at this point in the history
  • Loading branch information
0xOmarA committed Sep 13, 2023
1 parent 9e0ef9b commit eede4b1
Show file tree
Hide file tree
Showing 6 changed files with 1,022 additions and 5 deletions.
1 change: 1 addition & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"hammunet",
"kisharnet",
"Localnet",
"LSU's",
"mardunet",
"Milestonenet",
"moka",
Expand Down
16 changes: 16 additions & 0 deletions radix-engine-toolkit-core/src/functions/execution.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ use crate::instruction_visitor::visitors::transaction_type::general_transaction_
use crate::instruction_visitor::visitors::transaction_type::reserved_instructions::ReservedInstruction;
use crate::instruction_visitor::visitors::transaction_type::reserved_instructions::ReservedInstructionsVisitor;
use crate::instruction_visitor::visitors::transaction_type::simple_transfer_visitor::*;
use crate::instruction_visitor::visitors::transaction_type::stake_visitor::Stake;
use crate::instruction_visitor::visitors::transaction_type::stake_visitor::StakeVisitor;
use crate::instruction_visitor::visitors::transaction_type::transfer_visitor::*;
use crate::models::node_id::InvalidEntityTypeIdError;
use crate::models::node_id::TypedNodeId;
Expand All @@ -47,6 +49,7 @@ pub fn analyze(
let mut account_deposit_settings_visitor = AccountDepositSettingsVisitor::default();
let mut general_transaction_visitor = GeneralTransactionTypeVisitor::new(execution_trace);
let mut reserved_instructions_visitor = ReservedInstructionsVisitor::default();
let mut stake_transaction_visitor = StakeVisitor::new(execution_trace);

traverse(
instructions,
Expand All @@ -57,6 +60,7 @@ pub fn analyze(
&mut account_deposit_settings_visitor,
&mut general_transaction_visitor,
&mut reserved_instructions_visitor,
&mut stake_transaction_visitor,
],
)?;

Expand Down Expand Up @@ -94,6 +98,11 @@ pub fn analyze(
},
)))
}
if let Some((account, stakes)) = stake_transaction_visitor.output() {
transaction_types.push(TransactionType::StakeTransaction(Box::new(
StakeTransactionType { account, stakes },
)))
}
if let Some((account_withdraws, account_deposits)) = general_transaction_visitor.output() {
transaction_types.push(TransactionType::GeneralTransaction(Box::new(
GeneralTransactionType {
Expand Down Expand Up @@ -229,6 +238,7 @@ pub enum TransactionType {
Transfer(Box<TransferTransactionType>),
AccountDepositSettings(Box<AccountDepositSettingsTransactionType>),
GeneralTransaction(Box<GeneralTransactionType>),
StakeTransaction(Box<StakeTransactionType>),
}

#[derive(Clone, Debug, PartialEq, Eq)]
Expand Down Expand Up @@ -265,6 +275,12 @@ pub struct GeneralTransactionType {
HashMap<ResourceAddress, HashMap<NonFungibleLocalId, ScryptoValue>>,
}

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct StakeTransactionType {
pub account: ComponentAddress,
pub stakes: HashMap<ComponentAddress, Stake>,
}

#[derive(Clone, Debug)]
pub enum ExecutionModuleError {
TransactionWasNotCommittedSuccessfully(TransactionReceiptV1),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,5 @@ pub mod account_deposit_settings_visitor;
pub mod general_transaction_visitor;
pub mod reserved_instructions;
pub mod simple_transfer_visitor;
pub mod stake_visitor;
pub mod transfer_visitor;
Original file line number Diff line number Diff line change
@@ -0,0 +1,309 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// 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::instruction_visitor::core::error::InstructionVisitorError;
use crate::instruction_visitor::core::traits::InstructionVisitor;
use crate::sbor::indexed_manifest_value::IndexedManifestValue;
use crate::utils::{is_account, is_validator};

use radix_engine::system::system_modules::execution_trace::{ResourceSpecifier, WorktopChange};
use radix_engine::transaction::*;
use radix_engine_common::prelude::*;
use radix_engine_interface::blueprints::consensus_manager::VALIDATOR_STAKE_IDENT;
use scrypto::blueprints::account::*;
use scrypto::prelude::*;
use transaction::prelude::*;
use transaction::validation::ManifestIdAllocator;

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<ComponentAddress>,

/// Maps the validator component address to a map of the LSU's resource address and amount
/// obtained as part of staking.
validator_stake_mapping: HashMap<ComponentAddress, Stake>,

/// Tracks if the visitor is currently in an illegal state or not.
is_illegal_state: bool,

/// Used to allocate new ids
id_allocator: ManifestIdAllocator,

/// Tracks the buckets and their contents
bucket_tracker: HashMap<ManifestBucket, (ResourceAddress, Decimal)>,

/// The index of the current instruction.
instruction_index: usize,
}

impl<'r> StakeVisitor<'r> {
pub fn new(execution_trace: &'r TransactionExecutionTrace) -> Self {
Self {
execution_trace,
account_withdrawn_from: Default::default(),
validator_stake_mapping: Default::default(),
is_illegal_state: Default::default(),
id_allocator: Default::default(),
bucket_tracker: Default::default(),
instruction_index: Default::default(),
}
}

fn is_take_from_worktop_allowed(&self, resource_address: &ResourceAddress) -> bool {
*resource_address == XRD
|| self.validator_stake_mapping.values().any(
|Stake {
liquid_stake_units_resource_address,
..
}| liquid_stake_units_resource_address == resource_address,
)
}

pub fn output(self) -> Option<(ComponentAddress, HashMap<ComponentAddress, Stake>)> {
match (
self.is_illegal_state,
self.validator_stake_mapping.is_empty(),
self.account_withdrawn_from,
) {
(false, false, Some(account)) => Some((account, self.validator_stake_mapping)),
_ => None,
}
}
}

#[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,
}

impl<'r> InstructionVisitor for StakeVisitor<'r> {
fn is_enabled(&self) -> bool {
!self.is_illegal_state
}

fn post_visit(&mut self) -> Result<(), InstructionVisitorError> {
self.instruction_index += 1;
Ok(())
}

fn visit_instruction(
&mut self,
instruction: &InstructionV1,
) -> Result<(), InstructionVisitorError> {
match instruction {
InstructionV1::CallMethod {
address,
method_name,
args,
} => {
// Filter: We only permit static address - no dynamic or named addresses are allowed
let global_address = if let DynamicGlobalAddress::Static(address) = address {
address
} else {
self.is_illegal_state = true;
return Ok(());
};

/* Only withdraw of XRD is allowed from account */
if is_account(global_address) && method_name == ACCOUNT_WITHDRAW_IDENT {
// Ensure arguments are valid and that the resource withdrawn is XRD.
let Some(AccountWithdrawInput {
resource_address: XRD,
..
}) = manifest_encode(&args)
.ok()
.and_then(|encoded| manifest_decode(&encoded).ok())
else {
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"),
);
}
}
/*
Only permit account deposits to the same account withdrawn from and only with authed
methods.
*/
else if is_account(global_address)
&& (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 => {
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() {
self.is_illegal_state = true;
return Ok(());
}
}
}
/* Staking to a validator */
else if is_validator(global_address) && method_name == VALIDATOR_STAKE_IDENT {
let validator_address = ComponentAddress::try_from(*global_address)
.expect("We have checked that it's a component address");

let Some((bucket @ ManifestBucket(..),)) = manifest_encode(&args)
.ok()
.and_then(|encoded| manifest_decode(&encoded).ok())
else {
self.is_illegal_state = true;
return Ok(());
};
let Some((XRD, xrd_staked_amount)) = self.bucket_tracker.remove(&bucket) else {
self.is_illegal_state = true;
return Ok(());
};

let (liquid_stake_units_resource_address, liquid_stake_units_amount) =
match self
.execution_trace
.worktop_changes()
.get(&self.instruction_index)
.map(|x| x.as_slice())
{
Some(
[WorktopChange::Put(ResourceSpecifier::Amount(
resource_address,
amount,
))],
) => (*resource_address, *amount),
Some([]) | None => {
return Ok(());
}
_ => {
self.is_illegal_state = true;
return Ok(());
}
};

let entry = self
.validator_stake_mapping
.entry(validator_address)
.or_insert(Stake {
liquid_stake_units_resource_address,
liquid_stake_units_amount: Default::default(),
staked_xrd: Default::default(),
});
entry.liquid_stake_units_amount += liquid_stake_units_amount;
entry.staked_xrd += xrd_staked_amount;
}
}

InstructionV1::TakeAllFromWorktop { resource_address } => {
if self.is_take_from_worktop_allowed(resource_address) {
let amount = match self
.execution_trace
.worktop_changes()
.get(&self.instruction_index)
.map(|vec| vec.as_slice())
{
Some(
[WorktopChange::Take(ResourceSpecifier::Amount(
take_resource_address,
amount,
))],
) if resource_address == take_resource_address => *amount,
Some([]) | None => Decimal::ZERO,
_ => {
self.is_illegal_state = true;
return Ok(());
}
};
let bucket_id = self.id_allocator.new_bucket_id();
self.bucket_tracker
.insert(bucket_id, (*resource_address, amount));
} else {
self.is_illegal_state = true;
return Ok(());
}
}
InstructionV1::TakeFromWorktop {
resource_address,
amount,
} => {
if self.is_take_from_worktop_allowed(resource_address) {
let bucket_id = self.id_allocator.new_bucket_id();
self.bucket_tracker
.insert(bucket_id, (*resource_address, *amount));
} else {
self.is_illegal_state = true;
return Ok(());
}
}

/* Disallowed Instructions */
InstructionV1::CallFunction { .. }
| InstructionV1::CallRoyaltyMethod { .. }
| InstructionV1::CallMetadataMethod { .. }
| InstructionV1::CallRoleAssignmentMethod { .. }
| InstructionV1::CallDirectVaultMethod { .. }
| InstructionV1::DropNamedProofs
| InstructionV1::DropAllProofs
| InstructionV1::DropAuthZoneProofs { .. }
| InstructionV1::DropAuthZoneRegularProofs { .. }
| InstructionV1::DropAuthZoneSignatureProofs { .. }
| InstructionV1::CreateProofFromAuthZoneOfAll { .. }
| InstructionV1::CreateProofFromBucketOfAmount { .. }
| InstructionV1::CreateProofFromBucketOfNonFungibles { .. }
| InstructionV1::CreateProofFromBucketOfAll { .. }
| InstructionV1::BurnResource { .. }
| InstructionV1::CloneProof { .. }
| InstructionV1::DropProof { .. }
| InstructionV1::TakeNonFungiblesFromWorktop { .. }
| InstructionV1::ReturnToWorktop { .. }
| InstructionV1::AssertWorktopContainsAny { .. }
| InstructionV1::AssertWorktopContains { .. }
| InstructionV1::AssertWorktopContainsNonFungibles { .. }
| InstructionV1::PopFromAuthZone { .. }
| InstructionV1::PushToAuthZone { .. }
| InstructionV1::CreateProofFromAuthZoneOfAmount { .. }
| InstructionV1::CreateProofFromAuthZoneOfNonFungibles { .. }
| InstructionV1::AllocateGlobalAddress { .. } => {
self.is_illegal_state = true;
return Ok(());
}
};
Ok(())
}
}
Loading

0 comments on commit eede4b1

Please sign in to comment.