From 2612345992e0c26f03e257ffde334bbb963a7abe Mon Sep 17 00:00:00 2001 From: kevin Heifner Date: Wed, 1 Apr 2026 09:53:21 -0500 Subject: [PATCH 1/2] tests: port PR #270 RAM-billing and KV API coverage tests to master Cherry-pick of 35790492ef adapted to master's kv host API. Adds 38 new kv_api_tests cases and 2 new protocol_feature_tests cases covering payer-change billing, erase refund payer attribution, secondary-index billing, cross-account billing, read-only rejection, payer authorization edge cases, notification billing, mixed-payer transactions, ROA quota enforcement, privileged-sysio bypass, key/value size boundaries, iterator invalidation under mutation, and cross-contract secondary reads. Adaptations vs the abandoned feature/kv-secondary-primary-id base: drop the index_id argument from kv_idx_* calls (master collapsed table+index_id into uint32_t table_id), replace name-literal table ids with uint32_t constants (300-305), rename contract-side key_format -> test_table_id, restore the no-arg kv_billing_tester::get_ram_usage() overload, qualify read_only_rejects_kv_* tests with the kv_api_tester::t reference, and bump the new protocol_feature_tests ROA budgets from 0.0941 to 0.0986 SYS to match master's other tests in this file. --- unittests/kv_api_tests.cpp | 610 +++++++++++++++++- unittests/protocol_feature_tests.cpp | 89 +++ .../test_kv_api/test_kv_api.abi | 388 ++++++++++- .../test_kv_api/test_kv_api.cpp | 413 ++++++++++++ .../test_kv_api/test_kv_api.wasm | Bin 105926 -> 116833 bytes 5 files changed, 1482 insertions(+), 18 deletions(-) diff --git a/unittests/kv_api_tests.cpp b/unittests/kv_api_tests.cpp index 01fa7b2986..2f93a4b812 100644 --- a/unittests/kv_api_tests.cpp +++ b/unittests/kv_api_tests.cpp @@ -224,6 +224,32 @@ BOOST_FIXTURE_TEST_CASE(large_value_1024, kv_api_tester) { BOOST_CHECK_NO_THROW(run_action("testmaxval"_n)); } +// Key/value boundary tests on UPDATE (create tests above) + +BOOST_FIXTURE_TEST_CASE(max_key_256_update, kv_api_tester) { + BOOST_CHECK_NO_THROW(run_action("tstmaxkeyupd"_n)); +} + +BOOST_FIXTURE_TEST_CASE(oversize_key_257_update, kv_api_tester) { + BOOST_CHECK_THROW(run_action("tstovrkyupd"_n), kv_key_too_large); +} + +BOOST_FIXTURE_TEST_CASE(max_value_262144_create, kv_api_tester) { + BOOST_CHECK_NO_THROW(run_action("tstmaxvalcr"_n)); +} + +BOOST_FIXTURE_TEST_CASE(oversize_value_262145_create, kv_api_tester) { + BOOST_CHECK_THROW(run_action("tstovrvalcr"_n), kv_value_too_large); +} + +BOOST_FIXTURE_TEST_CASE(max_value_262144_update, kv_api_tester) { + BOOST_CHECK_NO_THROW(run_action("tstmaxvalupd"_n)); +} + +BOOST_FIXTURE_TEST_CASE(oversize_value_262145_update, kv_api_tester) { + BOOST_CHECK_THROW(run_action("tstovrvalupd"_n), kv_value_too_large); +} + // ─── Value operations ────────────────────────────────────────────────────── BOOST_FIXTURE_TEST_CASE(partial_read, kv_api_tester) { @@ -528,7 +554,11 @@ struct kv_billing_tester : validating_tester { } int64_t get_ram_usage() { - return control->get_resource_limits_manager().get_account_ram_usage(test_account); + return get_ram_usage(test_account); + } + + int64_t get_ram_usage(name acct) { + return control->get_resource_limits_manager().get_account_ram_usage(acct); } void ram_store(uint32_t key_id, uint32_t val_size) { @@ -552,9 +582,9 @@ struct kv_billing_tester : validating_tester { BOOST_FIXTURE_TEST_CASE(billing_create_row, kv_billing_tester) { // Creating a row should bill key_size + value_size + KV_OVERHEAD - auto before = get_ram_usage(); + auto before = get_ram_usage(test_account); ram_store(1, 100); // 4-byte key, 100-byte value - auto after = get_ram_usage(); + auto after = get_ram_usage(test_account); int64_t expected = 4 + 100 + KV_OVERHEAD; // key + value + overhead int64_t actual = after - before; @@ -565,9 +595,9 @@ BOOST_FIXTURE_TEST_CASE(billing_create_row, kv_billing_tester) { BOOST_FIXTURE_TEST_CASE(billing_update_grow, kv_billing_tester) { // Growing a value should bill the delta ram_store(2, 50); - auto before = get_ram_usage(); + auto before = get_ram_usage(test_account); ram_update(2, 200); // grow from 50 to 200 - auto after = get_ram_usage(); + auto after = get_ram_usage(test_account); int64_t expected = 200 - 50; // only value delta, key+overhead unchanged int64_t actual = after - before; @@ -578,9 +608,9 @@ BOOST_FIXTURE_TEST_CASE(billing_update_grow, kv_billing_tester) { BOOST_FIXTURE_TEST_CASE(billing_update_shrink, kv_billing_tester) { // Shrinking a value should refund the delta ram_store(3, 200); - auto before = get_ram_usage(); + auto before = get_ram_usage(test_account); ram_update(3, 50); // shrink from 200 to 50 - auto after = get_ram_usage(); + auto after = get_ram_usage(test_account); int64_t expected = 50 - 200; // negative delta = refund int64_t actual = after - before; @@ -591,9 +621,9 @@ BOOST_FIXTURE_TEST_CASE(billing_update_shrink, kv_billing_tester) { BOOST_FIXTURE_TEST_CASE(billing_update_same_size, kv_billing_tester) { // Updating with same size should bill zero ram_store(4, 100); - auto before = get_ram_usage(); + auto before = get_ram_usage(test_account); ram_update(4, 100); // same size - auto after = get_ram_usage(); + auto after = get_ram_usage(test_account); BOOST_TEST_MESSAGE("billing_update_same_size: delta=" << (after - before)); BOOST_REQUIRE_EQUAL(after - before, 0); @@ -602,9 +632,9 @@ BOOST_FIXTURE_TEST_CASE(billing_update_same_size, kv_billing_tester) { BOOST_FIXTURE_TEST_CASE(billing_erase_refund, kv_billing_tester) { // Erasing should refund the full amount billed on create ram_store(5, 100); - auto before = get_ram_usage(); + auto before = get_ram_usage(test_account); ram_erase(5); - auto after = get_ram_usage(); + auto after = get_ram_usage(test_account); int64_t expected = -(4 + 100 + KV_OVERHEAD); // full refund int64_t actual = after - before; @@ -614,10 +644,10 @@ BOOST_FIXTURE_TEST_CASE(billing_erase_refund, kv_billing_tester) { BOOST_FIXTURE_TEST_CASE(billing_create_erase_net_zero, kv_billing_tester) { // Create + erase should result in net zero RAM change - auto before = get_ram_usage(); + auto before = get_ram_usage(test_account); ram_store(6, 500); ram_erase(6); - auto after = get_ram_usage(); + auto after = get_ram_usage(test_account); BOOST_TEST_MESSAGE("billing_create_erase_net_zero: delta=" << (after - before)); BOOST_REQUIRE_EQUAL(after - before, 0); @@ -625,11 +655,11 @@ BOOST_FIXTURE_TEST_CASE(billing_create_erase_net_zero, kv_billing_tester) { BOOST_FIXTURE_TEST_CASE(billing_multiple_rows, kv_billing_tester) { // Multiple rows should each be billed independently - auto before = get_ram_usage(); + auto before = get_ram_usage(test_account); ram_store(10, 100); ram_store(11, 200); ram_store(12, 300); - auto after = get_ram_usage(); + auto after = get_ram_usage(test_account); int64_t expected = 3 * (4 + KV_OVERHEAD) + 100 + 200 + 300; // 3 rows, different values int64_t actual = after - before; @@ -683,4 +713,554 @@ BOOST_FIXTURE_TEST_CASE(billing_idx_update_shrink, kv_billing_tester) { BOOST_REQUIRE_EQUAL(actual, expected); } +// ════════════════════════════════════════════════════════════════════════════ +// Payer change billing tests — verify correct billing when payer changes +// ════════════════════════════════════════════════════════════════════════════ + +static constexpr int64_t KV_IDX_OVERHEAD = config::billable_size_v; + +struct kv_payer_billing_tester : validating_tester { + kv_payer_billing_tester() { + create_accounts({test_account, "alice"_n, "bob"_n}); + produce_block(); + set_code(test_account, test_contracts::test_kv_api_wasm()); + set_abi(test_account, test_contracts::test_kv_api_abi().c_str()); + produce_block(); + } + + int64_t get_ram_usage(name acct) { + return control->get_resource_limits_manager().get_account_ram_usage(acct); + } + + // Push rampyrset with proper sysio.payer authorization for cross-account billing. + // When authorize_payer=true and payer is a third party, the payer's sysio.payer + // permission must be first in the auth list (enforced by authorization_manager). + void ram_pyr_set(uint32_t key_id, uint32_t val_size, name payer, + bool authorize_payer = true) { + vector auths; + if (payer != name{0} && payer != test_account && authorize_payer) { + auths.push_back({payer, config::sysio_payer_name}); + auths.push_back({payer, config::active_name}); + } else { + auths.push_back({test_account, config::active_name}); + } + push_action(test_account, "rampyrset"_n, auths, + mutable_variant_object()("key_id", key_id)("val_size", val_size)("payer", payer)); + produce_block(); + } + + // Self-pay store (payer = receiver) + void ram_store(uint32_t key_id, uint32_t val_size) { + push_action(test_account, "ramstore"_n, test_account, + mutable_variant_object()("key_id", key_id)("val_size", val_size)); + produce_block(); + } + + void ram_erase(uint32_t key_id) { + push_action(test_account, "ramerase"_n, test_account, + mutable_variant_object()("key_id", key_id)); + produce_block(); + } +}; + +// Payer change, same value size — old payer fully refunded, new payer charged +BOOST_FIXTURE_TEST_CASE(payer_change_same_size, kv_payer_billing_tester) { + // Create row with payer=alice + const uint32_t VAL_SIZE = 100; + int64_t expected_billable = 4 + VAL_SIZE + KV_OVERHEAD; + + auto alice_before = get_ram_usage("alice"_n); + ram_pyr_set(1, VAL_SIZE, "alice"_n); + auto alice_after_create = get_ram_usage("alice"_n); + BOOST_REQUIRE_EQUAL(alice_after_create - alice_before, expected_billable); + + // Update same key, change payer to bob, same size + auto bob_before = get_ram_usage("bob"_n); + alice_before = get_ram_usage("alice"_n); + ram_pyr_set(1, VAL_SIZE, "bob"_n); + auto alice_after = get_ram_usage("alice"_n); + auto bob_after = get_ram_usage("bob"_n); + + // alice fully refunded, bob charged same amount + BOOST_REQUIRE_EQUAL(alice_after - alice_before, -expected_billable); + BOOST_REQUIRE_EQUAL(bob_after - bob_before, expected_billable); +} + +// Payer change with value growth — old payer refunded old amount, new payer charged new amount +BOOST_FIXTURE_TEST_CASE(payer_change_with_growth, kv_payer_billing_tester) { + const uint32_t SMALL = 50, LARGE = 200; + int64_t small_billable = 4 + SMALL + KV_OVERHEAD; + int64_t large_billable = 4 + LARGE + KV_OVERHEAD; + + ram_pyr_set(1, SMALL, "alice"_n); + + auto alice_before = get_ram_usage("alice"_n); + auto bob_before = get_ram_usage("bob"_n); + ram_pyr_set(1, LARGE, "bob"_n); + auto alice_after = get_ram_usage("alice"_n); + auto bob_after = get_ram_usage("bob"_n); + + // alice refunded full old amount, bob charged full new (larger) amount + BOOST_REQUIRE_EQUAL(alice_after - alice_before, -small_billable); + BOOST_REQUIRE_EQUAL(bob_after - bob_before, large_billable); +} + +// Payer change with value shrink — old payer refunded old amount, new payer charged smaller amount +BOOST_FIXTURE_TEST_CASE(payer_change_with_shrink, kv_payer_billing_tester) { + const uint32_t LARGE = 200, SMALL = 50; + int64_t large_billable = 4 + LARGE + KV_OVERHEAD; + int64_t small_billable = 4 + SMALL + KV_OVERHEAD; + + ram_pyr_set(1, LARGE, "alice"_n); + + auto alice_before = get_ram_usage("alice"_n); + auto bob_before = get_ram_usage("bob"_n); + ram_pyr_set(1, SMALL, "bob"_n); + auto alice_after = get_ram_usage("alice"_n); + auto bob_after = get_ram_usage("bob"_n); + + // alice refunded full old amount, bob charged full new (smaller) amount + BOOST_REQUIRE_EQUAL(alice_after - alice_before, -large_billable); + BOOST_REQUIRE_EQUAL(bob_after - bob_before, small_billable); +} + +// Payer change back to self (payer=0 means receiver) +BOOST_FIXTURE_TEST_CASE(payer_change_back_to_self, kv_payer_billing_tester) { + const uint32_t VAL_SIZE = 100; + int64_t expected_billable = 4 + VAL_SIZE + KV_OVERHEAD; + + // Create with payer=alice + ram_pyr_set(1, VAL_SIZE, "alice"_n); + + // Change payer back to receiver (payer=0 means receiver=test_account) + // Only test_account needs to sign since alice's delta is negative + auto alice_before = get_ram_usage("alice"_n); + auto contract_before = get_ram_usage(test_account); + ram_pyr_set(1, VAL_SIZE, name{0}); + auto alice_after = get_ram_usage("alice"_n); + auto contract_after = get_ram_usage(test_account); + + BOOST_REQUIRE_EQUAL(alice_after - alice_before, -expected_billable); + BOOST_REQUIRE_EQUAL(contract_after - contract_before, expected_billable); +} + +// Erase refunds the stored payer (not the contract/receiver) +BOOST_FIXTURE_TEST_CASE(erase_refunds_correct_payer, kv_payer_billing_tester) { + const uint32_t VAL_SIZE = 100; + int64_t expected_billable = 4 + VAL_SIZE + KV_OVERHEAD; + + // Create with payer=alice + ram_pyr_set(1, VAL_SIZE, "alice"_n); + + // Erase — alice should get the refund, not the contract + auto alice_before = get_ram_usage("alice"_n); + auto contract_before = get_ram_usage(test_account); + ram_erase(1); + auto alice_after = get_ram_usage("alice"_n); + auto contract_after = get_ram_usage(test_account); + + BOOST_REQUIRE_EQUAL(alice_after - alice_before, -expected_billable); + BOOST_REQUIRE_EQUAL(contract_after - contract_before, 0); +} + +// Payer change without new payer authorization — should fail. +// Alice is not in the auth list at all, so the unprivileged has_authorization +// check (Path 1 in validate_account_ram_deltas) fires first -> +// unauthorized_ram_usage_increase. +BOOST_FIXTURE_TEST_CASE(payer_change_unauth_fails, kv_payer_billing_tester) { + // Create with self-pay + ram_store(1, 100); + + // Try to change payer to alice without alice signing + BOOST_CHECK_THROW( + ram_pyr_set(1, 100, "alice"_n, false), + unauthorized_ram_usage_increase + ); +} + +// Payer signed with active but NOT sysio.payer — should fail. +// Alice is in the auth list (passes Path 1 has_authorization), but without +// the sysio.payer permission role Path 2 rejects -> unsatisfied_authorization. +BOOST_FIXTURE_TEST_CASE(payer_change_active_only_fails, kv_payer_billing_tester) { + ram_store(1, 100); + + // Alice signs with active only — no sysio.payer permission + BOOST_CHECK_THROW( + push_action(test_account, "rampyrset"_n, + vector{{"alice"_n, config::active_name}}, + mutable_variant_object()("key_id", 1)("val_size", 100)("payer", "alice")), + unsatisfied_authorization + ); +} + +// Payer change — old payer not authorized, but succeeds +// because old payer's delta is negative (refund doesn't need auth) +BOOST_FIXTURE_TEST_CASE(payer_change_old_payer_unauth_ok, kv_payer_billing_tester) { + const uint32_t VAL_SIZE = 100; + int64_t billable = 4 + VAL_SIZE + KV_OVERHEAD; + + // Create with payer=alice + ram_pyr_set(1, VAL_SIZE, "alice"_n); + + // Change payer from alice->bob. Only bob signs. + // alice is refunded (negative delta -> no auth needed). + auto alice_before = get_ram_usage("alice"_n); + auto bob_before = get_ram_usage("bob"_n); + ram_pyr_set(1, VAL_SIZE, "bob"_n); + auto alice_after = get_ram_usage("alice"_n); + auto bob_after = get_ram_usage("bob"_n); + + BOOST_REQUIRE_EQUAL(alice_after - alice_before, -billable); + BOOST_REQUIRE_EQUAL(bob_after - bob_before, billable); +} + +// Multiple actions with different payers in one transaction — independent deltas +BOOST_FIXTURE_TEST_CASE(mixed_payer_independent_deltas, kv_payer_billing_tester) { + auto alice_before = get_ram_usage("alice"_n); + auto bob_before = get_ram_usage("bob"_n); + + // Single transaction: action1 bills alice, action2 bills bob + signed_transaction trx; + trx.actions.push_back(get_action(test_account, "rampyrset"_n, + {{"alice"_n, config::sysio_payer_name}, {"alice"_n, config::active_name}}, + mutable_variant_object()("key_id", 1)("val_size", 100)("payer", "alice"))); + trx.actions.push_back(get_action(test_account, "rampyrset"_n, + {{"bob"_n, config::sysio_payer_name}, {"bob"_n, config::active_name}}, + mutable_variant_object()("key_id", 2)("val_size", 200)("payer", "bob"))); + set_transaction_headers(trx); + trx.sign(get_private_key("alice"_n, "active"), control->get_chain_id()); + trx.sign(get_private_key("bob"_n, "active"), control->get_chain_id()); + push_transaction(trx); + produce_block(); + + auto alice_after = get_ram_usage("alice"_n); + auto bob_after = get_ram_usage("bob"_n); + + BOOST_REQUIRE_EQUAL(alice_after - alice_before, 4 + 100 + KV_OVERHEAD); + BOOST_REQUIRE_EQUAL(bob_after - bob_before, 4 + 200 + KV_OVERHEAD); +} + +// Create + erase across actions in one transaction — net zero for payer +BOOST_FIXTURE_TEST_CASE(mixed_payer_create_erase_net_zero, kv_payer_billing_tester) { + auto alice_before = get_ram_usage("alice"_n); + + // Single transaction: action1 creates with payer=alice, action2 erases + signed_transaction trx; + trx.actions.push_back(get_action(test_account, "rampyrset"_n, + {{"alice"_n, config::sysio_payer_name}, {"alice"_n, config::active_name}}, + mutable_variant_object()("key_id", 1)("val_size", 100)("payer", "alice"))); + trx.actions.push_back(get_action(test_account, "ramerase"_n, + {{test_account, config::active_name}}, + mutable_variant_object()("key_id", 1))); + set_transaction_headers(trx); + trx.sign(get_private_key("alice"_n, "active"), control->get_chain_id()); + trx.sign(get_private_key(test_account, "active"), control->get_chain_id()); + push_transaction(trx); + produce_block(); + + auto alice_after = get_ram_usage("alice"_n); + BOOST_REQUIRE_EQUAL(alice_after - alice_before, 0); +} + +// ════════════════════════════════════════════════════════════════════════════ +// Secondary index billing tests +// ════════════════════════════════════════════════════════════════════════════ + +// kv_idx_store bills sec_key_size + pri_key_size + overhead +BOOST_FIXTURE_TEST_CASE(idx_billing_store, kv_billing_tester) { + auto before = get_ram_usage(test_account); + push_action(test_account, "ramidxstore"_n, test_account, + mutable_variant_object()("sec_size", 8)("pri_size", 4)); + produce_block(); + auto after = get_ram_usage(test_account); + + int64_t expected = 8 + 4 + KV_IDX_OVERHEAD; + BOOST_TEST_MESSAGE("idx_billing_store: expected=" << expected << " actual=" << (after - before)); + BOOST_REQUIRE_EQUAL(after - before, expected); +} + +// kv_idx_remove refunds the full billable amount +BOOST_FIXTURE_TEST_CASE(idx_billing_remove, kv_billing_tester) { + push_action(test_account, "ramidxstore"_n, test_account, + mutable_variant_object()("sec_size", 8)("pri_size", 4)); + produce_block(); + + auto before = get_ram_usage(test_account); + push_action(test_account, "ramidxremov"_n, test_account, + mutable_variant_object()("sec_size", 8)("pri_size", 4)); + produce_block(); + auto after = get_ram_usage(test_account); + + int64_t expected = -(8 + 4 + KV_IDX_OVERHEAD); + BOOST_TEST_MESSAGE("idx_billing_remove: expected=" << expected << " actual=" << (after - before)); + BOOST_REQUIRE_EQUAL(after - before, expected); +} + +// kv_idx_update with size change — bills only the sec key delta +BOOST_FIXTURE_TEST_CASE(idx_billing_update_size_change, kv_billing_tester) { + push_action(test_account, "ramidxstore"_n, test_account, + mutable_variant_object()("sec_size", 8)("pri_size", 4)); + produce_block(); + + auto before = get_ram_usage(test_account); + push_action(test_account, "ramidxupdat"_n, test_account, + mutable_variant_object()("old_ss", 8)("new_ss", 20)("pri_size", 4)); + produce_block(); + auto after = get_ram_usage(test_account); + + int64_t expected = 20 - 8; // sec key delta only + BOOST_TEST_MESSAGE("idx_billing_update_grow: expected=" << expected << " actual=" << (after - before)); + BOOST_REQUIRE_EQUAL(after - before, expected); +} + +// kv_idx_update same size — zero delta +BOOST_FIXTURE_TEST_CASE(idx_billing_update_same_size, kv_billing_tester) { + push_action(test_account, "ramidxstore"_n, test_account, + mutable_variant_object()("sec_size", 8)("pri_size", 4)); + produce_block(); + + auto before = get_ram_usage(test_account); + push_action(test_account, "ramidxupdat"_n, test_account, + mutable_variant_object()("old_ss", 8)("new_ss", 8)("pri_size", 4)); + produce_block(); + auto after = get_ram_usage(test_account); + + BOOST_TEST_MESSAGE("idx_billing_update_same: delta=" << (after - before)); + BOOST_REQUIRE_EQUAL(after - before, 0); +} + +// ════════════════════════════════════════════════════════════════════════════ +// Cross-account secondary index billing — payer != receiver +// ════════════════════════════════════════════════════════════════════════════ + +// Secondary index store bills the specified payer, not the contract +BOOST_FIXTURE_TEST_CASE(idx_billing_payer_not_receiver, kv_payer_billing_tester) { + auto alice_before = get_ram_usage("alice"_n); + auto contract_before = get_ram_usage(test_account); + + push_action(test_account, "ramidxstpyr"_n, + {{"alice"_n, config::sysio_payer_name}, {"alice"_n, config::active_name}}, + mutable_variant_object()("payer", "alice")("sec_size", 8)("pri_size", 4)); + produce_block(); + + auto alice_after = get_ram_usage("alice"_n); + auto contract_after = get_ram_usage(test_account); + + int64_t expected = 8 + 4 + KV_IDX_OVERHEAD; + BOOST_REQUIRE_EQUAL(alice_after - alice_before, expected); + BOOST_REQUIRE_EQUAL(contract_after - contract_before, 0); +} + +// Secondary index remove refunds the stored payer, not the contract +BOOST_FIXTURE_TEST_CASE(idx_remove_refunds_payer, kv_payer_billing_tester) { + // Store with alice as payer + push_action(test_account, "ramidxstpyr"_n, + {{"alice"_n, config::sysio_payer_name}, {"alice"_n, config::active_name}}, + mutable_variant_object()("payer", "alice")("sec_size", 8)("pri_size", 4)); + produce_block(); + + // Remove — should refund alice, not the contract + auto alice_before = get_ram_usage("alice"_n); + auto contract_before = get_ram_usage(test_account); + push_action(test_account, "ramidxremov"_n, test_account, + mutable_variant_object()("sec_size", 8)("pri_size", 4)); + produce_block(); + + auto alice_after = get_ram_usage("alice"_n); + auto contract_after = get_ram_usage(test_account); + + int64_t expected = -(8 + 4 + KV_IDX_OVERHEAD); + BOOST_REQUIRE_EQUAL(alice_after - alice_before, expected); + BOOST_REQUIRE_EQUAL(contract_after - contract_before, 0); +} + +// Secondary index update with payer change +BOOST_FIXTURE_TEST_CASE(idx_update_payer_change, kv_payer_billing_tester) { + // Store with alice as payer + push_action(test_account, "ramidxstpyr"_n, + {{"alice"_n, config::sysio_payer_name}, {"alice"_n, config::active_name}}, + mutable_variant_object()("payer", "alice")("sec_size", 8)("pri_size", 4)); + produce_block(); + + int64_t old_billable = 8 + 4 + KV_IDX_OVERHEAD; + int64_t new_billable = 12 + 4 + KV_IDX_OVERHEAD; + + // Update, change payer from alice to bob + auto alice_before = get_ram_usage("alice"_n); + auto bob_before = get_ram_usage("bob"_n); + push_action(test_account, "ramidxuppyr"_n, + {{"bob"_n, config::sysio_payer_name}, {"bob"_n, config::active_name}}, + mutable_variant_object()("payer", "bob")("old_ss", 8)("new_ss", 12)("pri_size", 4)); + produce_block(); + + auto alice_after = get_ram_usage("alice"_n); + auto bob_after = get_ram_usage("bob"_n); + + // alice fully refunded, bob charged new amount + BOOST_REQUIRE_EQUAL(alice_after - alice_before, -old_billable); + BOOST_REQUIRE_EQUAL(bob_after - bob_before, new_billable); +} + +// ════════════════════════════════════════════════════════════════════════════ +// Read-only transaction rejection tests +// ════════════════════════════════════════════════════════════════════════════ + +BOOST_FIXTURE_TEST_CASE(read_only_rejects_kv_erase, kv_api_tester) { + signed_transaction trx; + trx.actions.emplace_back( + vector{}, test_account, "tstrdoerase"_n, bytes{} + ); + t.set_transaction_headers(trx); + BOOST_CHECK_THROW( + t.push_transaction(trx, fc::time_point::maximum(), kv_shared_tester::DEFAULT_BILLED_CPU_TIME_US, false, + transaction_metadata::trx_type::read_only), + table_operation_not_permitted); +} + +BOOST_FIXTURE_TEST_CASE(read_only_rejects_kv_idx_store, kv_api_tester) { + signed_transaction trx; + trx.actions.emplace_back( + vector{}, test_account, "tstrdoidxst"_n, bytes{} + ); + t.set_transaction_headers(trx); + BOOST_CHECK_THROW( + t.push_transaction(trx, fc::time_point::maximum(), kv_shared_tester::DEFAULT_BILLED_CPU_TIME_US, false, + transaction_metadata::trx_type::read_only), + table_operation_not_permitted); +} + +BOOST_FIXTURE_TEST_CASE(read_only_rejects_kv_idx_remove, kv_api_tester) { + signed_transaction trx; + trx.actions.emplace_back( + vector{}, test_account, "tstrdoidxrm"_n, bytes{} + ); + t.set_transaction_headers(trx); + BOOST_CHECK_THROW( + t.push_transaction(trx, fc::time_point::maximum(), kv_shared_tester::DEFAULT_BILLED_CPU_TIME_US, false, + transaction_metadata::trx_type::read_only), + table_operation_not_permitted); +} + +BOOST_FIXTURE_TEST_CASE(read_only_rejects_kv_idx_update, kv_api_tester) { + signed_transaction trx; + trx.actions.emplace_back( + vector{}, test_account, "tstrdoidxup"_n, bytes{} + ); + t.set_transaction_headers(trx); + BOOST_CHECK_THROW( + t.push_transaction(trx, fc::time_point::maximum(), kv_shared_tester::DEFAULT_BILLED_CPU_TIME_US, false, + transaction_metadata::trx_type::read_only), + table_operation_not_permitted); +} + +// ════════════════════════════════════════════════════════════════════════════ +// Notification context billing tests +// ════════════════════════════════════════════════════════════════════════════ + +static const name notify_account = "kvnotify"_n; + +struct kv_notify_billing_tester : validating_tester { + kv_notify_billing_tester() { + create_accounts({test_account, notify_account, "alice"_n}); + produce_block(); + set_code(test_account, test_contracts::test_kv_api_wasm()); + set_abi(test_account, test_contracts::test_kv_api_abi().c_str()); + set_code(notify_account, test_contracts::test_kv_api_wasm()); + set_abi(notify_account, test_contracts::test_kv_api_abi().c_str()); + produce_block(); + } + + int64_t get_ram_usage(name acct) { + return control->get_resource_limits_manager().get_account_ram_usage(acct); + } +}; + +// Self-pay kv_set in notification context — RAM billed to notified receiver +BOOST_FIXTURE_TEST_CASE(notify_self_pay_bills_receiver, kv_notify_billing_tester) { + auto notify_before = get_ram_usage(notify_account); + auto sender_before = get_ram_usage(test_account); + + push_action(test_account, "ramnotify"_n, test_account, + mutable_variant_object()("key_id", 1)("val_size", 100)); + produce_block(); + + auto notify_after = get_ram_usage(notify_account); + auto sender_after = get_ram_usage(test_account); + + // RAM charged to notified receiver (kvnotify), not sender (kvtest) + int64_t expected = 4 + 100 + KV_OVERHEAD; + BOOST_REQUIRE_EQUAL(notify_after - notify_before, expected); + BOOST_REQUIRE_EQUAL(sender_after - sender_before, 0); +} + +// Third-party payer in notification context — always fails. +// Any cross-account RAM increase is blocked in notification context +// regardless of sysio.payer authorization. +BOOST_FIXTURE_TEST_CASE(notify_third_party_payer_fails, kv_notify_billing_tester) { + BOOST_CHECK_THROW( + push_action(test_account, "ramnotiferr"_n, + {{"alice"_n, config::sysio_payer_name}, {"alice"_n, config::active_name}}, + mutable_variant_object()("key_id", 1)("val_size", 100)("payer", "alice")), + unauthorized_ram_usage_increase + ); +} + +// ════════════════════════════════════════════════════════════════════════════ +// Cross-contract secondary index reads +// ════════════════════════════════════════════════════════════════════════════ + +// Contract B reads contract A's secondary index and primary row +BOOST_FIXTURE_TEST_CASE(cross_contract_idx_read, kv_notify_billing_tester) { + // Setup: kvnotify stores a row + secondary index entry + push_action(notify_account, "xcsetup"_n, notify_account, mutable_variant_object()); + produce_block(); + + // kvtest reads kvnotify's secondary index and primary row + BOOST_CHECK_NO_THROW( + push_action(test_account, "xcidxread"_n, test_account, + mutable_variant_object()("other", notify_account)) + ); +} + +// ════════════════════════════════════════════════════════════════════════════ +// Iterator invalidation under mutation +// ════════════════════════════════════════════════════════════════════════════ + +// Primary: update value while iterator points to row +BOOST_FIXTURE_TEST_CASE(iter_mutation_update_value, kv_api_tester) { + BOOST_CHECK_NO_THROW(run_action("tstmutupdval"_n)); +} + +// Primary: erase current row then next() advances to next valid +BOOST_FIXTURE_TEST_CASE(iter_mutation_erase_next, kv_api_tester) { + BOOST_CHECK_NO_THROW(run_action("tsterasenext"_n)); +} + +// Primary: erase all rows during iteration -> end +BOOST_FIXTURE_TEST_CASE(iter_mutation_erase_all, kv_api_tester) { + BOOST_CHECK_NO_THROW(run_action("tsteraseall"_n)); +} + +// Secondary: remove entry under sec iterator -> next advances +BOOST_FIXTURE_TEST_CASE(idx_iter_mutation_erase, kv_api_tester) { + BOOST_CHECK_NO_THROW(run_action("tstidxerase"_n)); +} + +// Secondary: update key under sec iterator -> old position invalid +BOOST_FIXTURE_TEST_CASE(idx_iter_mutation_update, kv_api_tester) { + BOOST_CHECK_NO_THROW(run_action("tstidxmutupd"_n)); +} + +// Secondary: insert during iteration -> new entry visible +BOOST_FIXTURE_TEST_CASE(idx_iter_mutation_insert, kv_api_tester) { + BOOST_CHECK_NO_THROW(run_action("tstidxinsert"_n)); +} + +// Fork/undo coverage note: +// All KV test fixtures use validating_tester, which replays every block on a +// second chain and compares integrity hashes. This implicitly verifies that +// kv_object and kv_index_object chainbase undo sessions work correctly — +// any mismatch would cause test failure. Savanna consensus does not support +// pop_block, so explicit undo testing is handled via the validating replay. + BOOST_AUTO_TEST_SUITE_END() diff --git a/unittests/protocol_feature_tests.cpp b/unittests/protocol_feature_tests.cpp index ecb3f19758..000699b848 100644 --- a/unittests/protocol_feature_tests.cpp +++ b/unittests/protocol_feature_tests.cpp @@ -1010,6 +1010,95 @@ BOOST_AUTO_TEST_CASE( ram_restrictions_with_roa_test ) { try { } FC_LOG_AND_RETHROW() } +// ROA limit enforcement on KV storage operations. +// All multi_index operations route through kv_set/kv_idx_store internally, +// so this tests the KV billing path hitting the ROA RAM ceiling. +BOOST_AUTO_TEST_CASE( kv_roa_limit_enforcement ) { try { + tester c( setup_policy::full ); + c.produce_block(); + + const auto& tester1_account = account_name("tester1"); + const auto& alice_account = account_name("alice"); + + c.create_accounts( {tester1_account, alice_account}, false, true, false ); + c.add_roa_policy(c.NODE_DADDY, tester1_account, "1.0000 SYS", "1.0000 SYS", "0.0986 SYS", 0, 0); + // Give alice a small RAM quota — enough for small data but not large + c.add_roa_policy(c.NODE_DADDY, alice_account, "1.0000 SYS", "1.0000 SYS", "0.0100 SYS", 0, 0); + c.produce_block(); + + c.set_code( tester1_account, test_contracts::ram_restrictions_test_wasm() ); + c.set_abi( tester1_account, test_contracts::ram_restrictions_test_abi() ); + c.produce_block(); + + // Small store works (within quota) + auto alice_payer = vector{ + {alice_account, config::sysio_payer_name}, {alice_account, config::active_name}}; + c.push_action( tester1_account, "setdata"_n, alice_payer, mutable_variant_object() + ("len1", 10)("len2", 0)("payer", alice_account) ); + c.produce_block(); + + // Large store exceeds alice's ROA RAM quota (kv_set -> update_db_usage -> ram limit) + BOOST_REQUIRE_EXCEPTION( + c.push_action( tester1_account, "setdata"_n, alice_payer, mutable_variant_object() + ("len1", 100000)("len2", 0)("payer", alice_account) ), + ram_usage_exceeded, + fc_exception_message_contains("has insufficient ram") + ); + +} FC_LOG_AND_RETHROW() } + +// Privileged sysio.* contract bypasses payer authorization. +// validate_account_ram_deltas allows sysio.* privileged contracts to bill +// any account's RAM without sysio.payer permission. +BOOST_AUTO_TEST_CASE( privileged_kv_payer_bypass ) { try { + tester c( setup_policy::full ); + c.produce_block(); + + const auto& priv_account = account_name("sysio.test"); + const auto& alice_account = account_name("alice"); + + c.create_account( priv_account, config::system_account_name, false, true, false, false ); + c.create_accounts( {alice_account}, false, true, false ); + c.add_roa_policy(c.NODE_DADDY, alice_account, "1.0000 SYS", "1.0000 SYS", "1.0000 SYS", 0, 0); + c.produce_block(); + + c.set_code( priv_account, test_contracts::ram_restrictions_test_wasm() ); + c.set_abi( priv_account, test_contracts::ram_restrictions_test_abi() ); + c.set_privileged( priv_account ); + c.produce_block(); + + // Privileged sysio.* contract billing alice WITHOUT sysio.payer auth succeeds + auto alice_ram_before = c.control->get_resource_limits_manager().get_account_ram_usage(alice_account); + c.push_action( priv_account, "setdata"_n, priv_account, mutable_variant_object() + ("len1", 10)("len2", 0)("payer", alice_account) ); + c.produce_block(); + + // alice's RAM increased even though she didn't authorize + auto alice_ram_after = c.control->get_resource_limits_manager().get_account_ram_usage(alice_account); + BOOST_REQUIRE_GT(alice_ram_after, alice_ram_before); + + // Non-sysio privileged contract still requires sysio.payer auth + const auto& priv2_account = account_name("privtest"); + c.create_accounts( {priv2_account}, false, true, false ); + c.add_roa_policy(c.NODE_DADDY, priv2_account, "1.0000 SYS", "1.0000 SYS", "0.0986 SYS", 0, 0); + c.produce_block(); + c.set_code( priv2_account, test_contracts::ram_restrictions_test_wasm() ); + c.set_abi( priv2_account, test_contracts::ram_restrictions_test_abi() ); + c.set_privileged( priv2_account ); + c.produce_block(); + + // Privileged but NOT sysio.* — skips the unprivileged has_authorization check + // (Path 1 in validate_account_ram_deltas) but still fails the sysio.payer + // permission check (Path 2) -> unsatisfied_authorization. + BOOST_REQUIRE_EXCEPTION( + c.push_action( priv2_account, "setdata"_n, priv2_account, mutable_variant_object() + ("len1", 10)("len2", 0)("payer", alice_account) ), + unsatisfied_authorization, + fc_exception_message_contains("Missing") + ); + +} FC_LOG_AND_RETHROW() } + /*** * Test KV RAM billing without ROA (unlimited RAM). * diff --git a/unittests/test-contracts/test_kv_api/test_kv_api.abi b/unittests/test-contracts/test_kv_api/test_kv_api.abi index 135c605f80..cc6477d9b7 100644 --- a/unittests/test-contracts/test_kv_api/test_kv_api.abi +++ b/unittests/test-contracts/test_kv_api/test_kv_api.abi @@ -41,6 +41,142 @@ } ] }, + { + "name": "ramidxremov", + "base": "", + "fields": [ + { + "name": "sec_size", + "type": "uint32" + }, + { + "name": "pri_size", + "type": "uint32" + } + ] + }, + { + "name": "ramidxstore", + "base": "", + "fields": [ + { + "name": "sec_size", + "type": "uint32" + }, + { + "name": "pri_size", + "type": "uint32" + } + ] + }, + { + "name": "ramidxstpyr", + "base": "", + "fields": [ + { + "name": "payer", + "type": "name" + }, + { + "name": "sec_size", + "type": "uint32" + }, + { + "name": "pri_size", + "type": "uint32" + } + ] + }, + { + "name": "ramidxupdat", + "base": "", + "fields": [ + { + "name": "old_ss", + "type": "uint32" + }, + { + "name": "new_ss", + "type": "uint32" + }, + { + "name": "pri_size", + "type": "uint32" + } + ] + }, + { + "name": "ramidxuppyr", + "base": "", + "fields": [ + { + "name": "payer", + "type": "name" + }, + { + "name": "old_ss", + "type": "uint32" + }, + { + "name": "new_ss", + "type": "uint32" + }, + { + "name": "pri_size", + "type": "uint32" + } + ] + }, + { + "name": "ramnotiferr", + "base": "", + "fields": [ + { + "name": "key_id", + "type": "uint32" + }, + { + "name": "val_size", + "type": "uint32" + }, + { + "name": "payer", + "type": "name" + } + ] + }, + { + "name": "ramnotify", + "base": "", + "fields": [ + { + "name": "key_id", + "type": "uint32" + }, + { + "name": "val_size", + "type": "uint32" + } + ] + }, + { + "name": "rampyrset", + "base": "", + "fields": [ + { + "name": "key_id", + "type": "uint32" + }, + { + "name": "val_size", + "type": "uint32" + }, + { + "name": "payer", + "type": "name" + } + ] + }, { "name": "ramstore", "base": "", @@ -352,11 +488,21 @@ "base": "", "fields": [] }, + { + "name": "tsteraseall", + "base": "", + "fields": [] + }, { "name": "tsterasedinv", "base": "", "fields": [] }, + { + "name": "tsterasenext", + "base": "", + "fields": [] + }, { "name": "tstfindmiss", "base": "", @@ -367,6 +513,21 @@ "base": "", "fields": [] }, + { + "name": "tstidxerase", + "base": "", + "fields": [] + }, + { + "name": "tstidxinsert", + "base": "", + "fields": [] + }, + { + "name": "tstidxmutupd", + "base": "", + "fields": [] + }, { "name": "tstidxpayer", "base": "", @@ -407,21 +568,56 @@ "base": "", "fields": [] }, + { + "name": "tstmaxkeyupd", + "base": "", + "fields": [] + }, + { + "name": "tstmaxvalcr", + "base": "", + "fields": [] + }, + { + "name": "tstmaxvalupd", + "base": "", + "fields": [] + }, { "name": "tstmodify", "base": "", "fields": [] }, + { + "name": "tstmutupdval", + "base": "", + "fields": [] + }, { "name": "tstnotifyram", "base": "", "fields": [] }, + { + "name": "tstovrkyupd", + "base": "", + "fields": [] + }, { "name": "tstovrszkey", "base": "", "fields": [] }, + { + "name": "tstovrvalcr", + "base": "", + "fields": [] + }, + { + "name": "tstovrvalupd", + "base": "", + "fields": [] + }, { "name": "tstpayeroth", "base": "", @@ -452,6 +648,26 @@ "base": "", "fields": [] }, + { + "name": "tstrdoerase", + "base": "", + "fields": [] + }, + { + "name": "tstrdoidxrm", + "base": "", + "fields": [] + }, + { + "name": "tstrdoidxst", + "base": "", + "fields": [] + }, + { + "name": "tstrdoidxup", + "base": "", + "fields": [] + }, { "name": "tstrdonly", "base": "", @@ -556,6 +772,21 @@ "name": "tstxzero", "base": "", "fields": [] + }, + { + "name": "xcidxread", + "base": "", + "fields": [ + { + "name": "other", + "type": "name" + } + ] + }, + { + "name": "xcsetup", + "base": "", + "fields": [] } ], "actions": [ @@ -564,6 +795,46 @@ "type": "ramerase", "ricardian_contract": "" }, + { + "name": "ramidxremov", + "type": "ramidxremov", + "ricardian_contract": "" + }, + { + "name": "ramidxstore", + "type": "ramidxstore", + "ricardian_contract": "" + }, + { + "name": "ramidxstpyr", + "type": "ramidxstpyr", + "ricardian_contract": "" + }, + { + "name": "ramidxupdat", + "type": "ramidxupdat", + "ricardian_contract": "" + }, + { + "name": "ramidxuppyr", + "type": "ramidxuppyr", + "ricardian_contract": "" + }, + { + "name": "ramnotiferr", + "type": "ramnotiferr", + "ricardian_contract": "" + }, + { + "name": "ramnotify", + "type": "ramnotify", + "ricardian_contract": "" + }, + { + "name": "rampyrset", + "type": "rampyrset", + "ricardian_contract": "" + }, { "name": "ramstore", "type": "ramstore", @@ -829,11 +1100,21 @@ "type": "tsterase", "ricardian_contract": "" }, + { + "name": "tsteraseall", + "type": "tsteraseall", + "ricardian_contract": "" + }, { "name": "tsterasedinv", "type": "tsterasedinv", "ricardian_contract": "" }, + { + "name": "tsterasenext", + "type": "tsterasenext", + "ricardian_contract": "" + }, { "name": "tstfindmiss", "type": "tstfindmiss", @@ -844,6 +1125,21 @@ "type": "tstfwditer", "ricardian_contract": "" }, + { + "name": "tstidxerase", + "type": "tstidxerase", + "ricardian_contract": "" + }, + { + "name": "tstidxinsert", + "type": "tstidxinsert", + "ricardian_contract": "" + }, + { + "name": "tstidxmutupd", + "type": "tstidxmutupd", + "ricardian_contract": "" + }, { "name": "tstidxpayer", "type": "tstidxpayer", @@ -879,21 +1175,56 @@ "type": "tstlbound", "ricardian_contract": "" }, + { + "name": "tstmaxkeyupd", + "type": "tstmaxkeyupd", + "ricardian_contract": "" + }, + { + "name": "tstmaxvalcr", + "type": "tstmaxvalcr", + "ricardian_contract": "" + }, + { + "name": "tstmaxvalupd", + "type": "tstmaxvalupd", + "ricardian_contract": "" + }, { "name": "tstmodify", "type": "tstmodify", "ricardian_contract": "" }, + { + "name": "tstmutupdval", + "type": "tstmutupdval", + "ricardian_contract": "" + }, { "name": "tstnotifyram", "type": "tstnotifyram", "ricardian_contract": "" }, + { + "name": "tstovrkyupd", + "type": "tstovrkyupd", + "ricardian_contract": "" + }, { "name": "tstovrszkey", "type": "tstovrszkey", "ricardian_contract": "" }, + { + "name": "tstovrvalcr", + "type": "tstovrvalcr", + "ricardian_contract": "" + }, + { + "name": "tstovrvalupd", + "type": "tstovrvalupd", + "ricardian_contract": "" + }, { "name": "tstpayeroth", "type": "tstpayeroth", @@ -924,6 +1255,26 @@ "type": "tstrbempty", "ricardian_contract": "" }, + { + "name": "tstrdoerase", + "type": "tstrdoerase", + "ricardian_contract": "" + }, + { + "name": "tstrdoidxrm", + "type": "tstrdoidxrm", + "ricardian_contract": "" + }, + { + "name": "tstrdoidxst", + "type": "tstrdoidxst", + "ricardian_contract": "" + }, + { + "name": "tstrdoidxup", + "type": "tstrdoidxup", + "ricardian_contract": "" + }, { "name": "tstrdonly", "type": "tstrdonly", @@ -1028,6 +1379,16 @@ "name": "tstxzero", "type": "tstxzero", "ricardian_contract": "" + }, + { + "name": "xcidxread", + "type": "xcidxread", + "ricardian_contract": "" + }, + { + "name": "xcsetup", + "type": "xcsetup", + "ricardian_contract": "" } ], "tables": [ @@ -1045,7 +1406,14 @@ "index_type": "i64", "key_names": ["scope","primary_key"], "key_types": ["name","uint64"], - "table_id": 62600 + "table_id": 62600, + "secondary_indexes": [ + { + "name": "byval", + "key_type": "float64", + "table_id": 64649 + } + ] }, { "name": "mi_row", @@ -1085,7 +1453,14 @@ "index_type": "i64", "key_names": ["scope","primary_key"], "key_types": ["name","uint64"], - "table_id": 13040 + "table_id": 13040, + "secondary_indexes": [ + { + "name": "byscore", + "key_type": "uint128", + "table_id": 8945 + } + ] }, { "name": "sectbl", @@ -1093,7 +1468,14 @@ "index_type": "i64", "key_names": ["scope","primary_key"], "key_types": ["name","uint64"], - "table_id": 7439 + "table_id": 7439, + "secondary_indexes": [ + { + "name": "byage", + "key_type": "uint64", + "table_id": 48144 + } + ] } ], "ricardian_clauses": [], diff --git a/unittests/test-contracts/test_kv_api/test_kv_api.cpp b/unittests/test-contracts/test_kv_api/test_kv_api.cpp index c0aae615ef..1153863803 100644 --- a/unittests/test-contracts/test_kv_api/test_kv_api.cpp +++ b/unittests/test-contracts/test_kv_api/test_kv_api.cpp @@ -15,6 +15,15 @@ static constexpr uint32_t test_table_id = 42; static constexpr uint32_t test_sec_table_id = 100; +// Table ids for the RAM-billing and read-only / cross-contract / iterator +// mutation actions added in the ram-billing test set. +static constexpr uint32_t ramidx_tid = 300; +static constexpr uint32_t rdo_tid = 301; +static constexpr uint32_t xc_tid = 302; +static constexpr uint32_t idxmut_tid = 303; +static constexpr uint32_t idxmut2_tid = 304; +static constexpr uint32_t idxmut3_tid = 305; + using namespace sysio; // ── Helpers ──────────────────────────────────────────────────────────────────── @@ -958,6 +967,64 @@ class [[sysio::contract("test_kv_api")]] test_kv_api : public contract { check(memcmp(buf, val, 1024) == 0, "maxval: data mismatch"); } + // ─── Boundary tests: key/value size limits on create AND update ────────── + + // Update existing row with max key (256 bytes) — should succeed + [[sysio::action]] + void tstmaxkeyupd() { + char key[256]; + memset(key, 0xB0, 256); + kv_set(test_table_id, 0, key, 256, "v1", 2); // create + kv_set(test_table_id, 0, key, 256, "v2", 2); // update — same key, different value + char buf[4]; uint32_t actual = 0; + check(kv_get(test_table_id, get_self().value, key, 256, buf, 4) == 2, "maxkeyupd: value size"); + check(memcmp(buf, "v2", 2) == 0, "maxkeyupd: value should be updated"); + } + + // Update with oversize key (257 bytes) — should fail + [[sysio::action]] + void tstovrkyupd() { + char key[257]; + memset(key, 0xB1, 257); + kv_set(test_table_id, 0, key, 257, "v", 1); + } + + // Create with max value size (256 KiB = 262144 bytes) + [[sysio::action]] + void tstmaxvalcr() { + char key[] = {(char)0xB2, 0x01}; + std::vector val(262144, 'V'); + kv_set(test_table_id, 0, key, 2, val.data(), 262144); + check(kv_get(test_table_id, get_self().value, key, 2, nullptr, 0) == 262144, "maxvalcr: size"); + } + + // Create with oversize value (262145 bytes) — should fail + [[sysio::action]] + void tstovrvalcr() { + char key[] = {(char)0xB3, 0x01}; + std::vector val(262145, 'X'); + kv_set(test_table_id, 0, key, 2, val.data(), 262145); + } + + // Update to max value size + [[sysio::action]] + void tstmaxvalupd() { + char key[] = {(char)0xB4, 0x01}; + kv_set(test_table_id, 0, key, 2, "small", 5); // create small + std::vector val(262144, 'W'); + kv_set(test_table_id, 0, key, 2, val.data(), 262144); // update to max + check(kv_get(test_table_id, get_self().value, key, 2, nullptr, 0) == 262144, "maxvalupd: size"); + } + + // Update to oversize value — should fail + [[sysio::action]] + void tstovrvalupd() { + char key[] = {(char)0xB5, 0x01}; + kv_set(test_table_id, 0, key, 2, "small", 5); // create small + std::vector val(262145, 'Y'); + kv_set(test_table_id, 0, key, 2, val.data(), 262145); // update to over max + } + // ─── 38. testpartread: kv_get with small buffer returns actual size ─────── [[sysio::action]] void testpartread() { @@ -2221,4 +2288,350 @@ class [[sysio::contract("test_kv_api")]] test_kv_api : public contract { ++it; check(it == idxA.end(), "tstxubound: end after upper_bound advance"); } + + // ════════════════════════════════════════════════════════════════════════════ + // Payer-parameterized RAM billing actions + // ════════════════════════════════════════════════════════════════════════════ + + // Store/update with explicit payer (payer=0 means receiver pays) + [[sysio::action]] + void rampyrset(uint32_t key_id, uint32_t val_size, sysio::name payer) { + char key[4]; + memcpy(key, &key_id, 4); + std::vector val(val_size, 'X'); + kv_set(test_table_id, payer.value, key, 4, val.data(), val_size); + } + + // ════════════════════════════════════════════════════════════════════════════ + // Secondary index RAM billing actions + // ════════════════════════════════════════════════════════════════════════════ + + // Store secondary index with parameterized key sizes + [[sysio::action]] + void ramidxstore(uint32_t sec_size, uint32_t pri_size) { + std::vector sec_key(sec_size, 'S'); + std::vector pri_key(pri_size, 'P'); + kv_idx_store(0, ramidx_tid, + pri_key.data(), pri_size, sec_key.data(), sec_size); + } + + // Remove secondary index + [[sysio::action]] + void ramidxremov(uint32_t sec_size, uint32_t pri_size) { + std::vector sec_key(sec_size, 'S'); + std::vector pri_key(pri_size, 'P'); + kv_idx_remove(ramidx_tid, + pri_key.data(), pri_size, sec_key.data(), sec_size); + } + + // Update secondary index (old sec filled with 'S', new with 'T') + [[sysio::action]] + void ramidxupdat(uint32_t old_ss, uint32_t new_ss, uint32_t pri_size) { + std::vector old_sec(old_ss, 'S'); + std::vector new_sec(new_ss, 'T'); + std::vector pri_key(pri_size, 'P'); + kv_idx_update(0, ramidx_tid, + pri_key.data(), pri_size, + old_sec.data(), old_ss, new_sec.data(), new_ss); + } + + // Secondary index store with explicit payer (for cross-account billing tests) + [[sysio::action]] + void ramidxstpyr(sysio::name payer, uint32_t sec_size, uint32_t pri_size) { + std::vector sec_key(sec_size, 'S'); + std::vector pri_key(pri_size, 'P'); + kv_idx_store(payer.value, ramidx_tid, + pri_key.data(), pri_size, sec_key.data(), sec_size); + } + + // Secondary index update with explicit payer + [[sysio::action]] + void ramidxuppyr(sysio::name payer, uint32_t old_ss, uint32_t new_ss, uint32_t pri_size) { + std::vector old_sec(old_ss, 'S'); + std::vector new_sec(new_ss, 'T'); + std::vector pri_key(pri_size, 'P'); + kv_idx_update(payer.value, ramidx_tid, + pri_key.data(), pri_size, + old_sec.data(), old_ss, new_sec.data(), new_ss); + } + + // ════════════════════════════════════════════════════════════════════════════ + // Read-only rejection actions + // ════════════════════════════════════════════════════════════════════════════ + + [[sysio::action]] + void tstrdoerase() { + char key[] = "rdoerase"; + kv_erase(test_table_id, key, sizeof(key)); + } + + [[sysio::action]] + void tstrdoidxst() { + char sec[] = "sec"; + char pri[] = "pri"; + kv_idx_store(0, rdo_tid, pri, 3, sec, 3); + } + + [[sysio::action]] + void tstrdoidxrm() { + char sec[] = "sec"; + char pri[] = "pri"; + kv_idx_remove(rdo_tid, pri, 3, sec, 3); + } + + [[sysio::action]] + void tstrdoidxup() { + char os[] = "sec"; + char ns[] = "new"; + char pri[] = "pri"; + kv_idx_update(0, rdo_tid, pri, 3, os, 3, ns, 3); + } + + // ════════════════════════════════════════════════════════════════════════════ + // Notification billing actions + // ════════════════════════════════════════════════════════════════════════════ + + // Send notification — receiver's on_notify handler does self-pay kv_set + [[sysio::action]] + void ramnotify(uint32_t key_id, uint32_t val_size) { + require_recipient("kvnotify"_n); + } + + // Notification handler: self-pay kv_set — RAM billed to notified receiver + [[sysio::on_notify("*::ramnotify")]] + void on_ramnotify(uint32_t key_id, uint32_t val_size) { + char key[4]; + memcpy(key, &key_id, 4); + std::vector val(val_size, 'N'); + kv_set(test_table_id, 0, key, 4, val.data(), val_size); + } + + // Send notification — receiver's handler tries third-party payer + [[sysio::action]] + void ramnotiferr(uint32_t key_id, uint32_t val_size, sysio::name payer) { + require_recipient("kvnotify"_n); + } + + // Notification handler: third-party payer in notify context — should fail + [[sysio::on_notify("*::ramnotiferr")]] + void on_ramnotiferr(uint32_t key_id, uint32_t val_size, sysio::name payer) { + char key[4]; + memcpy(key, &key_id, 4); + std::vector val(val_size, 'E'); + kv_set(test_table_id, payer.value, key, 4, val.data(), val_size); + } + + // ════════════════════════════════════════════════════════════════════════════ + // Iterator invalidation under mutation + // ════════════════════════════════════════════════════════════════════════════ + + // Update value of current row while iterator points to it — verify new value visible + [[sysio::action]] + void tstmutupdval() { + char prefix[] = {(char)0xA0}; + char key[] = {(char)0xA0, 0x01}; + kv_set(test_table_id, 0, key, 2, "old", 3); + + uint32_t h = kv_it_create(test_table_id, get_self().value, prefix, 1); + kv_it_lower_bound(h, key, 2); + check(kv_it_status(h) == 0, "mutupdval: should be valid"); + + // Update value while iterator points to row + kv_set(test_table_id, 0, key, 2, "newval", 6); + + // Iterator should still be valid (key unchanged) and see new value + char vbuf[16]; uint32_t vlen = 0; + int32_t st = kv_it_value(h, 0, vbuf, sizeof(vbuf), &vlen); + check(st == 0, "mutupdval: status after update"); + check(vlen == 6, "mutupdval: new value size"); + check(memcmp(vbuf, "newval", 6) == 0, "mutupdval: new value data"); + + kv_it_destroy(h); + } + + // Erase current row then call next() — should advance to next valid row + [[sysio::action]] + void tsterasenext() { + char prefix[] = {(char)0xA1}; + char k1[] = {(char)0xA1, 0x01}; + char k2[] = {(char)0xA1, 0x02}; + char k3[] = {(char)0xA1, 0x03}; + kv_set(test_table_id, 0, k1, 2, "a", 1); + kv_set(test_table_id, 0, k2, 2, "b", 1); + kv_set(test_table_id, 0, k3, 2, "c", 1); + + uint32_t h = kv_it_create(test_table_id, get_self().value, prefix, 1); + kv_it_lower_bound(h, k2, 2); + check(kv_it_status(h) == 0, "erasenext: on k2"); + + // Erase k2 under the iterator + kv_erase(test_table_id, k2, 2); + + // next() should advance to k3 + int32_t st = kv_it_next(h); + check(st == 0, "erasenext: next should find k3"); + + char kbuf[4]; uint32_t klen = 0; + kv_it_key(h, 0, kbuf, sizeof(kbuf), &klen); + check(klen == 2, "erasenext: key size"); + check(kbuf[1] == 0x03, "erasenext: should be at k3"); + + kv_it_destroy(h); + } + + // Erase all rows during iteration — iterator should reach end + [[sysio::action]] + void tsteraseall() { + char prefix[] = {(char)0xA2}; + char k1[] = {(char)0xA2, 0x01}; + char k2[] = {(char)0xA2, 0x02}; + kv_set(test_table_id, 0, k1, 2, "a", 1); + kv_set(test_table_id, 0, k2, 2, "b", 1); + + uint32_t h = kv_it_create(test_table_id, get_self().value, prefix, 1); + kv_it_lower_bound(h, k1, 2); + check(kv_it_status(h) == 0, "eraseall: on k1"); + + // Erase both rows + kv_erase(test_table_id, k1, 2); + kv_erase(test_table_id, k2, 2); + + // next() should reach end (status 1) + int32_t st = kv_it_next(h); + check(st == 1, "eraseall: should be at end"); + + kv_it_destroy(h); + } + + // Remove secondary index entry under sec iterator — verify detection + [[sysio::action]] + void tstidxerase() { + uint64_t self = get_self().value; + constexpr uint32_t table = idxmut_tid; + char sec1[] = "alpha"; + char sec2[] = "bravo"; + char pri1[] = "pk1"; + char pri2[] = "pk2"; + kv_idx_store(0, table, pri1, 3, sec1, 5); + kv_idx_store(0, table, pri2, 3, sec2, 5); + + // Position sec iterator on "alpha" + int32_t h = kv_idx_find_secondary(self, table, sec1, 5); + check(h >= 0, "idxerase: should find alpha"); + + // Remove "alpha" entry + kv_idx_remove(table, pri1, 3, sec1, 5); + + // next() should advance to "bravo" + int32_t st = kv_idx_next(h); + check(st == 0, "idxerase: next should find bravo"); + + char sbuf[16]; uint32_t slen = 0; + kv_idx_key(h, 0, sbuf, sizeof(sbuf), &slen); + check(slen == 5, "idxerase: sec key size"); + check(memcmp(sbuf, "bravo", 5) == 0, "idxerase: should be bravo"); + + kv_idx_destroy(h); + } + + // Update secondary key under sec iterator — iterator re-scans from old position + [[sysio::action]] + void tstidxmutupd() { + uint64_t self = get_self().value; + constexpr uint32_t table = idxmut2_tid; + char sec_old[] = "delta"; + char sec_new[] = "zebra"; + char pri[] = "pk1"; + kv_idx_store(0, table, pri, 3, sec_old, 5); + + // Position sec iterator on "delta" + int32_t h = kv_idx_find_secondary(self, table, sec_old, 5); + check(h >= 0, "idxmutupd: should find delta"); + + // Update "delta" -> "zebra" — removes old entry, inserts new one + kv_idx_update(0, table, pri, 3, sec_old, 5, sec_new, 5); + + // next() re-scans from "delta" position: "delta" gone, lower_bound lands on "zebra" + int32_t st = kv_idx_next(h); + check(st == 0, "idxmutupd: should find zebra"); + + char sbuf[16]; uint32_t slen = 0; + kv_idx_key(h, 0, sbuf, sizeof(sbuf), &slen); + check(slen == 5, "idxmutupd: key size"); + check(memcmp(sbuf, "zebra", 5) == 0, "idxmutupd: should be zebra"); + + kv_idx_destroy(h); + } + + // Insert new secondary entry during iteration — verify visible + [[sysio::action]] + void tstidxinsert() { + uint64_t self = get_self().value; + constexpr uint32_t table = idxmut3_tid; + char sec1[] = "aaa"; + char sec3[] = "ccc"; + char pri1[] = "pk1"; + char pri3[] = "pk3"; + kv_idx_store(0, table, pri1, 3, sec1, 3); + kv_idx_store(0, table, pri3, 3, sec3, 3); + + // Position on "aaa" + int32_t h = kv_idx_find_secondary(self, table, sec1, 3); + check(h >= 0, "idxinsert: should find aaa"); + + // Insert "bbb" between aaa and ccc + char sec2[] = "bbb"; + char pri2[] = "pk2"; + kv_idx_store(0, table, pri2, 3, sec2, 3); + + // next() should see "bbb" + int32_t st = kv_idx_next(h); + check(st == 0, "idxinsert: next should find bbb"); + + char sbuf[8]; uint32_t slen = 0; + kv_idx_key(h, 0, sbuf, sizeof(sbuf), &slen); + check(slen == 3, "idxinsert: key size"); + check(memcmp(sbuf, "bbb", 3) == 0, "idxinsert: should be bbb"); + + kv_idx_destroy(h); + } + + // ════════════════════════════════════════════════════════════════════════════ + // Cross-contract read actions + // ════════════════════════════════════════════════════════════════════════════ + + // Store a primary row + secondary index entry for cross-contract reads + [[sysio::action]] + void xcsetup() { + char key[] = "xcpri"; + char val[] = "xcval"; + kv_set(test_table_id, 0, key, 5, val, 5); + char sec[] = "xcsec"; + char pri[] = "xcpri"; + kv_idx_store(0, xc_tid, pri, 5, sec, 5); + } + + // Read another contract's secondary index + [[sysio::action]] + void xcidxread(sysio::name other) { + // Find secondary index entry on other contract + int32_t h = kv_idx_find_secondary(other.value, xc_tid, "xcsec", 5); + check(h >= 0, "xcidxread: secondary not found"); + + // Read primary key from the iterator + char pk_buf[16]; + uint32_t pk_sz = 0; + int32_t st = kv_idx_primary_key(h, 0, pk_buf, sizeof(pk_buf), &pk_sz); + check(st == 0, "xcidxread: iterator status"); + check(pk_sz == 5, "xcidxread: pri_key size"); + check(memcmp(pk_buf, "xcpri", 5) == 0, "xcidxread: pri_key data"); + + // Use that primary key to read the row from the other contract + char val_buf[16]; + int32_t val_sz = kv_get(test_table_id, other.value, "xcpri", 5, val_buf, sizeof(val_buf)); + check(val_sz == 5, "xcidxread: value size"); + check(memcmp(val_buf, "xcval", 5) == 0, "xcidxread: value data"); + + kv_idx_destroy(h); + } }; diff --git a/unittests/test-contracts/test_kv_api/test_kv_api.wasm b/unittests/test-contracts/test_kv_api/test_kv_api.wasm index 6b08769a0b89caa6a351c1ca3486b1ae9a08e7b2..56deac7965fda810feb7d9b04ac03d58e4bd8142 100755 GIT binary patch literal 116833 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 Date: Tue, 12 May 2026 16:46:09 -0500 Subject: [PATCH 2/2] test: bump kv_idx_payer_billing ROA budget for grown test_kv_api.wasm The new RAM-billing actions added in 2612345992 grew test_kv_api.wasm enough that the kv_idx_payer_billing test's 1.1000 SYS contract-account budget (1145144 bytes) no longer fits the contract on set_code (needs ~1170000 bytes after the 10x setcode multiplier). Bump to 1.2000 SYS. --- unittests/payer_choice_test.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/unittests/payer_choice_test.cpp b/unittests/payer_choice_test.cpp index f188685878..78237c5406 100644 --- a/unittests/payer_choice_test.cpp +++ b/unittests/payer_choice_test.cpp @@ -91,7 +91,7 @@ BOOST_AUTO_TEST_SUITE(payer_choice_test) // ROA: give kvtest enough RAM for contract deployment (WASM × 10 multiplier), // alice enough for data storage. c.register_node_owner(bob_account, 2); - c.add_roa_policy(bob_account, contract_account, "10.0000 SYS", "10.0000 SYS", "1.1000 SYS", 0, 0); + c.add_roa_policy(bob_account, contract_account, "10.0000 SYS", "10.0000 SYS", "1.2000 SYS", 0, 0); c.add_roa_policy(c.NODE_DADDY, alice_account, "100.0000 SYS", "100.0000 SYS", "100.0000 SYS", 0, 0); c.produce_block();