Skip to content

Commit

Permalink
feat(template): allow resource actions to be restricted by component (#…
Browse files Browse the repository at this point in the history
…868)

Description
---
Allows resource access rules to restrict actions to one or more specific
components/templates
Adds the ability to get pre-allocate component addresses in code before
component creation

Motivation and Context
---
Thanks to @mrnaveira for this idea!

Some use cases may want a user to mint their own badge by executing a
method on a specific template without explicit permission from the
component owner (permissionless). This PR allows a template author to be
permissable in who can perform resource actions, but force all users to
interact with their component/template to control how those resource
actions are used.

To set component-restricted rules in code, the component address is
required. Often access rules are set in the constructor of the
component, where the address is not known. This PR adds the ability ask
the runtime for a "pre-allocated" component address and later apply that
to the component that is to be created.

How Has This Been Tested?
---
New unit tests

What process can a PR reviewer use to test or verify this change?
---

Template using the new access rules, e.g
```rust
  let allocation = CallerContext::allocate_component_address();
            let tokens = ResourceBuilder::fungible()
                .initial_supply(1000)
                .mintable(AccessRule::Restricted(RestrictedAccessRule::Require(
                    RequireRule::Require(allocation.address().clone().into()),
                )))
                .build_bucket();

  Component::new(Self {
                tokens: Vault::from_bucket(tokens),
            })
            .with_address_allocation(allocation)
            .create()
```

Breaking Changes
---

- [x] None
- [ ] Requires data directory to be deleted
- [ ] Other - Please specify
  • Loading branch information
sdbondi authored Jan 4, 2024
1 parent 3d09615 commit 516c5d1
Show file tree
Hide file tree
Showing 24 changed files with 405 additions and 51 deletions.
7 changes: 7 additions & 0 deletions dan_layer/engine/src/runtime/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,11 @@ pub enum RuntimeError {
DuplicateBucket { bucket_id: BucketId },
#[error("Duplicate proof {proof_id}")]
DuplicateProof { proof_id: ProofId },

#[error("Address allocation not found with id {id}")]
AddressAllocationNotFound { id: u32 },
#[error("Address allocation type mismatch: {address}")]
AddressAllocationTypeMismatch { address: SubstateAddress },
}

impl RuntimeError {
Expand Down Expand Up @@ -251,6 +256,8 @@ pub enum TransactionCommitError {
DanglingProofs { count: usize },
#[error("Locked value (amount: {locked_amount}) remaining in vault {vault_id}")]
DanglingLockedValueInVault { vault_id: VaultId, locked_amount: Amount },
#[error("{count} dangling address allocations remain after transaction execution")]
DanglingAddressAllocations { count: usize },
#[error("{} orphaned substate(s) detected: {}", .substates.len(), .substates.join(", "))]
OrphanedSubstates { substates: Vec<String> },
#[error("{count} dangling items in workspace after transaction execution")]
Expand Down
7 changes: 7 additions & 0 deletions dan_layer/engine/src/runtime/impl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,12 @@ impl<TTemplateProvider: TemplateProvider<Template = LoadedTemplate>> RuntimeInte
.map(|l| l.address().as_component_address().unwrap());
Ok(InvokeResult::encode(&maybe_address)?)
}),
CallerContextAction::AllocateNewComponentAddress => self.tracker.write_with(|state| {
let (template, _) = state.current_template()?;
let address = self.tracker.id_provider().new_component_address(*template, None)?;
let allocation = state.new_address_allocation(address)?;
Ok(InvokeResult::encode(&allocation)?)
}),
}
}

