diff --git a/soroban-env-host/src/auth.rs b/soroban-env-host/src/auth.rs index 160f11a97..84bc96e23 100644 --- a/soroban-env-host/src/auth.rs +++ b/soroban-env-host/src/auth.rs @@ -176,7 +176,7 @@ use super::xdr::Hash; use crate::{ builtin_contracts::{account_contract::AccountEd25519Signature, base_types::BytesN}, host::error::TryBorrowOrErr, - xdr::PublicKey, + xdr::{ContractExecutable, PublicKey}, }; #[cfg(any(test, feature = "recording_mode"))] use rand::Rng; @@ -2072,9 +2072,44 @@ impl AccountAuthorizationTracker { // - Return budget error in case if it was suppressed above. let _ = acc.metered_clone(host.as_budget())?; } - // Skip custom accounts for now - emulating authentication for - // them requires a dummy signature. - ScAddress::Contract(_) => {} + // We only know for sure that the contract instance and Wasm will be + // loaded. + ScAddress::Contract(contract_id) => { + let instance_key = host.contract_instance_ledger_key(&contract_id)?; + let entry = host + .try_borrow_storage_mut()? + .try_get(&instance_key, host, None)?; + // In test scenarios we often may not have any actual instance, which is fine most + // of the time, so we don't return any errors. + // In simulation scenarios the instance will likely be there, and when it's + // not, we still make our best effort and include at least the necessary instance key + // into the footprint. + let instance = if let Some(entry) = entry { + match &entry.data { + LedgerEntryData::ContractData(e) => match &e.val { + ScVal::ContractInstance(instance) => instance.metered_clone(host)?, + _ => { + return Ok(()); + } + }, + _ => { + return Ok(()); + } + } + } else { + return Ok(()); + }; + + match &instance.executable { + ContractExecutable::Wasm(wasm_hash) => { + let wasm_key = host.contract_code_ledger_key(wasm_hash)?; + let _ = host + .try_borrow_storage_mut()? + .try_get(&wasm_key, host, None)?; + } + ContractExecutable::StellarAsset => (), + } + } } Ok(()) } diff --git a/soroban-env-host/src/e2e_testutils.rs b/soroban-env-host/src/e2e_testutils.rs index 84bee7a6a..7c64c8647 100644 --- a/soroban-env-host/src/e2e_testutils.rs +++ b/soroban-env-host/src/e2e_testutils.rs @@ -6,8 +6,9 @@ use crate::xdr::{ ExtensionPoint, HashIdPreimage, HashIdPreimageContractId, HostFunction, InvokeContractArgs, LedgerEntry, LedgerEntryData, LedgerEntryExt, LedgerKey, LedgerKeyContractCode, LedgerKeyContractData, Limits, PublicKey, ScAddress, ScBytes, ScContractInstance, ScMapEntry, - ScSymbol, ScVal, SequenceNumber, SorobanAuthorizationEntry, SorobanAuthorizedFunction, - SorobanAuthorizedInvocation, SorobanCredentials, Thresholds, Uint256, WriteXdr, + ScSymbol, ScVal, SequenceNumber, SorobanAddressCredentials, SorobanAuthorizationEntry, + SorobanAuthorizedFunction, SorobanAuthorizedInvocation, SorobanCredentials, Thresholds, + Uint256, WriteXdr, }; use crate::{Host, LedgerInfo}; use sha2::{Digest, Sha256}; @@ -126,8 +127,27 @@ impl CreateContractData { wasm: &[u8], refined_cost_inputs: bool, ) -> Self { - let deployer = get_account_id([123; 32]); - let contract_id_preimage = get_contract_id_preimage(&deployer, &salt); + Self::new_with_refined_contract_cost_inputs_and_deployer( + None, + salt, + wasm, + refined_cost_inputs, + ) + } + + pub fn new_with_refined_contract_cost_inputs_and_deployer( + deployer_with_nonce: Option<(ScAddress, i64)>, + salt: [u8; 32], + wasm: &[u8], + refined_cost_inputs: bool, + ) -> Self { + let source = get_account_id([123; 32]); + let deployer = if let Some((deployer, _)) = &deployer_with_nonce { + deployer.clone() + } else { + ScAddress::Account(source.clone()) + }; + let contract_id_preimage = get_contract_id_preimage_from_address(&deployer, &salt); let host_fn = HostFunction::CreateContract(CreateContractArgs { contract_id_preimage: contract_id_preimage.clone(), @@ -143,7 +163,8 @@ impl CreateContractData { key: ScVal::LedgerKeyContractInstance, durability: ContractDataDurability::Persistent, }); - let auth_entry = create_contract_auth(&contract_id_preimage, wasm); + let auth_entry = + create_contract_auth_for_address(deployer_with_nonce, &contract_id_preimage, wasm); let contract_entry = ledger_entry(LedgerEntryData::ContractData(ContractDataEntry { ext: ExtensionPoint::V0, @@ -159,7 +180,7 @@ impl CreateContractData { let wasm_entry = wasm_entry_with_refined_contract_cost_inputs(wasm, refined_cost_inputs); Self { - deployer, + deployer: source, wasm_key: get_wasm_key(wasm), wasm_entry, contract_key, @@ -184,13 +205,20 @@ pub fn get_account_id(pub_key: [u8; 32]) -> AccountId { AccountId(PublicKey::PublicKeyTypeEd25519(pub_key.try_into().unwrap())) } -pub fn get_contract_id_preimage(account_id: &AccountId, salt: &[u8; 32]) -> ContractIdPreimage { +pub fn get_contract_id_preimage_from_address( + address: &ScAddress, + salt: &[u8; 32], +) -> ContractIdPreimage { ContractIdPreimage::Address(ContractIdPreimageFromAddress { - address: ScAddress::Account(account_id.clone()), + address: address.clone(), salt: Uint256(*salt), }) } +pub fn get_contract_id_preimage(account_id: &AccountId, salt: &[u8; 32]) -> ContractIdPreimage { + get_contract_id_preimage_from_address(&ScAddress::Account(account_id.clone()), salt) +} + pub fn get_contract_id_hash(id_preimage: &ContractIdPreimage) -> [u8; 32] { let preimage = HashIdPreimage::ContractId(HashIdPreimageContractId { network_id: DEFAULT_NETWORK_ID.try_into().unwrap(), @@ -203,8 +231,26 @@ pub fn create_contract_auth( contract_id_preimage: &ContractIdPreimage, wasm: &[u8], ) -> SorobanAuthorizationEntry { + create_contract_auth_for_address(None, contract_id_preimage, wasm) +} + +pub fn create_contract_auth_for_address( + address_and_nonce: Option<(ScAddress, i64)>, + contract_id_preimage: &ContractIdPreimage, + wasm: &[u8], +) -> SorobanAuthorizationEntry { + let credentials = if let Some((address, nonce)) = address_and_nonce { + SorobanCredentials::Address(SorobanAddressCredentials { + address, + nonce, + signature_expiration_ledger: 0, + signature: ScVal::Void, + }) + } else { + SorobanCredentials::SourceAccount + }; SorobanAuthorizationEntry { - credentials: SorobanCredentials::SourceAccount, + credentials, root_invocation: SorobanAuthorizedInvocation { function: SorobanAuthorizedFunction::CreateContractV2HostFn(CreateContractArgsV2 { contract_id_preimage: contract_id_preimage.clone(), diff --git a/soroban-env-host/src/test/e2e_tests.rs b/soroban-env-host/src/test/e2e_tests.rs index fc4edd819..ce7c1658f 100644 --- a/soroban-env-host/src/test/e2e_tests.rs +++ b/soroban-env-host/src/test/e2e_tests.rs @@ -19,9 +19,10 @@ use crate::{ ContractIdPreimage, ContractIdPreimageFromAddress, CreateContractArgs, DiagnosticEvent, ExtensionPoint, HashIdPreimage, HashIdPreimageSorobanAuthorization, HostFunction, InvokeContractArgs, LedgerEntry, LedgerEntryData, LedgerFootprint, LedgerKey, - LedgerKeyContractCode, LedgerKeyContractData, Limits, ReadXdr, ScAddress, ScErrorCode, - ScErrorType, ScMap, ScVal, ScVec, SorobanAuthorizationEntry, SorobanCredentials, - SorobanResources, TtlEntry, Uint256, WriteXdr, + LedgerKeyContractCode, LedgerKeyContractData, Limits, ReadXdr, ScAddress, + ScContractInstance, ScErrorCode, ScErrorType, ScMap, ScNonceKey, ScVal, ScVec, + SorobanAuthorizationEntry, SorobanCredentials, SorobanResources, TtlEntry, Uint256, + WriteXdr, }, Host, HostError, LedgerInfo, }; @@ -1201,6 +1202,147 @@ fn test_create_contract_success_in_recording_mode() { ); } +#[test] +fn test_create_contract_success_in_recording_mode_with_custom_account() { + // We don't try to invoke `__check_auth` in recording mode in order to not output confusing + // side-effects. Thus any Wasm can stand for a custom account. + let custom_account_wasm = CONTRACT_STORAGE; + let custom_account_address = ScAddress::Contract([222; 32].into()); + let expected_nonce = 801925984706572462_i64; + + let cd = CreateContractData::new_with_refined_contract_cost_inputs_and_deployer( + Some((custom_account_address.clone(), expected_nonce)), + [111; 32], + ADD_I32, + true, + ); + + let custom_account_instance_entry = + ledger_entry(LedgerEntryData::ContractData(ContractDataEntry { + ext: ExtensionPoint::V0, + contract: custom_account_address.clone(), + key: ScVal::LedgerKeyContractInstance, + durability: ContractDataDurability::Persistent, + val: ScVal::ContractInstance(ScContractInstance { + executable: ContractExecutable::Wasm( + get_wasm_hash(custom_account_wasm).try_into().unwrap(), + ), + storage: None, + }), + })); + let ledger_info = default_ledger_info(); + let res = invoke_host_function_recording_helper( + true, + &cd.host_fn, + &cd.deployer, + None, + &ledger_info, + vec![ + ( + cd.wasm_entry.clone(), + Some(ledger_info.sequence_number + 100), + ), + ( + wasm_entry(custom_account_wasm), + Some(ledger_info.sequence_number + 1000), + ), + ( + custom_account_instance_entry.clone(), + Some(ledger_info.sequence_number + 1000), + ), + ], + &prng_seed(), + None, + ) + .unwrap(); + assert_eq!( + res.invoke_result.unwrap(), + ScVal::Address(cd.contract_address.clone()) + ); + assert!(res.contract_events.is_empty()); + + let nonce_key = ScVal::LedgerKeyNonce(ScNonceKey { + nonce: expected_nonce, + }); + let nonce_entry_key = LedgerKey::ContractData(LedgerKeyContractData { + contract: custom_account_address.clone(), + key: nonce_key.clone(), + durability: ContractDataDurability::Temporary, + }); + assert_eq!( + res.ledger_changes, + vec![ + LedgerEntryChangeHelper { + read_only: false, + key: cd.contract_key.clone(), + old_entry_size_bytes: 0, + new_value: Some(cd.contract_entry), + ttl_change: Some(LedgerEntryLiveUntilChange { + key_hash: compute_key_hash(&cd.contract_key), + durability: ContractDataDurability::Persistent, + old_live_until_ledger: 0, + new_live_until_ledger: ledger_info.sequence_number + + ledger_info.min_persistent_entry_ttl + - 1, + }), + }, + LedgerEntryChangeHelper::no_op_change( + &custom_account_instance_entry, + ledger_info.sequence_number + 1000, + ), + LedgerEntryChangeHelper { + read_only: false, + key: nonce_entry_key.clone(), + old_entry_size_bytes: 0, + new_value: Some(ledger_entry(LedgerEntryData::ContractData( + ContractDataEntry { + ext: ExtensionPoint::V0, + contract: custom_account_address.clone(), + key: nonce_key.clone(), + durability: ContractDataDurability::Temporary, + val: ScVal::Void, + } + ))), + ttl_change: Some(LedgerEntryLiveUntilChange { + key_hash: compute_key_hash(&nonce_entry_key), + durability: ContractDataDurability::Temporary, + old_live_until_ledger: 0, + new_live_until_ledger: ledger_info.sequence_number + ledger_info.max_entry_ttl + - 1, + }), + }, + LedgerEntryChangeHelper::no_op_change( + &cd.wasm_entry, + ledger_info.sequence_number + 100 + ), + LedgerEntryChangeHelper::no_op_change( + &wasm_entry(custom_account_wasm), + ledger_info.sequence_number + 1000 + ), + ] + ); + assert_eq!(res.auth, vec![cd.auth_entry]); + assert_eq!( + res.resources, + SorobanResources { + footprint: LedgerFootprint { + read_only: vec![ + ledger_entry_to_ledger_key(&custom_account_instance_entry, &Budget::default()) + .unwrap(), + cd.wasm_key, + get_wasm_key(custom_account_wasm), + ] + .try_into() + .unwrap(), + read_write: vec![cd.contract_key, nonce_entry_key].try_into().unwrap() + }, + instructions: 1767122, + read_bytes: 3816, + write_bytes: 176, + } + ); +} + #[test] fn test_create_contract_success_in_recording_mode_with_enforced_auth() { let cd = CreateContractData::new([111; 32], ADD_I32);