|
3 | 3 | // of this distribution or at http://www.apache.org/licenses/LICENSE-2.0
|
4 | 4 |
|
5 | 5 | #include "test/test.h"
|
| 6 | +#include "transactions/TransactionFrameBase.h" |
6 | 7 | #include "util/Logging.h"
|
7 | 8 | #include "util/ProtocolVersion.h"
|
8 | 9 | #include "util/UnorderedSet.h"
|
@@ -4771,3 +4772,331 @@ TEST_CASE("contract constructor support", "[tx][soroban]")
|
4771 | 4772 | REQUIRE(invocation.getReturnValue().u32() == 303);
|
4772 | 4773 | }
|
4773 | 4774 | }
|
| 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