Skip to content

Commit db843ff

Browse files
committed
Add tests for reusable module cache.
1 parent a33e287 commit db843ff

File tree

1 file changed

+329
-0
lines changed

1 file changed

+329
-0
lines changed

src/transactions/test/InvokeHostFunctionTests.cpp

+329
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
// of this distribution or at http://www.apache.org/licenses/LICENSE-2.0
44

55
#include "test/test.h"
6+
#include "transactions/TransactionFrameBase.h"
67
#include "util/Logging.h"
78
#include "util/ProtocolVersion.h"
89
#include "util/UnorderedSet.h"
@@ -4771,3 +4772,331 @@ TEST_CASE("contract constructor support", "[tx][soroban]")
47714772
REQUIRE(invocation.getReturnValue().u32() == 303);
47724773
}
47734774
}
4775+
4776+
#ifdef ENABLE_NEXT_PROTOCOL_VERSION_UNSAFE_FOR_PRODUCTION
4777+
4778+
static TransactionFrameBasePtr
4779+
makeAddTx(TestContract const& contract, int64_t instructions,
4780+
TestAccount& source)
4781+
{
4782+
auto fnName = "add";
4783+
auto sc7 = makeI32(7);
4784+
auto sc16 = makeI32(16);
4785+
auto spec = SorobanInvocationSpec()
4786+
.setInstructions(instructions)
4787+
.setReadBytes(2'000)
4788+
.setInclusionFee(12345)
4789+
.setNonRefundableResourceFee(33'000)
4790+
.setRefundableResourceFee(100'000);
4791+
auto invocation = contract.prepareInvocation(fnName, {sc7, sc16}, spec);
4792+
return invocation.createTx(&source);
4793+
}
4794+
4795+
static bool
4796+
wasms_are_cached(Application& app, std::vector<Hash> const& wasms)
4797+
{
4798+
auto moduleCache = app.getLedgerManager().getModuleCache();
4799+
for (auto const& wasm : wasms)
4800+
{
4801+
if (!moduleCache->contains_module(
4802+
app.getLedgerManager()
4803+
.getLastClosedLedgerHeader()
4804+
.header.ledgerVersion,
4805+
::rust::Slice{wasm.data(), wasm.size()}))
4806+
{
4807+
return false;
4808+
}
4809+
}
4810+
return true;
4811+
}
4812+
4813+
static const int64_t INVOKE_ADD_UNCACHED_COST_PASS = 500'000;
4814+
static const int64_t INVOKE_ADD_UNCACHED_COST_FAIL = 400'000;
4815+
4816+
static const int64_t INVOKE_ADD_CACHED_COST_PASS = 300'000;
4817+
static const int64_t INVOKE_ADD_CACHED_COST_FAIL = 200'000;
4818+
4819+
TEST_CASE("reusable module cache", "[soroban][modulecache]")
4820+
{
4821+
VirtualClock clock;
4822+
Config cfg = getTestConfig(0, Config::TESTDB_BUCKET_DB_PERSISTENT);
4823+
4824+
cfg.OVERRIDE_EVICTION_PARAMS_FOR_TESTING = true;
4825+
cfg.TESTING_STARTING_EVICTION_SCAN_LEVEL = 1;
4826+
4827+
// This test uses/tests/requires the reusable module cache.
4828+
if (!protocolVersionStartsFrom(
4829+
cfg.LEDGER_PROTOCOL_VERSION,
4830+
REUSABLE_SOROBAN_MODULE_CACHE_PROTOCOL_VERSION))
4831+
return;
4832+
4833+
// First upload some wasms
4834+
std::vector<RustBuf> testWasms = {rust_bridge::get_test_wasm_add_i32(),
4835+
rust_bridge::get_test_wasm_err(),
4836+
rust_bridge::get_test_wasm_complex()};
4837+
4838+
std::vector<Hash> contractHashes;
4839+
uint32_t ttl{0};
4840+
{
4841+
txtest::SorobanTest stest(cfg);
4842+
ttl = stest.getNetworkCfg().stateArchivalSettings().minPersistentTTL;
4843+
for (auto const& wasm : testWasms)
4844+
{
4845+
4846+
stest.deployWasmContract(wasm);
4847+
contractHashes.push_back(sha256(wasm));
4848+
}
4849+
// Check the module cache got populated by the uploads.
4850+
REQUIRE(wasms_are_cached(stest.getApp(), contractHashes));
4851+
}
4852+
4853+
// Restart the application and check module cache gets populated in the new
4854+
// app.
4855+
auto app = createTestApplication(clock, cfg, false, true);
4856+
REQUIRE(wasms_are_cached(*app, contractHashes));
4857+
4858+
// Crank the app forward a while until the wasms are evicted.
4859+
CLOG_INFO(Ledger, "advancing for {} ledgers to evict wasms", ttl);
4860+
for (int i = 0; i < ttl; ++i)
4861+
{
4862+
txtest::closeLedger(*app);
4863+
}
4864+
// Check the modules got evicted.
4865+
REQUIRE(!wasms_are_cached(*app, contractHashes));
4866+
}
4867+
4868+
TEST_CASE("Module cache across protocol versions", "[tx][soroban][modulecache]")
4869+
{
4870+
VirtualClock clock;
4871+
auto cfg = getTestConfig(0);
4872+
// Start in p22
4873+
cfg.TESTING_UPGRADE_LEDGER_PROTOCOL_VERSION =
4874+
static_cast<int>(REUSABLE_SOROBAN_MODULE_CACHE_PROTOCOL_VERSION) - 1;
4875+
auto app = createTestApplication(clock, cfg);
4876+
4877+
// Deploy and invoke contract in protocol 22
4878+
SorobanTest test(app);
4879+
auto const& addContract =
4880+
test.deployWasmContract(rust_bridge::get_test_wasm_add_i32());
4881+
4882+
auto invoke = [&](int64_t instructions) -> bool {
4883+
auto tx = makeAddTx(addContract, instructions, test.getRoot());
4884+
auto res = test.invokeTx(tx);
4885+
return isSuccessResult(res);
4886+
};
4887+
4888+
REQUIRE(!invoke(INVOKE_ADD_UNCACHED_COST_FAIL));
4889+
REQUIRE(invoke(INVOKE_ADD_UNCACHED_COST_PASS));
4890+
4891+
// The upload should have triggered a single compilation for the p23 module
4892+
// cache, which _exists_ in this version of stellar-core, and needs to be
4893+
// populated on each upload, is just not yet active.
4894+
REQUIRE(app->getLedgerManager()
4895+
.getSorobanMetrics()
4896+
.mModuleCacheNumEntries.count() == 1);
4897+
4898+
// Upgrade to protocol 23 (with the reusable module cache)
4899+
auto upgradeTo23 = LedgerUpgrade{LEDGER_UPGRADE_VERSION};
4900+
upgradeTo23.newLedgerVersion() =
4901+
static_cast<int>(REUSABLE_SOROBAN_MODULE_CACHE_PROTOCOL_VERSION);
4902+
executeUpgrade(*app, upgradeTo23);
4903+
4904+
// We can now run the same contract with fewer instructions
4905+
REQUIRE(!invoke(INVOKE_ADD_CACHED_COST_FAIL));
4906+
REQUIRE(invoke(INVOKE_ADD_CACHED_COST_PASS));
4907+
}
4908+
4909+
TEST_CASE("Module cache miss on immediate execution",
4910+
"[tx][soroban][modulecache]")
4911+
{
4912+
VirtualClock clock;
4913+
auto cfg = getTestConfig(0);
4914+
4915+
auto app = createTestApplication(clock, cfg);
4916+
auto upgradeTo22 = LedgerUpgrade{LEDGER_UPGRADE_VERSION};
4917+
upgradeTo22.newLedgerVersion() =
4918+
static_cast<int>(REUSABLE_SOROBAN_MODULE_CACHE_PROTOCOL_VERSION) - 1;
4919+
executeUpgrade(*app, upgradeTo22);
4920+
4921+
SorobanTest test(app);
4922+
auto wasm = rust_bridge::get_test_wasm_add_i32();
4923+
4924+
SECTION("separate ledger upload and execution")
4925+
{
4926+
// First upload the contract
4927+
auto const& contract = test.deployWasmContract(wasm);
4928+
4929+
// Confirm upload succeeded and triggered compilation
4930+
REQUIRE(app->getLedgerManager()
4931+
.getSorobanMetrics()
4932+
.mModuleCacheNumEntries.count() == 1);
4933+
4934+
// Try to execute with low instructions since we can use cached module.
4935+
auto txFail =
4936+
makeAddTx(contract, INVOKE_ADD_CACHED_COST_FAIL, test.getRoot());
4937+
REQUIRE(!isSuccessResult(test.invokeTx(txFail)));
4938+
4939+
auto txPass =
4940+
makeAddTx(contract, INVOKE_ADD_CACHED_COST_PASS, test.getRoot());
4941+
REQUIRE(isSuccessResult(test.invokeTx(txPass)));
4942+
}
4943+
4944+
SECTION("same ledger upload and execution")
4945+
{
4946+
4947+
// Here we're going to create 4 txs in the same ledger (so they have to
4948+
// come from 4 separate accounts). The 1st uploads a contract wasm, the
4949+
// 2nd creates a contract, and the 3rd and 4th run it.
4950+
//
4951+
// Because all 4 happen in the same ledger, there is no opportunity for
4952+
// the module cache to be populated between the upload and the
4953+
// execution. This should result in a cache miss and higher cost: the
4954+
// 3rd (invoking) tx fails and the 4th passes, but at the higher cost.
4955+
//
4956+
// Finally to confirm that the cache is populated, we run the same
4957+
// invocations in the next ledger and it should succeed at a lower cost.
4958+
4959+
auto minbal = test.getApp().getLedgerManager().getLastMinBalance(1);
4960+
TestAccount A(test.getRoot().create("A", minbal * 1000));
4961+
TestAccount B(test.getRoot().create("B", minbal * 1000));
4962+
TestAccount C(test.getRoot().create("C", minbal * 1000));
4963+
TestAccount D(test.getRoot().create("D", minbal * 1000));
4964+
4965+
// Transaction 1: the upload
4966+
auto uploadResources = defaultUploadWasmResourcesWithoutFootprint(
4967+
wasm, getLclProtocolVersion(test.getApp()));
4968+
auto uploadTx = makeSorobanWasmUploadTx(test.getApp(), A, wasm,
4969+
uploadResources, 1000);
4970+
4971+
// Transaction 2: create contract
4972+
Hash contractHash = sha256(wasm);
4973+
ContractExecutable executable = makeWasmExecutable(contractHash);
4974+
Hash salt = sha256("salt");
4975+
ContractIDPreimage contractPreimage = makeContractIDPreimage(B, salt);
4976+
HashIDPreimage hashPreimage = makeFullContractIdPreimage(
4977+
test.getApp().getNetworkID(), contractPreimage);
4978+
SCAddress contractId = makeContractAddress(xdrSha256(hashPreimage));
4979+
auto createResources = SorobanResources();
4980+
createResources.instructions = 5'000'000;
4981+
createResources.readBytes =
4982+
static_cast<uint32_t>(wasm.data.size() + 1000);
4983+
createResources.writeBytes = 1000;
4984+
auto createContractTx =
4985+
makeSorobanCreateContractTx(test.getApp(), B, contractPreimage,
4986+
executable, createResources, 1000);
4987+
4988+
// Transaction 3: invocation (with inadequate instructions to succeed)
4989+
TestContract contract(test, contractId,
4990+
{contractCodeKey(contractHash),
4991+
makeContractInstanceKey(contractId)});
4992+
auto invokeFailTx =
4993+
makeAddTx(contract, INVOKE_ADD_UNCACHED_COST_FAIL, C);
4994+
4995+
// Transaction 4: invocation (with inadequate instructions to succeed)
4996+
auto invokePassTx =
4997+
makeAddTx(contract, INVOKE_ADD_UNCACHED_COST_PASS, C);
4998+
4999+
// Run single ledger with all 4 txs. First 2 should pass, 3rd should
5000+
// fail, 4th should pass.
5001+
auto txResults = closeLedger(
5002+
*app, {uploadTx, createContractTx, invokeFailTx, invokePassTx},
5003+
/*strictOrder=*/true);
5004+
5005+
REQUIRE(txResults.results.size() == 4);
5006+
REQUIRE(
5007+
isSuccessResult(txResults.results[0].result)); // Upload succeeds
5008+
REQUIRE(
5009+
isSuccessResult(txResults.results[1].result)); // Create succeeds
5010+
REQUIRE(!isSuccessResult(
5011+
txResults.results[2].result)); // Invoke fails at 400k
5012+
REQUIRE(isSuccessResult(
5013+
txResults.results[3].result)); // Invoke passes at 500k
5014+
5015+
// But if we try again in next ledger, the cost threshold should be
5016+
// lower.
5017+
auto invokeTxFail2 =
5018+
makeAddTx(contract, INVOKE_ADD_CACHED_COST_FAIL, C);
5019+
auto invokeTxPass2 =
5020+
makeAddTx(contract, INVOKE_ADD_CACHED_COST_PASS, D);
5021+
txResults = closeLedger(*app, {invokeTxFail2, invokeTxPass2},
5022+
/*strictOrder=*/true);
5023+
REQUIRE(txResults.results.size() == 2);
5024+
REQUIRE(!isSuccessResult(txResults.results[0].result));
5025+
REQUIRE(isSuccessResult(txResults.results[1].result));
5026+
}
5027+
}
5028+
5029+
TEST_CASE("Module cache cost with restore gaps", "[tx][soroban][modulecache]")
5030+
{
5031+
VirtualClock clock;
5032+
auto cfg = getTestConfig(0);
5033+
5034+
cfg.OVERRIDE_EVICTION_PARAMS_FOR_TESTING = true;
5035+
cfg.TESTING_STARTING_EVICTION_SCAN_LEVEL = 1;
5036+
5037+
auto app = createTestApplication(clock, cfg);
5038+
auto& lm = app->getLedgerManager();
5039+
SorobanTest test(app);
5040+
auto wasm = rust_bridge::get_test_wasm_add_i32();
5041+
5042+
auto minbal = lm.getLastMinBalance(1);
5043+
TestAccount A(test.getRoot().create("A", minbal * 1000));
5044+
TestAccount B(test.getRoot().create("B", minbal * 1000));
5045+
5046+
auto contract = test.deployWasmContract(wasm);
5047+
auto contractKeys = contract.getKeys();
5048+
5049+
// Let contract expire
5050+
auto ttl = test.getNetworkCfg().stateArchivalSettings().minPersistentTTL;
5051+
auto proto = lm.getLastClosedLedgerHeader().header.ledgerVersion;
5052+
for (auto i = 0; i < ttl; ++i)
5053+
{
5054+
closeLedger(test.getApp());
5055+
}
5056+
auto moduleCache = lm.getModuleCache();
5057+
auto const wasmHash = sha256(wasm);
5058+
REQUIRE(!moduleCache->contains_module(
5059+
proto, ::rust::Slice{wasmHash.data(), wasmHash.size()}));
5060+
5061+
SECTION("scenario A: restore in one ledger, invoke in next")
5062+
{
5063+
// Restore contract in ledger N+1
5064+
test.invokeRestoreOp(contractKeys, 40096);
5065+
5066+
// Invoke in ledger N+2
5067+
// Because we have a gap between restore and invoke, the module cache
5068+
// will be populated and we need fewer instructions
5069+
auto tx1 = makeAddTx(contract, INVOKE_ADD_CACHED_COST_FAIL, A);
5070+
auto tx2 = makeAddTx(contract, INVOKE_ADD_CACHED_COST_PASS, B);
5071+
auto txResults = closeLedger(*app, {tx1, tx2}, /*strictOrder=*/true);
5072+
REQUIRE(txResults.results.size() == 2);
5073+
REQUIRE(!isSuccessResult(txResults.results[0].result));
5074+
REQUIRE(isSuccessResult(txResults.results[1].result));
5075+
}
5076+
5077+
SECTION("scenario B: restore and invoke in same ledger")
5078+
{
5079+
// Combine restore and invoke in ledger N+1
5080+
// First restore
5081+
SorobanResources resources;
5082+
resources.footprint.readWrite = contractKeys;
5083+
resources.instructions = 0;
5084+
resources.readBytes = 10'000;
5085+
resources.writeBytes = 10'000;
5086+
auto resourceFee = 300'000 + 40'000 * contractKeys.size();
5087+
auto tx1 = test.createRestoreTx(resources, 1'000, resourceFee);
5088+
5089+
// Then try to invoke immediately
5090+
// Because there is no gap between restore and invoke, the module cache
5091+
// won't be populated and we need more instructions.
5092+
auto tx2 = makeAddTx(contract, INVOKE_ADD_UNCACHED_COST_FAIL, A);
5093+
auto tx3 = makeAddTx(contract, INVOKE_ADD_UNCACHED_COST_PASS, B);
5094+
auto txResults =
5095+
closeLedger(*app, {tx1, tx2, tx3}, /*strictOrder=*/true);
5096+
REQUIRE(txResults.results.size() == 3);
5097+
REQUIRE(isSuccessResult(txResults.results[0].result));
5098+
REQUIRE(!isSuccessResult(txResults.results[1].result));
5099+
REQUIRE(isSuccessResult(txResults.results[2].result));
5100+
}
5101+
}
5102+
#endif

0 commit comments

Comments
 (0)