Expand Down Expand Up @@ -322,6 +328,7 @@ impl<TTemplateProvider: TemplateProvider<Template = LoadedTemplate>> RuntimeInte
arg.owner_rule,
arg.access_rules,
arg.component_id,
arg.address_allocation,
)?;
Ok(InvokeResult::encode(&component_address)?)
},
Expand Down
12 changes: 8 additions & 4 deletions dan_layer/engine/src/runtime/tracker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ use tari_engine_types::{
use tari_template_lib::{
auth::{ComponentAccessRules, OwnerRule},
crypto::RistrettoPublicKeyBytes,
models::{Amount, BucketId, ComponentAddress, Metadata, UnclaimedConfidentialOutputAddress},
models::{AddressAllocation, Amount, BucketId, ComponentAddress, Metadata, UnclaimedConfidentialOutputAddress},
Hash,
};
use tari_transaction::id_provider::IdProvider;
Expand Down Expand Up @@ -161,14 +161,18 @@ impl StateTracker {
owner_rule: OwnerRule,
access_rules: ComponentAccessRules,
component_id: Option<Hash>,
address_allocation: Option<AddressAllocation<ComponentAddress>>,
) -> Result<ComponentAddress, RuntimeError> {
self.write_with(|state| {
let (template_address, module_name) =
state.current_template().map(|(addr, name)| (*addr, name.to_string()))?;

let component_address = self
.id_provider()
.new_component_address(template_address, component_id)?;
let component_address = match address_allocation {
Some(address_allocation) => state.take_allocated_address(address_allocation.id())?,
None => self
.id_provider()
.new_component_address(template_address, component_id)?,
};

let component = ComponentBody { state: component_state };
let component = ComponentHeader {
Expand Down
33 changes: 20 additions & 13 deletions dan_layer/engine/src/runtime/tracker_auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ use tari_template_lib::auth::{
Ownership,
RequireRule,
ResourceAuthAction,
ResourceOrNonFungibleAddress,
RestrictedAccessRule,
RuleRequirement,
};

use crate::runtime::{
Expand Down Expand Up @@ -154,19 +154,19 @@ fn check_require_rule(
rule: &RequireRule,
) -> Result<bool, RuntimeError> {
match rule {
RequireRule::Require(resx_or_addr) => check_resource_or_non_fungible(state, scope, resx_or_addr),
RequireRule::AnyOf(resx_or_addrs) => {
for resx_or_addr in resx_or_addrs {
if check_resource_or_non_fungible(state, scope, resx_or_addr)? {
RequireRule::Require(requirement) => check_requirement(state, scope, requirement),
RequireRule::AnyOf(requirements) => {
for requirement in requirements {
if check_requirement(state, scope, requirement)? {
return Ok(true);
}
}

Ok(false)
},
RequireRule::AllOf(resx_or_addr) => {
for resx_or_addr in resx_or_addr {
if !check_resource_or_non_fungible(state, scope, resx_or_addr)? {
RequireRule::AllOf(requirement) => {
for requirement in requirement {
if !check_requirement(state, scope, requirement)? {
return Ok(false);
}
}
Expand All @@ -176,13 +176,13 @@ fn check_require_rule(
}
}

fn check_resource_or_non_fungible(
fn check_requirement(
state: &WorkingState,
scope: &AuthorizationScope,
resx_or_addr: &ResourceOrNonFungibleAddress,
requirement: &RuleRequirement,
) -> Result<bool, RuntimeError> {
match resx_or_addr {
ResourceOrNonFungibleAddress::Resource(resx) => {
match requirement {
RuleRequirement::Resource(resx) => {
if scope
.virtual_proofs()
.iter()
Expand All @@ -200,7 +200,7 @@ fn check_resource_or_non_fungible(
}
Ok(false)
},
ResourceOrNonFungibleAddress::NonFungibleAddress(addr) => {
RuleRequirement::NonFungibleAddress(addr) => {
if scope.virtual_proofs().contains(addr) {
return Ok(true);
}
Expand All @@ -217,5 +217,12 @@ fn check_resource_or_non_fungible(

Ok(false)
},
RuleRequirement::ScopedToComponent(address) => {
Ok(state.current_component()?.map_or(false, |current| current == *address))
},
RuleRequirement::ScopedToTemplate(address) => {
let (current, _) = state.current_template()?;
Ok(current == address)
},
}
}
43 changes: 43 additions & 0 deletions dan_layer/engine/src/runtime/working_state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ use tari_template_lib::{
args::{MintArg, ResourceDiscriminator},
constants::CONFIDENTIAL_TARI_RESOURCE_ADDRESS,
models::{
AddressAllocation,
Amount,
BucketId,
ComponentAddress,
Expand Down Expand Up @@ -73,6 +74,8 @@ pub(super) struct WorkingState {
events: Vec<Event>,
logs: Vec<LogEntry>,
buckets: HashMap<BucketId, Bucket>,
address_allocations: HashMap<u32, SubstateAddress>,
address_allocation_id: u32,
proofs: HashMap<ProofId, Proof>,

store: WorkingStateStore,
Expand Down Expand Up @@ -103,6 +106,8 @@ impl WorkingState {
logs: Vec::new(),
buckets: HashMap::new(),
proofs: HashMap::new(),
address_allocation_id: 0,
address_allocations: HashMap::new(),

store: WorkingStateStore::new(state_store),

Expand Down Expand Up @@ -317,6 +322,13 @@ impl WorkingState {
.into());
}

if !self.address_allocations.is_empty() {
return Err(TransactionCommitError::DanglingAddressAllocations {
count: self.address_allocations.len(),
}
.into());
}

for (_, vault) in self.store.new_vaults() {
if !vault.locked_balance().is_zero() {
return Err(TransactionCommitError::DanglingLockedValueInVault {
Expand Down Expand Up @@ -655,6 +667,28 @@ impl WorkingState {
Ok(())
}

pub fn new_address_allocation<T: Into<SubstateAddress> + Clone>(
&mut self,
address: T,
) -> Result<AddressAllocation<T>, RuntimeError> {
let id = self.address_allocation_id;
self.address_allocation_id += 1;
self.address_allocations.insert(id, address.clone().into());
let allocation = AddressAllocation::new(id, address);
Ok(allocation)
}

pub fn take_allocated_address<T: TryFrom<SubstateAddress, Error = SubstateAddress>>(
&mut self,
id: u32,
) -> Result<T, RuntimeError> {
let address = self
.address_allocations
.remove(&id)
.ok_or(RuntimeError::AddressAllocationNotFound { id })?;
T::try_from(address).map_err(|address| RuntimeError::AddressAllocationTypeMismatch { address })
}

pub fn pay_fee(&mut self, resource: ResourceContainer, return_vault: VaultId) -> Result<(), RuntimeError> {
self.fee_state.fee_payments.push((resource, return_vault));
Ok(())
Expand Down Expand Up @@ -845,6 +879,15 @@ impl WorkingState {
.ok_or(RuntimeError::NoActiveCallScope)
}

/// Returns the component that is currently in scope (if any)
pub fn current_component(&self) -> Result<Option<ComponentAddress>, RuntimeError> {
let frame = self.call_frames.last().ok_or(RuntimeError::NoActiveCallScope)?;
Ok(frame
.scope()
.get_current_component_lock()
.and_then(|lock| lock.address().as_component_address()))
}

pub fn push_frame(&mut self, mut new_frame: CallFrame, max_call_depth: usize) -> Result<(), RuntimeError> {
if self.call_frame_depth() + 1 > max_call_depth {
return Err(RuntimeError::MaxCallDepthExceeded {
Expand Down
66 changes: 63 additions & 3 deletions dan_layer/engine/tests/access_rules.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
// Copyright 2023 The Tari Project
// SPDX-License-Identifier: BSD-3-Clause

use std::collections::BTreeMap;
use std::collections::{BTreeMap, HashMap};

use tari_dan_engine::runtime::{ActionIdent, RuntimeError};
use tari_template_lib::{
Expand Down Expand Up @@ -224,7 +223,6 @@ mod component_access_rules {
}

mod resource_access_rules {
use std::collections::HashMap;

use super::*;

Expand Down Expand Up @@ -886,4 +884,66 @@ mod resource_access_rules {
vec![user_proof],
);
}

#[test]
fn it_restricts_resource_actions_to_component() {
let mut test = TemplateTest::new(["tests/templates/access_rules"]);

// Create sender and receiver accounts
let (owner_account, owner_proof, owner_key) = test.create_empty_account();

let access_rules_template = test.get_template_address("AccessRulesTest");

let result = test.execute_expect_success(
Transaction::builder()
.call_function(
access_rules_template,
"resource_actions_restricted_to_component",
args![],
)
.sign(&owner_key)
.build(),
vec![owner_proof.clone()],
);

let component_address = result.finalize.execution_results[0]
.decode::<ComponentAddress>()
.unwrap();
// Find the resource address for the tokens from the output substates
let token_resource = result
.finalize
.result
.accept()
.unwrap()
.up_iter()
.filter_map(|(addr, s)| s.substate_value().as_resource().map(|r| (addr, r)))
.filter(|(_, r)| r.resource_type().is_fungible())
.map(|(addr, _)| addr.as_resource_address().unwrap())
.next()
.unwrap();

// Minting using a template function will fail
let reason = test.execute_expect_failure(
Transaction::builder()
.call_function(access_rules_template, "mint_resource", args![token_resource])
.put_last_instruction_output_on_workspace("tokens")
.call_method(owner_account, "deposit", args![Workspace("tokens")])
.sign(&owner_key)
.build(),
vec![owner_proof.clone()],
);

assert_access_denied_for_action(reason, ResourceAuthAction::Mint);

// Minting in a component context will succeed
test.execute_expect_success(
Transaction::builder()
.call_method(component_address, "mint_more_tokens", args![Amount(1000)])
.put_last_instruction_output_on_workspace("tokens")
.call_method(owner_account, "deposit", args![Workspace("tokens")])
.sign(&owner_key)
.build(),
vec![owner_proof],
);
}
}
52 changes: 52 additions & 0 deletions dan_layer/engine/tests/address_allocation.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
// Copyright 2023 The Tari Project
// SPDX-License-Identifier: BSD-3-Clause

use tari_dan_engine::runtime::TransactionCommitError;
use tari_template_lib::{args, models::ComponentAddress};
use tari_template_test_tooling::{support::assert_error::assert_reject_reason, TemplateTest};
use tari_transaction::Transaction;

#[test]
fn it_uses_allocation_address() {
let mut test = TemplateTest::new(["tests/templates/address_allocation"]);

let result = test.execute_expect_success(
Transaction::builder()
.call_function(test.get_template_address("AddressAllocationTest"), "create", args![])
.sign(test.get_test_secret_key())
.build(),
vec![],
);

let actual = result
.finalize
.result
.accept()
.unwrap()
.up_iter()
.find_map(|(k, _)| k.as_component_address())
.unwrap();

let allocated = result.finalize.execution_results[0]
.indexed
.get_value::<ComponentAddress>("$.1")
.unwrap()
.unwrap();
assert_eq!(actual, allocated);
}

#[test]
fn it_fails_if_allocation_is_not_used() {
let mut test = TemplateTest::new(["tests/templates/address_allocation"]);
let template_addr = test.get_template_address("AddressAllocationTest");

let reason = test.execute_expect_failure(
Transaction::builder()
.call_function(template_addr, "drop_allocation", args![])
.sign(test.get_test_secret_key())
.build(),
vec![],
);

assert_reject_reason(reason, TransactionCommitError::DanglingAddressAllocations { count: 1 });
}
Loading

0 comments on commit 516c5d1

Please sign in to comment.