diff --git a/applications/tari_dan_wallet_daemon/src/handlers/accounts.rs b/applications/tari_dan_wallet_daemon/src/handlers/accounts.rs index b1e886dc8..0cd6a156a 100644 --- a/applications/tari_dan_wallet_daemon/src/handlers/accounts.rs +++ b/applications/tari_dan_wallet_daemon/src/handlers/accounts.rs @@ -676,7 +676,9 @@ async fn finish_claiming( }); } else { instructions.push(Instruction::CreateAccount { - owner_public_key: account_public_key.clone(), + public_key_address: account_public_key.clone(), + owner_rule: None, + access_rules: None, workspace_bucket: Some("bucket".to_string()), }); } @@ -884,7 +886,9 @@ pub async fn handle_transfer( inputs.push(address); } else { instructions.push(Instruction::CreateAccount { - owner_public_key: req.destination_public_key, + public_key_address: req.destination_public_key, + owner_rule: None, + access_rules: None, workspace_bucket: None, }); } diff --git a/applications/tari_validator_node_cli/src/command/account.rs b/applications/tari_validator_node_cli/src/command/account.rs index f5ad33ad5..b8555dac2 100644 --- a/applications/tari_validator_node_cli/src/command/account.rs +++ b/applications/tari_validator_node_cli/src/command/account.rs @@ -69,7 +69,9 @@ pub async fn handle_create( .ok_or_else(|| anyhow::anyhow!("No active key"))?; let instruction = Instruction::CreateAccount { - owner_public_key: key.public_key, + public_key_address: key.public_key, + owner_rule: None, + access_rules: None, workspace_bucket: None, }; diff --git a/bindings/src/types/Instruction.ts b/bindings/src/types/Instruction.ts index 1ef576682..4a1a0fd75 100644 --- a/bindings/src/types/Instruction.ts +++ b/bindings/src/types/Instruction.ts @@ -1,13 +1,22 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. import type { Amount } from "./Amount"; import type { Arg } from "./Arg"; +import type { ComponentAccessRules } from "./ComponentAccessRules"; import type { ComponentAddress } from "./ComponentAddress"; import type { ConfidentialClaim } from "./ConfidentialClaim"; import type { LogLevel } from "./LogLevel"; +import type { OwnerRule } from "./OwnerRule"; import type { ResourceAddress } from "./ResourceAddress"; export type Instruction = - | { CreateAccount: { owner_public_key: string; workspace_bucket: string | null } } + | { + CreateAccount: { + public_key_address: string; + owner_rule: OwnerRule | null; + access_rules: ComponentAccessRules | null; + workspace_bucket: string | null; + }; + } | { CallFunction: { template_address: Uint8Array; function: string; args: Array } } | { CallMethod: { component_address: ComponentAddress; method: string; args: Array } } | { PutLastInstructionOutputOnWorkspace: { key: Array } } diff --git a/dan_layer/engine/src/transaction/processor.rs b/dan_layer/engine/src/transaction/processor.rs index 6df7174ec..b6bfaa81e 100644 --- a/dan_layer/engine/src/transaction/processor.rs +++ b/dan_layer/engine/src/transaction/processor.rs @@ -43,10 +43,11 @@ use tari_template_lib::{ arg, args, args::{Arg, WorkspaceAction}, + auth::OwnerRule, crypto::RistrettoPublicKeyBytes, invoke_args, - models::{ComponentAddress, NonFungibleAddress}, - prelude::TemplateAddress, + models::{Bucket, ComponentAddress, NonFungibleAddress}, + prelude::{AccessRules, TemplateAddress}, }; use tari_transaction::Transaction; use tari_utilities::ByteArray; @@ -70,6 +71,7 @@ use crate::{ const LOG_TARGET: &str = "tari::dan::engine::instruction_processor"; pub const MAX_CALL_DEPTH: usize = 10; +const ACCOUNT_CONSTRUCTOR_FUNCTION: &str = "create"; pub struct TransactionProcessor { template_provider: Arc, @@ -240,9 +242,18 @@ impl + 'static> T debug!(target: LOG_TARGET, "instruction = {:?}", instruction); match instruction { Instruction::CreateAccount { - owner_public_key, + public_key_address, + owner_rule, + access_rules, workspace_bucket, - } => Self::create_account(template_provider, runtime, &owner_public_key, workspace_bucket), + } => Self::create_account( + template_provider, + runtime, + &public_key_address, + owner_rule, + access_rules, + workspace_bucket, + ), Instruction::CallFunction { template_address, function, @@ -312,7 +323,9 @@ impl + 'static> T pub fn create_account( template_provider: &TTemplateProvider, runtime: &Runtime, - owner_public_key: &PublicKey, + public_key_address: &PublicKey, + owner_rule: Option, + access_rules: Option, workspace_bucket: Option, ) -> Result { let template = template_provider @@ -325,23 +338,42 @@ impl + 'static> T address: ACCOUNT_TEMPLATE_ADDRESS, })?; - let function = if workspace_bucket.is_some() { - "create_with_bucket" + let function_def = template + .template_def() + .get_function(ACCOUNT_CONSTRUCTOR_FUNCTION) + .cloned() + .ok_or_else(|| TransactionError::FunctionNotFound { + name: ACCOUNT_CONSTRUCTOR_FUNCTION.to_string(), + })?; + + let account_address = new_component_address_from_public_key(&ACCOUNT_TEMPLATE_ADDRESS, public_key_address); + + // the publick key is the first argument of the Account template constructor + let public_key = RistrettoPublicKeyBytes::from_bytes(public_key_address.as_bytes()).unwrap(); + let mut args = args![NonFungibleAddress::from_public_key(public_key)]; + + // add the optional owner rule if specified + if let Some(owner_rule) = owner_rule { + args.push(arg![Literal(owner_rule)]); } else { - "create" - }; + let none: Option = None; + args.push(arg![Literal(none)]); + } - let function_def = template.template_def().get_function(function).cloned().ok_or_else(|| { - TransactionError::FunctionNotFound { - name: function.to_string(), - } - })?; - let owner_pk = RistrettoPublicKeyBytes::from_bytes(owner_public_key.as_bytes()).unwrap(); - let account_address = new_component_address_from_public_key(&ACCOUNT_TEMPLATE_ADDRESS, owner_public_key); + // add the optional access rules if specified + if let Some(access_rules) = access_rules { + args.push(arg![Literal(access_rules)]); + } else { + let none: Option = None; + args.push(arg![Literal(none)]); + } - let mut args = args![NonFungibleAddress::from_public_key(owner_pk)]; + // add the optional workspace bucket with the initial funds of the account if let Some(workspace_bucket) = workspace_bucket { args.push(arg![Workspace(workspace_bucket)]); + } else { + let none: Option = None; + args.push(arg![Literal(none)]); } let args = runtime.resolve_args(args)?; diff --git a/dan_layer/engine/tests/account.rs b/dan_layer/engine/tests/account.rs index fad265693..63cbc415d 100644 --- a/dan_layer/engine/tests/account.rs +++ b/dan_layer/engine/tests/account.rs @@ -1,16 +1,19 @@ // Copyright 2023 The Tari Project // SPDX-License-Identifier: BSD-3-Clause -use tari_crypto::{keys::PublicKey, ristretto::RistrettoPublicKey}; +use tari_crypto::{keys::PublicKey, ristretto::RistrettoPublicKey, tari_utilities::ByteArray}; use tari_dan_engine::runtime::{ActionIdent, RuntimeError}; use tari_engine_types::instruction::Instruction; use tari_template_lib::{ args, + auth::AccessRule, constants::XTR, models::{Amount, ComponentAddress, ResourceAddress}, + prelude::AccessRules, }; use tari_template_test_tooling::{ support::assert_error::{assert_access_denied_for_action, assert_reject_reason}, + test_faucet_component, TemplateTest, }; use tari_transaction::Transaction; @@ -252,3 +255,50 @@ fn gasless() { let balance = result.expect_return::>(3); assert_eq!(balance[0].1, 100); } + +#[test] +fn custom_access_rules() { + let mut template_test = TemplateTest::new::<_, &str>([]); + + // First we create a account with a custom rule that anyone can withdraw + let (owner_proof, public_key, secret_key) = template_test.create_owner_proof(); + + let access_rules = AccessRules::new() + .add_method_rule("balance", AccessRule::AllowAll) + .add_method_rule("get_balances", AccessRule::AllowAll) + .add_method_rule("deposit", AccessRule::AllowAll) + .add_method_rule("deposit_all", AccessRule::AllowAll) + .add_method_rule("get_non_fungible_ids", AccessRule::AllowAll) + // We are going to make it so anyone can withdraw + .default(AccessRule::AllowAll); + + let result = template_test.execute_expect_success( + Transaction::builder() + .call_method(test_faucet_component(), "take_free_coins", args![]) + .put_last_instruction_output_on_workspace("bucket") + // Create component with the same ID + .create_account_with_custom_rules( + public_key, + None, + Some(access_rules), + Some("bucket"), + ) + // Signed by source account so that it can pay the fees for the new account creation + .sign(&secret_key) + .build(), + vec![owner_proof], + ); + let user_account = result.finalize.execution_results[2].decode().unwrap(); + + // We create another account and we we will withdraw from the custom one + let (user2_account, user2_account_proof, user2_secret_key) = template_test.create_funded_account(); + template_test.execute_expect_success( + Transaction::builder() + .call_method(user_account, "withdraw", args![XTR, Amount(100)]) + .put_last_instruction_output_on_workspace("b") + .call_method(user2_account, "deposit", args![Workspace("b")]) + .build() + .sign(&user2_secret_key), + vec![user2_account_proof], + ); +} diff --git a/dan_layer/engine/tests/airdrop.rs b/dan_layer/engine/tests/airdrop.rs index 152bc198b..06ae9aa78 100644 --- a/dan_layer/engine/tests/airdrop.rs +++ b/dan_layer/engine/tests/airdrop.rs @@ -32,7 +32,9 @@ fn airdrop() { let instructions = iter::repeat_with(|| { let (_, owner_public_key, _) = template_test.create_owner_proof(); Instruction::CreateAccount { - owner_public_key, + public_key_address: owner_public_key, + owner_rule: None, + access_rules: None, workspace_bucket: None, } }) diff --git a/dan_layer/engine_types/src/instruction.rs b/dan_layer/engine_types/src/instruction.rs index 470a8e7e7..d65b5c021 100644 --- a/dan_layer/engine_types/src/instruction.rs +++ b/dan_layer/engine_types/src/instruction.rs @@ -8,8 +8,9 @@ use tari_common_types::types::PublicKey; use tari_crypto::tari_utilities::hex::Hex; use tari_template_lib::{ args::{Arg, LogLevel}, + auth::OwnerRule, models::{ComponentAddress, ResourceAddress, TemplateAddress}, - prelude::Amount, + prelude::{AccessRules, Amount}, }; #[cfg(feature = "ts")] use ts_rs::TS; @@ -21,7 +22,9 @@ use crate::{confidential::ConfidentialClaim, serde_with}; pub enum Instruction { CreateAccount { #[cfg_attr(feature = "ts", ts(type = "string"))] - owner_public_key: PublicKey, + public_key_address: PublicKey, + owner_rule: Option, + access_rules: Option, #[cfg_attr(feature = "ts", ts(type = "string | null"))] workspace_bucket: Option, }, @@ -71,10 +74,16 @@ impl Display for Instruction { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { Self::CreateAccount { - owner_public_key, + public_key_address, + owner_rule, + access_rules, workspace_bucket, } => { - write!(f, "CreateAccount {{ owner_public_key: {}, bucket: ", owner_public_key,)?; + write!( + f, + "CreateAccount {{ public_key_address: {}, owner_rule: {:?}, acces_rules: {:?}, bucket: ", + public_key_address, owner_rule, access_rules + )?; match workspace_bucket { Some(bucket) => write!(f, "{}", bucket)?, None => write!(f, "None")?, diff --git a/dan_layer/p2p/proto/transaction.proto b/dan_layer/p2p/proto/transaction.proto index fa28d630d..871294292 100644 --- a/dan_layer/p2p/proto/transaction.proto +++ b/dan_layer/p2p/proto/transaction.proto @@ -72,14 +72,17 @@ message Instruction { bytes claim_validator_fees_validator_public_key = 15; uint64 claim_validator_fees_epoch = 16; - bytes create_account_owner_public_key = 17; - string create_account_workspace_bucket = 18; + bytes create_account_public_key = 17; + OwnerRule create_account_owner_rule = 18; + AccessRules create_account_access_rules = 19; + string create_account_workspace_bucket = 20; // AssertBucketContains - bytes resource_address = 19; - int64 min_amount = 20; + bytes resource_address = 21; + int64 min_amount = 22; } + message Arg { enum ArgType { LITERAL = 0; @@ -136,3 +139,11 @@ message ViewableBalanceProof { bytes s_m = 7; bytes s_r = 8; } + +message OwnerRule { + bytes encoded_owner_rule = 1; +} + +message AccessRules { + bytes encoded_access_rules = 1; +} \ No newline at end of file diff --git a/dan_layer/p2p/src/conversions/transaction.rs b/dan_layer/p2p/src/conversions/transaction.rs index 0246c52ae..251d79d74 100644 --- a/dan_layer/p2p/src/conversions/transaction.rs +++ b/dan_layer/p2p/src/conversions/transaction.rs @@ -23,13 +23,14 @@ use std::convert::{TryFrom, TryInto}; use anyhow::anyhow; -use tari_bor::decode_exact; +use tari_bor::{decode_exact, encode}; use tari_common_types::types::{Commitment, PrivateKey, PublicKey}; use tari_crypto::{ristretto::RistrettoComSig, tari_utilities::ByteArray}; use tari_dan_common_types::{Epoch, SubstateRequirement, VersionedSubstateId}; use tari_engine_types::{confidential::ConfidentialClaim, instruction::Instruction, substate::SubstateId}; use tari_template_lib::{ args::Arg, + auth::OwnerRule, crypto::{BalanceProofSignature, PedersonCommitmentBytes, RistrettoPublicKeyBytes}, models::{ Amount, @@ -40,6 +41,7 @@ use tari_template_lib::{ ObjectKey, ViewableBalanceProof, }, + prelude::AccessRules, }; use tari_transaction::{Transaction, UnsignedTransaction}; @@ -193,8 +195,10 @@ impl TryFrom for Instruction { InstructionType::try_from(request.instruction_type).map_err(|e| anyhow!("invalid instruction_type {e}"))?; let instruction = match instruction_type { InstructionType::CreateAccount => Instruction::CreateAccount { - owner_public_key: PublicKey::from_canonical_bytes(&request.create_account_owner_public_key) - .map_err(|e| anyhow!("create_account_owner_public_key: {}", e))?, + public_key_address: PublicKey::from_canonical_bytes(&request.create_account_public_key) + .map_err(|e| anyhow!("create_account_public_key: {}", e))?, + owner_rule: request.create_account_owner_rule.map(TryInto::try_into).transpose()?, + access_rules: request.create_account_access_rules.map(TryInto::try_into).transpose()?, workspace_bucket: Some(request.create_account_workspace_bucket).filter(|s| !s.is_empty()), }, InstructionType::Function => { @@ -267,11 +271,15 @@ impl From for proto::transaction::Instruction { match instruction { Instruction::CreateAccount { - owner_public_key, + public_key_address, + owner_rule, + access_rules, workspace_bucket, } => { result.instruction_type = InstructionType::CreateAccount as i32; - result.create_account_owner_public_key = owner_public_key.to_vec(); + result.create_account_public_key = public_key_address.to_vec(); + result.create_account_owner_rule = owner_rule.map(Into::into); + result.create_account_access_rules = access_rules.map(Into::into); result.create_account_workspace_bucket = workspace_bucket.unwrap_or_default(); }, Instruction::CallFunction { @@ -597,3 +605,39 @@ impl From for proto::transaction::ViewableBalanceProof { } } } + +// -------------------------------- OwnerRule -------------------------------- // + +impl From for proto::transaction::OwnerRule { + fn from(value: OwnerRule) -> Self { + Self { + encoded_owner_rule: encode(&value).unwrap(), + } + } +} + +impl TryFrom for OwnerRule { + type Error = anyhow::Error; + + fn try_from(value: proto::transaction::OwnerRule) -> Result { + Ok(decode_exact(&value.encoded_owner_rule)?) + } +} + +// -------------------------------- AccessRules -------------------------------- // + +impl From for proto::transaction::AccessRules { + fn from(value: AccessRules) -> Self { + Self { + encoded_access_rules: encode(&value).unwrap(), + } + } +} + +impl TryFrom for AccessRules { + type Error = anyhow::Error; + + fn try_from(value: proto::transaction::AccessRules) -> Result { + Ok(decode_exact(&value.encoded_access_rules)?) + } +} diff --git a/dan_layer/template_builtin/templates/account/src/lib.rs b/dan_layer/template_builtin/templates/account/src/lib.rs index 49a445c68..e34977863 100644 --- a/dan_layer/template_builtin/templates/account/src/lib.rs +++ b/dan_layer/template_builtin/templates/account/src/lib.rs @@ -33,31 +33,27 @@ mod account_template { } impl Account { - pub fn create(owner_token: NonFungibleAddress) -> Component { - Self::internal_create(owner_token, None) - } - - pub fn create_with_bucket(owner_token: NonFungibleAddress, bucket: Bucket) -> Component { - Self::internal_create(owner_token, Some(bucket)) - } - - fn internal_create(owner_token: NonFungibleAddress, bucket: Option) -> Component { + pub fn create(public_key_token: NonFungibleAddress, owner_rule: Option, access_rules: Option, bucket: Option) -> Component { // extract the public key from the token - // we only allow owner tokens that correspond to public keys - let public_key = owner_token + // we only allow tokens that correspond to public keys + let public_key = public_key_token .to_public_key() - .unwrap_or_else(|| panic!("owner_token is not a valid public key: {}", owner_token)); - - // only the owner of the token will be able to withdraw funds from the account - let withdraw_rule = - AccessRule::Restricted(RestrictedAccessRule::Require(RequireRule::Require(owner_token.into()))); - let rules = AccessRules::new() - .add_method_rule("balance", AccessRule::AllowAll) - .add_method_rule("get_balances", AccessRule::AllowAll) - .add_method_rule("deposit", AccessRule::AllowAll) - .add_method_rule("deposit_all", AccessRule::AllowAll) - .add_method_rule("get_non_fungible_ids", AccessRule::AllowAll) - .default(withdraw_rule); + .unwrap_or_else(|| panic!("public_key_token is not a valid public key: {}", public_key_token)); + + let owner_rule = owner_rule.unwrap_or( + OwnerRule::ByPublicKey(public_key) + ); + + let access_rules = access_rules.unwrap_or( + AccessRules::new() + .add_method_rule("balance", AccessRule::AllowAll) + .add_method_rule("get_balances", AccessRule::AllowAll) + .add_method_rule("deposit", AccessRule::AllowAll) + .add_method_rule("deposit_all", AccessRule::AllowAll) + .add_method_rule("get_non_fungible_ids", AccessRule::AllowAll) + // By defaul, only the owner of the token will be able to withdraw funds from the account + .default(AccessRule::Restricted(RestrictedAccessRule::Require(RequireRule::Require(public_key_token.into())))) + ); // add the funds from the (optional) bucket let mut vaults = BTreeMap::new(); @@ -66,9 +62,9 @@ mod account_template { } Component::new(Self { vaults }) - .with_access_rules(rules) + .with_access_rules(access_rules) .with_public_key_address(public_key) - .with_owner_rule(OwnerRule::ByPublicKey(public_key)) + .with_owner_rule(owner_rule) .create() } diff --git a/dan_layer/template_lib/src/auth/owner_rule.rs b/dan_layer/template_lib/src/auth/owner_rule.rs index 70c243fbd..f5dde459a 100644 --- a/dan_layer/template_lib/src/auth/owner_rule.rs +++ b/dan_layer/template_lib/src/auth/owner_rule.rs @@ -12,7 +12,7 @@ pub struct Ownership<'a> { } /// An enum for all possible ways to specify ownership of values -#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)] +#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize, PartialEq)] #[cfg_attr( feature = "ts", derive(ts_rs::TS), diff --git a/dan_layer/template_test_tooling/src/template_test.rs b/dan_layer/template_test_tooling/src/template_test.rs index 390ac008b..8e8d62c3e 100644 --- a/dan_layer/template_test_tooling/src/template_test.rs +++ b/dan_layer/template_test_tooling/src/template_test.rs @@ -297,7 +297,9 @@ impl TemplateTest { let result = self .execute_and_commit( vec![Instruction::CreateAccount { - owner_public_key, + public_key_address: owner_public_key, + owner_rule: None, + access_rules: None, workspace_bucket, }], proofs, @@ -420,6 +422,28 @@ impl TemplateTest { (component, owner_proof, secret_key) } + pub fn create_custom_funded_account(&mut self) -> (ComponentAddress, NonFungibleAddress, RistrettoSecretKey) { + let (owner_proof, public_key, secret_key) = self.create_owner_proof(); + let old_fail_fees = self.enable_fees; + self.enable_fees = false; + let result = self.execute_expect_success( + Transaction::builder() + .call_method(test_faucet_component(), "take_free_coins", args![]) + .put_last_instruction_output_on_workspace("bucket") + .create_account_with_bucket(public_key, "bucket") + .sign(&secret_key) + .build(), + vec![owner_proof.clone()], + ); + + let component = result.finalize.execution_results[2] + .decode::() + .unwrap(); + + self.enable_fees = old_fail_fees; + (component, owner_proof, secret_key) + } + fn next_key_seed(&mut self) -> u8 { let seed = self.key_seed; self.key_seed += 1; diff --git a/dan_layer/transaction/src/builder.rs b/dan_layer/transaction/src/builder.rs index b61e30e34..d62a9a5a7 100644 --- a/dan_layer/transaction/src/builder.rs +++ b/dan_layer/transaction/src/builder.rs @@ -7,7 +7,9 @@ use tari_engine_types::{confidential::ConfidentialClaim, instruction::Instructio use tari_template_lib::{ args, args::Arg, + auth::OwnerRule, models::{Amount, ComponentAddress, ConfidentialWithdrawProof, ResourceAddress}, + prelude::AccessRules, }; use crate::{unsigned_transaction::UnsignedTransaction, Transaction, TransactionSignature}; @@ -62,18 +64,37 @@ impl TransactionBuilder { pub fn create_account(self, owner_public_key: PublicKey) -> Self { self.add_instruction(Instruction::CreateAccount { - owner_public_key, + public_key_address: owner_public_key, + owner_rule: None, + access_rules: None, workspace_bucket: None, }) } pub fn create_account_with_bucket>(self, owner_public_key: PublicKey, workspace_bucket: T) -> Self { self.add_instruction(Instruction::CreateAccount { - owner_public_key, + public_key_address: owner_public_key, + owner_rule: None, + access_rules: None, workspace_bucket: Some(workspace_bucket.into()), }) } + pub fn create_account_with_custom_rules>( + self, + public_key_address: PublicKey, + owner_rule: Option, + access_rules: Option, + workspace_bucket: Option, + ) -> Self { + self.add_instruction(Instruction::CreateAccount { + public_key_address, + owner_rule, + access_rules, + workspace_bucket: workspace_bucket.map(|b| b.into()), + }) + } + pub fn call_function(self, template_address: TemplateAddress, function: &str, args: Vec) -> Self { self.add_instruction(Instruction::CallFunction { template_address,