From 516c5d177b4d3404683f2b27993ab91198535b46 Mon Sep 17 00:00:00 2001 From: Stan Bondi Date: Thu, 4 Jan 2024 11:47:01 +0400 Subject: [PATCH] feat(template): allow resource actions to be restricted by component (#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 --- dan_layer/engine/src/runtime/error.rs | 7 ++ dan_layer/engine/src/runtime/impl.rs | 7 ++ dan_layer/engine/src/runtime/tracker.rs | 12 ++-- dan_layer/engine/src/runtime/tracker_auth.rs | 33 ++++++---- dan_layer/engine/src/runtime/working_state.rs | 43 ++++++++++++ dan_layer/engine/tests/access_rules.rs | 66 ++++++++++++++++++- dan_layer/engine/tests/address_allocation.rs | 52 +++++++++++++++ .../tests/templates/access_rules/src/lib.rs | 44 +++++++++++-- .../templates/address_allocation/Cargo.toml | 13 ++++ .../templates/address_allocation/src/lib.rs | 26 ++++++++ dan_layer/engine_types/src/lib.rs | 1 + dan_layer/engine_types/src/substate.rs | 11 ++++ dan_layer/template_lib/src/args/types.rs | 3 + .../template_lib/src/auth/access_rules.rs | 34 +++++++--- dan_layer/template_lib/src/caller_context.rs | 11 +++- .../template_lib/src/component/instance.rs | 18 ++++- .../template_lib/src/component/manager.rs | 4 ++ dan_layer/template_lib/src/engine.rs | 4 +- .../src/models/address_allocation.rs | 22 +++++++ dan_layer/template_lib/src/models/mod.rs | 3 + dan_layer/template_lib/src/prelude.rs | 1 + .../src/resource/builder/non_fungible.rs | 10 ++- .../template_lib/src/resource/manager.rs | 12 ++-- .../src/template/dispatcher.rs | 19 +++++- 24 files changed, 405 insertions(+), 51 deletions(-) create mode 100644 dan_layer/engine/tests/address_allocation.rs create mode 100644 dan_layer/engine/tests/templates/address_allocation/Cargo.toml create mode 100644 dan_layer/engine/tests/templates/address_allocation/src/lib.rs create mode 100644 dan_layer/template_lib/src/models/address_allocation.rs diff --git a/dan_layer/engine/src/runtime/error.rs b/dan_layer/engine/src/runtime/error.rs index 8a095fe62..e268bed22 100644 --- a/dan_layer/engine/src/runtime/error.rs +++ b/dan_layer/engine/src/runtime/error.rs @@ -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 { @@ -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 }, #[error("{count} dangling items in workspace after transaction execution")] diff --git a/dan_layer/engine/src/runtime/impl.rs b/dan_layer/engine/src/runtime/impl.rs index 7bb2e37b1..aad20a005 100644 --- a/dan_layer/engine/src/runtime/impl.rs +++ b/dan_layer/engine/src/runtime/impl.rs @@ -282,6 +282,12 @@ impl> 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)?) + }), } } @@ -322,6 +328,7 @@ impl> RuntimeInte arg.owner_rule, arg.access_rules, arg.component_id, + arg.address_allocation, )?; Ok(InvokeResult::encode(&component_address)?) }, diff --git a/dan_layer/engine/src/runtime/tracker.rs b/dan_layer/engine/src/runtime/tracker.rs index c3cd1e105..bcf7f5986 100644 --- a/dan_layer/engine/src/runtime/tracker.rs +++ b/dan_layer/engine/src/runtime/tracker.rs @@ -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; @@ -161,14 +161,18 @@ impl StateTracker { owner_rule: OwnerRule, access_rules: ComponentAccessRules, component_id: Option, + address_allocation: Option>, ) -> Result { 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 { diff --git a/dan_layer/engine/src/runtime/tracker_auth.rs b/dan_layer/engine/src/runtime/tracker_auth.rs index e86ece26b..6f8564047 100644 --- a/dan_layer/engine/src/runtime/tracker_auth.rs +++ b/dan_layer/engine/src/runtime/tracker_auth.rs @@ -7,8 +7,8 @@ use tari_template_lib::auth::{ Ownership, RequireRule, ResourceAuthAction, - ResourceOrNonFungibleAddress, RestrictedAccessRule, + RuleRequirement, }; use crate::runtime::{ @@ -154,19 +154,19 @@ fn check_require_rule( rule: &RequireRule, ) -> Result { 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); } } @@ -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 { - match resx_or_addr { - ResourceOrNonFungibleAddress::Resource(resx) => { + match requirement { + RuleRequirement::Resource(resx) => { if scope .virtual_proofs() .iter() @@ -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); } @@ -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) + }, } } diff --git a/dan_layer/engine/src/runtime/working_state.rs b/dan_layer/engine/src/runtime/working_state.rs index 73d80c3d8..069bc4eaa 100644 --- a/dan_layer/engine/src/runtime/working_state.rs +++ b/dan_layer/engine/src/runtime/working_state.rs @@ -36,6 +36,7 @@ use tari_template_lib::{ args::{MintArg, ResourceDiscriminator}, constants::CONFIDENTIAL_TARI_RESOURCE_ADDRESS, models::{ + AddressAllocation, Amount, BucketId, ComponentAddress, @@ -73,6 +74,8 @@ pub(super) struct WorkingState { events: Vec, logs: Vec, buckets: HashMap, + address_allocations: HashMap, + address_allocation_id: u32, proofs: HashMap, store: WorkingStateStore, @@ -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), @@ -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 { @@ -655,6 +667,28 @@ impl WorkingState { Ok(()) } + pub fn new_address_allocation + Clone>( + &mut self, + address: T, + ) -> Result, 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>( + &mut self, + id: u32, + ) -> Result { + 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(()) @@ -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, 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 { diff --git a/dan_layer/engine/tests/access_rules.rs b/dan_layer/engine/tests/access_rules.rs index 74d8ede1d..5dd1f2378 100644 --- a/dan_layer/engine/tests/access_rules.rs +++ b/dan_layer/engine/tests/access_rules.rs @@ -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::{ @@ -224,7 +223,6 @@ mod component_access_rules { } mod resource_access_rules { - use std::collections::HashMap; use super::*; @@ -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::() + .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], + ); + } } diff --git a/dan_layer/engine/tests/address_allocation.rs b/dan_layer/engine/tests/address_allocation.rs new file mode 100644 index 000000000..8ac3d7246 --- /dev/null +++ b/dan_layer/engine/tests/address_allocation.rs @@ -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::("$.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 }); +} diff --git a/dan_layer/engine/tests/templates/access_rules/src/lib.rs b/dan_layer/engine/tests/templates/access_rules/src/lib.rs index 83528242b..9e8ed20c7 100644 --- a/dan_layer/engine/tests/templates/access_rules/src/lib.rs +++ b/dan_layer/engine/tests/templates/access_rules/src/lib.rs @@ -2,13 +2,13 @@ // SPDX-License-Identifier: BSD-3-Clause use tari_template_lib::prelude::*; +const BADGE_NAMES: [&str; 4] = ["mint", "burn", "withdraw", "deposit"]; pub fn create_badge_resource(recall_rule: AccessRule) -> Bucket { ResourceBuilder::non_fungible() - .with_non_fungible(NonFungibleId::from_string("mint"), &(), &()) - .with_non_fungible(NonFungibleId::from_string("burn"), &(), &()) - .with_non_fungible(NonFungibleId::from_string("withdraw"), &(), &()) - .with_non_fungible(NonFungibleId::from_string("deposit"), &(), &()) + .mint_many_with(BADGE_NAMES, |name| { + (NonFungibleId::from_string(name), (Vec::new(), Vec::new())) + }) .recallable(recall_rule) .build_bucket() } @@ -126,6 +126,29 @@ mod access_rules_template { .create() } + pub fn resource_actions_restricted_to_component() -> Component { + let badges = create_badge_resource(AccessRule::AllowAll); + + let allocation = CallerContext::allocate_component_address(); + let tokens = ResourceBuilder::fungible() + .initial_supply(1000) + .mintable(AccessRule::Restricted(RestrictedAccessRule::Require( + RequireRule::Require(allocation.address().clone().into()), + ))) + // Only access rules apply, this just makes the test simpler because we do not need to change the transaction signer + .with_owner_rule(OwnerRule::None) + .build_bucket(); + + Component::new(Self { + value: 0, + tokens: Vault::from_bucket(tokens), + badges: Vault::from_bucket(badges), + }) + .with_address_allocation(allocation) + .with_access_rules(ComponentAccessRules::new().default(AccessRule::AllowAll)) + .create() + } + pub fn take_badge_by_name(&mut self, name: String) -> Bucket { self.badges.withdraw_non_fungible(NonFungibleId::from_string(&name)) } @@ -178,5 +201,18 @@ mod access_rules_template { pub fn create_proof_from_bucket(bucket: Bucket) -> Proof { bucket.create_proof() } + + pub fn mint_resource(resource: ResourceAddress) -> Bucket { + let manager = ResourceManager::get(resource); + match manager.resource_type() { + ResourceType::Fungible => manager.mint_fungible(1000.into()), + ResourceType::NonFungible => manager.mint_non_fungible(NonFungibleId::random(), &(), &()), + ty => panic!("Unsupported resource type {:?}", ty), + } + } + + pub fn mint_more_tokens(&mut self, amount: Amount) -> Bucket { + ResourceManager::get(self.tokens.resource_address()).mint_fungible(amount) + } } } diff --git a/dan_layer/engine/tests/templates/address_allocation/Cargo.toml b/dan_layer/engine/tests/templates/address_allocation/Cargo.toml new file mode 100644 index 000000000..6def99a37 --- /dev/null +++ b/dan_layer/engine/tests/templates/address_allocation/Cargo.toml @@ -0,0 +1,13 @@ +[workspace] +[package] +name = "address_allocation" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +tari_template_lib = { path = "../../../../template_lib" } + +[lib] +crate-type = ["cdylib", "lib"] diff --git a/dan_layer/engine/tests/templates/address_allocation/src/lib.rs b/dan_layer/engine/tests/templates/address_allocation/src/lib.rs new file mode 100644 index 000000000..803c9d0a0 --- /dev/null +++ b/dan_layer/engine/tests/templates/address_allocation/src/lib.rs @@ -0,0 +1,26 @@ +// Copyright 2023 The Tari Project +// SPDX-License-Identifier: BSD-3-Clause + +use tari_template_lib::prelude::*; + +#[template] +mod template { + use super::*; + + pub struct AddressAllocationTest {} + + impl AddressAllocationTest { + pub fn create() -> (Component, ComponentAddress) { + let allocation = CallerContext::allocate_component_address(); + let address = allocation.address().clone(); + ( + Component::new(Self {}).with_address_allocation(allocation).create(), + address, + ) + } + + pub fn drop_allocation() { + let _allocation = CallerContext::allocate_component_address(); + } + } +} diff --git a/dan_layer/engine_types/src/lib.rs b/dan_layer/engine_types/src/lib.rs index 4e6bc2b14..4a701e528 100644 --- a/dan_layer/engine_types/src/lib.rs +++ b/dan_layer/engine_types/src/lib.rs @@ -30,4 +30,5 @@ mod template; pub use template::{calculate_template_binary_hash, parse_template_address, TemplateAddress}; mod argument_parser; + pub use argument_parser::parse_arg; diff --git a/dan_layer/engine_types/src/substate.rs b/dan_layer/engine_types/src/substate.rs index 3daa57d14..26f74e440 100644 --- a/dan_layer/engine_types/src/substate.rs +++ b/dan_layer/engine_types/src/substate.rs @@ -263,6 +263,17 @@ impl From for SubstateAddress { } } +impl TryFrom for ComponentAddress { + type Error = SubstateAddress; + + fn try_from(value: SubstateAddress) -> Result { + match value { + SubstateAddress::Component(addr) => Ok(addr), + _ => Err(value), + } + } +} + impl Display for SubstateAddress { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { diff --git a/dan_layer/template_lib/src/args/types.rs b/dan_layer/template_lib/src/args/types.rs index 685e426cc..e75f8fab8 100644 --- a/dan_layer/template_lib/src/args/types.rs +++ b/dan_layer/template_lib/src/args/types.rs @@ -33,6 +33,7 @@ use crate::{ auth::{OwnerRule, ResourceAccessRules}, crypto::PedersonCommitmentBytes, models::{ + AddressAllocation, Amount, BucketId, ComponentAddress, @@ -157,6 +158,7 @@ pub struct CreateComponentArg { pub owner_rule: OwnerRule, pub access_rules: ComponentAccessRules, pub component_id: Option, + pub address_allocation: Option>, } // -------------------------------- Events -------------------------------- // @@ -478,6 +480,7 @@ pub struct CallerContextInvokeArg { pub enum CallerContextAction { GetCallerPublicKey, GetComponentAddress, + AllocateNewComponentAddress, } // -------------------------------- CallInvoke -------------------------------- // diff --git a/dan_layer/template_lib/src/auth/access_rules.rs b/dan_layer/template_lib/src/auth/access_rules.rs index 0c942e7a6..80b98c463 100644 --- a/dan_layer/template_lib/src/auth/access_rules.rs +++ b/dan_layer/template_lib/src/auth/access_rules.rs @@ -4,7 +4,7 @@ use serde::{Deserialize, Serialize}; use tari_template_abi::rust::collections::BTreeMap; -use crate::models::{NonFungibleAddress, ResourceAddress}; +use crate::models::{ComponentAddress, NonFungibleAddress, ResourceAddress, TemplateAddress}; /// Represents the types of possible access control rules over a component method or resource #[derive(Debug, Clone, Serialize, Deserialize)] @@ -54,31 +54,49 @@ impl RestrictedAccessRule { } } -/// An enum that allows passing either a resource or a non-fungible argument +/// Specifies a requirement for a [RequireRule]. #[derive(Debug, Clone, Serialize, Deserialize)] -pub enum ResourceOrNonFungibleAddress { +pub enum RuleRequirement { + /// Requires ownership of a specific resource Resource(ResourceAddress), + /// Requires ownership of a specific non-fungible token NonFungibleAddress(NonFungibleAddress), + /// Requires execution within a specific component + ScopedToComponent(ComponentAddress), + /// Requires execution within a specific template + ScopedToTemplate(TemplateAddress), } -impl From for ResourceOrNonFungibleAddress { +impl From for RuleRequirement { fn from(address: ResourceAddress) -> Self { Self::Resource(address) } } -impl From for ResourceOrNonFungibleAddress { +impl From for RuleRequirement { fn from(address: NonFungibleAddress) -> Self { Self::NonFungibleAddress(address) } } +impl From for RuleRequirement { + fn from(address: ComponentAddress) -> Self { + Self::ScopedToComponent(address) + } +} + +impl From for RuleRequirement { + fn from(address: TemplateAddress) -> Self { + Self::ScopedToTemplate(address) + } +} + /// An enum that represents the possible ways to require access to components or resources #[derive(Debug, Clone, Serialize, Deserialize)] pub enum RequireRule { - Require(ResourceOrNonFungibleAddress), - AnyOf(Vec), - AllOf(Vec), + Require(RuleRequirement), + AnyOf(Vec), + AllOf(Vec), } /// Information needed to specify access rules to methods of a component diff --git a/dan_layer/template_lib/src/caller_context.rs b/dan_layer/template_lib/src/caller_context.rs index ed7355ab4..2204234ae 100644 --- a/dan_layer/template_lib/src/caller_context.rs +++ b/dan_layer/template_lib/src/caller_context.rs @@ -8,7 +8,7 @@ use tari_template_abi::{call_engine, EngineOp}; use crate::{ args::{CallerContextAction, CallerContextInvokeArg, InvokeResult}, crypto::RistrettoPublicKeyBytes, - models::ComponentAddress, + models::{AddressAllocation, ComponentAddress}, }; /// Allows a template to access information about the current instruction's caller @@ -35,4 +35,13 @@ impl CallerContext { .expect("Failed to decode Option") .expect("Not in a component instance context") } + + pub fn allocate_component_address() -> AddressAllocation { + let resp: InvokeResult = call_engine(EngineOp::CallerContextInvoke, &CallerContextInvokeArg { + action: CallerContextAction::AllocateNewComponentAddress, + }); + + resp.decode() + .expect("Failed to decode AddressAllocation") + } } diff --git a/dan_layer/template_lib/src/component/instance.rs b/dan_layer/template_lib/src/component/instance.rs index f39e8f299..22171ac31 100644 --- a/dan_layer/template_lib/src/component/instance.rs +++ b/dan_layer/template_lib/src/component/instance.rs @@ -6,7 +6,7 @@ use std::marker::PhantomData; use crate::{ auth::{ComponentAccessRules, OwnerRule}, engine, - models::ComponentAddress, + models::{AddressAllocation, ComponentAddress}, Hash, }; @@ -16,6 +16,7 @@ pub struct ComponentBuilder { owner_rule: OwnerRule, access_rules: ComponentAccessRules, component_id: Option, + address_allocation: Option>, } impl ComponentBuilder { @@ -26,9 +27,16 @@ impl ComponentBuilder { owner_rule: OwnerRule::default(), access_rules: ComponentAccessRules::new(), component_id: None, + address_allocation: None, } } + /// Use an allocated address for the component. + pub fn with_address_allocation(mut self, allocation: AddressAllocation) -> Self { + self.address_allocation = Some(allocation); + self + } + /// Sets up who will be the owner of the component. /// Component owners are the only ones allowed to update the component's access rules after creation pub fn with_owner_rule(mut self, owner_rule: OwnerRule) -> Self { @@ -50,7 +58,13 @@ impl ComponentBuilder { /// Creates the new component and returns it pub fn create(self) -> Component { - let address = engine().create_component(self.component, self.owner_rule, self.access_rules, self.component_id); + let address = engine().create_component( + self.component, + self.owner_rule, + self.access_rules, + self.component_id, + self.address_allocation, + ); Component::from_address(address) } } diff --git a/dan_layer/template_lib/src/component/manager.rs b/dan_layer/template_lib/src/component/manager.rs index d1843d430..9454c346f 100644 --- a/dan_layer/template_lib/src/component/manager.rs +++ b/dan_layer/template_lib/src/component/manager.rs @@ -134,4 +134,8 @@ impl ComponentManager { .decode() .expect("failed to decode component template address from engine") } + + pub fn component_address(&self) -> ComponentAddress { + self.address + } } diff --git a/dan_layer/template_lib/src/engine.rs b/dan_layer/template_lib/src/engine.rs index 9f3c7fb07..d24b88800 100644 --- a/dan_layer/template_lib/src/engine.rs +++ b/dan_layer/template_lib/src/engine.rs @@ -30,7 +30,7 @@ use crate::{ component::ComponentManager, context::Context, get_context, - models::ComponentAddress, + models::{AddressAllocation, ComponentAddress}, prelude::ComponentAccessRules, Hash, }; @@ -57,6 +57,7 @@ impl TariEngine { owner_rule: OwnerRule, access_rules: ComponentAccessRules, component_id: Option, + address_allocation: Option>, ) -> ComponentAddress { let encoded_state = to_value(&initial_state).unwrap(); @@ -68,6 +69,7 @@ impl TariEngine { owner_rule, access_rules, component_id, + address_allocation }], }); diff --git a/dan_layer/template_lib/src/models/address_allocation.rs b/dan_layer/template_lib/src/models/address_allocation.rs new file mode 100644 index 000000000..7f1f65c29 --- /dev/null +++ b/dan_layer/template_lib/src/models/address_allocation.rs @@ -0,0 +1,22 @@ +// Copyright 2024 The Tari Project +// SPDX-License-Identifier: BSD-3-Clause + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct AddressAllocation { + id: u32, + address: T, +} + +impl AddressAllocation { + pub fn new(id: u32, address: T) -> Self { + Self { id, address } + } + + pub fn id(&self) -> u32 { + self.id + } + + pub fn address(&self) -> &T { + &self.address + } +} diff --git a/dan_layer/template_lib/src/models/mod.rs b/dan_layer/template_lib/src/models/mod.rs index 025acb98d..09d22254e 100644 --- a/dan_layer/template_lib/src/models/mod.rs +++ b/dan_layer/template_lib/src/models/mod.rs @@ -23,6 +23,9 @@ //! The collection of all struct definitions that represent data in the Tari network (e.g., resources, components, //! proofs, etc.) +mod address_allocation; +pub use address_allocation::AddressAllocation; + mod non_fungible_index; pub use non_fungible_index::NonFungibleIndexAddress; diff --git a/dan_layer/template_lib/src/prelude.rs b/dan_layer/template_lib/src/prelude.rs index 95bef383f..3b83dbc76 100644 --- a/dan_layer/template_lib/src/prelude.rs +++ b/dan_layer/template_lib/src/prelude.rs @@ -40,6 +40,7 @@ pub use crate::{ events::emit_event, invoke_args, models::{ + AddressAllocation, Amount, Bucket, BucketId, diff --git a/dan_layer/template_lib/src/resource/builder/non_fungible.rs b/dan_layer/template_lib/src/resource/builder/non_fungible.rs index 343c5de12..6adf51e56 100644 --- a/dan_layer/template_lib/src/resource/builder/non_fungible.rs +++ b/dan_layer/template_lib/src/resource/builder/non_fungible.rs @@ -5,7 +5,6 @@ use std::collections::BTreeMap; use serde::Serialize; use tari_bor::encode; -use tari_template_abi::rust::{fmt, ops::RangeInclusive}; use super::TOKEN_SYMBOL; use crate::{ @@ -130,13 +129,12 @@ impl NonFungibleResourceBuilder { /// Sets up multiple initial non-fungible tokens to be minted on resource creation by applying the provided function /// N times - pub fn mint_many_with(mut self, bounds: RangeInclusive, mut f: F) -> Self + pub fn mint_many_with(mut self, iter: I, f: F) -> Self where - F: FnMut(T) -> (NonFungibleId, (Vec, Vec)), - T: TryFrom, - T::Error: fmt::Debug, + F: FnMut(V) -> (NonFungibleId, (Vec, Vec)), + I: IntoIterator, { - self.tokens_ids.extend(bounds.map(|n| f(n.try_into().unwrap()))); + self.tokens_ids.extend(iter.into_iter().map(f)); self } diff --git a/dan_layer/template_lib/src/resource/manager.rs b/dan_layer/template_lib/src/resource/manager.rs index 525d6ef6f..c48532c99 100644 --- a/dan_layer/template_lib/src/resource/manager.rs +++ b/dan_layer/template_lib/src/resource/manager.rs @@ -96,7 +96,7 @@ impl ResourceManager { /// * `metadata` - Collection of information used to describe the resource /// * `mint_arg` - Specification of the initial tokens that will be minted on resource creation pub fn create( - &mut self, + &self, resource_type: ResourceType, owner_rule: OwnerRule, access_rules: ResourceAccessRules, @@ -130,7 +130,7 @@ impl ResourceManager { /// /// * `proof` - A zero-knowledge proof that specifies the amount of tokens to be minted and returned back to the /// caller - pub fn mint_confidential(&mut self, proof: ConfidentialOutputProof) -> Bucket { + pub fn mint_confidential(&self, proof: ConfidentialOutputProof) -> Bucket { self.mint_internal(MintResourceArg { mint_arg: MintArg::Confidential { proof: Box::new(proof) }, }) @@ -150,7 +150,7 @@ impl ResourceManager { /// * `mutable_data` - Initial data that the token will hold and that can potentially be updated in future /// instructions pub fn mint_non_fungible( - &mut self, + &self, id: NonFungibleId, metadata: &T, mutable_data: &U, @@ -178,7 +178,7 @@ impl ResourceManager { /// instructions /// * `supply` - The amount of new tokens to be minted pub fn mint_many_non_fungible( - &mut self, + &self, metadata: &T, mutable_data: &U, supply: usize, @@ -204,7 +204,7 @@ impl ResourceManager { /// # Arguments /// /// * `amount` - The amount of new tokens to be minted - pub fn mint_fungible(&mut self, amount: Amount) -> Bucket { + pub fn mint_fungible(&self, amount: Amount) -> Bucket { self.mint_internal(MintResourceArg { mint_arg: MintArg::Fungible { amount }, }) @@ -368,7 +368,7 @@ impl ResourceManager { Bucket::from_id(bucket_id) } - fn mint_internal(&mut self, arg: MintResourceArg) -> Bucket { + fn mint_internal(&self, arg: MintResourceArg) -> Bucket { let resp: InvokeResult = call_engine(EngineOp::ResourceInvoke, &ResourceInvokeArg { resource_ref: self.expect_resource_address(), action: ResourceAction::Mint, diff --git a/dan_layer/template_macros/src/template/dispatcher.rs b/dan_layer/template_macros/src/template/dispatcher.rs index 2f9f2876b..108f378f7 100644 --- a/dan_layer/template_macros/src/template/dispatcher.rs +++ b/dan_layer/template_macros/src/template/dispatcher.rs @@ -201,7 +201,13 @@ fn replace_self_in_single_value(type_path: &TypePath) -> Option { if type_ident == "Self" { // When we return self we use default rules - which only permit the owner of the component to call methods return Some(parse_quote! { - let rtn = engine().create_component(rtn, ::tari_template_lib::auth::OwnerRule::default(), ::tari_template_lib::auth::ComponentAccessRules::new(), None); + let rtn = engine().create_component( + rtn, + ::tari_template_lib::auth::OwnerRule::default(), + ::tari_template_lib::auth::ComponentAccessRules::new(), + None, + None, + ); }); } @@ -219,9 +225,16 @@ fn replace_self_in_tuple(type_tuple: &TypeTuple) -> Stmt { let ident = path.path.segments[0].ident.clone(); let field_expr = build_tuple_field_expr("rtn".to_string(), i as u32); if ident == "Self" { - // When we return self we use default rules - which only permit the owner of the component to call methods + // When we return self we use default rules - which only permit the owner of the component to call + // methods parse_quote! { - engine().create_component(#field_expr, ::tari_template_lib::auth::OwnerRule::default(), ::tari_template_lib::auth::ComponentAccessRules::new(), None) + engine().create_component( + #field_expr, + ::tari_template_lib::auth::OwnerRule::default(), + :tari_template_lib::auth::ComponentAccessRules::new(), + None, + None, + ) } } else { field_expr