Skip to content

Commit

Permalink
Emulate access to custom account instance and Wasm in recording auth. (
Browse files Browse the repository at this point in the history
…#1476)

### What

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).

### Why

Better emulate the authorization behavior (in some edge cases this might
allow for single-pass simulation of custom accounts).

Resolves #1443

### Known limitations

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).
  • Loading branch information
dmkozh authored Nov 5, 2024
1 parent b0220c0 commit 61831e8
Show file tree
Hide file tree
Showing 3 changed files with 239 additions and 16 deletions.
43 changes: 39 additions & 4 deletions soroban-env-host/src/auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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(())
}
Expand Down
64 changes: 55 additions & 9 deletions soroban-env-host/src/e2e_testutils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -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(),
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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(),
Expand All @@ -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(),
Expand Down
148 changes: 145 additions & 3 deletions soroban-env-host/src/test/e2e_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
Expand Down Expand Up @@ -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);
Expand Down

0 comments on commit 61831e8

Please sign in to comment.