Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions soroban-sdk/src/env.rs
Original file line number Diff line number Diff line change
Expand Up @@ -600,6 +600,42 @@ use std::{path::Path, rc::Rc};
#[cfg(any(test, feature = "testutils"))]
use xdr::{LedgerEntry, LedgerKey, LedgerKeyContractData, SorobanAuthorizationEntry};

/// Wraps a [`SnapshotSource`](internal::storage::SnapshotSource) and short
/// circuits lookups for the empty WASM hash ContractCode entry used by native
/// test contracts, returning `Ok(None)` without ever consulting the inner
/// source. This is an optimization that avoids wasteful (and always-failing)
Comment thread
leighmcculloch marked this conversation as resolved.
Outdated
/// lookups against remote snapshot sources for an entry that will never exist
/// on any real network.
#[cfg(any(test, feature = "testutils"))]
struct FilteringSnapshotSource {
inner: Rc<dyn internal::storage::SnapshotSource>,
}

#[cfg(any(test, feature = "testutils"))]
impl internal::storage::SnapshotSource for FilteringSnapshotSource {
fn get(
&self,
key: &Rc<xdr::LedgerKey>,
) -> Result<Option<(Rc<xdr::LedgerEntry>, Option<u32>)>, soroban_env_host::HostError> {
// The SHA256 of empty/zero bytes, which the host uses as the synthetic
// WASM hash for native (non-WASM) test contracts. ContractCode entries
// for this hash never exist on any real network, so lookups for them
// are filtered out before reaching a `SnapshotSource`.
const EMPTY_WASM_HASH: [u8; 32] = [
0xe3, 0xb0, 0xc4, 0x42, 0x98, 0xfc, 0x1c, 0x14, 0x9a, 0xfb, 0xf4, 0xc8, 0x99, 0x6f,
0xb9, 0x24, 0x27, 0xae, 0x41, 0xe4, 0x64, 0x9b, 0x93, 0x4c, 0xa4, 0x95, 0x99, 0x1b,
0x78, 0x52, 0xb8, 0x55,
];
if let xdr::LedgerKey::ContractCode(xdr::LedgerKeyContractCode { hash, .. }) = key.as_ref()
{
if hash.0 == EMPTY_WASM_HASH {
return Ok(None);
}
}
self.inner.get(key)
}
}

#[cfg(any(test, feature = "testutils"))]
#[cfg_attr(feature = "docs", doc(cfg(feature = "testutils")))]
impl Env {
Expand Down Expand Up @@ -684,6 +720,14 @@ impl Env {
1
};

// Wrap the incoming snapshot source so that lookups for the empty WASM
// hash ContractCode entry (used by native test contracts) are short
// circuited and never reach the underlying source. See issue #1635
// "Empty WASM hash leaks to SnapshotSource".
Comment thread
leighmcculloch marked this conversation as resolved.
Outdated
let recording_footprint: Rc<dyn internal::storage::SnapshotSource> =
Rc::new(FilteringSnapshotSource {
inner: recording_footprint,
});
let storage = internal::storage::Storage::with_recording_footprint(recording_footprint);
let budget = internal::budget::Budget::default();
let env_impl = internal::EnvImpl::with_storage_and_budget(storage, budget.clone());
Expand Down
1 change: 1 addition & 0 deletions soroban-sdk/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ mod prng;
mod prng_range;
mod proptest_scval_cmp;
mod proptest_val_cmp;
mod snapshot_source_empty_wasm_hash;
mod storage_testutils;
mod token_client;
mod vec_slice;
79 changes: 79 additions & 0 deletions soroban-sdk/src/tests/snapshot_source_empty_wasm_hash.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
use crate::{self as soroban_sdk};
use soroban_sdk::{
contract, contractimpl,
testutils::{HostError, SnapshotSource, SnapshotSourceInput},
xdr, Env,
};
use std::{cell::RefCell, rc::Rc};

#[contract]
pub struct Contract;

#[contractimpl]
impl Contract {
pub fn hello(_env: Env) {}
}

/// The SHA256 of empty/zero bytes, used by the host as the synthetic WASM hash
/// for native (non-WASM) test contracts.
const EMPTY_WASM_HASH: [u8; 32] = [
0xe3, 0xb0, 0xc4, 0x42, 0x98, 0xfc, 0x1c, 0x14, 0x9a, 0xfb, 0xf4, 0xc8, 0x99, 0x6f, 0xb9, 0x24,
0x27, 0xae, 0x41, 0xe4, 0x64, 0x9b, 0x93, 0x4c, 0xa4, 0x95, 0x99, 0x1b, 0x78, 0x52, 0xb8, 0x55,
];

/// A snapshot source that records every key it is asked for and always returns
/// `Ok(None)`.
struct RecordingSnapshotSource {
keys: Rc<RefCell<Vec<xdr::LedgerKey>>>,
}

impl SnapshotSource for RecordingSnapshotSource {
fn get(
&self,
key: &Rc<xdr::LedgerKey>,
) -> Result<Option<(Rc<xdr::LedgerEntry>, Option<u32>)>, HostError> {
self.keys.borrow_mut().push(key.as_ref().clone());
Ok(None)
}
}

#[test]
fn test_empty_wasm_hash_not_requested_from_snapshot_source() {
let keys = Rc::new(RefCell::new(Vec::new()));
let input = SnapshotSourceInput {
source: Rc::new(RecordingSnapshotSource { keys: keys.clone() }),
ledger_info: None,
snapshot: None,
};

let env = Env::from_ledger_snapshot(input);

// Registering a native test contract should not cause a lookup for the
// empty WASM hash ContractCode entry against the snapshot source.
let _ = env.register(Contract, ());

let recorded = keys.borrow();

// The empty WASM hash ContractCode entry must never be requested.
let requested_empty_wasm = recorded.iter().any(|k| {
matches!(
k,
xdr::LedgerKey::ContractCode(xdr::LedgerKeyContractCode { hash, .. })
if hash.0 == EMPTY_WASM_HASH
)
});
assert!(
!requested_empty_wasm,
"empty WASM hash ContractCode entry should not be requested from the snapshot source, recorded keys: {recorded:?}"
);

// Sanity check: other lookups (such as the contract instance ContractData
// entry) are still allowed to reach the snapshot source.
let requested_contract_data = recorded
.iter()
.any(|k| matches!(k, xdr::LedgerKey::ContractData(_)));
assert!(
requested_contract_data,
"expected the contract instance ContractData lookup to reach the snapshot source, recorded keys: {recorded:?}"
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
{
"generators": {
"address": 1,
"nonce": 0,
"mux_id": 0
},
"auth": [
[]
],
"ledger": {
"protocol_version": 26,
"sequence_number": 0,
"timestamp": 0,
"network_id": "0000000000000000000000000000000000000000000000000000000000000000",
"base_reserve": 0,
"min_persistent_entry_ttl": 4096,
"min_temp_entry_ttl": 16,
"max_entry_ttl": 6312000,
"ledger_entries": [
{
"entry": {
"last_modified_ledger_seq": 0,
"data": {
"contract_data": {
"ext": "v0",
"contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM",
"key": "ledger_key_contract_instance",
"durability": "persistent",
"val": {
"contract_instance": {
"executable": {
"wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
},
"storage": null
}
}
}
},
"ext": "v0"
},
"live_until": 4095
},
{
"entry": {
"last_modified_ledger_seq": 0,
"data": {
"contract_code": {
"ext": "v0",
"hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"code": ""
}
},
"ext": "v0"
},
"live_until": 4095
}
]
},
"events": []
}
Loading