From 61af43deb14329bd7a635e4379beff61d76573ec Mon Sep 17 00:00:00 2001 From: Dmytro Kozhevin Date: Fri, 18 Oct 2024 15:22:24 -0400 Subject: [PATCH] Emulate access to custom account instance and Wasm in recording auth. The change gracefully handles failures (e.g. allows for the instance not being present) in order to not spam diagnostics in unit tests that don't care about the footprint (which makes up for majority of the unit tests). In theory, we could also try calling `__check_auth` on the contract (if possible), but that will likely produce confusing diagnostics for most of the contracts (e.g. signature verification errors). --- soroban-env-host/src/auth.rs | 43 ++++++- soroban-env-host/src/e2e_testutils.rs | 64 +++++++++-- soroban-env-host/src/test/e2e_tests.rs | 148 ++++++++++++++++++++++++- 3 files changed, 239 insertions(+), 16 deletions(-) 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);