From 09aee7cb9ef4e21bb1fe2053ef7409997ed44aa7 Mon Sep 17 00:00:00 2001 From: kevin Heifner Date: Tue, 15 Sep 2026 15:21:15 -0500 Subject: [PATCH 01/29] feat(chain): make slug_name a first-class ABI type MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit slug_name fields rendered as `{"value": }` and now render as the decoded slug. The carrier is dual: a value below 2^42 has no string spelling, so it renders as the raw integer — keeping the conversion total and injective, and never throwing, because only chain_code is bound to the proven source outpost, so a non-canonical token_code is plantable and a throw would make a whole table unreadable over get_table_rows. The KV leaf and both key switches delegate to those conversions, so the carrier exists in one place and key bytes are unchanged (the struct-node path recursed one uint64 child to write_be64; the leaf path is write_be64). from_variant keeps a transitional `{value}` arm because a slug is object-only without variant conversions, so no JSON writer can straddle the cross-repo landing window; it goes when no writer emits the object form. Both test fixtures that declared a `slug_name` struct are renamed to composite_key. abigen matches builtins on the bare name, so they would have been emitted as the builtin, taken the leaf branch, and stopped exercising struct-key expansion while still passing. Change-Id: I8fd77d41b598210bcad0807eec7c97509eb150e6 --- contracts/tests/emissions_tests.cpp | 10 +- contracts/tests/sysio.chains_tests.cpp | 25 +- contracts/tests/sysio.dispatch_tests.cpp | 172 +++++----- contracts/tests/sysio.dispute_tests.cpp | 18 +- .../tests/sysio.epoch_flushwtdw_tests.cpp | 20 +- contracts/tests/sysio.msgch_chain_tests.cpp | 14 +- contracts/tests/sysio.msgch_tests.cpp | 8 +- contracts/tests/sysio.opreg_tests.cpp | 32 +- contracts/tests/sysio.reserv_tests.cpp | 324 +++++++++--------- contracts/tests/sysio.tokens_tests.cpp | 8 +- contracts/tests/sysio.uwrit_tests.cpp | 44 +-- libraries/chain/abi_serializer.cpp | 2 + .../include/sysio/chain/database_utils.hpp | 41 ++- libraries/libfc/include/fc/slug_name.hpp | 55 +++ libraries/libfc/test/test_slug_name.cpp | 101 ++++++ .../src/batch_operator_plugin.cpp | 13 +- .../src/underwriter_plugin.cpp | 24 +- tests/get_table_tests.cpp | 11 +- unittests/be_key_codec_tests.cpp | 120 +++++-- .../get_table_test/get_table_test.abi | 26 +- .../get_table_test/get_table_test.cpp | 2 +- .../get_table_test/get_table_test.hpp | 26 +- .../get_table_test/get_table_test.wasm | Bin 22208 -> 22208 bytes 23 files changed, 691 insertions(+), 405 deletions(-) diff --git a/contracts/tests/emissions_tests.cpp b/contracts/tests/emissions_tests.cpp index f1e4bad8f3..2ef918df41 100644 --- a/contracts/tests/emissions_tests.cpp +++ b/contracts/tests/emissions_tests.cpp @@ -576,7 +576,7 @@ class sysio_emissions_tester : public tester { deploy_reserv(); auto codename = [](std::string_view value) { - return mvo()("value", fc::slug_name{value}.value); + return std::string{value}; }; BOOST_REQUIRE_EQUAL(success(), push_reserv_action(RESERV, "regreserve"_n, mvo() ("chain_code", codename("ETH"))("token_code", codename("ETH"))("reserve_code", codename("PRIMARY")) @@ -5060,7 +5060,7 @@ BOOST_FIXTURE_TEST_CASE( expired_wire_claims_unblock_a_balance_blocked_epoch, sy create_t5_holding_accounts(); deploy_reserv(); - auto codename = [](std::string_view s) { return mvo()("value", fc::slug_name{s}.value); }; + auto codename = [](std::string_view s) { return std::string{s}; }; // Move real WIRE into reserv custody so a claim has backing. regreserve is bootstrap-window // only, which holds here: current_epoch_index is still 0. @@ -5970,9 +5970,9 @@ struct producer_score_tester : public producer_eligibility_tester { /// The tier packed into a `rank_score`, mirroring `producer_rank::tier_of`. static uint64_t tier_of(uint64_t rank_score) { return rank_score >> composite_bits; } - /// A `slug_name` in the shape the ABI serializes it: a single `value` field. - static fc::mutable_variant_object slug_mvo(std::string_view code) { - return mvo()("value", fc::slug_name{code}.value); + /// A `slug_name` in the shape the ABI serializes it: its decoded string. + static std::string slug_mvo(std::string_view code) { + return std::string{code}; } /// One `(chain, token, min_bond)` entry for opreg's `req_*_collat` vectors. The diff --git a/contracts/tests/sysio.chains_tests.cpp b/contracts/tests/sysio.chains_tests.cpp index d6a89d631b..563d4a7809 100644 --- a/contracts/tests/sysio.chains_tests.cpp +++ b/contracts/tests/sysio.chains_tests.cpp @@ -26,9 +26,9 @@ using mvo = fc::mutable_variant_object; namespace { -/// A `slug_name` renders in JSON/ABI as `{value: }`. -inline fc::mutable_variant_object codename_mvo(std::string_view s) { - return mvo()("value", fc::slug_name{s}.value); +/// A `slug_name` renders in JSON/ABI as its decoded string spelling. +inline std::string codename(std::string_view s) { + return std::string{s}; } // Well-formed sample addresses for the accept paths. @@ -88,7 +88,7 @@ class sysio_chains_tester : public tester { const fc::variant_object& outpost) { return push_chains("regchain"_n, mvo() ("kind", kind) - ("code", codename_mvo(code)) + ("code", codename(code)) ("external_chain_id", external_chain_id) ("name", std::string(code)) ("description", std::string{}) @@ -97,7 +97,7 @@ class sysio_chains_tester : public tester { action_result setoutpost(std::string_view code, const fc::variant_object& outpost) { return push_chains("setoutpost"_n, mvo() - ("code", codename_mvo(code)) + ("code", codename(code)) ("outpost", outpost)); } @@ -122,6 +122,21 @@ class sysio_chains_tester : public tester { BOOST_AUTO_TEST_SUITE(sysio_chains_tests) // ── EVM: all four role addresses are accepted and stored verbatim ── +// ── The `code` CELL renders as its decoded slug, not a `{value}` wrapper ───── +// `sysio.chains::chains` is keyed on `code`, so this table is the one whose +// `next_key` spelling the slug_name ABI builtin changes — and before this case +// the suite only ever used `code` as a KEY ARGUMENT (get_chain's get_row_by_id), +// never asserting the rendered cell. A wrong carrier here had no coverage. +BOOST_FIXTURE_TEST_CASE(regchain_code_cell_renders_as_the_decoded_slug, sysio_chains_tester) { try { + BOOST_REQUIRE_EQUAL(success(), regchain(ChainKind::CHAIN_KIND_EVM, "ETH", 1, + evm_outpost_mvo(EVM_OPP, EVM_INBOUND, EVM_OPREG, EVM_DEPOSIT))); + auto row = get_chain("ETH"); + BOOST_REQUIRE(!row.is_null()); + BOOST_REQUIRE_MESSAGE(row["code"].is_string(), + "a slug_name cell must render as its decoded string, not a wrapper"); + BOOST_REQUIRE_EQUAL("ETH", row["code"].as_string()); +} FC_LOG_AND_RETHROW() } + BOOST_FIXTURE_TEST_CASE(regchain_evm_addresses_stored, sysio_chains_tester) { try { BOOST_REQUIRE_EQUAL(success(), regchain(ChainKind::CHAIN_KIND_EVM, "ETH", 1, evm_outpost_mvo(EVM_OPP, EVM_INBOUND, EVM_OPREG, EVM_DEPOSIT))); diff --git a/contracts/tests/sysio.dispatch_tests.cpp b/contracts/tests/sysio.dispatch_tests.cpp index 1b6d72e458..fe7f87e30b 100644 --- a/contracts/tests/sysio.dispatch_tests.cpp +++ b/contracts/tests/sysio.dispatch_tests.cpp @@ -60,8 +60,8 @@ constexpr uint8_t PROTOBUF_VARINT_CONTINUATION_BIT = 0x80u; constexpr uint32_t PROTOBUF_FIELD_TAG_SHIFT = 3u; /// SlugName mvo helper for v6 action arguments. -inline fc::mutable_variant_object codename_mvo(std::string_view s) { - return mvo()("value", fc::slug_name{s}.value); +inline std::string codename(std::string_view s) { + return std::string{s}; } /** Append one unsigned protobuf varint to a hostile-wire-format fixture. */ @@ -101,8 +101,8 @@ inline fc::variant chain_min_bond_mvo(std::string_view chain_code, std::string_view token_code, uint64_t min_bond) { return fc::variant(mvo() - ("chain_code", codename_mvo(chain_code)) - ("token_code", codename_mvo(token_code)) + ("chain_code", codename(chain_code)) + ("token_code", codename(token_code)) ("min_bond", min_bond) ("config_timestamp_ms", uint64_t{0})); } @@ -536,7 +536,7 @@ class sysio_dispatch_tester : public tester { BOOST_REQUIRE_EQUAL(success(), push(CHAINS_ACCOUNT, chains_abi, CHAINS_ACCOUNT, "regchain"_n, mvo() ("kind", outpost_kind) - ("code", codename_mvo(outpost_code)) + ("code", codename(outpost_code)) ("external_chain_id", 31337) ("name", std::string("outpost-test")) ("description", std::string{}) @@ -621,8 +621,8 @@ class sysio_dispatch_tester : public tester { const auto token_v = fc::slug_name{token_code}.value; const auto& arr = op["balances"].get_array(); for (const auto& b : arr) { - if (b["chain_code"]["value"].as_uint64() == chain_v && - b["token_code"]["value"].as_uint64() == token_v) { + if (b["chain_code"].as().value == chain_v && + b["token_code"].as().value == token_v) { return b; } } @@ -666,8 +666,8 @@ class sysio_dispatch_tester : public tester { // depositinle does require_auth(get_self()); sign as opreg for a direct call. return push(OPREG_ACCOUNT, opreg_abi, OPREG_ACCOUNT, "depositinle"_n, mvo() ("account", account.to_string()) - ("chain_code", codename_mvo(chain_code)) - ("token_code", codename_mvo(token_code)) + ("chain_code", codename(chain_code)) + ("token_code", codename(token_code)) ("amount", amount) ("actor_chain", ChainKind::CHAIN_KIND_EVM) ("actor_address", std::vector(20, '\x06')) @@ -692,9 +692,9 @@ class sysio_dispatch_tester : public tester { ("uwreq_id", uwreq_id) ("underwriter", underwriter.to_string()) ("chain_code", outpost_chain_code) - ("from_chain_code", codename_mvo(from_chain)) - ("from_token_code", codename_mvo(from_token)) - ("reserve_code", codename_mvo(reserve)) + ("from_chain_code", codename(from_chain)) + ("from_token_code", codename(from_token)) + ("reserve_code", codename(reserve)) ("uic_bytes", uic_bytes)); } @@ -707,9 +707,9 @@ class sysio_dispatch_tester : public tester { ("uwreq_id", uwreq_id) ("underwriter", underwriter.to_string()) ("chain_code", outpost_chain_code) - ("from_chain_code", codename_mvo(from_chain)) - ("from_token_code", codename_mvo(from_token)) - ("reserve_code", codename_mvo(reserve)) + ("from_chain_code", codename(from_chain)) + ("from_token_code", codename(from_token)) + ("reserve_code", codename(reserve)) ("uic_bytes", uic_bytes)); } @@ -1062,8 +1062,8 @@ class sysio_dispatch_tester : public tester { auto row = uwrit_abi.binary_to_variant("lock_sum", raw, abi_serializer::create_yield_function(abi_serializer_max_time)); if (row["underwriter"].as_string() == underwriter.to_string() && - row["chain_code"]["value"].as_uint64() == target_chain && - row["token_code"]["value"].as_uint64() == target_token) { + row["chain_code"].as().value == target_chain && + row["token_code"].as().value == target_token) { return row["amount"].as_uint64(); } } catch (...) { @@ -1094,8 +1094,8 @@ class sysio_dispatch_tester : public tester { auto row = uwrit_abi.binary_to_variant("lock_entry", raw, abi_serializer::create_yield_function(abi_serializer_max_time)); if (row["underwriter"].as_string() == underwriter.to_string() && - row["chain_code"]["value"].as_uint64() == target_chain && - row["token_code"]["value"].as_uint64() == target_token) { + row["chain_code"].as().value == target_chain && + row["token_code"].as().value == target_token) { total += row["amount"].as_uint64(); } } catch (...) { @@ -1121,8 +1121,8 @@ class sysio_dispatch_tester : public tester { std::string_view token_code, uint64_t amount) { return push(OPREG_ACCOUNT, opreg_abi, UWRIT_ACCOUNT, "releaselock"_n, mvo() ("account", account.to_string()) - ("chain_code", codename_mvo(chain_code)) - ("token_code", codename_mvo(token_code)) + ("chain_code", codename(chain_code)) + ("token_code", codename(token_code)) ("amount", amount)); } @@ -1162,9 +1162,9 @@ class sysio_dispatch_tester : public tester { try { auto row = reserv_abi.binary_to_variant("reserve_row", raw, abi_serializer::create_yield_function(abi_serializer_max_time)); - if (row["chain_code"]["value"].as_uint64() == target_chain && - row["token_code"]["value"].as_uint64() == target_token && - row["reserve_code"]["value"].as_uint64() == target_reserve) { + if (row["chain_code"].as().value == target_chain && + row["token_code"].as().value == target_token && + row["reserve_code"].as().value == target_reserve) { return row; } } catch (...) { @@ -1214,9 +1214,9 @@ class sysio_dispatch_tester : public tester { std::string_view r, uint64_t chain_amount, uint64_t wire_amount) { return push(RESERV_ACCOUNT, reserv_abi, RESERV_ACCOUNT, "regreserve"_n, mvo() - ("chain_code", codename_mvo(c)) - ("token_code", codename_mvo(t)) - ("reserve_code", codename_mvo(r)) + ("chain_code", codename(c)) + ("token_code", codename(t)) + ("reserve_code", codename(r)) ("name", std::string(c)) ("description", std::string{}) ("initial_chain_amount", chain_amount) @@ -1233,9 +1233,9 @@ class sysio_dispatch_tester : public tester { action_result debit_reserve_chain(std::string_view c, std::string_view t, std::string_view r, uint64_t amount) { return push(RESERV_ACCOUNT, reserv_abi, UWRIT_ACCOUNT, "debit"_n, mvo() - ("chain_code", codename_mvo(c)) - ("token_code", codename_mvo(t)) - ("reserve_code", codename_mvo(r)) + ("chain_code", codename(c)) + ("token_code", codename(t)) + ("reserve_code", codename(r)) ("amount", amount)); } @@ -1262,7 +1262,7 @@ class sysio_dispatch_tester : public tester { BOOST_REQUIRE_EQUAL(success(), push(CHAINS_ACCOUNT, chains_abi, CHAINS_ACCOUNT, "regchain"_n, mvo() ("kind", ChainKind::CHAIN_KIND_WIRE) - ("code", codename_mvo("WIRE")) + ("code", codename("WIRE")) ("external_chain_id", 0) ("name", std::string("wire-depot")) ("description", std::string{}) @@ -1276,7 +1276,7 @@ class sysio_dispatch_tester : public tester { /// must have been called first. void setup_eth_to_sol_uwreq(uint64_t att_id) { BOOST_REQUIRE_EQUAL(success(), push(CHAINS_ACCOUNT, chains_abi, CHAINS_ACCOUNT, "regchain"_n, mvo() - ("kind", ChainKind::CHAIN_KIND_SVM)("code", codename_mvo("SOLANA")) + ("kind", ChainKind::CHAIN_KIND_SVM)("code", codename("SOLANA")) ("external_chain_id", 900)("name", std::string("solana-test"))("description", std::string{}) ("outpost", sysio_system::test_support::no_outpost_mvo()))); setup_wire_token_and_reserves(); @@ -1461,8 +1461,8 @@ BOOST_FIXTURE_TEST_CASE(dispatch_routes_withdraw_request_to_opreg, sysio_dispatc BOOST_REQUIRE_EQUAL(UWRIT_OP.to_string(), row["account"].as_string()); BOOST_REQUIRE_EQUAL(static_cast(WITHDRAW_AMOUNT), row["amount"].as_uint64()); - BOOST_REQUIRE_EQUAL(eth_code, row["chain_code"]["value"].as_uint64()); - BOOST_REQUIRE_EQUAL(eth_code, row["token_code"]["value"].as_uint64()); + BOOST_REQUIRE_EQUAL(eth_code, row["chain_code"].as().value); + BOOST_REQUIRE_EQUAL(eth_code, row["token_code"].as().value); } FC_LOG_AND_RETHROW() } BOOST_FIXTURE_TEST_CASE(dispatch_silently_drops_out_of_scope_types, sysio_dispatch_tester) { try { @@ -1506,7 +1506,7 @@ BOOST_FIXTURE_TEST_CASE(operator_action_mismatched_source_chain_is_dropped, // SOLANA is a real, active outpost, so the ONLY thing wrong with the payloads below is that they // were proven-delivered from ETH rather than SOLANA — the exact WSA-005 forgery. BOOST_REQUIRE_EQUAL(success(), push(CHAINS_ACCOUNT, chains_abi, CHAINS_ACCOUNT, "regchain"_n, mvo() - ("kind", ChainKind::CHAIN_KIND_SVM)("code", codename_mvo("SOLANA")) + ("kind", ChainKind::CHAIN_KIND_SVM)("code", codename("SOLANA")) ("external_chain_id", 900)("name", std::string("solana-test"))("description", std::string{}) ("outpost", sysio_system::test_support::no_outpost_mvo()))); @@ -1539,7 +1539,7 @@ BOOST_FIXTURE_TEST_CASE(swap_request_mismatched_source_chain_is_refunded, sysio_dispatch_tester) { try { bootstrap_for_dispatch(); // ETH source outpost BOOST_REQUIRE_EQUAL(success(), push(CHAINS_ACCOUNT, chains_abi, CHAINS_ACCOUNT, "regchain"_n, mvo() - ("kind", ChainKind::CHAIN_KIND_SVM)("code", codename_mvo("SOLANA")) + ("kind", ChainKind::CHAIN_KIND_SVM)("code", codename("SOLANA")) ("external_chain_id", 900)("name", std::string("solana-test"))("description", std::string{}) ("outpost", sysio_system::test_support::no_outpost_mvo()))); setup_wire_token_and_reserves(); // ACTIVE ETH/ETH/PRIMARY + SOLANA/SOL/PRIMARY reserves @@ -1576,7 +1576,7 @@ BOOST_FIXTURE_TEST_CASE(swap_request_identical_reserve_identity_is_refunded, sysio_dispatch_tester) { try { bootstrap_for_dispatch(); BOOST_REQUIRE_EQUAL(success(), push(CHAINS_ACCOUNT, chains_abi, CHAINS_ACCOUNT, "regchain"_n, mvo() - ("kind", ChainKind::CHAIN_KIND_SVM)("code", codename_mvo("SOLANA")) + ("kind", ChainKind::CHAIN_KIND_SVM)("code", codename("SOLANA")) ("external_chain_id", 900)("name", std::string("solana-test"))("description", std::string{}) ("outpost", sysio_system::test_support::no_outpost_mvo()))); setup_wire_token_and_reserves(); @@ -1610,7 +1610,7 @@ BOOST_FIXTURE_TEST_CASE(underwrite_commit_mismatched_source_chain_is_dropped, sysio_dispatch_tester) { try { bootstrap_for_dispatch(); BOOST_REQUIRE_EQUAL(success(), push(CHAINS_ACCOUNT, chains_abi, CHAINS_ACCOUNT, "regchain"_n, mvo() - ("kind", ChainKind::CHAIN_KIND_SVM)("code", codename_mvo("SOLANA")) + ("kind", ChainKind::CHAIN_KIND_SVM)("code", codename("SOLANA")) ("external_chain_id", 900)("name", std::string("solana-test"))("description", std::string{}) ("outpost", sysio_system::test_support::no_outpost_mvo()))); setup_wire_token_and_reserves(); @@ -1669,13 +1669,13 @@ BOOST_FIXTURE_TEST_CASE(underwrite_commit_two_evm_chains_route_per_chain, bootstrap_for_dispatch(); // ETH (EVM) source outpost // A SECOND active EVM chain — same VM family, distinct chain_code. BOOST_REQUIRE_EQUAL(success(), push(CHAINS_ACCOUNT, chains_abi, CHAINS_ACCOUNT, "regchain"_n, mvo() - ("kind", ChainKind::CHAIN_KIND_EVM)("code", codename_mvo("POLYGON")) + ("kind", ChainKind::CHAIN_KIND_EVM)("code", codename("POLYGON")) ("external_chain_id", 137)("name", std::string("polygon-test"))("description", std::string{}) ("outpost", sysio_system::test_support::no_outpost_mvo()))); // SOLANA is registered only because the shared reserve-setup helper seeds a // SOLANA/SOL reserve; it is otherwise unused by this two-EVM scenario. BOOST_REQUIRE_EQUAL(success(), push(CHAINS_ACCOUNT, chains_abi, CHAINS_ACCOUNT, "regchain"_n, mvo() - ("kind", ChainKind::CHAIN_KIND_SVM)("code", codename_mvo("SOLANA")) + ("kind", ChainKind::CHAIN_KIND_SVM)("code", codename("SOLANA")) ("external_chain_id", 900)("name", std::string("solana-test"))("description", std::string{}) ("outpost", sysio_system::test_support::no_outpost_mvo()))); setup_wire_token_and_reserves(); @@ -1965,7 +1965,7 @@ BOOST_FIXTURE_TEST_CASE(node_owner_reg_from_other_evm_outpost_is_dropped, sysio_ bootstrap_for_dispatch(); // registers "ETH" — an EVM outpost, but NOT the node-owner source // Register the real node-owner source too, so the ONLY thing wrong below is the delivering outpost. BOOST_REQUIRE_EQUAL(success(), push(CHAINS_ACCOUNT, chains_abi, CHAINS_ACCOUNT, "regchain"_n, mvo() - ("kind", ChainKind::CHAIN_KIND_EVM)("code", codename_mvo("ETHEREUM")) + ("kind", ChainKind::CHAIN_KIND_EVM)("code", codename("ETHEREUM")) ("external_chain_id", 1)("name", std::string("ethereum-mainnet"))("description", std::string{}) ("outpost", sysio_system::test_support::no_outpost_mvo()))); const auto other_evm = fc::slug_name{"ETH"}.value; // active EVM outpost, but not "ETHEREUM" @@ -2215,7 +2215,7 @@ BOOST_FIXTURE_TEST_CASE(swap_missing_dst_authex_recovers_after_exact_uic_replay, BOOST_REQUIRE_EQUAL(success(), push(CHAINS_ACCOUNT, chains_abi, CHAINS_ACCOUNT, "regchain"_n, mvo() ("kind", ChainKind::CHAIN_KIND_SVM) - ("code", codename_mvo("SOLANA")) + ("code", codename("SOLANA")) ("external_chain_id", 900) ("name", std::string("solana-test")) ("description", std::string{}) @@ -2299,7 +2299,7 @@ BOOST_FIXTURE_TEST_CASE(swap_zero_quote_from_active_reserve_fails_closed, BOOST_REQUIRE_EQUAL(success(), push(CHAINS_ACCOUNT, chains_abi, CHAINS_ACCOUNT, "regchain"_n, mvo() ("kind", ChainKind::CHAIN_KIND_SVM) - ("code", codename_mvo("SOLANA")) + ("code", codename("SOLANA")) ("external_chain_id", 900) ("name", std::string("solana-test")) ("description", std::string{}) @@ -2538,9 +2538,9 @@ BOOST_FIXTURE_TEST_CASE(swap_slippage_bound_does_not_compound_across_checkpoints // Move the destination reserve so the price walks a second step down: debit // 7% of its token side (UWRIT-authorized, the same primitive settlement uses). BOOST_REQUIRE_EQUAL(success(), push(RESERV_ACCOUNT, reserv_abi, UWRIT_ACCOUNT, "debit"_n, mvo() - ("chain_code", codename_mvo("ETH")) - ("token_code", codename_mvo("ETH")) - ("reserve_code", codename_mvo("SECOND")) + ("chain_code", codename("ETH")) + ("token_code", codename("ETH")) + ("reserve_code", codename("SECOND")) ("amount", uint64_t{70'000'000'000}))); // Pin the scenario: the drift is inside tolerance of the PREVIOUS quote (so @@ -2607,9 +2607,9 @@ BOOST_FIXTURE_TEST_CASE(swap_underbonded_candidate_cannot_terminally_reject, // Drift the price far outside the tolerance — the request IS terminally // doomed, but this candidate must not be the one to close it. BOOST_REQUIRE_EQUAL(success(), push(RESERV_ACCOUNT, reserv_abi, UWRIT_ACCOUNT, "debit"_n, mvo() - ("chain_code", codename_mvo("ETH")) - ("token_code", codename_mvo("ETH")) - ("reserve_code", codename_mvo("SECOND")) + ("chain_code", codename("ETH")) + ("token_code", codename("ETH")) + ("reserve_code", codename("SECOND")) ("amount", uint64_t{500'000'000'000}))); const auto src_uic = create_signed_uic(UWRIT_OP, ATT_ID, eth, eth, primary); @@ -3688,7 +3688,7 @@ BOOST_FIXTURE_TEST_CASE(swap_malformed_destination_signature_preserves_valid_sou BOOST_REQUIRE_EQUAL(success(), push( CHAINS_ACCOUNT, chains_abi, CHAINS_ACCOUNT, "regchain"_n, mvo() ("kind", ChainKind::CHAIN_KIND_SVM) - ("code", codename_mvo("SOLANA")) + ("code", codename("SOLANA")) ("external_chain_id", 900) ("name", std::string("solana-test")) ("description", std::string{}) @@ -3809,7 +3809,7 @@ BOOST_FIXTURE_TEST_CASE(swap_forged_claim_cannot_overwrite_honest_candidate, BOOST_REQUIRE_EQUAL(success(), push( CHAINS_ACCOUNT, chains_abi, CHAINS_ACCOUNT, "regchain"_n, mvo() ("kind", ChainKind::CHAIN_KIND_SVM) - ("code", codename_mvo("SOLANA")) + ("code", codename("SOLANA")) ("external_chain_id", 900) ("name", std::string("solana-test")) ("description", std::string{}) @@ -4060,7 +4060,7 @@ BOOST_FIXTURE_TEST_CASE(swap_request_malformed_bytes_do_not_abort_consensus_deli sysio_dispatch_tester) { try { bootstrap_for_dispatch(); // ETH source outpost BOOST_REQUIRE_EQUAL(success(), push(CHAINS_ACCOUNT, chains_abi, CHAINS_ACCOUNT, "regchain"_n, mvo() - ("kind", ChainKind::CHAIN_KIND_SVM)("code", codename_mvo("SOLANA")) + ("kind", ChainKind::CHAIN_KIND_SVM)("code", codename("SOLANA")) ("external_chain_id", 900)("name", std::string("solana-test"))("description", std::string{}) ("outpost", sysio_system::test_support::no_outpost_mvo()))); setup_wire_token_and_reserves(); @@ -4105,7 +4105,7 @@ BOOST_FIXTURE_TEST_CASE(swap_request_malformed_bytes_do_not_abort_consensus_deli const auto req = get_uwreq(first_att_id + 3); BOOST_REQUIRE(!req.is_null()); BOOST_REQUIRE_EQUAL("UNDERWRITE_REQUEST_STATUS_PENDING", req["status"].as_string()); - BOOST_REQUIRE_EQUAL(eth, req["src_chain_code"]["value"].as_uint64()); + BOOST_REQUIRE_EQUAL(eth, req["src_chain_code"].as().value); BOOST_REQUIRE_EQUAL(100u, req["src_amount"].as_uint64()); } FC_LOG_AND_RETHROW() } @@ -4117,7 +4117,7 @@ BOOST_FIXTURE_TEST_CASE(createuwreq_duplicate_attestation_id_is_idempotent, sysio_dispatch_tester) { try { bootstrap_for_dispatch(); BOOST_REQUIRE_EQUAL(success(), push(CHAINS_ACCOUNT, chains_abi, CHAINS_ACCOUNT, "regchain"_n, mvo() - ("kind", ChainKind::CHAIN_KIND_SVM)("code", codename_mvo("SOLANA")) + ("kind", ChainKind::CHAIN_KIND_SVM)("code", codename("SOLANA")) ("external_chain_id", 900)("name", std::string("solana-test"))("description", std::string{}) ("outpost", sysio_system::test_support::no_outpost_mvo()))); setup_wire_token_and_reserves(); @@ -4243,7 +4243,7 @@ BOOST_FIXTURE_TEST_CASE(swap_request_negative_source_is_reverted, BOOST_REQUIRE_EQUAL(success(), push(CHAINS_ACCOUNT, chains_abi, CHAINS_ACCOUNT, "regchain"_n, mvo() ("kind", ChainKind::CHAIN_KIND_SVM) - ("code", codename_mvo("SOLANA")) + ("code", codename("SOLANA")) ("external_chain_id", 900) ("name", std::string("solana-test")) ("description", std::string{}) @@ -4363,7 +4363,7 @@ BOOST_FIXTURE_TEST_CASE(swap_race_time_reserve_drain_rejects_request, BOOST_REQUIRE_EQUAL(success(), push(CHAINS_ACCOUNT, chains_abi, CHAINS_ACCOUNT, "regchain"_n, mvo() ("kind", ChainKind::CHAIN_KIND_SVM) - ("code", codename_mvo("SOLANA")) + ("code", codename("SOLANA")) ("external_chain_id", 900) ("name", std::string("solana-test")) ("description", std::string{}) @@ -4437,7 +4437,7 @@ BOOST_FIXTURE_TEST_CASE(swap_replayed_uic_variance_drift_rejects_request, BOOST_REQUIRE_EQUAL(success(), push(CHAINS_ACCOUNT, chains_abi, CHAINS_ACCOUNT, "regchain"_n, mvo() ("kind", ChainKind::CHAIN_KIND_SVM) - ("code", codename_mvo("SOLANA")) + ("code", codename("SOLANA")) ("external_chain_id", 900) ("name", std::string("solana-test")) ("description", std::string{}) @@ -4608,10 +4608,10 @@ BOOST_FIXTURE_TEST_CASE(swap_same_token_legs_exact_balance_wins, const auto l2 = get_lock(2); BOOST_REQUIRE(!l1.is_null()); BOOST_REQUIRE(!l2.is_null()); - BOOST_REQUIRE_EQUAL(eth, l1["chain_code"]["value"].as_uint64()); - BOOST_REQUIRE_EQUAL(eth, l1["token_code"]["value"].as_uint64()); - BOOST_REQUIRE_EQUAL(eth, l2["chain_code"]["value"].as_uint64()); - BOOST_REQUIRE_EQUAL(eth, l2["token_code"]["value"].as_uint64()); + BOOST_REQUIRE_EQUAL(eth, l1["chain_code"].as().value); + BOOST_REQUIRE_EQUAL(eth, l1["token_code"].as().value); + BOOST_REQUIRE_EQUAL(eth, l2["chain_code"].as().value); + BOOST_REQUIRE_EQUAL(eth, l2["token_code"].as().value); BOOST_REQUIRE_EQUAL(100u, l1["amount"].as_uint64()); BOOST_REQUIRE_EQUAL(quote, l2["amount"].as_uint64()); BOOST_REQUIRE_EQUAL(100u + quote, l1["amount"].as_uint64() + l2["amount"].as_uint64()); @@ -4778,9 +4778,9 @@ BOOST_FIXTURE_TEST_CASE(drainfwq_bounds_rows_per_epoch, sysio_dispatch_tester) { push(UWRIT_ACCOUNT, uwrit_abi, "swapuser"_n, "swapfromwire"_n, mvo() ("user", "swapuser") ("wire_amount", uint64_t{1'000'000} + i) - ("dst_chain_code", codename_mvo("ETH")) - ("dst_token_code", codename_mvo("ETH")) - ("dst_reserve_code", codename_mvo("PRIMARY")) + ("dst_chain_code", codename("ETH")) + ("dst_token_code", codename("ETH")) + ("dst_reserve_code", codename("PRIMARY")) ("target_amount", uint64_t{1'000'000}) ("target_tolerance_bps", uint32_t{10000}) ("recipient_kind", sysio::opp::types::ChainKind::CHAIN_KIND_EVM) @@ -4836,9 +4836,9 @@ BOOST_FIXTURE_TEST_CASE(swapfromwire_enforces_min_amount, sysio_dispatch_tester) return push(UWRIT_ACCOUNT, uwrit_abi, "swapuser"_n, "swapfromwire"_n, mvo() ("user", "swapuser") ("wire_amount", wire_amount) - ("dst_chain_code", codename_mvo("ETH")) - ("dst_token_code", codename_mvo("ETH")) - ("dst_reserve_code", codename_mvo("PRIMARY")) + ("dst_chain_code", codename("ETH")) + ("dst_token_code", codename("ETH")) + ("dst_reserve_code", codename("PRIMARY")) ("target_amount", uint64_t{1'000'000}) ("target_tolerance_bps", uint32_t{10000}) ("recipient_kind", sysio::opp::types::ChainKind::CHAIN_KIND_EVM) @@ -4907,9 +4907,9 @@ BOOST_FIXTURE_TEST_CASE(drainfwq_charges_revert_fee_on_caller_fault, sysio_dispa push(UWRIT_ACCOUNT, uwrit_abi, "swapuser"_n, "swapfromwire"_n, mvo() ("user", "swapuser") ("wire_amount", ESCROW) - ("dst_chain_code", codename_mvo("ETH")) - ("dst_token_code", codename_mvo("ETH")) - ("dst_reserve_code", codename_mvo("PRIMARY")) + ("dst_chain_code", codename("ETH")) + ("dst_token_code", codename("ETH")) + ("dst_reserve_code", codename("PRIMARY")) ("target_amount", uint64_t{1}) ("target_tolerance_bps", uint32_t{0}) ("recipient_kind", sysio::opp::types::ChainKind::CHAIN_KIND_EVM) @@ -4967,9 +4967,9 @@ BOOST_FIXTURE_TEST_CASE(drainfwq_full_refund_on_system_caused_revert, sysio_disp push(UWRIT_ACCOUNT, uwrit_abi, "swapuser"_n, "swapfromwire"_n, mvo() ("user", "swapuser") ("wire_amount", ESCROW) - ("dst_chain_code", codename_mvo("ETH")) - ("dst_token_code", codename_mvo("ETH")) - ("dst_reserve_code", codename_mvo("PRIMARY")) + ("dst_chain_code", codename("ETH")) + ("dst_token_code", codename("ETH")) + ("dst_reserve_code", codename("PRIMARY")) ("target_amount", uint64_t{1'000'000}) ("target_tolerance_bps", uint32_t{10000}) ("recipient_kind", sysio::opp::types::ChainKind::CHAIN_KIND_EVM) @@ -5027,9 +5027,9 @@ BOOST_FIXTURE_TEST_CASE(blocking_refund_recipient_cannot_stall_drainfwq, sysio_d push(UWRIT_ACCOUNT, uwrit_abi, "swapuser"_n, "swapfromwire"_n, mvo() ("user", "swapuser") ("wire_amount", ESCROW) - ("dst_chain_code", codename_mvo("ETH")) - ("dst_token_code", codename_mvo("ETH")) - ("dst_reserve_code", codename_mvo("PRIMARY")) + ("dst_chain_code", codename("ETH")) + ("dst_token_code", codename("ETH")) + ("dst_reserve_code", codename("PRIMARY")) ("target_amount", uint64_t{1'000'000}) ("target_tolerance_bps", uint32_t{10000}) ("recipient_kind", sysio::opp::types::ChainKind::CHAIN_KIND_EVM) @@ -5168,9 +5168,9 @@ BOOST_FIXTURE_TEST_CASE(uwreq_from_wire_pending_timeout_refunds_escrow, "swapfromwire"_n, mvo() ("user", "swapuser") ("wire_amount", ESCROW) - ("dst_chain_code", codename_mvo("ETH")) - ("dst_token_code", codename_mvo("ETH")) - ("dst_reserve_code", codename_mvo("PRIMARY")) + ("dst_chain_code", codename("ETH")) + ("dst_token_code", codename("ETH")) + ("dst_reserve_code", codename("PRIMARY")) ("target_amount", uint64_t{1'000'000}) ("target_tolerance_bps", uint32_t{10000}) ("recipient_kind", ChainKind::CHAIN_KIND_EVM) @@ -5562,9 +5562,9 @@ class sysio_uwchal_tester : public sysio_dispatch_tester { try { auto row = reserv_abi.binary_to_variant( "reserve_row", raw, abi_serializer::create_yield_function(abi_serializer_max_time)); - if (row["chain_code"]["value"].as_uint64() == target_chain && - row["token_code"]["value"].as_uint64() == target_token && - row["reserve_code"]["value"].as_uint64() == target_reserve) { + if (row["chain_code"].as().value == target_chain && + row["token_code"].as().value == target_token && + row["reserve_code"].as().value == target_reserve) { return row; } } catch (...) { @@ -5587,8 +5587,8 @@ class sysio_uwchal_tester : public sysio_dispatch_tester { for (const auto& bal : op["balances"].get_array()) { const uint64_t amount = bal["balance"].as_uint64(); if (amount == 0) continue; - const auto chain = bal["chain_code"]["value"].as_uint64(), - token = bal["token_code"]["value"].as_uint64(); + const auto chain = bal["chain_code"].as().value, + token = bal["token_code"].as().value; const uint64_t wire = fc::slug_name{"WIRE"}.value; if (chain == wire && token == wire) { total += amount; // already WIRE — no curve @@ -5623,8 +5623,8 @@ class sysio_uwchal_tester : public sysio_dispatch_tester { try { auto row = reserv_abi.binary_to_variant("reserve_row", raw, abi_serializer::create_yield_function(abi_serializer_max_time)); - if (row["chain_code"]["value"].as_uint64() == target_chain && - row["token_code"]["value"].as_uint64() == target_token && + if (row["chain_code"].as().value == target_chain && + row["token_code"].as().value == target_token && row["status"].as_string() == "RESERVE_STATUS_ACTIVE") { return row; } diff --git a/contracts/tests/sysio.dispute_tests.cpp b/contracts/tests/sysio.dispute_tests.cpp index 5a221122fa..5945a2dd3f 100644 --- a/contracts/tests/sysio.dispute_tests.cpp +++ b/contracts/tests/sysio.dispute_tests.cpp @@ -51,8 +51,8 @@ using mvo = fc::mutable_variant_object; namespace { /// SlugName mvo helper for v6 chain-registry action arguments. -inline fc::mutable_variant_object codename_mvo(std::string_view s) { - return mvo()("value", fc::slug_name{s}.value); +inline std::string codename(std::string_view s) { + return std::string{s}; } /// Build an `authority` whose active permission is the account's own active key plus a list of @@ -213,7 +213,7 @@ class sysio_dispute_tester : public tester { ("is_bootstrapped", true))); BOOST_REQUIRE_EQUAL(success(), push(CHAINS_ACCOUNT, chains_abi, CHAINS_ACCOUNT, "regchain"_n, mvo() - ("kind", ChainKind::CHAIN_KIND_EVM)("code", codename_mvo("ETH")) + ("kind", ChainKind::CHAIN_KIND_EVM)("code", codename("ETH")) ("external_chain_id", 31337)("name", std::string("ethereum-test"))("description", std::string{}) ("outpost", sysio_system::test_support::no_outpost_mvo()))); @@ -369,15 +369,15 @@ class sysio_dispute_tester : public tester { static fc::variant make_chain_min_bond(std::string_view chain_code, std::string_view token_code, uint64_t min_bond) { return fc::variant(mvo() - ("chain_code", codename_mvo(chain_code))("token_code", codename_mvo(token_code)) + ("chain_code", codename(chain_code))("token_code", codename(token_code)) ("min_bond", min_bond)("config_timestamp_ms", uint64_t{0})); } action_result depositinle(name account, std::string_view chain_code, std::string_view token_code, uint64_t amount) { return push(OPREG_ACCOUNT, opreg_abi, OPREG_ACCOUNT, "depositinle"_n, mvo() - ("account", account.to_string())("chain_code", codename_mvo(chain_code)) - ("token_code", codename_mvo(token_code))("amount", amount) + ("account", account.to_string())("chain_code", codename(chain_code)) + ("token_code", codename(token_code))("amount", amount) ("actor_chain", ChainKind::CHAIN_KIND_EVM)("actor_address", std::vector{}) ("original_message_id", std::string(64, '0'))); } @@ -385,8 +385,8 @@ class sysio_dispute_tester : public tester { action_result withdrawinle(name account, std::string_view chain_code, std::string_view token_code, uint64_t amount) { return push(OPREG_ACCOUNT, opreg_abi, OPREG_ACCOUNT, "withdrawinle"_n, mvo() - ("account", account.to_string())("chain_code", codename_mvo(chain_code)) - ("token_code", codename_mvo(token_code))("amount", amount)); + ("account", account.to_string())("chain_code", codename(chain_code)) + ("token_code", codename(token_code))("amount", amount)); } action_result flushwtdw(uint32_t up_to_epoch) { @@ -903,7 +903,7 @@ BOOST_FIXTURE_TEST_CASE(chkdispute_unpauses_only_after_last_open_dispute, sysio_ // A second EVM outpost (distinct external_chain_id) so a second (outpost, epoch) dispute can // exist concurrently with the ETH one. BOOST_REQUIRE_EQUAL(success(), push(CHAINS_ACCOUNT, chains_abi, CHAINS_ACCOUNT, "regchain"_n, mvo() - ("kind", ChainKind::CHAIN_KIND_EVM)("code", codename_mvo("BASE")) + ("kind", ChainKind::CHAIN_KIND_EVM)("code", codename("BASE")) ("external_chain_id", 8453)("name", std::string("base-test"))("description", std::string{}) ("outpost", sysio_system::test_support::no_outpost_mvo()))); const uint64_t base_code = fc::slug_name{"BASE"}.value; diff --git a/contracts/tests/sysio.epoch_flushwtdw_tests.cpp b/contracts/tests/sysio.epoch_flushwtdw_tests.cpp index 9f2ae6916b..9c4f0a390c 100644 --- a/contracts/tests/sysio.epoch_flushwtdw_tests.cpp +++ b/contracts/tests/sysio.epoch_flushwtdw_tests.cpp @@ -145,8 +145,8 @@ class sysio_epoch_flushwtdw_tester : public tester { } static fc::slug_name cn(std::string_view s) { return fc::slug_name{s}; } - static fc::mutable_variant_object codename_mvo(std::string_view s) { - return mvo()("value", fc::slug_name{s}.value); + static std::string codename(std::string_view s) { + return std::string{s}; } /// Push an action against any deployed contract. @@ -240,7 +240,7 @@ class sysio_epoch_flushwtdw_tester : public tester { BOOST_REQUIRE_EQUAL(success(), push(CHAINS_ACCOUNT, chains_abi, CHAINS_ACCOUNT, "regchain"_n, mvo() ("kind", ChainKind::CHAIN_KIND_SVM) - ("code", codename_mvo("SOL")) + ("code", codename("SOL")) ("external_chain_id", 1) ("name", std::string("solana-test")) ("description", std::string{}) @@ -248,7 +248,7 @@ class sysio_epoch_flushwtdw_tester : public tester { BOOST_REQUIRE_EQUAL(success(), push(CHAINS_ACCOUNT, chains_abi, CHAINS_ACCOUNT, "regchain"_n, mvo() ("kind", ChainKind::CHAIN_KIND_EVM) - ("code", codename_mvo("ETH")) + ("code", codename("ETH")) ("external_chain_id", 31337) ("name", std::string("ethereum-test")) ("description", std::string{}) @@ -299,8 +299,8 @@ class sysio_epoch_flushwtdw_tester : public tester { std::string_view token_code, uint64_t amount) { return push(OPREG_ACCOUNT, opreg_abi, OPREG_ACCOUNT, "depositinle"_n, mvo() ("account", account.to_string()) - ("chain_code", codename_mvo(chain_code)) - ("token_code", codename_mvo(token_code)) + ("chain_code", codename(chain_code)) + ("token_code", codename(token_code)) ("amount", amount) ("actor_chain", ChainKind::CHAIN_KIND_EVM) ("actor_address", std::vector{}) @@ -311,8 +311,8 @@ class sysio_epoch_flushwtdw_tester : public tester { std::string_view token_code, uint64_t amount) { return push(OPREG_ACCOUNT, opreg_abi, OPREG_ACCOUNT, "withdrawinle"_n, mvo() ("account", account.to_string()) - ("chain_code", codename_mvo(chain_code)) - ("token_code", codename_mvo(token_code)) + ("chain_code", codename(chain_code)) + ("token_code", codename(token_code)) ("amount", amount)); } @@ -346,8 +346,8 @@ class sysio_epoch_flushwtdw_tester : public tester { const auto token_v = cn(token_code).value; const auto& arr = op["balances"].get_array(); for (const auto& b : arr) { - if (b["chain_code"]["value"].as_uint64() == chain_v && - b["token_code"]["value"].as_uint64() == token_v) { + if (b["chain_code"].as().value == chain_v && + b["token_code"].as().value == token_v) { return b["balance"].as_uint64(); } } diff --git a/contracts/tests/sysio.msgch_chain_tests.cpp b/contracts/tests/sysio.msgch_chain_tests.cpp index 2b9934cd98..0b36555ef3 100644 --- a/contracts/tests/sysio.msgch_chain_tests.cpp +++ b/contracts/tests/sysio.msgch_chain_tests.cpp @@ -50,8 +50,8 @@ using mvo = fc::mutable_variant_object; namespace { -inline fc::mutable_variant_object codename_mvo(std::string_view s) { - return mvo()("value", fc::slug_name{s}.value); +inline std::string codename(std::string_view s) { + return std::string{s}; } using fc::slug_name_literals::operator""_s; @@ -420,7 +420,7 @@ class sysio_msgch_chain_tester : public tester { BOOST_REQUIRE_EQUAL(success(), push(CHAINS_ACCOUNT, chains_abi, CHAINS_ACCOUNT, "regchain"_n, mvo() ("kind", kind) - ("code", codename_mvo(code)) + ("code", codename(code)) ("external_chain_id", chain_id) ("name", std::string("outpost-test")) ("description", std::string{}) @@ -798,8 +798,8 @@ class sysio_msgch_chain_tester : public tester { static fc::variant make_chain_min_bond(std::string_view chain_code, std::string_view token_code, uint64_t min_bond) { return fc::variant(mvo() - ("chain_code", codename_mvo(chain_code)) - ("token_code", codename_mvo(token_code)) + ("chain_code", codename(chain_code)) + ("token_code", codename(token_code)) ("min_bond", min_bond) ("config_timestamp_ms", uint64_t{0})); } @@ -845,8 +845,8 @@ class sysio_msgch_chain_tester : public tester { opp::types::ChainKind actor_chain = opp::types::ChainKind::CHAIN_KIND_EVM) { return push(OPREG_ACCOUNT, opreg_abi, OPREG_ACCOUNT, "depositinle"_n, mvo() ("account", account.to_string()) - ("chain_code", codename_mvo(chain_code)) - ("token_code", codename_mvo(token_code)) + ("chain_code", codename(chain_code)) + ("token_code", codename(token_code)) ("amount", amount) ("actor_chain", actor_chain) ("actor_address", std::vector{}) diff --git a/contracts/tests/sysio.msgch_tests.cpp b/contracts/tests/sysio.msgch_tests.cpp index e237de42bd..2ecd13e4cc 100644 --- a/contracts/tests/sysio.msgch_tests.cpp +++ b/contracts/tests/sysio.msgch_tests.cpp @@ -21,8 +21,8 @@ namespace { /// Build a slug_name mvo: `{"value": }` matches the ABI surface for /// `sysio::slug_name` fields. -inline fc::mutable_variant_object codename_mvo(std::string_view s) { - return mvo()("value", fc::slug_name{s}.value); +inline std::string codename(std::string_view s) { + return std::string{s}; } } // anonymous namespace @@ -98,7 +98,7 @@ class sysio_msgch_tester : public tester { uint32_t chain_id) { base_tester::push_action(CHAINS_ACCOUNT, "regchain"_n, CHAINS_ACCOUNT, mvo() ("kind", kind) - ("code", codename_mvo(code)) + ("code", codename(code)) ("external_chain_id", chain_id) ("name", std::string("outpost")) ("description", std::string{}) @@ -268,7 +268,7 @@ class sysio_msgch_envlog_tester : public tester { BOOST_REQUIRE_EQUAL(success(), push_action(CHAINS_ACCOUNT, CHAINS_ACCOUNT, "regchain"_n, mvo() ("kind", kind) - ("code", codename_mvo(code)) + ("code", codename(code)) ("external_chain_id", chain_id) ("name", std::string("outpost")) ("description", std::string{}) diff --git a/contracts/tests/sysio.opreg_tests.cpp b/contracts/tests/sysio.opreg_tests.cpp index 6901709aa9..fcfa7f4bac 100644 --- a/contracts/tests/sysio.opreg_tests.cpp +++ b/contracts/tests/sysio.opreg_tests.cpp @@ -251,10 +251,10 @@ class sysio_opreg_tester : public tester { static fc::slug_name cn(std::string_view s) { return fc::slug_name{s}; } - /// Build a slug_name mvo suitable for an action argument: - /// `{"value": }` matches the ABI surface for slug_name fields. - static fc::mutable_variant_object codename_mvo(std::string_view s) { - return mvo()("value", fc::slug_name{s}.value); + /// A slug_name action argument: the ABI builtin takes the decoded string + /// spelling, so the codename is passed through as-is. + static std::string codename(std::string_view s) { + return std::string{s}; } // ── Action helpers ── @@ -297,8 +297,8 @@ class sysio_opreg_tester : public tester { std::string_view token_code, uint64_t min_bond) { return fc::variant(mvo() - ("chain_code", codename_mvo(chain_code)) - ("token_code", codename_mvo(token_code)) + ("chain_code", codename(chain_code)) + ("token_code", codename(token_code)) ("min_bond", min_bond) ("config_timestamp_ms", uint64_t{0})); } @@ -406,8 +406,8 @@ class sysio_opreg_tester : public tester { const std::string& original_message_id_hex = std::string(64, '0')) { return push_opreg_action(OPREG_ACCOUNT, "depositinle"_n, mvo() ("account", account) - ("chain_code", codename_mvo(chain_code)) - ("token_code", codename_mvo(token_code)) + ("chain_code", codename(chain_code)) + ("token_code", codename(token_code)) ("amount", amount) ("actor_chain", actor_chain) ("actor_address", actor_address) @@ -420,8 +420,8 @@ class sysio_opreg_tester : public tester { uint64_t amount) { return push_opreg_action(OPREG_ACCOUNT, "withdrawinle"_n, mvo() ("account", account) - ("chain_code", codename_mvo(chain_code)) - ("token_code", codename_mvo(token_code)) + ("chain_code", codename(chain_code)) + ("token_code", codename(token_code)) ("amount", amount)); } @@ -471,8 +471,8 @@ class sysio_opreg_tester : public tester { uint64_t amount) { return push_opreg_action(signer, "releaselock"_n, mvo() ("account", account) - ("chain_code", codename_mvo(chain_code)) - ("token_code", codename_mvo(token_code)) + ("chain_code", codename(chain_code)) + ("token_code", codename(token_code)) ("amount", amount)); } @@ -941,8 +941,8 @@ BOOST_FIXTURE_TEST_CASE(deposit_credits_balance_row, sysio_opreg_tester) { try { auto op = get_operator("uwrit.alice"_n); auto balances = op["balances"].get_array(); BOOST_REQUIRE_EQUAL(1, balances.size()); - BOOST_REQUIRE_EQUAL(cn("ETH").value, balances[0]["chain_code"]["value"].as_uint64()); - BOOST_REQUIRE_EQUAL(cn("ETH").value, balances[0]["token_code"]["value"].as_uint64()); + BOOST_REQUIRE_EQUAL(cn("ETH").value, balances[0]["chain_code"].as().value); + BOOST_REQUIRE_EQUAL(cn("ETH").value, balances[0]["token_code"].as().value); BOOST_REQUIRE_EQUAL(1'000'000, balances[0]["balance"].as_uint64()); } FC_LOG_AND_RETHROW() } @@ -1035,8 +1035,8 @@ BOOST_FIXTURE_TEST_CASE(deposit_custodies_and_credits_wire_units, sysio_opreg_te auto op = get_operator(OPERATOR); auto balances = op["balances"].get_array(); BOOST_REQUIRE_EQUAL(1u, balances.size()); - BOOST_REQUIRE_EQUAL(cn(kWireCodename).value, balances[0]["chain_code"]["value"].as_uint64()); - BOOST_REQUIRE_EQUAL(cn(kWireCodename).value, balances[0]["token_code"]["value"].as_uint64()); + BOOST_REQUIRE_EQUAL(cn(kWireCodename).value, balances[0]["chain_code"].as().value); + BOOST_REQUIRE_EQUAL(cn(kWireCodename).value, balances[0]["token_code"].as().value); BOOST_REQUIRE_EQUAL(DEPOSIT, balances[0]["balance"].as_uint64()); BOOST_REQUIRE(OperatorStatus::OPERATOR_STATUS_ACTIVE == op["status"].as()); } FC_LOG_AND_RETHROW() } diff --git a/contracts/tests/sysio.reserv_tests.cpp b/contracts/tests/sysio.reserv_tests.cpp index bc81d3ded8..18b02e69fb 100644 --- a/contracts/tests/sysio.reserv_tests.cpp +++ b/contracts/tests/sysio.reserv_tests.cpp @@ -135,7 +135,7 @@ class sysio_reserve_tester : public tester { uint32_t external_chain_id) { return push_to(CHAINS_ACCOUNT, chains_abi_ser, CHAINS_ACCOUNT, "regchain"_n, mvo() ("kind", kind) - ("code", codename_mvo(code)) + ("code", codename(code)) ("external_chain_id", external_chain_id) ("name", std::string("outpost")) ("description", std::string{}) @@ -174,13 +174,13 @@ class sysio_reserve_tester : public tester { std::string_view to_chain, std::string_view to_token, std::string_view to_reserve) { auto trace = tester::push_action(RESERVE_ACCOUNT, "swapquote"_n, RESERVE_ACCOUNT, mvo() - ("from_chain_code", codename_mvo(from_chain)) - ("from_token_code", codename_mvo(from_token)) - ("from_reserve_code", codename_mvo(from_reserve)) + ("from_chain_code", codename(from_chain)) + ("from_token_code", codename(from_token)) + ("from_reserve_code", codename(from_reserve)) ("from_amount", from_amount) - ("to_chain_code", codename_mvo(to_chain)) - ("to_token_code", codename_mvo(to_token)) - ("to_reserve_code", codename_mvo(to_reserve))); + ("to_chain_code", codename(to_chain)) + ("to_token_code", codename(to_token)) + ("to_reserve_code", codename(to_reserve))); BOOST_REQUIRE(trace && !trace->action_traces.empty()); return fc::raw::unpack(trace->action_traces[0].return_value); } @@ -188,8 +188,8 @@ class sysio_reserve_tester : public tester { // ── SlugName helpers (v6) ── static fc::slug_name cn(std::string_view s) { return fc::slug_name{s}; } - static fc::mutable_variant_object codename_mvo(std::string_view s) { - return mvo()("value", fc::slug_name{s}.value); + static std::string codename(std::string_view s) { + return std::string{s}; } /// `regreserve` is the v6 bootstrap-window action for inserting a reserve @@ -208,9 +208,9 @@ class sysio_reserve_tester : public tester { const std::string& description = "", uint32_t source_token_precision = 9) { return push_action(RESERVE_ACCOUNT, "regreserve"_n, mvo() - ("chain_code", codename_mvo(chain_code)) - ("token_code", codename_mvo(token_code)) - ("reserve_code", codename_mvo(reserve_code)) + ("chain_code", codename(chain_code)) + ("token_code", codename(token_code)) + ("reserve_code", codename(reserve_code)) ("name", name_str) ("description", description) ("initial_chain_amount", initial_chain_amount) @@ -298,9 +298,9 @@ class sysio_reserve_tester : public tester { auto row = abi_ser.binary_to_variant( "reserve_row", raw, abi_serializer::create_yield_function(abi_serializer_max_time)); - if (row["chain_code"]["value"].as_uint64() == target_chain && - row["token_code"]["value"].as_uint64() == target_token && - row["reserve_code"]["value"].as_uint64() == target_reserve) { + if (row["chain_code"].as().value == target_chain && + row["token_code"].as().value == target_token && + row["reserve_code"].as().value == target_reserve) { return row; } } catch (...) { @@ -481,9 +481,9 @@ BOOST_FIXTURE_TEST_CASE(regreserve_bounds_metadata, sysio_reserve_tester) { try BOOST_FIXTURE_TEST_CASE(oncrtreserve_requires_msgch_auth, sysio_reserve_tester) { try { BOOST_REQUIRE(push_action(RESERVE_ACCOUNT, "oncrtreserve"_n, mvo() - ("chain_code", codename_mvo("ETH")) - ("token_code", codename_mvo("ETH")) - ("reserve_code", codename_mvo("USERRES")) + ("chain_code", codename("ETH")) + ("token_code", codename("ETH")) + ("reserve_code", codename("USERRES")) ("name", "user reserve") ("description", "") ("external_token_amount", 1000) @@ -504,9 +504,9 @@ BOOST_FIXTURE_TEST_CASE(oncrtreserve_unregistered_chain_soft_skips_before_queueo deploy_msgch(); BOOST_REQUIRE_EQUAL(success(), push_action(MSGCH_ACCOUNT, "oncrtreserve"_n, mvo() - ("chain_code", codename_mvo("NOCHAIN")) - ("token_code", codename_mvo("ETH")) - ("reserve_code", codename_mvo("USERRES")) + ("chain_code", codename("NOCHAIN")) + ("token_code", codename("ETH")) + ("reserve_code", codename("USERRES")) ("name", "user reserve") ("description", "") ("external_token_amount", 1000) @@ -527,9 +527,9 @@ BOOST_FIXTURE_TEST_CASE(oncrtreserve_unlinked_creator_is_cancelled, sysio_reserv // rejected by inserting a CANCELLED row (idempotency + audit) and // queueing RESERVE_CREATE_CANCELLED back. Never throws. BOOST_REQUIRE_EQUAL(success(), push_action(MSGCH_ACCOUNT, "oncrtreserve"_n, mvo() - ("chain_code", codename_mvo("ETH")) - ("token_code", codename_mvo("ETH")) - ("reserve_code", codename_mvo("USERRES")) + ("chain_code", codename("ETH")) + ("token_code", codename("ETH")) + ("reserve_code", codename("USERRES")) ("name", "user reserve") ("description", "") ("external_token_amount", 1000) @@ -564,9 +564,9 @@ BOOST_FIXTURE_TEST_CASE(oncrtreserve_creator_chain_kind_mismatch_is_cancelled, const std::vector creator_address(20, '\x01'); BOOST_REQUIRE_EQUAL(success(), push_action(MSGCH_ACCOUNT, "oncrtreserve"_n, mvo() - ("chain_code", codename_mvo("ETH")) - ("token_code", codename_mvo("ETH")) - ("reserve_code", codename_mvo("USERRES")) + ("chain_code", codename("ETH")) + ("token_code", codename("ETH")) + ("reserve_code", codename("USERRES")) ("name", "mismatched creator") ("description", "") ("external_token_amount", 1000) @@ -606,9 +606,9 @@ BOOST_FIXTURE_TEST_CASE(oncnclrsv_requires_registry_creator_kind, const std::vector creator_address(20, '\x01'); BOOST_REQUIRE_EQUAL(success(), push_action(MSGCH_ACCOUNT, "oncrtreserve"_n, mvo() - ("chain_code", codename_mvo("ETH")) - ("token_code", codename_mvo("ETH")) - ("reserve_code", codename_mvo("CANCEL")) + ("chain_code", codename("ETH")) + ("token_code", codename("ETH")) + ("reserve_code", codename("CANCEL")) ("name", "cancel kind guard") ("description", "") ("external_token_amount", 1000) @@ -625,9 +625,9 @@ BOOST_FIXTURE_TEST_CASE(oncnclrsv_requires_registry_creator_kind, BOOST_REQUIRE_EQUAL("RESERVE_STATUS_PENDING", pending["status"].as_string()); BOOST_REQUIRE_EQUAL(success(), push_action(MSGCH_ACCOUNT, "oncnclrsv"_n, mvo() - ("chain_code", codename_mvo("ETH")) - ("token_code", codename_mvo("ETH")) - ("reserve_code", codename_mvo("CANCEL")) + ("chain_code", codename("ETH")) + ("token_code", codename("ETH")) + ("reserve_code", codename("CANCEL")) ("creator_chain_kind", ChainKind::CHAIN_KIND_SVM) ("creator_chain_addr", creator_address))); @@ -637,9 +637,9 @@ BOOST_FIXTURE_TEST_CASE(oncnclrsv_requires_registry_creator_kind, "attestations"_n, 1).empty()); BOOST_REQUIRE_EQUAL(success(), push_action(MSGCH_ACCOUNT, "oncnclrsv"_n, mvo() - ("chain_code", codename_mvo("ETH")) - ("token_code", codename_mvo("ETH")) - ("reserve_code", codename_mvo("CANCEL")) + ("chain_code", codename("ETH")) + ("token_code", codename("ETH")) + ("reserve_code", codename("CANCEL")) ("creator_chain_kind", ChainKind::CHAIN_KIND_EVM) ("creator_chain_addr", creator_address))); @@ -660,9 +660,9 @@ BOOST_FIXTURE_TEST_CASE(oncrtreserve_invalid_creator_address_is_cancelled, const auto creator_key = em_pubkey_bytes(creator_pub); BOOST_REQUIRE_EQUAL(success(), push_action(MSGCH_ACCOUNT, "oncrtreserve"_n, mvo() - ("chain_code", codename_mvo("ETH")) - ("token_code", codename_mvo("ETH")) - ("reserve_code", codename_mvo("BADADDR")) + ("chain_code", codename("ETH")) + ("token_code", codename("ETH")) + ("reserve_code", codename("BADADDR")) ("name", "malformed creator address") ("description", "") ("external_token_amount", 1000) @@ -687,9 +687,9 @@ BOOST_FIXTURE_TEST_CASE(oncrtreserve_invalid_creator_address_is_cancelled, BOOST_FIXTURE_TEST_CASE(oncrtreserve_cancelled_relay_does_not_double_refund, sysio_reserve_tester) { try { auto crt = [&]() { return push_action(MSGCH_ACCOUNT, "oncrtreserve"_n, mvo() - ("chain_code", codename_mvo("ETH")) - ("token_code", codename_mvo("ETH")) - ("reserve_code", codename_mvo("USERRES")) + ("chain_code", codename("ETH")) + ("token_code", codename("ETH")) + ("reserve_code", codename("USERRES")) ("name", "user reserve") ("description", "") ("external_token_amount", 1000) @@ -729,9 +729,9 @@ BOOST_FIXTURE_TEST_CASE(oncrtreserve_cancelled_is_reclaimable_by_linked_creator, // 1) An UNLINKED creator squats the triple → CANCELLED. BOOST_REQUIRE_EQUAL(success(), push_action(MSGCH_ACCOUNT, "oncrtreserve"_n, mvo() - ("chain_code", codename_mvo("ETH")) - ("token_code", codename_mvo("ETH")) - ("reserve_code", codename_mvo("USERRES")) + ("chain_code", codename("ETH")) + ("token_code", codename("ETH")) + ("reserve_code", codename("USERRES")) ("name", "squatter") ("description", "squat") ("external_token_amount", 1000) @@ -756,9 +756,9 @@ BOOST_FIXTURE_TEST_CASE(oncrtreserve_cancelled_is_reclaimable_by_linked_creator, recordlink("alice"_n, ChainKind::CHAIN_KIND_EVM, creator_pub)); BOOST_REQUIRE_EQUAL(success(), push_action(MSGCH_ACCOUNT, "oncrtreserve"_n, mvo() - ("chain_code", codename_mvo("ETH")) - ("token_code", codename_mvo("ETH")) - ("reserve_code", codename_mvo("USERRES")) + ("chain_code", codename("ETH")) + ("token_code", codename("ETH")) + ("reserve_code", codename("USERRES")) ("name", "rightful owner") ("description", "reclaimed") ("external_token_amount", 5000) @@ -802,9 +802,9 @@ BOOST_FIXTURE_TEST_CASE(oncrtreserve_invalid_amount_is_cancelled, sysio_reserve_ // invalid inbound amount). The link is valid, so the amount alone forces the // cancel/refund — proving the amount path no longer drops silently. BOOST_REQUIRE_EQUAL(success(), push_action(MSGCH_ACCOUNT, "oncrtreserve"_n, mvo() - ("chain_code", codename_mvo("ETH")) - ("token_code", codename_mvo("ETH")) - ("reserve_code", codename_mvo("USERRES")) + ("chain_code", codename("ETH")) + ("token_code", codename("ETH")) + ("reserve_code", codename("USERRES")) ("name", "invalid amount") ("description", "") ("external_token_amount", 0) @@ -849,9 +849,9 @@ BOOST_FIXTURE_TEST_CASE(oncrtreserve_oversized_metadata_is_cancelled, sysio_rese const std::string& name, const std::string& description) { return push_action(MSGCH_ACCOUNT, "oncrtreserve"_n, mvo() - ("chain_code", codename_mvo("ETH")) - ("token_code", codename_mvo("ETH")) - ("reserve_code", codename_mvo(reserve_code)) + ("chain_code", codename("ETH")) + ("token_code", codename("ETH")) + ("reserve_code", codename(reserve_code)) ("name", name) ("description", description) ("external_token_amount", 1000) @@ -903,9 +903,9 @@ BOOST_FIXTURE_TEST_CASE(matchreserve_rejects_unknown_reserve, sysio_reserve_test BOOST_REQUIRE_EQUAL( error("assertion failure with message: matchreserve: reserve not found"), push_action("alice"_n, "matchreserve"_n, mvo() - ("chain_code", codename_mvo("ETH")) - ("token_code", codename_mvo("ETH")) - ("reserve_code", codename_mvo("NOPE")) + ("chain_code", codename("ETH")) + ("token_code", codename("ETH")) + ("reserve_code", codename("NOPE")) ("matcher", "alice") ("wire_amount", 100))); } FC_LOG_AND_RETHROW() } @@ -917,9 +917,9 @@ BOOST_FIXTURE_TEST_CASE(matchreserve_rejects_non_pending, sysio_reserve_tester) BOOST_REQUIRE_EQUAL( error("assertion failure with message: matchreserve: reserve is not PENDING"), push_action("alice"_n, "matchreserve"_n, mvo() - ("chain_code", codename_mvo("ETH")) - ("token_code", codename_mvo("ETH")) - ("reserve_code", codename_mvo("PRIMARY")) + ("chain_code", codename("ETH")) + ("token_code", codename("ETH")) + ("reserve_code", codename("PRIMARY")) ("matcher", "alice") ("wire_amount", 1000))); } FC_LOG_AND_RETHROW() } @@ -928,13 +928,13 @@ BOOST_FIXTURE_TEST_CASE(matchreserve_rejects_non_pending, sysio_reserve_tester) BOOST_FIXTURE_TEST_CASE(applyswap_requires_uwrit_auth, sysio_reserve_tester) { try { BOOST_REQUIRE(push_action("alice"_n, "applyswap"_n, mvo() - ("src_chain_code", codename_mvo("ETH")) - ("src_token_code", codename_mvo("ETH")) - ("src_reserve_code", codename_mvo("PRIMARY")) + ("src_chain_code", codename("ETH")) + ("src_token_code", codename("ETH")) + ("src_reserve_code", codename("PRIMARY")) ("src_amount", 100) - ("dst_chain_code", codename_mvo("SOLANA")) - ("dst_token_code", codename_mvo("SOL")) - ("dst_reserve_code", codename_mvo("PRIMARY")) + ("dst_chain_code", codename("SOLANA")) + ("dst_token_code", codename("SOL")) + ("dst_reserve_code", codename("PRIMARY")) ("dst_amount", 50) ("underwriter", "underwriter1") ).find("missing authority of sysio.uwrit") != std::string::npos); @@ -948,13 +948,13 @@ BOOST_FIXTURE_TEST_CASE(applyswap_applies_four_legs, sysio_reserve_tester) { try // w = cp_output(1000, 1000, 100) = 1000*100 / (1000+100) = 90 (floor). BOOST_REQUIRE_EQUAL(success(), push_action(UWRIT_ACCOUNT, "applyswap"_n, mvo() - ("src_chain_code", codename_mvo("ETH")) - ("src_token_code", codename_mvo("ETH")) - ("src_reserve_code", codename_mvo("PRIMARY")) + ("src_chain_code", codename("ETH")) + ("src_token_code", codename("ETH")) + ("src_reserve_code", codename("PRIMARY")) ("src_amount", 100) - ("dst_chain_code", codename_mvo("SOLANA")) - ("dst_token_code", codename_mvo("SOL")) - ("dst_reserve_code", codename_mvo("PRIMARY")) + ("dst_chain_code", codename("SOLANA")) + ("dst_token_code", codename("SOL")) + ("dst_reserve_code", codename("PRIMARY")) ("dst_amount", 50) ("underwriter", "underwriter1"))); @@ -984,13 +984,13 @@ BOOST_FIXTURE_TEST_CASE(applyswap_rejects_debit_above_curve_output, sysio_reserv constexpr int64_t CURVE_OUT = 82; auto apply = [&](int64_t dst_amount) { return push_action(UWRIT_ACCOUNT, "applyswap"_n, mvo() - ("src_chain_code", codename_mvo("ETH")) - ("src_token_code", codename_mvo("ETH")) - ("src_reserve_code", codename_mvo("PRIMARY")) + ("src_chain_code", codename("ETH")) + ("src_token_code", codename("ETH")) + ("src_reserve_code", codename("PRIMARY")) ("src_amount", 100) - ("dst_chain_code", codename_mvo("SOLANA")) - ("dst_token_code", codename_mvo("SOL")) - ("dst_reserve_code", codename_mvo("PRIMARY")) + ("dst_chain_code", codename("SOLANA")) + ("dst_token_code", codename("SOL")) + ("dst_reserve_code", codename("PRIMARY")) ("dst_amount", dst_amount) ("underwriter", "underwriter1")); }; @@ -1022,13 +1022,13 @@ BOOST_FIXTURE_TEST_CASE(applyswap_charges_fee_and_routes_50_50, sysio_reserve_te // fee = 999'000'999 * 10 / 10000 = 999'000 ; underwriter = reward = 499'500 ; // net = 999'000'999 - 999'000 = 998'001'999. BOOST_REQUIRE_EQUAL(success(), push_action(UWRIT_ACCOUNT, "applyswap"_n, mvo() - ("src_chain_code", codename_mvo("ETH")) - ("src_token_code", codename_mvo("ETH")) - ("src_reserve_code", codename_mvo("PRIMARY")) + ("src_chain_code", codename("ETH")) + ("src_token_code", codename("ETH")) + ("src_reserve_code", codename("PRIMARY")) ("src_amount", 1'000'000'000ULL) - ("dst_chain_code", codename_mvo("SOLANA")) - ("dst_token_code", codename_mvo("SOL")) - ("dst_reserve_code", codename_mvo("PRIMARY")) + ("dst_chain_code", codename("SOLANA")) + ("dst_token_code", codename("SOL")) + ("dst_reserve_code", codename("PRIMARY")) ("dst_amount", 100'000'000ULL) ("underwriter", "underwriter1"))); @@ -1081,13 +1081,13 @@ BOOST_FIXTURE_TEST_CASE(setconfig_emissions_share_routes_pool_to_treasury, sysio // Same swap as the 50/50 test: fee 999'000, underwriter 499'500, pool 499'500. BOOST_REQUIRE_EQUAL(success(), push_action(UWRIT_ACCOUNT, "applyswap"_n, mvo() - ("src_chain_code", codename_mvo("ETH")) - ("src_token_code", codename_mvo("ETH")) - ("src_reserve_code", codename_mvo("PRIMARY")) + ("src_chain_code", codename("ETH")) + ("src_token_code", codename("ETH")) + ("src_reserve_code", codename("PRIMARY")) ("src_amount", 1'000'000'000ULL) - ("dst_chain_code", codename_mvo("SOLANA")) - ("dst_token_code", codename_mvo("SOL")) - ("dst_reserve_code", codename_mvo("PRIMARY")) + ("dst_chain_code", codename("SOLANA")) + ("dst_token_code", codename("SOL")) + ("dst_reserve_code", codename("PRIMARY")) ("dst_amount", 100'000'000ULL) ("underwriter", "underwriter1"))); @@ -1109,16 +1109,16 @@ BOOST_FIXTURE_TEST_CASE(setrsvfee_guards_owner_status_and_bounds, sysio_reserve_ BOOST_REQUIRE_EQUAL( error("assertion failure with message: setrsvfee: reserve has no owner"), push_action(RESERVE_ACCOUNT, "setrsvfee"_n, mvo() - ("chain_code", codename_mvo("ETH"))("token_code", codename_mvo("ETH")) - ("reserve_code", codename_mvo("PRIMARY"))("owner_fee_bps", 100))); + ("chain_code", codename("ETH"))("token_code", codename("ETH")) + ("reserve_code", codename("PRIMARY"))("owner_fee_bps", 100))); // An OWNED reserve: only the owner may set the fee. BOOST_REQUIRE_EQUAL(success(), regreserve("SOLANA", "SOL", "PRIMARY", 1000, 1000, 5000, false, "alice"_n)); auto setFee = [&](name signer, uint32_t bps) { return push_action(signer, "setrsvfee"_n, mvo() - ("chain_code", codename_mvo("SOLANA"))("token_code", codename_mvo("SOL")) - ("reserve_code", codename_mvo("PRIMARY"))("owner_fee_bps", bps)); + ("chain_code", codename("SOLANA"))("token_code", codename("SOL")) + ("reserve_code", codename("PRIMARY"))("owner_fee_bps", bps)); }; BOOST_REQUIRE(setFee(UNDERWRITER_ACCOUNT, 100).find("missing authority of alice") != std::string::npos); @@ -1137,8 +1137,8 @@ BOOST_FIXTURE_TEST_CASE(setrsvfee_guards_owner_status_and_bounds, sysio_reserve_ BOOST_REQUIRE_EQUAL( error("assertion failure with message: setrsvfee: reserve not found"), push_action("alice"_n, "setrsvfee"_n, mvo() - ("chain_code", codename_mvo("ETH"))("token_code", codename_mvo("NOPE")) - ("reserve_code", codename_mvo("PRIMARY"))("owner_fee_bps", 10))); + ("chain_code", codename("ETH"))("token_code", codename("NOPE")) + ("reserve_code", codename("PRIMARY"))("owner_fee_bps", 10))); } FC_LOG_AND_RETHROW() } BOOST_FIXTURE_TEST_CASE(applyswap_charges_both_reserve_owner_fees, sysio_reserve_tester) { try { @@ -1153,24 +1153,24 @@ BOOST_FIXTURE_TEST_CASE(applyswap_charges_both_reserve_owner_fees, sysio_reserve // src 100 bps (1%), dst 200 bps (2%) on the same WIRE leg. BOOST_REQUIRE_EQUAL(success(), push_action("alice"_n, "setrsvfee"_n, mvo() - ("chain_code", codename_mvo("ETH"))("token_code", codename_mvo("ETH")) - ("reserve_code", codename_mvo("PRIMARY"))("owner_fee_bps", 100))); + ("chain_code", codename("ETH"))("token_code", codename("ETH")) + ("reserve_code", codename("PRIMARY"))("owner_fee_bps", 100))); BOOST_REQUIRE_EQUAL(success(), push_action(UNDERWRITER_ACCOUNT, "setrsvfee"_n, mvo() - ("chain_code", codename_mvo("SOLANA"))("token_code", codename_mvo("SOL")) - ("reserve_code", codename_mvo("PRIMARY"))("owner_fee_bps", 200))); + ("chain_code", codename("SOLANA"))("token_code", codename("SOL")) + ("reserve_code", codename("PRIMARY"))("owner_fee_bps", 200))); const int64_t resv_before = wire_balance(RESERVE_ACCOUNT); // w_gross = 999'000'999. network 10bps = 999'000; src 1% = 9'990'009; // dst 2% = 19'980'019; net = w_gross - (999'000 + 9'990'009 + 19'980'019). BOOST_REQUIRE_EQUAL(success(), push_action(UWRIT_ACCOUNT, "applyswap"_n, mvo() - ("src_chain_code", codename_mvo("ETH")) - ("src_token_code", codename_mvo("ETH")) - ("src_reserve_code", codename_mvo("PRIMARY")) + ("src_chain_code", codename("ETH")) + ("src_token_code", codename("ETH")) + ("src_reserve_code", codename("PRIMARY")) ("src_amount", 1'000'000'000ULL) - ("dst_chain_code", codename_mvo("SOLANA")) - ("dst_token_code", codename_mvo("SOL")) - ("dst_reserve_code", codename_mvo("PRIMARY")) + ("dst_chain_code", codename("SOLANA")) + ("dst_token_code", codename("SOL")) + ("dst_reserve_code", codename("PRIMARY")) ("dst_amount", 100'000'000ULL) ("underwriter", "underwriter1"))); @@ -1198,13 +1198,13 @@ BOOST_FIXTURE_TEST_CASE(single_reserve_paths_charge_only_their_own_side, sysio_r regreserve("ETH", "ETH", "PRIMARY", 1'000'000'000'000ULL, 1'000'000'000'000ULL, 5000, false, "alice"_n)); BOOST_REQUIRE_EQUAL(success(), push_action("alice"_n, "setrsvfee"_n, mvo() - ("chain_code", codename_mvo("ETH"))("token_code", codename_mvo("ETH")) - ("reserve_code", codename_mvo("PRIMARY"))("owner_fee_bps", 100))); + ("chain_code", codename("ETH"))("token_code", codename("ETH")) + ("reserve_code", codename("PRIMARY"))("owner_fee_bps", 100))); BOOST_REQUIRE_EQUAL(success(), push_action(UWRIT_ACCOUNT, "paywire"_n, mvo() - ("src_chain_code", codename_mvo("ETH")) - ("src_token_code", codename_mvo("ETH")) - ("src_reserve_code", codename_mvo("PRIMARY")) + ("src_chain_code", codename("ETH")) + ("src_token_code", codename("ETH")) + ("src_reserve_code", codename("PRIMARY")) ("src_amount", 1'000'000'000ULL) ("recipient", "alice") ("wire_out", 100'000'000ULL) @@ -1219,12 +1219,12 @@ BOOST_FIXTURE_TEST_CASE(single_reserve_paths_charge_only_their_own_side, sysio_r regreserve("SOLANA", "SOL", "PRIMARY", 1'000'000'000'000ULL, 1'000'000'000'000ULL, 5000, false, UNDERWRITER_ACCOUNT)); BOOST_REQUIRE_EQUAL(success(), push_action(UNDERWRITER_ACCOUNT, "setrsvfee"_n, mvo() - ("chain_code", codename_mvo("SOLANA"))("token_code", codename_mvo("SOL")) - ("reserve_code", codename_mvo("PRIMARY"))("owner_fee_bps", 200))); + ("chain_code", codename("SOLANA"))("token_code", codename("SOL")) + ("reserve_code", codename("PRIMARY"))("owner_fee_bps", 200))); BOOST_REQUIRE_EQUAL(success(), push_action(UWRIT_ACCOUNT, "applyfromwire"_n, mvo() - ("dst_chain_code", codename_mvo("SOLANA")) - ("dst_token_code", codename_mvo("SOL")) - ("dst_reserve_code", codename_mvo("PRIMARY")) + ("dst_chain_code", codename("SOLANA")) + ("dst_token_code", codename("SOL")) + ("dst_reserve_code", codename("PRIMARY")) ("wire_in", 1'000'000'000ULL) ("dst_amount", 100'000'000ULL) ("underwriter", "underwriter1"))); @@ -1243,20 +1243,20 @@ BOOST_FIXTURE_TEST_CASE(claimrsvfee_pays_owner_and_guards_auth, sysio_reserve_te auto claim = [&](name signer) { return push_action(signer, "claimrsvfee"_n, mvo() - ("chain_code", codename_mvo("ETH"))("token_code", codename_mvo("ETH")) - ("reserve_code", codename_mvo("PRIMARY"))); + ("chain_code", codename("ETH"))("token_code", codename("ETH")) + ("reserve_code", codename("PRIMARY"))); }; // Nothing earned yet. BOOST_REQUIRE_EQUAL( error("assertion failure with message: claimrsvfee: no unclaimed balance"), claim("alice"_n)); BOOST_REQUIRE_EQUAL(success(), push_action("alice"_n, "setrsvfee"_n, mvo() - ("chain_code", codename_mvo("ETH"))("token_code", codename_mvo("ETH")) - ("reserve_code", codename_mvo("PRIMARY"))("owner_fee_bps", 100))); + ("chain_code", codename("ETH"))("token_code", codename("ETH")) + ("reserve_code", codename("PRIMARY"))("owner_fee_bps", 100))); BOOST_REQUIRE_EQUAL(success(), push_action(UWRIT_ACCOUNT, "paywire"_n, mvo() - ("src_chain_code", codename_mvo("ETH")) - ("src_token_code", codename_mvo("ETH")) - ("src_reserve_code", codename_mvo("PRIMARY")) + ("src_chain_code", codename("ETH")) + ("src_token_code", codename("ETH")) + ("src_reserve_code", codename("PRIMARY")) ("src_amount", 1'000'000'000ULL) ("recipient", UNDERWRITER_ACCOUNT) ("wire_out", 100'000'000ULL) @@ -1316,11 +1316,11 @@ BOOST_FIXTURE_TEST_CASE(swapquote_prices_the_reserve_owner_fees, sysio_reserve_t BOOST_CHECK_EQUAL(before, expected(0, 0)); BOOST_REQUIRE_EQUAL(success(), push_action("alice"_n, "setrsvfee"_n, mvo() - ("chain_code", codename_mvo("ETH"))("token_code", codename_mvo("ETH")) - ("reserve_code", codename_mvo("PRIMARY"))("owner_fee_bps", OWNER_FEE))); + ("chain_code", codename("ETH"))("token_code", codename("ETH")) + ("reserve_code", codename("PRIMARY"))("owner_fee_bps", OWNER_FEE))); BOOST_REQUIRE_EQUAL(success(), push_action(UNDERWRITER_ACCOUNT, "setrsvfee"_n, mvo() - ("chain_code", codename_mvo("SOLANA"))("token_code", codename_mvo("SOL")) - ("reserve_code", codename_mvo("PRIMARY"))("owner_fee_bps", OWNER_FEE))); + ("chain_code", codename("SOLANA"))("token_code", codename("SOL")) + ("reserve_code", codename("PRIMARY"))("owner_fee_bps", OWNER_FEE))); produce_block(); // Both owner fees are now priced in, off the same gross WIRE leg. @@ -1333,8 +1333,8 @@ BOOST_FIXTURE_TEST_CASE(swapquote_prices_the_reserve_owner_fees, sysio_reserve_t // by only that side's share, which a quote summing the wrong reserve's rate // (or double-counting one) would not reproduce. BOOST_REQUIRE_EQUAL(success(), push_action(UNDERWRITER_ACCOUNT, "setrsvfee"_n, mvo() - ("chain_code", codename_mvo("SOLANA"))("token_code", codename_mvo("SOL")) - ("reserve_code", codename_mvo("PRIMARY"))("owner_fee_bps", 0))); + ("chain_code", codename("SOLANA"))("token_code", codename("SOL")) + ("reserve_code", codename("PRIMARY"))("owner_fee_bps", 0))); produce_block(); const uint64_t source_only = swapquote_value("ETH", "ETH", "PRIMARY", FROM, "SOLANA", "SOL", "PRIMARY"); @@ -1363,13 +1363,13 @@ BOOST_FIXTURE_TEST_CASE(claimuwfee_pays_accrual_and_zeroes_balance, sysio_reserv BOOST_REQUIRE_EQUAL(success(), regreserve("SOLANA", "SOL", "PRIMARY", 1'000'000'000'000ULL, 1'000'000'000'000ULL)); BOOST_REQUIRE_EQUAL(success(), push_action(UWRIT_ACCOUNT, "applyswap"_n, mvo() - ("src_chain_code", codename_mvo("ETH")) - ("src_token_code", codename_mvo("ETH")) - ("src_reserve_code", codename_mvo("PRIMARY")) + ("src_chain_code", codename("ETH")) + ("src_token_code", codename("ETH")) + ("src_reserve_code", codename("PRIMARY")) ("src_amount", 1'000'000'000ULL) - ("dst_chain_code", codename_mvo("SOLANA")) - ("dst_token_code", codename_mvo("SOL")) - ("dst_reserve_code", codename_mvo("PRIMARY")) + ("dst_chain_code", codename("SOLANA")) + ("dst_token_code", codename("SOL")) + ("dst_reserve_code", codename("PRIMARY")) ("dst_amount", 100'000'000ULL) ("underwriter", "underwriter1"))); @@ -1421,13 +1421,13 @@ BOOST_FIXTURE_TEST_CASE(applyswap_accrues_per_underwriter_and_accumulates, sysio auto swap_won_by = [&](const char* underwriter) { return push_action(UWRIT_ACCOUNT, "applyswap"_n, mvo() - ("src_chain_code", codename_mvo("ETH")) - ("src_token_code", codename_mvo("ETH")) - ("src_reserve_code", codename_mvo("PRIMARY")) + ("src_chain_code", codename("ETH")) + ("src_token_code", codename("ETH")) + ("src_reserve_code", codename("PRIMARY")) ("src_amount", 1'000'000'000ULL) - ("dst_chain_code", codename_mvo("SOLANA")) - ("dst_token_code", codename_mvo("SOL")) - ("dst_reserve_code", codename_mvo("PRIMARY")) + ("dst_chain_code", codename("SOLANA")) + ("dst_token_code", codename("SOL")) + ("dst_reserve_code", codename("PRIMARY")) ("dst_amount", 100'000'000ULL) ("underwriter", underwriter)); }; @@ -1465,13 +1465,13 @@ BOOST_FIXTURE_TEST_CASE(drainrewards_sweeps_bucket_to_treasury, sysio_reserve_te BOOST_REQUIRE_EQUAL(success(), regreserve("SOLANA", "SOL", "PRIMARY", 1'000'000'000'000ULL, 1'000'000'000'000ULL)); BOOST_REQUIRE_EQUAL(success(), push_action(UWRIT_ACCOUNT, "applyswap"_n, mvo() - ("src_chain_code", codename_mvo("ETH")) - ("src_token_code", codename_mvo("ETH")) - ("src_reserve_code", codename_mvo("PRIMARY")) + ("src_chain_code", codename("ETH")) + ("src_token_code", codename("ETH")) + ("src_reserve_code", codename("PRIMARY")) ("src_amount", 1'000'000'000ULL) - ("dst_chain_code", codename_mvo("SOLANA")) - ("dst_token_code", codename_mvo("SOL")) - ("dst_reserve_code", codename_mvo("PRIMARY")) + ("dst_chain_code", codename("SOLANA")) + ("dst_token_code", codename("SOL")) + ("dst_reserve_code", codename("PRIMARY")) ("dst_amount", 100'000'000ULL) ("underwriter", "underwriter1"))); @@ -1519,9 +1519,9 @@ BOOST_FIXTURE_TEST_CASE(applyfromwire_credits_wire_and_debits_chain, sysio_reser regreserve("SOLANA", "SOL", "PRIMARY", 1000, 1000)); BOOST_REQUIRE_EQUAL(success(), push_action(UWRIT_ACCOUNT, "applyfromwire"_n, mvo() - ("dst_chain_code", codename_mvo("SOLANA")) - ("dst_token_code", codename_mvo("SOL")) - ("dst_reserve_code", codename_mvo("PRIMARY")) + ("dst_chain_code", codename("SOLANA")) + ("dst_token_code", codename("SOL")) + ("dst_reserve_code", codename("PRIMARY")) ("wire_in", 200) ("dst_amount", 100) ("underwriter", "underwriter1"))); @@ -1544,9 +1544,9 @@ BOOST_FIXTURE_TEST_CASE(applyfromwire_rejects_debit_above_curve_output, constexpr int64_t CURVE_OUT = 166; auto apply = [&](int64_t dst_amount) { return push_action(UWRIT_ACCOUNT, "applyfromwire"_n, mvo() - ("dst_chain_code", codename_mvo("SOLANA")) - ("dst_token_code", codename_mvo("SOL")) - ("dst_reserve_code", codename_mvo("PRIMARY")) + ("dst_chain_code", codename("SOLANA")) + ("dst_token_code", codename("SOL")) + ("dst_reserve_code", codename("PRIMARY")) ("wire_in", 200) ("dst_amount", dst_amount) ("underwriter", "underwriter1")); @@ -1577,9 +1577,9 @@ BOOST_FIXTURE_TEST_CASE(paywire_pays_real_wire_from_custody, sysio_reserve_teste // Swap-to-WIRE settlement: source books move + alice is paid REAL WIRE. BOOST_REQUIRE_EQUAL(success(), push_action(UWRIT_ACCOUNT, "paywire"_n, mvo() - ("src_chain_code", codename_mvo("ETH")) - ("src_token_code", codename_mvo("ETH")) - ("src_reserve_code", codename_mvo("PRIMARY")) + ("src_chain_code", codename("ETH")) + ("src_token_code", codename("ETH")) + ("src_reserve_code", codename("PRIMARY")) ("src_amount", 100) ("recipient", "alice") ("wire_out", CURVE_OUT) @@ -1622,9 +1622,9 @@ BOOST_FIXTURE_TEST_CASE(paywire_rejects_payout_above_curve_output, sysio_reserve BOOST_REQUIRE_EQUAL( error("assertion failure with message: paywire: payout exceeds the post-fee WIRE the source leg produced"), push_action(UWRIT_ACCOUNT, "paywire"_n, mvo() - ("src_chain_code", codename_mvo("ETH")) - ("src_token_code", codename_mvo("ETH")) - ("src_reserve_code", codename_mvo("PRIMARY")) + ("src_chain_code", codename("ETH")) + ("src_token_code", codename("ETH")) + ("src_reserve_code", codename("PRIMARY")) ("src_amount", 100) ("recipient", "alice") ("wire_out", 200) @@ -1632,9 +1632,9 @@ BOOST_FIXTURE_TEST_CASE(paywire_rejects_payout_above_curve_output, sysio_reserve // The curve's own output settles cleanly against the same reserve. BOOST_REQUIRE_EQUAL(success(), push_action(UWRIT_ACCOUNT, "paywire"_n, mvo() - ("src_chain_code", codename_mvo("ETH")) - ("src_token_code", codename_mvo("ETH")) - ("src_reserve_code", codename_mvo("PRIMARY")) + ("src_chain_code", codename("ETH")) + ("src_token_code", codename("ETH")) + ("src_reserve_code", codename("PRIMARY")) ("src_amount", 100) ("recipient", "alice") ("wire_out", 9) diff --git a/contracts/tests/sysio.tokens_tests.cpp b/contracts/tests/sysio.tokens_tests.cpp index a477cc00ba..38688a9865 100644 --- a/contracts/tests/sysio.tokens_tests.cpp +++ b/contracts/tests/sysio.tokens_tests.cpp @@ -59,8 +59,8 @@ class sysio_tokens_tester : public tester { } } - static fc::mutable_variant_object codename(std::string_view s) { - return mvo()("value", fc::slug_name{s}.value); + static std::string codename(std::string_view s) { + return std::string{s}; } /// `sysio.tokens::regtoken` for a chain-native token; the metadata strings are the @@ -98,8 +98,8 @@ class sysio_tokens_tester : public tester { auto row = abi_ser.binary_to_variant( "chain_token_row", raw, abi_serializer::create_yield_function(abi_serializer_max_time)); - if (row["chain_code"]["value"].as_uint64() == target_chain && - row["token_code"]["value"].as_uint64() == target_token) { + if (row["chain_code"].as().value == target_chain && + row["token_code"].as().value == target_token) { return row; } } catch (...) { diff --git a/contracts/tests/sysio.uwrit_tests.cpp b/contracts/tests/sysio.uwrit_tests.cpp index 0b5abea2dc..e8ea946100 100644 --- a/contracts/tests/sysio.uwrit_tests.cpp +++ b/contracts/tests/sysio.uwrit_tests.cpp @@ -21,8 +21,8 @@ using mvo = fc::mutable_variant_object; namespace { /// SlugName mvo helper for v6 action arguments. -inline fc::mutable_variant_object codename_mvo(std::string_view s) { - return mvo()("value", fc::slug_name{s}.value); +inline std::string codename(std::string_view s) { + return std::string{s}; } } // anonymous namespace @@ -351,9 +351,9 @@ BOOST_FIXTURE_TEST_CASE(swapfromwire_rejects_zero_wire_amount, sysio_uwrit_teste push_uwrit_action("uwrit.a"_n, "swapfromwire"_n, mvo() ("user", "uwrit.a") ("wire_amount", 0) - ("dst_chain_code", codename_mvo("SOLANA")) - ("dst_token_code", codename_mvo("SOL")) - ("dst_reserve_code", codename_mvo("PRIMARY")) + ("dst_chain_code", codename("SOLANA")) + ("dst_token_code", codename("SOL")) + ("dst_reserve_code", codename("PRIMARY")) ("target_amount", 100) ("target_tolerance_bps", 50) ("recipient_kind", sysio::opp::types::ChainKind::CHAIN_KIND_SVM) @@ -370,9 +370,9 @@ BOOST_FIXTURE_TEST_CASE(swapfromwire_rejects_below_minimum, sysio_uwrit_tester) push_uwrit_action("uwrit.a"_n, "swapfromwire"_n, mvo() ("user", "uwrit.a") ("wire_amount", 4'999'999'999ull) - ("dst_chain_code", codename_mvo("SOLANA")) - ("dst_token_code", codename_mvo("SOL")) - ("dst_reserve_code", codename_mvo("PRIMARY")) + ("dst_chain_code", codename("SOLANA")) + ("dst_token_code", codename("SOL")) + ("dst_reserve_code", codename("PRIMARY")) ("target_amount", 100) ("target_tolerance_bps", 50) ("recipient_kind", sysio::opp::types::ChainKind::CHAIN_KIND_SVM) @@ -391,9 +391,9 @@ BOOST_FIXTURE_TEST_CASE(swapfromwire_rejects_unregistered_target_chain, sysio_uw push_uwrit_action("uwrit.a"_n, "swapfromwire"_n, mvo() ("user", "uwrit.a") ("wire_amount", 5'000'000'000ull) - ("dst_chain_code", codename_mvo("SOLANA")) - ("dst_token_code", codename_mvo("SOL")) - ("dst_reserve_code", codename_mvo("PRIMARY")) + ("dst_chain_code", codename("SOLANA")) + ("dst_token_code", codename("SOL")) + ("dst_reserve_code", codename("PRIMARY")) ("target_amount", 100) ("target_tolerance_bps", 50) ("recipient_kind", sysio::opp::types::ChainKind::CHAIN_KIND_SVM) @@ -412,9 +412,9 @@ BOOST_FIXTURE_TEST_CASE(rcrdcommit_requires_msgch_auth, sysio_uwrit_tester) { tr ("uwreq_id", 1) ("underwriter", "uwrit.a") ("chain_code", 1) - ("from_chain_code", codename_mvo("ETH")) - ("from_token_code", codename_mvo("ETH")) - ("reserve_code", codename_mvo("PRIMARY")) + ("from_chain_code", codename("ETH")) + ("from_token_code", codename("ETH")) + ("reserve_code", codename("PRIMARY")) ("uic_bytes", std::vector{}) ).find("missing authority of sysio.msgch") != std::string::npos); } FC_LOG_AND_RETHROW() } @@ -429,9 +429,9 @@ BOOST_FIXTURE_TEST_CASE(rcrdcommit_rejects_unknown_uwreq, sysio_uwrit_tester) { ("uwreq_id", 42) ("underwriter", "uwrit.a") ("chain_code", 1) - ("from_chain_code", codename_mvo("ETH")) - ("from_token_code", codename_mvo("ETH")) - ("reserve_code", codename_mvo("PRIMARY")) + ("from_chain_code", codename("ETH")) + ("from_token_code", codename("ETH")) + ("reserve_code", codename("PRIMARY")) ("uic_bytes", std::vector{}) ) ); @@ -444,8 +444,8 @@ BOOST_FIXTURE_TEST_CASE(sumlocks_zero_for_unbonded_underwriter, sysio_uwrit_test BOOST_REQUIRE_EQUAL(success(), push_uwrit_action("uwrit.a"_n, "sumlocks"_n, mvo() ("underwriter", "uwrit.a") - ("chain_code", codename_mvo("ETH")) - ("token_code", codename_mvo("ETH")) + ("chain_code", codename("ETH")) + ("token_code", codename("ETH")) ) ); } FC_LOG_AND_RETHROW() } @@ -466,9 +466,9 @@ BOOST_FIXTURE_TEST_CASE(rcrdcommit_same_chain_swap_auth, sysio_uwrit_tester) { t ("uwreq_id", 7) ("underwriter", "uwrit.a") ("chain_code", 1) - ("from_chain_code", codename_mvo("ETH")) // src == dst chain - ("from_token_code", codename_mvo("USDC")) // distinguishes legs - ("reserve_code", codename_mvo("PRIMARY")) + ("from_chain_code", codename("ETH")) // src == dst chain + ("from_token_code", codename("USDC")) // distinguishes legs + ("reserve_code", codename("PRIMARY")) ("uic_bytes", std::vector{}) ).find("missing authority of sysio.msgch") != std::string::npos); } FC_LOG_AND_RETHROW() } diff --git a/libraries/chain/abi_serializer.cpp b/libraries/chain/abi_serializer.cpp index 83de928b4e..65bfd7ffd9 100644 --- a/libraries/chain/abi_serializer.cpp +++ b/libraries/chain/abi_serializer.cpp @@ -1,4 +1,5 @@ #include +#include #include #include #include @@ -135,6 +136,7 @@ namespace sysio::chain { built_in_types.emplace("symbol", pack_unpack()); built_in_types.emplace("symbol_code", pack_unpack()); + built_in_types.emplace("slug_name", pack_unpack()); built_in_types.emplace("asset", pack_unpack()); built_in_types.emplace("extended_asset", pack_unpack()); built_in_types.emplace("bitset", pack_unpack()); diff --git a/libraries/chain/include/sysio/chain/database_utils.hpp b/libraries/chain/include/sysio/chain/database_utils.hpp index 17a19c12df..978e637378 100644 --- a/libraries/chain/include/sysio/chain/database_utils.hpp +++ b/libraries/chain/include/sysio/chain/database_utils.hpp @@ -1,5 +1,7 @@ #pragma once +#include + #include #include #include @@ -270,7 +272,7 @@ struct writer { enum class key_leaf_kind { uint8, int8, uint16, int16, uint32, int32, uint64, int64, uint128, int128, checksum256, - name, boolean, string, + name, slug_name, boolean, string, float32, float64, float128, }; @@ -304,6 +306,7 @@ inline constexpr auto leaf_key_spellings = std::to_array({ {"int128", key_leaf_kind::int128}, {"checksum256", key_leaf_kind::checksum256}, {"name", key_leaf_kind::name}, + {"slug_name", key_leaf_kind::slug_name}, {"bool", key_leaf_kind::boolean}, {"string", key_leaf_kind::string}, {"float32", key_leaf_kind::float32}, @@ -361,6 +364,17 @@ inline fc::variant decode_field(reader& r, key_leaf_kind kind) { return v; } case key_leaf_kind::name: return fc::variant(name(r.read_be64()).to_string()); + case key_leaf_kind::slug_name: { + // Delegates to fc::slug_name's to_variant, so next_key inherits the same + // total, injective carrier: a canonical slug decodes to its string, zero + // to "", and a non-canonical value to the raw integer. A string-only + // decode would send every sub-2^42 key to "" and re-encode it to 0, + // restarting pagination at the top of the table. + const fc::slug_name s{ r.read_be64() }; + fc::variant v; + fc::to_variant(s, v); + return v; + } case key_leaf_kind::boolean: return fc::variant(r.read_u8() != 0); case key_leaf_kind::string: return fc::variant(r.read_nul_escaped_string()); case key_leaf_kind::float32: { @@ -437,6 +451,16 @@ inline void encode_field(writer& w, key_leaf_kind kind, const fc::variant& val) return; } case key_leaf_kind::name: w.write_be64(name(val.as_string()).to_uint64_t()); return; + case key_leaf_kind::slug_name: { + // Delegates to fc::slug_name's from_variant so the dual carrier (string / + // "" / integer, plus the transitional object) is implemented exactly once. + // Byte-identical to the struct-node path it replaces: that recursed one + // uint64 child to write_be64, and so does this. + fc::slug_name s; + fc::from_variant(val, s); + w.write_be64(s.value); + return; + } case key_leaf_kind::boolean: w.write_u8(val.as_bool() ? 1 : 0); return; case key_leaf_kind::string: w.write_nul_escaped_string(val.as_string()); return; case key_leaf_kind::float32: { @@ -476,11 +500,16 @@ inline void encode_field(writer& w, key_leaf_kind kind, const fc::variant& val) } // ── ABI-aware key shapes ──────────────────────────────────────────────────── -// kv/multi_index keys are not limited to the builtin leaf types above: CDT's -// to_key reflects through typedefs and struct key types — e.g. `slug_name` -// (struct { value: uint64 }) is the primary key of the v6 registry tables -// (sysio.chains chains, sysio.tokens tokens/chaintokens, sysio.reserv -// reserves). A key_shape is the resolved encode/decode plan for one key +// kv/multi_index keys are not limited to the builtin leaf types above: a key +// may be a struct whose fields encode in declaration order. Note `sysio::kv` +// does NOT route through CDT's `to_key` — `make_key` writes into a +// `be_key_stream` whose operator<< set is closed (the integrals, name, the +// floats, bool, string, vector), so a struct key resolves through +// SYSLIB_SERIALIZE's generic-DataStream friend template and recurses to +// write_be64 per member. `slug_name` is the primary key of the v6 registry +// tables (sysio.chains chains, sysio.tokens tokens/chaintokens, sysio.reserv +// reserves) and is now a LEAF above, not a struct key; the byte encoding is +// unchanged either way. A key_shape is the resolved encode/decode plan for one key // field: a leaf with a codec-supported type, or a struct node whose children // encode in declaration order (matching to_key's reflected-field walk). Leaf // types and their kinds are defined above, with the codec (key_leaf_kind / diff --git a/libraries/libfc/include/fc/slug_name.hpp b/libraries/libfc/include/fc/slug_name.hpp index b97f7cfa12..30c3e34322 100644 --- a/libraries/libfc/include/fc/slug_name.hpp +++ b/libraries/libfc/include/fc/slug_name.hpp @@ -17,6 +17,8 @@ #include #include +#include +#include #include #include @@ -82,4 +84,57 @@ inline constexpr slug_name operator""_s() { using slug_name_literals::operator""_s; +/// JSON carrier for a slug_name — a DUAL carrier, and deliberately so. +/// +/// A canonical slug renders as its string spelling (`"LIQSOL"`), and the zero +/// sentinel as the empty string. A value below 2^42 renders as the **raw +/// integer**, because `to_string()` cannot represent it: `zero_terminates` is +/// true, so decoding stops at the first zero symbol slot, and every such value +/// collapses to `""`. Emitting the integer instead keeps this conversion TOTAL +/// and INJECTIVE over all 2^64 — `""` means exactly zero and nothing else. +/// +/// The integer arm must not be replaced by a throw. Only `chain_code` is bound +/// to the proven source outpost, so a non-canonical `token_code` is plantable +/// from a forgeable attestation payload; a throwing conversion would let one +/// such row make an entire table unreadable over `get_table_rows`. +/// +/// A JSON integer cannot collide with a JSON string, which is what makes the +/// two carriers unambiguous. A numeric *string* would not: the slug alphabet +/// contains digits, so `"7"` is itself a valid canonical slug. +inline void to_variant(const slug_name& s, fc::variant& v) { + const std::string text = s.to_string(); + // `pack` is the non-validating encoder, so this is a pure round-trip test: + // the string spelling is used only when it recovers the value exactly. + if (slug_name::pack(text) == s.value) { + v = text; + return; + } + v = s.value; +} + +/// Accepts every carrier `to_variant` can emit, plus — TRANSITIONALLY — the +/// `{"value": }` object that abigen's reflected struct emitted before +/// `slug_name` became an ABI builtin. +/// +/// The object arm is what makes the cross-repo landing window survivable: with +/// no variant conversions, a slug converts through +/// `FC_REFLECT_TEMPLATE(basic_name, (value))` and is therefore +/// object-only, while a string/integer-only reader rejects that object. There +/// is no value both spellings accept, so a JSON *writer* cannot straddle the +/// window the way a reader can. Delete this arm once no writer emits the +/// object form. +inline void from_variant(const fc::variant& v, slug_name& s) { + if (v.is_string()) { + // Validating: the ctor round-trip-checks and rejects a non-canonical or + // out-of-alphabet spelling. `""` is the zero sentinel. + s = slug_name{ v.get_string() }; + return; + } + if (v.is_object()) { + s = slug_name{ v.get_object()["value"].as_uint64() }; + return; + } + s = slug_name{ v.as_uint64() }; +} + } // namespace fc diff --git a/libraries/libfc/test/test_slug_name.cpp b/libraries/libfc/test/test_slug_name.cpp index 4f0bf9eae8..bd477100b3 100644 --- a/libraries/libfc/test/test_slug_name.cpp +++ b/libraries/libfc/test/test_slug_name.cpp @@ -4,6 +4,8 @@ #include #include #include +#include +#include #include #include @@ -344,4 +346,103 @@ BOOST_AUTO_TEST_CASE(non_zero_terminator_trait_accepts_alphabet_zero) { "zero_terminates is false"); } +// ── variant carrier ──────────────────────────────────────────────────────── +// to_variant is a DUAL carrier: a canonical slug renders as its string, zero as +// "", and a value with no string spelling as the raw integer. It must be TOTAL +// and INJECTIVE over all 2^64 — a non-canonical code is plantable from a +// forgeable attestation payload (only chain_code is bound to the proven source +// outpost), so a throwing conversion would let one row make a whole table +// unreadable over get_table_rows. + +BOOST_AUTO_TEST_CASE(variant_canonical_slug_is_a_string) { + fc::variant v; + fc::to_variant(slug_name{"LIQSOL"}, v); + BOOST_REQUIRE(v.is_string()); + BOOST_CHECK_EQUAL(v.as_string(), "LIQSOL"); + + slug_name back; + fc::from_variant(v, back); + BOOST_CHECK(back == slug_name{"LIQSOL"}); +} + +BOOST_AUTO_TEST_CASE(variant_zero_is_the_empty_string_both_ways) { + fc::variant v; + fc::to_variant(slug_name{uint64_t{0}}, v); + BOOST_REQUIRE(v.is_string()); + BOOST_CHECK_EQUAL(v.as_string(), ""); + + slug_name back{uint64_t{12345}}; + fc::from_variant(fc::variant(std::string{}), back); + BOOST_CHECK_EQUAL(back.value, 0u); +} + +BOOST_AUTO_TEST_CASE(variant_non_canonical_uses_the_integer_carrier) { + // Every value below 1<<42 has a zero in the char[0] slot, so to_string() + // truncates it to "" and the string spelling cannot recover it. The carrier + // must therefore be the integer, or the conversion stops being injective. + for (uint64_t raw : {uint64_t{1}, uint64_t{7}, uint64_t{42}, + uint64_t{(uint64_t{1} << 42) - 1}}) { + fc::variant v; + fc::to_variant(slug_name{raw}, v); + BOOST_REQUIRE_MESSAGE(v.is_integer(), "raw=" << raw << " must use the integer carrier"); + slug_name back; + fc::from_variant(v, back); + BOOST_CHECK_EQUAL(back.value, raw); + } +} + +BOOST_AUTO_TEST_CASE(variant_never_throws_on_any_value) { + // The anti-DoS property. If this is ever "tidied" into a throw, one planted + // row makes get_table_rows fail for an entire table. + for (uint64_t raw : {uint64_t{0}, uint64_t{1}, uint64_t{9}, uint64_t{1} << 41, + uint64_t{1} << 42, ~uint64_t{0}}) { + fc::variant v; + BOOST_CHECK_NO_THROW(fc::to_variant(slug_name{raw}, v)); + } +} + +BOOST_AUTO_TEST_CASE(variant_every_carrier_round_trips_exactly) { + // Totality + injectivity across the boundary, including the canonical floor. + for (uint64_t raw : {uint64_t{0}, uint64_t{1}, uint64_t{(uint64_t{1} << 42) - 1}, + uint64_t{1} << 42, fc::slug_name{"A"}.value, + fc::slug_name{"LIQSOL"}.value, fc::slug_name{"12345678"}.value}) { + fc::variant v; + fc::to_variant(slug_name{raw}, v); + slug_name back; + fc::from_variant(v, back); + BOOST_CHECK_EQUAL(back.value, raw); + } +} + +BOOST_AUTO_TEST_CASE(variant_an_all_digit_slug_is_a_string_not_its_own_decimal) { + // The slug alphabet contains digits, so "7" is itself a valid canonical slug + // — which is exactly why the non-canonical carrier must be a JSON integer + // and not a numeric string. A numeric string would be ambiguous. + fc::variant v; + fc::to_variant(slug_name{"7"}, v); + BOOST_REQUIRE(v.is_string()); + BOOST_CHECK_EQUAL(v.as_string(), "7"); + BOOST_CHECK_NE(slug_name{"7"}.value, 7u); +} + +BOOST_AUTO_TEST_CASE(variant_accepts_the_transitional_object_carrier) { + // TRANSITIONAL: the shape abigen's reflected struct emitted before slug_name + // became an ABI builtin. Deleted once no writer emits it. + slug_name back; + fc::from_variant(fc::variant(fc::mutable_variant_object("value", uint64_t{7})), back); + BOOST_CHECK_EQUAL(back.value, 7u); + + fc::from_variant( + fc::variant(fc::mutable_variant_object("value", fc::slug_name{"LIQSOL"}.value)), back); + BOOST_CHECK(back == slug_name{"LIQSOL"}); +} + +BOOST_AUTO_TEST_CASE(variant_rejects_a_non_canonical_string_spelling) { + // The string arm validates: an out-of-alphabet or non-canonical spelling is + // a hard error, not a silent zero. + slug_name back; + BOOST_CHECK_THROW(fc::from_variant(fc::variant(std::string{"liqsol"}), back), fc::exception); + BOOST_CHECK_THROW(fc::from_variant(fc::variant(std::string{"TOOOLONGXX"}), back), fc::exception); +} + BOOST_AUTO_TEST_SUITE_END() diff --git a/plugins/batch_operator_plugin/src/batch_operator_plugin.cpp b/plugins/batch_operator_plugin/src/batch_operator_plugin.cpp index 62afe0f9ac..2a6296ea0e 100644 --- a/plugins/batch_operator_plugin/src/batch_operator_plugin.cpp +++ b/plugins/batch_operator_plugin/src/batch_operator_plugin.cpp @@ -612,16 +612,13 @@ struct batch_operator_plugin::impl { outposts.clear(); for (auto& row : rows.rows) { auto obj = row.get_object(); - // The `code` field on the Chain proto is a `slug_name` struct - // wrapping a uint64 (see slug_name.hpp). The JSON view exposes - // it as `{value: }`. Unpack defensively. + // `code` is a `slug_name`. fc::slug_name's own from_variant accepts + // every carrier — the decoded slug string, "" for zero, a raw integer, + // and the legacy `{value: }` object — so the shape does not + // have to be probed here. uint64_t code_val = 0; if (auto code_obj = obj.find(chains::field::code); code_obj != obj.end()) { - if (code_obj->value().is_object()) { - code_val = code_obj->value().get_object()["value"].as_uint64(); - } else { - code_val = code_obj->value().as_uint64(); - } + code_val = code_obj->value().as().value; } bool is_depot = obj[chains::field::is_depot].as_bool(); bool active = obj[chains::field::active].as_bool(); diff --git a/plugins/underwriter_plugin/src/underwriter_plugin.cpp b/plugins/underwriter_plugin/src/underwriter_plugin.cpp index bf7ddbf9f4..b8dfab6787 100644 --- a/plugins/underwriter_plugin/src/underwriter_plugin.cpp +++ b/plugins/underwriter_plugin/src/underwriter_plugin.cpp @@ -1208,9 +1208,11 @@ struct underwriter_plugin::impl { depot_chain_code.reset(); for (auto& row : rows.rows) { auto obj = row.get_object(); - // `code` is a `slug_name` — serialised as `{"value": }`. - const auto& code_obj = obj["code"].get_object(); - uint64_t chain_code = code_obj["value"].as_uint64(); + // `code` is a `slug_name`. Read it through fc::slug_name's own + // from_variant, which accepts every carrier the depot emits — the + // decoded slug string, "" for the zero sentinel, and a raw integer for + // a non-canonical value — plus the legacy `{"value": }` object. + uint64_t chain_code = obj["code"].as().value; if (obj.contains("is_depot") && obj["is_depot"].as_bool()) { // Record the depot's own code for exact per-leg depot // detection (to/from-WIRE swaps), then skip caching it as an @@ -1366,7 +1368,7 @@ struct underwriter_plugin::impl { credit_lines.clear(); // v6 schema: balance / lock / withdraw rows carry `chain_code` - // and `token_code` slug_names (serialised as `{"value": }`) + // and `token_code` slug_names (read via fc::slug_name's from_variant) // — not the v5 `chain` (ChainKind enum) / `token_kind` (TokenKind // enum). Translation: // chain_code → ChainKind via `outpost_chain_kinds` map @@ -1385,7 +1387,7 @@ struct underwriter_plugin::impl { auto tk_rows = read_all("sysio.tokens", "sysio.tokens", "tokens"); for (auto& row : tk_rows.rows) { auto obj = row.get_object(); - uint64_t code = obj["code"].get_object()["value"].as_uint64(); + uint64_t code = obj["code"].as().value; token_kind_by_code[code] = obj["kind"].as(); } } @@ -1400,8 +1402,8 @@ struct underwriter_plugin::impl { if (!obj.contains("chain_code") || !obj.contains("token_code")) { return std::nullopt; } - uint64_t chain_code = obj["chain_code"].get_object()["value"].as_uint64(); - uint64_t token_code = obj["token_code"].get_object()["value"].as_uint64(); + uint64_t chain_code = obj["chain_code"].as().value; + uint64_t token_code = obj["token_code"].as().value; if (!outpost_chain_kinds.contains(chain_code) || !token_kind_by_code.contains(token_code)) { return std::nullopt; @@ -1544,9 +1546,9 @@ struct underwriter_plugin::impl { // v6 data-model schema: src/dst identity lives on the uwreq row as // `(chain_code, token_code, reserve_code)` slug_name triples plus a // `*_amount`. Populated by `sysio.uwrit::createuwreq` from the - // originating SwapRequest. The ABI surfaces slug_name as - // `{value: uint64}`; we lift the inner uint64 directly into - // `fc::slug_name` to mirror the host-side packing. + // originating SwapRequest. Each is read through fc::slug_name's own + // from_variant, so every carrier the depot emits is accepted without + // this plugin knowing which one it is. if (!obj.contains(uwrit::request_field::source_chain_code) || !obj.contains(uwrit::request_field::source_amount) || !obj.contains(uwrit::request_field::destination_chain_code) || @@ -1557,7 +1559,7 @@ struct underwriter_plugin::impl { continue; } auto read_codename = [&](const char* key) -> fc::slug_name { - return fc::slug_name{obj[key]["value"].as_uint64()}; + return obj[key].as(); }; req.src_chain_code = read_codename( uwrit::request_field::source_chain_code); diff --git a/tests/get_table_tests.cpp b/tests/get_table_tests.cpp index 42c2f3bc30..7c48ef223d 100644 --- a/tests/get_table_tests.cpp +++ b/tests/get_table_tests.cpp @@ -452,7 +452,7 @@ BOOST_FIXTURE_TEST_CASE( get_table_next_key_test, validating_tester ) try { push_action("test"_n, "addhashobj"_n, "test"_n, mutable_variant_object()("hashinput", "firstinput")); push_action("test"_n, "addhashobj"_n, "test"_n, mutable_variant_object()("hashinput", "secondinput")); push_action("test"_n, "addhashobj"_n, "test"_n, mutable_variant_object()("hashinput", "thirdinput")); - // structobjs: kv::table keyed by the reflected struct slug_name{value} — the + // structobjs: kv::table keyed by the reflected struct composite_key{value} — the // sysio.chains `chains` key shape. Drives the struct-key path in (sec-10). push_action("test"_n, "addstruct"_n, "test"_n, mutable_variant_object()("code", 10)("payload", 100)); push_action("test"_n, "addstruct"_n, "test"_n, mutable_variant_object()("code", 20)("payload", 200)); @@ -747,15 +747,18 @@ BOOST_FIXTURE_TEST_CASE( get_table_next_key_test, validating_tester ) try { } // (sec-10) structobjs primary key — kv::table keyed by the reflected struct - // `slug_name { value: uint64 }`, the exact key shape of the v6 - // registry tables (sysio.chains `chains`, key_types ["slug_name"]). + // `composite_key { value: uint64 }` — the struct key shape the v6 + // registry tables used before `slug_name` became an ABI builtin. + // Deliberately not named `slug_name`: that spelling now resolves as + // a codec LEAF, which would bypass struct expansion entirely and + // leave this case passing while testing nothing. // Exercises the ABI-aware BE key codec's struct-key expansion on the // live get_table_rows path: a JSON bound of the documented nested // `{ "code": { "value": N } }` form (encode_key), and a `next_key` // pagination cursor that round-trips that same nested shape // (decode_key -> next_key -> encode_key). Before the codec became // ABI-aware, any JSON bound here asserted - // "Unsupported BE key type: slug_name". + // "Unsupported BE key type: composite_key". { // (a) JSON bound of the documented nested struct shape filters inclusively. chain_apis::read_only::get_table_rows_params p; diff --git a/unittests/be_key_codec_tests.cpp b/unittests/be_key_codec_tests.cpp index 4f22e1cdf4..954224dc9b 100644 --- a/unittests/be_key_codec_tests.cpp +++ b/unittests/be_key_codec_tests.cpp @@ -1,3 +1,4 @@ +#include #include #include @@ -11,7 +12,7 @@ namespace codec = sysio::chain::be_key_codec; /** * Direct coverage for the ABI-aware BE key codec: typedef resolution, struct - * key expansion (the `slug_name` shape keying the v6 registry tables), + * key expansion (a single-uint64 struct key, the pre-builtin `slug_name` shape), * abigen template-spelling canonicalization, the float128 leaf, and the * shape builder's rejection paths. End-to-end bound/pagination behaviour is * covered in tests/get_table_tests.cpp; these pin the codec layer itself. @@ -19,16 +20,22 @@ namespace codec = sysio::chain::be_key_codec; namespace { -/// ABI fixture: a slug_name-style struct key, a two-hop typedef chain onto -/// it, a nested struct key, and a based struct (rejected by design). +/// ABI fixture: a single-uint64 struct key, a two-hop typedef chain onto it, a +/// nested struct key, and a based struct (rejected by design). +/// +/// Deliberately NOT named `slug_name`: that spelling is an abi_serializer +/// builtin and a `leaf_key_spellings` entry, so a fixture using it would take +/// the leaf branch in `build_key_shape` and stop exercising the struct-key and +/// typedef-chain paths — while still passing. This is the repo's only coverage +/// of those paths. abi_def make_test_abi() { abi_def abi; abi.types.emplace_back(type_def{"chain_code_t", "code_alias"}); - abi.types.emplace_back(type_def{"code_alias", "slug_name"}); - abi.structs.emplace_back(struct_def{"slug_name", "", {field_def{"value", "uint64"}}}); + abi.types.emplace_back(type_def{"code_alias", "composite_key"}); + abi.structs.emplace_back(struct_def{"composite_key", "", {field_def{"value", "uint64"}}}); abi.structs.emplace_back( - struct_def{"pair_key", "", {field_def{"code", "slug_name"}, field_def{"idx", "uint32"}}}); - abi.structs.emplace_back(struct_def{"based_key", "slug_name", {field_def{"extra", "uint64"}}}); + struct_def{"pair_key", "", {field_def{"code", "composite_key"}, field_def{"idx", "uint32"}}}); + abi.structs.emplace_back(struct_def{"based_key", "composite_key", {field_def{"extra", "uint64"}}}); return abi; } @@ -51,9 +58,9 @@ bool key_less(const std::vector& a, const std::vector& b) { BOOST_AUTO_TEST_SUITE(be_key_codec_tests) -BOOST_AUTO_TEST_CASE(slug_name_struct_roundtrip) { +BOOST_AUTO_TEST_CASE(composite_key_struct_roundtrip) { auto abi = make_test_abi(); - auto shapes = codec::build_key_shapes(abi, {"code"}, {"slug_name"}); + auto shapes = codec::build_key_shapes(abi, {"code"}, {"composite_key"}); auto bytes = codec::encode_key(fc::variant(fc::mutable_variant_object("code", slug(42))), shapes); BOOST_REQUIRE_EQUAL(bytes.size(), 8u); // single uint64 field, BE @@ -63,17 +70,88 @@ BOOST_AUTO_TEST_CASE(slug_name_struct_roundtrip) { decoded.get_object()["code"].get_object()["value"].as_uint64(), 42u); } -BOOST_AUTO_TEST_CASE(slug_name_byte_order_matches_value_order) { +// ── slug_name as a codec LEAF ─────────────────────────────────────────────── +// `slug_name` is an abi_serializer builtin and a leaf_key_spellings entry, so +// it needs no abi.structs entry here — build_key_shapes resolves it through +// leaf_kind_of. These pin the leaf's carrier and the two properties the v6 +// registry tables depend on: byte compatibility with the struct-key encoding it +// replaced, and prefix grouping. + +BOOST_AUTO_TEST_CASE(slug_name_leaf_bytes_match_the_struct_node_it_replaced) { + // The no-migration guarantee: the struct-node path recursed one uint64 child + // to write_be64, and the leaf path IS write_be64. `composite_key` still + // exercises the struct path, so it is the reference encoding. + auto abi = make_test_abi(); + const uint64_t packed = fc::slug_name{"LIQSOL"}.value; + BOOST_CHECK(encode_single(abi, "slug_name", fc::variant("LIQSOL")) + == encode_single(abi, "composite_key", slug(packed))); +} + +BOOST_AUTO_TEST_CASE(slug_name_leaf_roundtrips_a_canonical_slug_as_a_string) { + auto abi = make_test_abi(); + auto shapes = codec::build_key_shapes(abi, {"code"}, {"slug_name"}); + auto bytes = codec::encode_key( + fc::variant(fc::mutable_variant_object("code", "LIQSOL")), shapes); + BOOST_REQUIRE_EQUAL(bytes.size(), 8u); + auto decoded = codec::decode_key(bytes.data(), bytes.size(), shapes); + BOOST_CHECK_EQUAL(decoded.get_object()["code"].as_string(), "LIQSOL"); +} + +BOOST_AUTO_TEST_CASE(slug_name_leaf_roundtrips_the_zero_sentinel_as_empty) { + auto abi = make_test_abi(); + auto shapes = codec::build_key_shapes(abi, {"code"}, {"slug_name"}); + auto bytes = codec::encode_key( + fc::variant(fc::mutable_variant_object("code", "")), shapes); + auto decoded = codec::decode_key(bytes.data(), bytes.size(), shapes); + BOOST_CHECK_EQUAL(decoded.get_object()["code"].as_string(), ""); + BOOST_CHECK(bytes == encode_single(abi, "composite_key", slug(0))); +} + +BOOST_AUTO_TEST_CASE(slug_name_leaf_roundtrips_a_non_canonical_value_as_an_integer) { + // A value below 2^42 has no string spelling (to_string truncates at the first + // zero symbol slot), so the carrier is the raw integer. Without this, decode + // would emit "" and re-encode to 0 — restarting pagination at the top of the + // table for any row holding a plantable non-canonical code. + auto abi = make_test_abi(); + auto shapes = codec::build_key_shapes(abi, {"code"}, {"slug_name"}); + auto bytes = codec::encode_key( + fc::variant(fc::mutable_variant_object("code", 7u)), shapes); + auto decoded = codec::decode_key(bytes.data(), bytes.size(), shapes); + BOOST_CHECK(decoded.get_object()["code"].is_integer()); + BOOST_CHECK_EQUAL(decoded.get_object()["code"].as_uint64(), 7u); + // And it re-encodes to the same key — the round trip pagination relies on. + BOOST_CHECK(bytes == codec::encode_key(decoded, shapes)); +} + +BOOST_AUTO_TEST_CASE(slug_name_leaf_groups_shared_prefixes) { + // The property slug_name was designed for: MSB-first 6-bit packing puts + // char[0] at bits [42..47], so a shared textual prefix is a shared leading + // BYTE prefix of the key — k symbols share 2 + floor(6k/8) bytes, the 2 from + // the unused top 16 bits. Grouping is byte-exact only at k = 4 and k = 8. + auto abi = make_test_abi(); + auto shared_bytes = [&](const char* a, const char* b) { + auto ka = encode_single(abi, "slug_name", fc::variant(a)); + auto kb = encode_single(abi, "slug_name", fc::variant(b)); + size_t n = 0; + while (n < ka.size() && n < kb.size() && ka[n] == kb[n]) ++n; + return n; + }; + BOOST_CHECK_EQUAL(shared_bytes("LIQSOL", "LIQETH"), 4u); // k=3 -> 2 + 2 + BOOST_CHECK_EQUAL(shared_bytes("WIRE", "WIREUSD"), 5u); // k=4 -> 2 + 3, aligned + BOOST_CHECK_EQUAL(shared_bytes("USDC", "USDT"), 4u); // k=3 -> 2 + 2 +} + +BOOST_AUTO_TEST_CASE(composite_key_byte_order_matches_value_order) { auto abi = make_test_abi(); - auto lo = encode_single(abi, "slug_name", slug(2)); - auto hi = encode_single(abi, "slug_name", slug(7)); + auto lo = encode_single(abi, "composite_key", slug(2)); + auto hi = encode_single(abi, "composite_key", slug(7)); BOOST_CHECK(key_less(lo, hi)); } BOOST_AUTO_TEST_CASE(typedef_chain_resolves_to_struct) { auto abi = make_test_abi(); - // chain_code_t -> code_alias -> slug_name: same encoding as the struct itself. - auto direct = encode_single(abi, "slug_name", slug(99)); + // chain_code_t -> code_alias -> composite_key: same encoding as the struct itself. + auto direct = encode_single(abi, "composite_key", slug(99)); auto aliased = encode_single(abi, "chain_code_t", slug(99)); BOOST_CHECK(direct == aliased); } @@ -147,7 +225,7 @@ BOOST_AUTO_TEST_CASE(rejections) { BOOST_CHECK_THROW(codec::build_key_shapes(abi, {"k"}, {"based_key"}), fc::exception); // Bound object missing a struct field. - auto shapes = codec::build_key_shapes(abi, {"code"}, {"slug_name"}); + auto shapes = codec::build_key_shapes(abi, {"code"}, {"composite_key"}); fc::variant missing(fc::mutable_variant_object( "code", fc::mutable_variant_object("wrong_field", 1))); BOOST_CHECK_THROW(codec::encode_key(missing, shapes), fc::exception); @@ -265,22 +343,22 @@ BOOST_AUTO_TEST_CASE(typedef_cycle_is_rejected) { BOOST_CHECK_EXCEPTION(codec::build_key_shapes(self, {"k"}, {"s"}), fc::exception, has_cycle_msg); } -// Scoped table whose within-scope primary key is a struct (slug_name). The real +// Scoped table whose within-scope primary key is a struct (composite_key). The real // v6 registry tables are unscoped, but chain_plugin supports scoped tables by // stripping the leading scope field's shape from the bound shapes and encoding // only the within-scope portion (see get_table_rows' scope_key_count erase). // This pins that slice-then-encode path for a struct-typed within-scope key: -// build the full [scope=name, code=slug_name] shapes, drop the scope shape as +// build the full [scope=name, code=composite_key] shapes, drop the scope shape as // the plugin does for a scoped JSON bound, and round-trip the struct remainder. BOOST_AUTO_TEST_CASE(scoped_struct_key_within_scope_roundtrip) { auto abi = make_test_abi(); // Full key list: a leading "scope" leaf (name) followed by a struct "code" - // (slug_name) — the shape of a scoped kv table keyed by a struct per scope. - auto full = codec::build_key_shapes(abi, {"scope", "code"}, {"name", "slug_name"}); + // (composite_key) — the shape of a scoped kv table keyed by a struct per scope. + auto full = codec::build_key_shapes(abi, {"scope", "code"}, {"name", "composite_key"}); BOOST_REQUIRE_EQUAL(full.size(), 2u); BOOST_CHECK(full[0].is_leaf); // scope resolves to the name leaf - BOOST_CHECK(!full[1].is_leaf); // code is the slug_name struct node + BOOST_CHECK(!full[1].is_leaf); // code is the composite_key struct node // chain_plugin strips the leading scope shape for a scoped bound; the // remaining shapes encode/decode the within-scope key only. @@ -294,7 +372,7 @@ BOOST_AUTO_TEST_CASE(scoped_struct_key_within_scope_roundtrip) { // The leading scope is a pure prefix: the within-scope struct bytes are // identical to that struct keyed on its own. - BOOST_CHECK(bytes == encode_single(abi, "slug_name", slug(42))); + BOOST_CHECK(bytes == encode_single(abi, "composite_key", slug(42))); } BOOST_AUTO_TEST_SUITE_END() diff --git a/unittests/test-contracts/get_table_test/get_table_test.abi b/unittests/test-contracts/get_table_test/get_table_test.abi index b889481d0a..f7a1e4a528 100644 --- a/unittests/test-contracts/get_table_test/get_table_test.abi +++ b/unittests/test-contracts/get_table_test/get_table_test.abi @@ -37,6 +37,16 @@ } ] }, + { + "name": "composite_key", + "base": "", + "fields": [ + { + "name": "value", + "type": "uint64" + } + ] + }, { "name": "erasenumobj", "base": "", @@ -105,23 +115,13 @@ } ] }, - { - "name": "slug_name", - "base": "", - "fields": [ - { - "name": "value", - "type": "uint64" - } - ] - }, { "name": "structobj", "base": "", "fields": [ { "name": "code", - "type": "slug_name" + "type": "composite_key" }, { "name": "payload", @@ -135,7 +135,7 @@ "fields": [ { "name": "code", - "type": "slug_name" + "type": "composite_key" } ] } @@ -239,7 +239,7 @@ "type": "structobj", "index_type": "i64", "key_names": ["code"], - "key_types": ["slug_name"], + "key_types": ["composite_key"], "table_id": 12649 } ], diff --git a/unittests/test-contracts/get_table_test/get_table_test.cpp b/unittests/test-contracts/get_table_test/get_table_test.cpp index f3d06a9fde..15d7e41190 100644 --- a/unittests/test-contracts/get_table_test/get_table_test.cpp +++ b/unittests/test-contracts/get_table_test/get_table_test.cpp @@ -42,5 +42,5 @@ void get_table_test::addhashobj(std::string hashinput) { void get_table_test::addstruct(uint64_t code, uint64_t payload) { structobjs structobjs_table( get_self() ); - structobjs_table.emplace( get_self(), { slug_name{code} }, { slug_name{code}, payload } ); + structobjs_table.emplace( get_self(), { composite_key{code} }, { composite_key{code}, payload } ); } diff --git a/unittests/test-contracts/get_table_test/get_table_test.hpp b/unittests/test-contracts/get_table_test/get_table_test.hpp index 9851fcf5cf..9a28d0755a 100644 --- a/unittests/test-contracts/get_table_test/get_table_test.hpp +++ b/unittests/test-contracts/get_table_test/get_table_test.hpp @@ -98,25 +98,29 @@ class [[sysio::contract]] get_table_test : public sysio::contract { > hashobjs; // Struct-keyed kv::table — drives the ABI-aware BE key codec's struct - // expansion on the live get_table_rows path. Mirrors the v6 registry - // tables (e.g. sysio.chains `chains`), whose primary key is the reflected - // struct `slug_name { value: uint64 }`. abigen emits - // `key_types: ["code"->"slug_name"]` for this table, so JSON bounds and - // `next_key` pagination must round-trip the nested `{ "code": { "value": N } }` - // key shape — coverage a flat scalar key cannot provide. - struct slug_name { + // expansion on the live get_table_rows path, so JSON bounds and `next_key` + // pagination must round-trip the nested `{ "code": { "value": N } }` key + // shape — coverage a flat scalar key cannot provide. + // + // Deliberately NOT named `composite_key`: that spelling is an abi_serializer + // builtin and a `leaf_key_spellings` entry, and abigen's builtin match is + // on the namespace-stripped bare name — so a member struct called + // `composite_key` would be emitted as the builtin, take the leaf branch in + // `build_key_shape`, and stop exercising struct expansion at all. The + // suite would keep passing while testing nothing it was written for. + struct composite_key { uint64_t value = 0; - SYSLIB_SERIALIZE(slug_name, (value)) + SYSLIB_SERIALIZE(composite_key, (value)) }; struct structobj_key { - slug_name code; + composite_key code; uint64_t primary_key() const { return code.value; } SYSLIB_SERIALIZE(structobj_key, (code)) }; struct [[sysio::table("structobjs")]] structobj { - slug_name code; + composite_key code; uint64_t payload = 0; SYSLIB_SERIALIZE(structobj, (code)(payload)) }; @@ -137,7 +141,7 @@ class [[sysio::contract]] get_table_test : public sysio::contract { void addhashobj(std::string hashinput); /// Insert a row into the struct-keyed kv::table `structobjs`. - /// @param code the slug_name value forming the struct primary key + /// @param code the composite_key value forming the struct primary key /// @param payload arbitrary row payload [[sysio::action]] void addstruct(uint64_t code, uint64_t payload); diff --git a/unittests/test-contracts/get_table_test/get_table_test.wasm b/unittests/test-contracts/get_table_test/get_table_test.wasm index c1e882bfaf3bf9b0da4fb9281f5909c99fa36837..7a27b01039ff5f839960e0b3f5cd687b587ab3b1 100755 GIT binary patch delta 1745 zcmZ`&ZA?>V6u#%S)ZTv3w@~_VDR3z$-&j5bN!MGGs8sdEev?hsHATA5C1EAD%X zo1SPk<8)b;Io*tLcZNgf+vAe3Ub)4#_hUN9fSpg3Oszla5>_H3_Xik?6DMxOAYSp&E1Z@)Voq9X;MOj>1;OY1$3o z#d&iXRN;H(W~j#d=6R^WP&q-V{ zuVpEUo(*j5nAqTk!q}5I!%ATY&#`uMhQvuA@$4x{6OgdN^DaEAB&<0Ae!Q6BHq@%y zYUokvqJ_JK&91gnJ=@>TIjB7Vb8Ybfj$6k8vkB*`=pNC_OC^&;qs|-5(K;vUg_@S#D zn(>}%fF9k_z6a_sCWavkzZ0E=b5%T^y+3gEA70`E`sn~;c2VL2?rBn;(`*V8+1H^( z@w$5fTJcy;83gch&Piy)vfKh_$Nt=Y=)g<4-Ox#yp2eX&+y7+yIG&XT;SjD$F__2w z`Fr6g-p%h29_R1{w0gdPW0$IysKG(f>FB^K4X#v&N&L29JHa;l z3Q1P`E`km3`NoN^7yMgsqA_EWLoFnm#-+wNIEcSA`stPuzkl66gEM|Fg}&eqtXmG? zPktZ8%57=~2kvVsfmwXB=?u&%dCi;BAc940ulu7!Q%TK1b!rY$I$6sO6Q7HDJ+TMa zqPjFCm(Qnk#Bu5fm-1PA55N)Sj}DiH@}1RlndGe=B$|eU9aM*N!C}h%Z$T|ktNz|Q z2}?M(!3KhTyI2}6gk5QqLAZt=Vrqo8u`cgo~H$WyZ z&^Hbzu%|zOKlhEnveMK4J`XP`t6@U%GX6SnC(}T!AaV7YM3?b%pcQ)s@J@Lfgiu%E yYuRZr5_N$E*XJkJ=N;r<)8yI-TZdmBv?pGRPDr z4<{#Qqp@T%o=8Q!RH(`+KnnN*X8TqfcvE?-qlsz0sI&yB}Yajt-DnMYA);)hb%s9N%hCt}Gs$8t9% z;s<6I;#{GSILH<0i}ePPGZ;jX7j-(bsL^Rk^cJw%?4Wm)>UA2?0HEq%hcbPcQ|EL# zbvma@M?+&>{^`IC8dy(;($Z4U0f0^wqK;zG-|BW~Ktn?{x)ihUu2!$-WKQCmg`-_~ z!4QKw#bn$8P>=ge8=(PDn%ZC!-Y^}6%^1wDhDMyrKL!k=?)@Z33(BDo@3~!2g!$4D zbTL!okVAqBovEPWNFI78DxMA-!4I?iH=x4i8W>?B5TA;MM-E@s*A(w4d%_2(1K^pKDxmza~ol| zl{^45S}ZQui;Why-t3TgNtE)~Ufg48C$|qR+wH?-5jF_gWaKXBpc=_4t&GVOYEP(%7yP;8u*akrGvo?IsUIkmQD!7g4$pm*oGbSB_ z4$CrhnQP*W5^?Y6v=Uc9QCTbAaU3a~lFvWnC7w={8M8XbrEH2++Z3C|`O+_;U3sFc z2SCO-=SB$P`_ALgfs$(-gfQgV3Z3}2D-2zfrWx#UoBt)-%n(M1gvM}J-G7j zQ{Ta(>bQFO_wb;J$PWT5FyKE`B^a)r!P2G@B~tAG*hz%c*uFqPJ(V8Ylx$;<3Ue2J zUUL(6Q?b_L*R@5Dd3zSG*WQH@T#D4--*r|f#)oy+U=LoapMWTCYB&Wk<)?=4`2I1q znCn#V9oY(2dxc|k(bdMuQM7D)b;Zcys|sWL90cBi_Zt5q%)d0PC3(MTIJZcj!(Gh{ zg!ZjwD@l_zgm;_mxek+<-?D{Z_qTXRCR)xw2^RXss2gwlYw;6b(HfhYeYPL3_-0`i ztNj5wWzrv5u^+$-{yGYM+b^$JX3!JxQLH_I4zS_JfekQ+KL$?1K_%L{CJ*LuLVhE# zKsCMG&<(j9SO}R282>$`mwVQ7M From b50882e93b59b31c07239998315d0b759783e87e Mon Sep 17 00:00:00 2001 From: kevin Heifner Date: Wed, 16 Sep 2026 08:07:59 -0500 Subject: [PATCH 02/29] fix(chain): carry a non-canonical slug_name across the JSON text boundary MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit fc::json quotes a uint64 above 0xffffffff (io/json.cpp:695), so the integer carrier came back from a next_key cursor as a decimal STRING and hit from_variant's validating string arm, which rejected it as too long. That broke next_key -> lower_bound for every non-canonical slug key >= 2^32 — reachable because token_code is a key field of sysio.reserv::reserves and, per this type's own threat model, plantable. The struct carrier it replaced round-tripped fine, so this was a regression, not a pre-existing gap. Length disambiguates exactly: a canonical slug is at most 8 symbols, a stringified uint64 past 0xffffffff is at least 10 digits. "12345678" still parses as a slug, so `"7"` remains the slug 7 rather than the integer. Also from review of the parent commit: - sysio.epoch_tests was the last writer of the {"value":N} object form, so it passed only through the transitional from_variant arm this change documents as deletable. Converted to the string carrier. - Delete the codename() test wrapper — 11 definitions, 2 lambdas, 305 call sites. fc::variant already takes const char*, std::string and string_view directly, and takes fc::slug_name through to_variant, so the wrapper was identity. Three of its doc comments still described the {"value":N} carrier this campaign replaced. - get_table_test.hpp's tripwire comment named composite_key where it meant slug_name, so it forbade the name the struct actually has — pointing the next author at the rename that would restore the false green. - get_table_tests.cpp asserted in the present tense that the struct fixture is the sysio.chains key shape; that table keys on a leaf now. New tests, each closing a gap a green suite was hiding: - codename_tests: the JSON-text round trip (fc::json appeared nowhere in either changed test file, so every case stopped at the variant layer), and the writable input surface — const char*, std::string, string_view, "..."_s and an fc::slug_name value all landing on one cell. - abi_tests: the built_in_types registration, which had exactly one assertion guarding it anywhere in the tree. - be_key_codec_tests: multi-leaf slug keys. Three of the five registry tables key on 2-3 slugs; every shape here was a single leaf, which cannot observe a field-ordering or offset error. unit_test 1535, plugin_test 295, contracts_unit_test 762, codename_tests 37. Change-Id: I56856a431d42ca21cefe73174f53a9cf6707dda7 --- contracts/tests/emissions_tests.cpp | 31 +- contracts/tests/sysio.chains_tests.cpp | 9 +- contracts/tests/sysio.dispatch_tests.cpp | 127 ++++--- contracts/tests/sysio.dispute_tests.cpp | 19 +- .../tests/sysio.epoch_flushwtdw_tests.cpp | 17 +- contracts/tests/sysio.epoch_tests.cpp | 3 +- contracts/tests/sysio.msgch_chain_tests.cpp | 14 +- contracts/tests/sysio.msgch_tests.cpp | 11 +- contracts/tests/sysio.opreg_tests.cpp | 22 +- contracts/tests/sysio.reserv_tests.cpp | 317 +++++++++--------- contracts/tests/sysio.tokens_tests.cpp | 14 +- contracts/tests/sysio.uwrit_tests.cpp | 45 ++- libraries/libfc/include/fc/slug_name.hpp | 26 +- libraries/libfc/test/test_slug_name.cpp | 84 +++++ tests/get_table_tests.cpp | 3 +- unittests/abi_tests.cpp | 51 +++ unittests/be_key_codec_tests.cpp | 68 ++++ .../get_table_test/get_table_test.hpp | 4 +- 18 files changed, 513 insertions(+), 352 deletions(-) diff --git a/contracts/tests/emissions_tests.cpp b/contracts/tests/emissions_tests.cpp index 2ef918df41..0b5fc3d6bc 100644 --- a/contracts/tests/emissions_tests.cpp +++ b/contracts/tests/emissions_tests.cpp @@ -20,7 +20,6 @@ // - setup_producers(N) now auto-registers each producer as an opreg operator so existing // producer-pay tests pass through the opreg filter without test-level churn. - #include "contracts.hpp" // fp_math.hpp is dependency-free __int128 fixed-point math; reused here so @@ -575,24 +574,21 @@ class sysio_emissions_tester : public tester { const account_name UWRIT = "sysio.uwrit"_n; deploy_reserv(); - auto codename = [](std::string_view value) { - return std::string{value}; - }; BOOST_REQUIRE_EQUAL(success(), push_reserv_action(RESERV, "regreserve"_n, mvo() - ("chain_code", codename("ETH"))("token_code", codename("ETH"))("reserve_code", codename("PRIMARY")) + ("chain_code", "ETH")("token_code", "ETH")("reserve_code", "PRIMARY") ("name", "eth")("description", "") ("initial_chain_amount", 1'000'000'000'000ULL)("initial_wire_amount", 1'000'000'000'000ULL) ("source_token_precision", 9u)("connector_weight_bps", 5000u)("is_private", false)("owner", name{}))); BOOST_REQUIRE_EQUAL(success(), push_reserv_action(RESERV, "regreserve"_n, mvo() - ("chain_code", codename("SOLANA"))("token_code", codename("SOL"))("reserve_code", codename("PRIMARY")) + ("chain_code", "SOLANA")("token_code", "SOL")("reserve_code", "PRIMARY") ("name", "sol")("description", "") ("initial_chain_amount", 1'000'000'000'000ULL)("initial_wire_amount", 1'000'000'000'000ULL) ("source_token_precision", 9u)("connector_weight_bps", 5000u)("is_private", false)("owner", name{}))); BOOST_REQUIRE_EQUAL(success(), push_reserv_action(UWRIT, "applyswap"_n, mvo() - ("src_chain_code", codename("ETH"))("src_token_code", codename("ETH"))("src_reserve_code", codename("PRIMARY")) + ("src_chain_code", "ETH")("src_token_code", "ETH")("src_reserve_code", "PRIMARY") ("src_amount", 1'000'000'000ULL) - ("dst_chain_code", codename("SOLANA"))("dst_token_code", codename("SOL")) - ("dst_reserve_code", codename("PRIMARY")) + ("dst_chain_code", "SOLANA")("dst_token_code", "SOL") + ("dst_reserve_code", "PRIMARY") ("dst_amount", 100'000'000ULL)("underwriter", name{}))); const int64_t balance = reserv_reward_balance(); @@ -5060,13 +5056,11 @@ BOOST_FIXTURE_TEST_CASE( expired_wire_claims_unblock_a_balance_blocked_epoch, sy create_t5_holding_accounts(); deploy_reserv(); - auto codename = [](std::string_view s) { return std::string{s}; }; - // Move real WIRE into reserv custody so a claim has backing. regreserve is bootstrap-window // only, which holds here: current_epoch_index is still 0. constexpr uint64_t RESERVE_SEED = 1'000'000'000'000ULL; BOOST_REQUIRE_EQUAL( success(), push_reserv_action(RESERV, "regreserve"_n, mvo() - ("chain_code", codename("ETH"))("token_code", codename("ETH"))("reserve_code", codename("PRIMARY")) + ("chain_code", "ETH")("token_code", "ETH")("reserve_code", "PRIMARY") ("name", "eth")("description", "") ("initial_chain_amount", RESERVE_SEED)("initial_wire_amount", RESERVE_SEED) ("source_token_precision", 9u)("connector_weight_bps", 5000u)("is_private", false)("owner", name{}) ) ); @@ -5970,18 +5964,13 @@ struct producer_score_tester : public producer_eligibility_tester { /// The tier packed into a `rank_score`, mirroring `producer_rank::tier_of`. static uint64_t tier_of(uint64_t rank_score) { return rank_score >> composite_bits; } - /// A `slug_name` in the shape the ABI serializes it: its decoded string. - static std::string slug_mvo(std::string_view code) { - return std::string{code}; - } - /// One `(chain, token, min_bond)` entry for opreg's `req_*_collat` vectors. The /// `config_timestamp_ms` supplied here is ignored -- `setconfig` overwrites it with on-chain /// time so consumers never trust the caller's clock. static fc::variant min_bond_mvo(std::string_view chain, std::string_view token, uint64_t min_bond) { return fc::variant(mvo() - ("chain_code", slug_mvo(chain)) - ("token_code", slug_mvo(token)) + ("chain_code", chain) + ("token_code", token) ("min_bond", min_bond) ("config_timestamp_ms", uint64_t{0})); } @@ -6022,8 +6011,8 @@ struct producer_score_tester : public producer_eligibility_tester { std::string_view token = collateral_token) { return push_opreg_action(OPREG, "depositinle"_n, mvo() ("account", account) - ("chain_code", slug_mvo(chain)) - ("token_code", slug_mvo(token)) + ("chain_code", chain) + ("token_code", token) ("amount", amount) ("actor_chain", ChainKind::CHAIN_KIND_EVM) ("actor_address", std::vector(20, '\x06')) diff --git a/contracts/tests/sysio.chains_tests.cpp b/contracts/tests/sysio.chains_tests.cpp index 563d4a7809..26cf8c1900 100644 --- a/contracts/tests/sysio.chains_tests.cpp +++ b/contracts/tests/sysio.chains_tests.cpp @@ -26,11 +26,6 @@ using mvo = fc::mutable_variant_object; namespace { -/// A `slug_name` renders in JSON/ABI as its decoded string spelling. -inline std::string codename(std::string_view s) { - return std::string{s}; -} - // Well-formed sample addresses for the accept paths. constexpr auto EVM_OPP = "0x5FbDB2315678afecb367f032d93F642f64180aa3"; // OPP.sol constexpr auto EVM_INBOUND = "0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512"; // OPPInbound.sol @@ -88,7 +83,7 @@ class sysio_chains_tester : public tester { const fc::variant_object& outpost) { return push_chains("regchain"_n, mvo() ("kind", kind) - ("code", codename(code)) + ("code", code) ("external_chain_id", external_chain_id) ("name", std::string(code)) ("description", std::string{}) @@ -97,7 +92,7 @@ class sysio_chains_tester : public tester { action_result setoutpost(std::string_view code, const fc::variant_object& outpost) { return push_chains("setoutpost"_n, mvo() - ("code", codename(code)) + ("code", code) ("outpost", outpost)); } diff --git a/contracts/tests/sysio.dispatch_tests.cpp b/contracts/tests/sysio.dispatch_tests.cpp index fe7f87e30b..5b8cf72a9d 100644 --- a/contracts/tests/sysio.dispatch_tests.cpp +++ b/contracts/tests/sysio.dispatch_tests.cpp @@ -59,11 +59,6 @@ constexpr uint32_t PROTOBUF_VARINT_PAYLOAD_BITS = 7u; constexpr uint8_t PROTOBUF_VARINT_CONTINUATION_BIT = 0x80u; constexpr uint32_t PROTOBUF_FIELD_TAG_SHIFT = 3u; -/// SlugName mvo helper for v6 action arguments. -inline std::string codename(std::string_view s) { - return std::string{s}; -} - /** Append one unsigned protobuf varint to a hostile-wire-format fixture. */ void append_proto_varint(std::vector& out, uint64_t value) { do { @@ -101,8 +96,8 @@ inline fc::variant chain_min_bond_mvo(std::string_view chain_code, std::string_view token_code, uint64_t min_bond) { return fc::variant(mvo() - ("chain_code", codename(chain_code)) - ("token_code", codename(token_code)) + ("chain_code", chain_code) + ("token_code", token_code) ("min_bond", min_bond) ("config_timestamp_ms", uint64_t{0})); } @@ -536,7 +531,7 @@ class sysio_dispatch_tester : public tester { BOOST_REQUIRE_EQUAL(success(), push(CHAINS_ACCOUNT, chains_abi, CHAINS_ACCOUNT, "regchain"_n, mvo() ("kind", outpost_kind) - ("code", codename(outpost_code)) + ("code", outpost_code) ("external_chain_id", 31337) ("name", std::string("outpost-test")) ("description", std::string{}) @@ -666,8 +661,8 @@ class sysio_dispatch_tester : public tester { // depositinle does require_auth(get_self()); sign as opreg for a direct call. return push(OPREG_ACCOUNT, opreg_abi, OPREG_ACCOUNT, "depositinle"_n, mvo() ("account", account.to_string()) - ("chain_code", codename(chain_code)) - ("token_code", codename(token_code)) + ("chain_code", chain_code) + ("token_code", token_code) ("amount", amount) ("actor_chain", ChainKind::CHAIN_KIND_EVM) ("actor_address", std::vector(20, '\x06')) @@ -692,9 +687,9 @@ class sysio_dispatch_tester : public tester { ("uwreq_id", uwreq_id) ("underwriter", underwriter.to_string()) ("chain_code", outpost_chain_code) - ("from_chain_code", codename(from_chain)) - ("from_token_code", codename(from_token)) - ("reserve_code", codename(reserve)) + ("from_chain_code", from_chain) + ("from_token_code", from_token) + ("reserve_code", reserve) ("uic_bytes", uic_bytes)); } @@ -707,9 +702,9 @@ class sysio_dispatch_tester : public tester { ("uwreq_id", uwreq_id) ("underwriter", underwriter.to_string()) ("chain_code", outpost_chain_code) - ("from_chain_code", codename(from_chain)) - ("from_token_code", codename(from_token)) - ("reserve_code", codename(reserve)) + ("from_chain_code", from_chain) + ("from_token_code", from_token) + ("reserve_code", reserve) ("uic_bytes", uic_bytes)); } @@ -1121,8 +1116,8 @@ class sysio_dispatch_tester : public tester { std::string_view token_code, uint64_t amount) { return push(OPREG_ACCOUNT, opreg_abi, UWRIT_ACCOUNT, "releaselock"_n, mvo() ("account", account.to_string()) - ("chain_code", codename(chain_code)) - ("token_code", codename(token_code)) + ("chain_code", chain_code) + ("token_code", token_code) ("amount", amount)); } @@ -1214,9 +1209,9 @@ class sysio_dispatch_tester : public tester { std::string_view r, uint64_t chain_amount, uint64_t wire_amount) { return push(RESERV_ACCOUNT, reserv_abi, RESERV_ACCOUNT, "regreserve"_n, mvo() - ("chain_code", codename(c)) - ("token_code", codename(t)) - ("reserve_code", codename(r)) + ("chain_code", c) + ("token_code", t) + ("reserve_code", r) ("name", std::string(c)) ("description", std::string{}) ("initial_chain_amount", chain_amount) @@ -1233,9 +1228,9 @@ class sysio_dispatch_tester : public tester { action_result debit_reserve_chain(std::string_view c, std::string_view t, std::string_view r, uint64_t amount) { return push(RESERV_ACCOUNT, reserv_abi, UWRIT_ACCOUNT, "debit"_n, mvo() - ("chain_code", codename(c)) - ("token_code", codename(t)) - ("reserve_code", codename(r)) + ("chain_code", c) + ("token_code", t) + ("reserve_code", r) ("amount", amount)); } @@ -1262,7 +1257,7 @@ class sysio_dispatch_tester : public tester { BOOST_REQUIRE_EQUAL(success(), push(CHAINS_ACCOUNT, chains_abi, CHAINS_ACCOUNT, "regchain"_n, mvo() ("kind", ChainKind::CHAIN_KIND_WIRE) - ("code", codename("WIRE")) + ("code", "WIRE") ("external_chain_id", 0) ("name", std::string("wire-depot")) ("description", std::string{}) @@ -1276,7 +1271,7 @@ class sysio_dispatch_tester : public tester { /// must have been called first. void setup_eth_to_sol_uwreq(uint64_t att_id) { BOOST_REQUIRE_EQUAL(success(), push(CHAINS_ACCOUNT, chains_abi, CHAINS_ACCOUNT, "regchain"_n, mvo() - ("kind", ChainKind::CHAIN_KIND_SVM)("code", codename("SOLANA")) + ("kind", ChainKind::CHAIN_KIND_SVM)("code", "SOLANA") ("external_chain_id", 900)("name", std::string("solana-test"))("description", std::string{}) ("outpost", sysio_system::test_support::no_outpost_mvo()))); setup_wire_token_and_reserves(); @@ -1506,7 +1501,7 @@ BOOST_FIXTURE_TEST_CASE(operator_action_mismatched_source_chain_is_dropped, // SOLANA is a real, active outpost, so the ONLY thing wrong with the payloads below is that they // were proven-delivered from ETH rather than SOLANA — the exact WSA-005 forgery. BOOST_REQUIRE_EQUAL(success(), push(CHAINS_ACCOUNT, chains_abi, CHAINS_ACCOUNT, "regchain"_n, mvo() - ("kind", ChainKind::CHAIN_KIND_SVM)("code", codename("SOLANA")) + ("kind", ChainKind::CHAIN_KIND_SVM)("code", "SOLANA") ("external_chain_id", 900)("name", std::string("solana-test"))("description", std::string{}) ("outpost", sysio_system::test_support::no_outpost_mvo()))); @@ -1539,7 +1534,7 @@ BOOST_FIXTURE_TEST_CASE(swap_request_mismatched_source_chain_is_refunded, sysio_dispatch_tester) { try { bootstrap_for_dispatch(); // ETH source outpost BOOST_REQUIRE_EQUAL(success(), push(CHAINS_ACCOUNT, chains_abi, CHAINS_ACCOUNT, "regchain"_n, mvo() - ("kind", ChainKind::CHAIN_KIND_SVM)("code", codename("SOLANA")) + ("kind", ChainKind::CHAIN_KIND_SVM)("code", "SOLANA") ("external_chain_id", 900)("name", std::string("solana-test"))("description", std::string{}) ("outpost", sysio_system::test_support::no_outpost_mvo()))); setup_wire_token_and_reserves(); // ACTIVE ETH/ETH/PRIMARY + SOLANA/SOL/PRIMARY reserves @@ -1576,7 +1571,7 @@ BOOST_FIXTURE_TEST_CASE(swap_request_identical_reserve_identity_is_refunded, sysio_dispatch_tester) { try { bootstrap_for_dispatch(); BOOST_REQUIRE_EQUAL(success(), push(CHAINS_ACCOUNT, chains_abi, CHAINS_ACCOUNT, "regchain"_n, mvo() - ("kind", ChainKind::CHAIN_KIND_SVM)("code", codename("SOLANA")) + ("kind", ChainKind::CHAIN_KIND_SVM)("code", "SOLANA") ("external_chain_id", 900)("name", std::string("solana-test"))("description", std::string{}) ("outpost", sysio_system::test_support::no_outpost_mvo()))); setup_wire_token_and_reserves(); @@ -1610,7 +1605,7 @@ BOOST_FIXTURE_TEST_CASE(underwrite_commit_mismatched_source_chain_is_dropped, sysio_dispatch_tester) { try { bootstrap_for_dispatch(); BOOST_REQUIRE_EQUAL(success(), push(CHAINS_ACCOUNT, chains_abi, CHAINS_ACCOUNT, "regchain"_n, mvo() - ("kind", ChainKind::CHAIN_KIND_SVM)("code", codename("SOLANA")) + ("kind", ChainKind::CHAIN_KIND_SVM)("code", "SOLANA") ("external_chain_id", 900)("name", std::string("solana-test"))("description", std::string{}) ("outpost", sysio_system::test_support::no_outpost_mvo()))); setup_wire_token_and_reserves(); @@ -1669,13 +1664,13 @@ BOOST_FIXTURE_TEST_CASE(underwrite_commit_two_evm_chains_route_per_chain, bootstrap_for_dispatch(); // ETH (EVM) source outpost // A SECOND active EVM chain — same VM family, distinct chain_code. BOOST_REQUIRE_EQUAL(success(), push(CHAINS_ACCOUNT, chains_abi, CHAINS_ACCOUNT, "regchain"_n, mvo() - ("kind", ChainKind::CHAIN_KIND_EVM)("code", codename("POLYGON")) + ("kind", ChainKind::CHAIN_KIND_EVM)("code", "POLYGON") ("external_chain_id", 137)("name", std::string("polygon-test"))("description", std::string{}) ("outpost", sysio_system::test_support::no_outpost_mvo()))); // SOLANA is registered only because the shared reserve-setup helper seeds a // SOLANA/SOL reserve; it is otherwise unused by this two-EVM scenario. BOOST_REQUIRE_EQUAL(success(), push(CHAINS_ACCOUNT, chains_abi, CHAINS_ACCOUNT, "regchain"_n, mvo() - ("kind", ChainKind::CHAIN_KIND_SVM)("code", codename("SOLANA")) + ("kind", ChainKind::CHAIN_KIND_SVM)("code", "SOLANA") ("external_chain_id", 900)("name", std::string("solana-test"))("description", std::string{}) ("outpost", sysio_system::test_support::no_outpost_mvo()))); setup_wire_token_and_reserves(); @@ -1965,7 +1960,7 @@ BOOST_FIXTURE_TEST_CASE(node_owner_reg_from_other_evm_outpost_is_dropped, sysio_ bootstrap_for_dispatch(); // registers "ETH" — an EVM outpost, but NOT the node-owner source // Register the real node-owner source too, so the ONLY thing wrong below is the delivering outpost. BOOST_REQUIRE_EQUAL(success(), push(CHAINS_ACCOUNT, chains_abi, CHAINS_ACCOUNT, "regchain"_n, mvo() - ("kind", ChainKind::CHAIN_KIND_EVM)("code", codename("ETHEREUM")) + ("kind", ChainKind::CHAIN_KIND_EVM)("code", "ETHEREUM") ("external_chain_id", 1)("name", std::string("ethereum-mainnet"))("description", std::string{}) ("outpost", sysio_system::test_support::no_outpost_mvo()))); const auto other_evm = fc::slug_name{"ETH"}.value; // active EVM outpost, but not "ETHEREUM" @@ -2215,7 +2210,7 @@ BOOST_FIXTURE_TEST_CASE(swap_missing_dst_authex_recovers_after_exact_uic_replay, BOOST_REQUIRE_EQUAL(success(), push(CHAINS_ACCOUNT, chains_abi, CHAINS_ACCOUNT, "regchain"_n, mvo() ("kind", ChainKind::CHAIN_KIND_SVM) - ("code", codename("SOLANA")) + ("code", "SOLANA") ("external_chain_id", 900) ("name", std::string("solana-test")) ("description", std::string{}) @@ -2299,7 +2294,7 @@ BOOST_FIXTURE_TEST_CASE(swap_zero_quote_from_active_reserve_fails_closed, BOOST_REQUIRE_EQUAL(success(), push(CHAINS_ACCOUNT, chains_abi, CHAINS_ACCOUNT, "regchain"_n, mvo() ("kind", ChainKind::CHAIN_KIND_SVM) - ("code", codename("SOLANA")) + ("code", "SOLANA") ("external_chain_id", 900) ("name", std::string("solana-test")) ("description", std::string{}) @@ -2538,9 +2533,9 @@ BOOST_FIXTURE_TEST_CASE(swap_slippage_bound_does_not_compound_across_checkpoints // Move the destination reserve so the price walks a second step down: debit // 7% of its token side (UWRIT-authorized, the same primitive settlement uses). BOOST_REQUIRE_EQUAL(success(), push(RESERV_ACCOUNT, reserv_abi, UWRIT_ACCOUNT, "debit"_n, mvo() - ("chain_code", codename("ETH")) - ("token_code", codename("ETH")) - ("reserve_code", codename("SECOND")) + ("chain_code", "ETH") + ("token_code", "ETH") + ("reserve_code", "SECOND") ("amount", uint64_t{70'000'000'000}))); // Pin the scenario: the drift is inside tolerance of the PREVIOUS quote (so @@ -2607,9 +2602,9 @@ BOOST_FIXTURE_TEST_CASE(swap_underbonded_candidate_cannot_terminally_reject, // Drift the price far outside the tolerance — the request IS terminally // doomed, but this candidate must not be the one to close it. BOOST_REQUIRE_EQUAL(success(), push(RESERV_ACCOUNT, reserv_abi, UWRIT_ACCOUNT, "debit"_n, mvo() - ("chain_code", codename("ETH")) - ("token_code", codename("ETH")) - ("reserve_code", codename("SECOND")) + ("chain_code", "ETH") + ("token_code", "ETH") + ("reserve_code", "SECOND") ("amount", uint64_t{500'000'000'000}))); const auto src_uic = create_signed_uic(UWRIT_OP, ATT_ID, eth, eth, primary); @@ -3688,7 +3683,7 @@ BOOST_FIXTURE_TEST_CASE(swap_malformed_destination_signature_preserves_valid_sou BOOST_REQUIRE_EQUAL(success(), push( CHAINS_ACCOUNT, chains_abi, CHAINS_ACCOUNT, "regchain"_n, mvo() ("kind", ChainKind::CHAIN_KIND_SVM) - ("code", codename("SOLANA")) + ("code", "SOLANA") ("external_chain_id", 900) ("name", std::string("solana-test")) ("description", std::string{}) @@ -3809,7 +3804,7 @@ BOOST_FIXTURE_TEST_CASE(swap_forged_claim_cannot_overwrite_honest_candidate, BOOST_REQUIRE_EQUAL(success(), push( CHAINS_ACCOUNT, chains_abi, CHAINS_ACCOUNT, "regchain"_n, mvo() ("kind", ChainKind::CHAIN_KIND_SVM) - ("code", codename("SOLANA")) + ("code", "SOLANA") ("external_chain_id", 900) ("name", std::string("solana-test")) ("description", std::string{}) @@ -4060,7 +4055,7 @@ BOOST_FIXTURE_TEST_CASE(swap_request_malformed_bytes_do_not_abort_consensus_deli sysio_dispatch_tester) { try { bootstrap_for_dispatch(); // ETH source outpost BOOST_REQUIRE_EQUAL(success(), push(CHAINS_ACCOUNT, chains_abi, CHAINS_ACCOUNT, "regchain"_n, mvo() - ("kind", ChainKind::CHAIN_KIND_SVM)("code", codename("SOLANA")) + ("kind", ChainKind::CHAIN_KIND_SVM)("code", "SOLANA") ("external_chain_id", 900)("name", std::string("solana-test"))("description", std::string{}) ("outpost", sysio_system::test_support::no_outpost_mvo()))); setup_wire_token_and_reserves(); @@ -4117,7 +4112,7 @@ BOOST_FIXTURE_TEST_CASE(createuwreq_duplicate_attestation_id_is_idempotent, sysio_dispatch_tester) { try { bootstrap_for_dispatch(); BOOST_REQUIRE_EQUAL(success(), push(CHAINS_ACCOUNT, chains_abi, CHAINS_ACCOUNT, "regchain"_n, mvo() - ("kind", ChainKind::CHAIN_KIND_SVM)("code", codename("SOLANA")) + ("kind", ChainKind::CHAIN_KIND_SVM)("code", "SOLANA") ("external_chain_id", 900)("name", std::string("solana-test"))("description", std::string{}) ("outpost", sysio_system::test_support::no_outpost_mvo()))); setup_wire_token_and_reserves(); @@ -4243,7 +4238,7 @@ BOOST_FIXTURE_TEST_CASE(swap_request_negative_source_is_reverted, BOOST_REQUIRE_EQUAL(success(), push(CHAINS_ACCOUNT, chains_abi, CHAINS_ACCOUNT, "regchain"_n, mvo() ("kind", ChainKind::CHAIN_KIND_SVM) - ("code", codename("SOLANA")) + ("code", "SOLANA") ("external_chain_id", 900) ("name", std::string("solana-test")) ("description", std::string{}) @@ -4363,7 +4358,7 @@ BOOST_FIXTURE_TEST_CASE(swap_race_time_reserve_drain_rejects_request, BOOST_REQUIRE_EQUAL(success(), push(CHAINS_ACCOUNT, chains_abi, CHAINS_ACCOUNT, "regchain"_n, mvo() ("kind", ChainKind::CHAIN_KIND_SVM) - ("code", codename("SOLANA")) + ("code", "SOLANA") ("external_chain_id", 900) ("name", std::string("solana-test")) ("description", std::string{}) @@ -4437,7 +4432,7 @@ BOOST_FIXTURE_TEST_CASE(swap_replayed_uic_variance_drift_rejects_request, BOOST_REQUIRE_EQUAL(success(), push(CHAINS_ACCOUNT, chains_abi, CHAINS_ACCOUNT, "regchain"_n, mvo() ("kind", ChainKind::CHAIN_KIND_SVM) - ("code", codename("SOLANA")) + ("code", "SOLANA") ("external_chain_id", 900) ("name", std::string("solana-test")) ("description", std::string{}) @@ -4778,9 +4773,9 @@ BOOST_FIXTURE_TEST_CASE(drainfwq_bounds_rows_per_epoch, sysio_dispatch_tester) { push(UWRIT_ACCOUNT, uwrit_abi, "swapuser"_n, "swapfromwire"_n, mvo() ("user", "swapuser") ("wire_amount", uint64_t{1'000'000} + i) - ("dst_chain_code", codename("ETH")) - ("dst_token_code", codename("ETH")) - ("dst_reserve_code", codename("PRIMARY")) + ("dst_chain_code", "ETH") + ("dst_token_code", "ETH") + ("dst_reserve_code", "PRIMARY") ("target_amount", uint64_t{1'000'000}) ("target_tolerance_bps", uint32_t{10000}) ("recipient_kind", sysio::opp::types::ChainKind::CHAIN_KIND_EVM) @@ -4836,9 +4831,9 @@ BOOST_FIXTURE_TEST_CASE(swapfromwire_enforces_min_amount, sysio_dispatch_tester) return push(UWRIT_ACCOUNT, uwrit_abi, "swapuser"_n, "swapfromwire"_n, mvo() ("user", "swapuser") ("wire_amount", wire_amount) - ("dst_chain_code", codename("ETH")) - ("dst_token_code", codename("ETH")) - ("dst_reserve_code", codename("PRIMARY")) + ("dst_chain_code", "ETH") + ("dst_token_code", "ETH") + ("dst_reserve_code", "PRIMARY") ("target_amount", uint64_t{1'000'000}) ("target_tolerance_bps", uint32_t{10000}) ("recipient_kind", sysio::opp::types::ChainKind::CHAIN_KIND_EVM) @@ -4907,9 +4902,9 @@ BOOST_FIXTURE_TEST_CASE(drainfwq_charges_revert_fee_on_caller_fault, sysio_dispa push(UWRIT_ACCOUNT, uwrit_abi, "swapuser"_n, "swapfromwire"_n, mvo() ("user", "swapuser") ("wire_amount", ESCROW) - ("dst_chain_code", codename("ETH")) - ("dst_token_code", codename("ETH")) - ("dst_reserve_code", codename("PRIMARY")) + ("dst_chain_code", "ETH") + ("dst_token_code", "ETH") + ("dst_reserve_code", "PRIMARY") ("target_amount", uint64_t{1}) ("target_tolerance_bps", uint32_t{0}) ("recipient_kind", sysio::opp::types::ChainKind::CHAIN_KIND_EVM) @@ -4967,9 +4962,9 @@ BOOST_FIXTURE_TEST_CASE(drainfwq_full_refund_on_system_caused_revert, sysio_disp push(UWRIT_ACCOUNT, uwrit_abi, "swapuser"_n, "swapfromwire"_n, mvo() ("user", "swapuser") ("wire_amount", ESCROW) - ("dst_chain_code", codename("ETH")) - ("dst_token_code", codename("ETH")) - ("dst_reserve_code", codename("PRIMARY")) + ("dst_chain_code", "ETH") + ("dst_token_code", "ETH") + ("dst_reserve_code", "PRIMARY") ("target_amount", uint64_t{1'000'000}) ("target_tolerance_bps", uint32_t{10000}) ("recipient_kind", sysio::opp::types::ChainKind::CHAIN_KIND_EVM) @@ -5027,9 +5022,9 @@ BOOST_FIXTURE_TEST_CASE(blocking_refund_recipient_cannot_stall_drainfwq, sysio_d push(UWRIT_ACCOUNT, uwrit_abi, "swapuser"_n, "swapfromwire"_n, mvo() ("user", "swapuser") ("wire_amount", ESCROW) - ("dst_chain_code", codename("ETH")) - ("dst_token_code", codename("ETH")) - ("dst_reserve_code", codename("PRIMARY")) + ("dst_chain_code", "ETH") + ("dst_token_code", "ETH") + ("dst_reserve_code", "PRIMARY") ("target_amount", uint64_t{1'000'000}) ("target_tolerance_bps", uint32_t{10000}) ("recipient_kind", sysio::opp::types::ChainKind::CHAIN_KIND_EVM) @@ -5168,9 +5163,9 @@ BOOST_FIXTURE_TEST_CASE(uwreq_from_wire_pending_timeout_refunds_escrow, "swapfromwire"_n, mvo() ("user", "swapuser") ("wire_amount", ESCROW) - ("dst_chain_code", codename("ETH")) - ("dst_token_code", codename("ETH")) - ("dst_reserve_code", codename("PRIMARY")) + ("dst_chain_code", "ETH") + ("dst_token_code", "ETH") + ("dst_reserve_code", "PRIMARY") ("target_amount", uint64_t{1'000'000}) ("target_tolerance_bps", uint32_t{10000}) ("recipient_kind", ChainKind::CHAIN_KIND_EVM) diff --git a/contracts/tests/sysio.dispute_tests.cpp b/contracts/tests/sysio.dispute_tests.cpp index 5945a2dd3f..ab760df361 100644 --- a/contracts/tests/sysio.dispute_tests.cpp +++ b/contracts/tests/sysio.dispute_tests.cpp @@ -50,11 +50,6 @@ using mvo = fc::mutable_variant_object; namespace { -/// SlugName mvo helper for v6 chain-registry action arguments. -inline std::string codename(std::string_view s) { - return std::string{s}; -} - /// Build an `authority` whose active permission is the account's own active key plus a list of /// `{actor, sysio.code}` co-signers — lets the listed contracts authorize inline actions as `account`. authority active_with_code_authors(name account, const std::vector& code_authors) { @@ -213,7 +208,7 @@ class sysio_dispute_tester : public tester { ("is_bootstrapped", true))); BOOST_REQUIRE_EQUAL(success(), push(CHAINS_ACCOUNT, chains_abi, CHAINS_ACCOUNT, "regchain"_n, mvo() - ("kind", ChainKind::CHAIN_KIND_EVM)("code", codename("ETH")) + ("kind", ChainKind::CHAIN_KIND_EVM)("code", "ETH") ("external_chain_id", 31337)("name", std::string("ethereum-test"))("description", std::string{}) ("outpost", sysio_system::test_support::no_outpost_mvo()))); @@ -369,15 +364,15 @@ class sysio_dispute_tester : public tester { static fc::variant make_chain_min_bond(std::string_view chain_code, std::string_view token_code, uint64_t min_bond) { return fc::variant(mvo() - ("chain_code", codename(chain_code))("token_code", codename(token_code)) + ("chain_code", chain_code)("token_code", token_code) ("min_bond", min_bond)("config_timestamp_ms", uint64_t{0})); } action_result depositinle(name account, std::string_view chain_code, std::string_view token_code, uint64_t amount) { return push(OPREG_ACCOUNT, opreg_abi, OPREG_ACCOUNT, "depositinle"_n, mvo() - ("account", account.to_string())("chain_code", codename(chain_code)) - ("token_code", codename(token_code))("amount", amount) + ("account", account.to_string())("chain_code", chain_code) + ("token_code", token_code)("amount", amount) ("actor_chain", ChainKind::CHAIN_KIND_EVM)("actor_address", std::vector{}) ("original_message_id", std::string(64, '0'))); } @@ -385,8 +380,8 @@ class sysio_dispute_tester : public tester { action_result withdrawinle(name account, std::string_view chain_code, std::string_view token_code, uint64_t amount) { return push(OPREG_ACCOUNT, opreg_abi, OPREG_ACCOUNT, "withdrawinle"_n, mvo() - ("account", account.to_string())("chain_code", codename(chain_code)) - ("token_code", codename(token_code))("amount", amount)); + ("account", account.to_string())("chain_code", chain_code) + ("token_code", token_code)("amount", amount)); } action_result flushwtdw(uint32_t up_to_epoch) { @@ -903,7 +898,7 @@ BOOST_FIXTURE_TEST_CASE(chkdispute_unpauses_only_after_last_open_dispute, sysio_ // A second EVM outpost (distinct external_chain_id) so a second (outpost, epoch) dispute can // exist concurrently with the ETH one. BOOST_REQUIRE_EQUAL(success(), push(CHAINS_ACCOUNT, chains_abi, CHAINS_ACCOUNT, "regchain"_n, mvo() - ("kind", ChainKind::CHAIN_KIND_EVM)("code", codename("BASE")) + ("kind", ChainKind::CHAIN_KIND_EVM)("code", "BASE") ("external_chain_id", 8453)("name", std::string("base-test"))("description", std::string{}) ("outpost", sysio_system::test_support::no_outpost_mvo()))); const uint64_t base_code = fc::slug_name{"BASE"}.value; diff --git a/contracts/tests/sysio.epoch_flushwtdw_tests.cpp b/contracts/tests/sysio.epoch_flushwtdw_tests.cpp index 9c4f0a390c..e0fe9ff674 100644 --- a/contracts/tests/sysio.epoch_flushwtdw_tests.cpp +++ b/contracts/tests/sysio.epoch_flushwtdw_tests.cpp @@ -130,7 +130,6 @@ class sysio_epoch_flushwtdw_tester : public tester { abi_serializer::create_yield_function(abi_serializer_max_time)); } - void deploy(name account, std::vector wasm, std::vector abi, abi_serializer& out_ser) { set_code(account, wasm); @@ -145,9 +144,6 @@ class sysio_epoch_flushwtdw_tester : public tester { } static fc::slug_name cn(std::string_view s) { return fc::slug_name{s}; } - static std::string codename(std::string_view s) { - return std::string{s}; - } /// Push an action against any deployed contract. action_result push(name contract, abi_serializer& ser, name signer, @@ -213,7 +209,6 @@ class sysio_epoch_flushwtdw_tester : public tester { "initt5"_n, mvo() ("start_time", fc::time_point_sec(control->head().block_time())))); - BOOST_REQUIRE_EQUAL(success(), push(OPREG_ACCOUNT, opreg_abi, OPREG_ACCOUNT, "setconfig"_n, mvo() ("max_available_producers", 21) @@ -240,7 +235,7 @@ class sysio_epoch_flushwtdw_tester : public tester { BOOST_REQUIRE_EQUAL(success(), push(CHAINS_ACCOUNT, chains_abi, CHAINS_ACCOUNT, "regchain"_n, mvo() ("kind", ChainKind::CHAIN_KIND_SVM) - ("code", codename("SOL")) + ("code", "SOL") ("external_chain_id", 1) ("name", std::string("solana-test")) ("description", std::string{}) @@ -248,7 +243,7 @@ class sysio_epoch_flushwtdw_tester : public tester { BOOST_REQUIRE_EQUAL(success(), push(CHAINS_ACCOUNT, chains_abi, CHAINS_ACCOUNT, "regchain"_n, mvo() ("kind", ChainKind::CHAIN_KIND_EVM) - ("code", codename("ETH")) + ("code", "ETH") ("external_chain_id", 31337) ("name", std::string("ethereum-test")) ("description", std::string{}) @@ -299,8 +294,8 @@ class sysio_epoch_flushwtdw_tester : public tester { std::string_view token_code, uint64_t amount) { return push(OPREG_ACCOUNT, opreg_abi, OPREG_ACCOUNT, "depositinle"_n, mvo() ("account", account.to_string()) - ("chain_code", codename(chain_code)) - ("token_code", codename(token_code)) + ("chain_code", chain_code) + ("token_code", token_code) ("amount", amount) ("actor_chain", ChainKind::CHAIN_KIND_EVM) ("actor_address", std::vector{}) @@ -311,8 +306,8 @@ class sysio_epoch_flushwtdw_tester : public tester { std::string_view token_code, uint64_t amount) { return push(OPREG_ACCOUNT, opreg_abi, OPREG_ACCOUNT, "withdrawinle"_n, mvo() ("account", account.to_string()) - ("chain_code", codename(chain_code)) - ("token_code", codename(token_code)) + ("chain_code", chain_code) + ("token_code", token_code) ("amount", amount)); } diff --git a/contracts/tests/sysio.epoch_tests.cpp b/contracts/tests/sysio.epoch_tests.cpp index 4b1bf95b85..95a3797c3d 100644 --- a/contracts/tests/sysio.epoch_tests.cpp +++ b/contracts/tests/sysio.epoch_tests.cpp @@ -104,10 +104,9 @@ class sysio_epoch_tester : public tester { uint32_t external_chain_id, const std::string& name_str = "test outpost", const std::string& description = "") { - auto code_v = fc::slug_name{code_str}; return push_chains_action(CHAINS_ACCOUNT, "regchain"_n, mvo() ("kind", kind) - ("code", mvo()("value", code_v.value)) + ("code", code_str) ("external_chain_id", external_chain_id) ("name", name_str) ("description", description) diff --git a/contracts/tests/sysio.msgch_chain_tests.cpp b/contracts/tests/sysio.msgch_chain_tests.cpp index 0b36555ef3..9b92d34645 100644 --- a/contracts/tests/sysio.msgch_chain_tests.cpp +++ b/contracts/tests/sysio.msgch_chain_tests.cpp @@ -50,10 +50,6 @@ using mvo = fc::mutable_variant_object; namespace { -inline std::string codename(std::string_view s) { - return std::string{s}; -} - using fc::slug_name_literals::operator""_s; constexpr uint64_t ETH_OUTPOST_ID = "ETH"_s.value; @@ -420,7 +416,7 @@ class sysio_msgch_chain_tester : public tester { BOOST_REQUIRE_EQUAL(success(), push(CHAINS_ACCOUNT, chains_abi, CHAINS_ACCOUNT, "regchain"_n, mvo() ("kind", kind) - ("code", codename(code)) + ("code", code) ("external_chain_id", chain_id) ("name", std::string("outpost-test")) ("description", std::string{}) @@ -798,8 +794,8 @@ class sysio_msgch_chain_tester : public tester { static fc::variant make_chain_min_bond(std::string_view chain_code, std::string_view token_code, uint64_t min_bond) { return fc::variant(mvo() - ("chain_code", codename(chain_code)) - ("token_code", codename(token_code)) + ("chain_code", chain_code) + ("token_code", token_code) ("min_bond", min_bond) ("config_timestamp_ms", uint64_t{0})); } @@ -845,8 +841,8 @@ class sysio_msgch_chain_tester : public tester { opp::types::ChainKind actor_chain = opp::types::ChainKind::CHAIN_KIND_EVM) { return push(OPREG_ACCOUNT, opreg_abi, OPREG_ACCOUNT, "depositinle"_n, mvo() ("account", account.to_string()) - ("chain_code", codename(chain_code)) - ("token_code", codename(token_code)) + ("chain_code", chain_code) + ("token_code", token_code) ("amount", amount) ("actor_chain", actor_chain) ("actor_address", std::vector{}) diff --git a/contracts/tests/sysio.msgch_tests.cpp b/contracts/tests/sysio.msgch_tests.cpp index 2ecd13e4cc..a5c198d9f2 100644 --- a/contracts/tests/sysio.msgch_tests.cpp +++ b/contracts/tests/sysio.msgch_tests.cpp @@ -19,12 +19,6 @@ using mvo = fc::mutable_variant_object; namespace { -/// Build a slug_name mvo: `{"value": }` matches the ABI surface for -/// `sysio::slug_name` fields. -inline std::string codename(std::string_view s) { - return std::string{s}; -} - } // anonymous namespace class sysio_msgch_tester : public tester { @@ -98,7 +92,7 @@ class sysio_msgch_tester : public tester { uint32_t chain_id) { base_tester::push_action(CHAINS_ACCOUNT, "regchain"_n, CHAINS_ACCOUNT, mvo() ("kind", kind) - ("code", codename(code)) + ("code", code) ("external_chain_id", chain_id) ("name", std::string("outpost")) ("description", std::string{}) @@ -268,7 +262,7 @@ class sysio_msgch_envlog_tester : public tester { BOOST_REQUIRE_EQUAL(success(), push_action(CHAINS_ACCOUNT, CHAINS_ACCOUNT, "regchain"_n, mvo() ("kind", kind) - ("code", codename(code)) + ("code", code) ("external_chain_id", chain_id) ("name", std::string("outpost")) ("description", std::string{}) @@ -365,7 +359,6 @@ constexpr uint64_t SOL_OUTPOST_ID = "SOL"_s.value; constexpr auto EVM_TEST_ATTESTATION_TYPE = opp::types::ATTESTATION_TYPE_OPERATORS; constexpr auto SWAP_REMIT_ATTESTATION_TYPE = opp::types::ATTESTATION_TYPE_SWAP_REMIT; - } // anonymous namespace /// Smoke: queueout + buildenv writes one row to `envlog` with the diff --git a/contracts/tests/sysio.opreg_tests.cpp b/contracts/tests/sysio.opreg_tests.cpp index fcfa7f4bac..d919adbadb 100644 --- a/contracts/tests/sysio.opreg_tests.cpp +++ b/contracts/tests/sysio.opreg_tests.cpp @@ -251,12 +251,6 @@ class sysio_opreg_tester : public tester { static fc::slug_name cn(std::string_view s) { return fc::slug_name{s}; } - /// A slug_name action argument: the ABI builtin takes the decoded string - /// spelling, so the codename is passed through as-is. - static std::string codename(std::string_view s) { - return std::string{s}; - } - // ── Action helpers ── action_result push_opreg_action(name signer, name action_name, const variant_object& data) { @@ -297,8 +291,8 @@ class sysio_opreg_tester : public tester { std::string_view token_code, uint64_t min_bond) { return fc::variant(mvo() - ("chain_code", codename(chain_code)) - ("token_code", codename(token_code)) + ("chain_code", chain_code) + ("token_code", token_code) ("min_bond", min_bond) ("config_timestamp_ms", uint64_t{0})); } @@ -406,8 +400,8 @@ class sysio_opreg_tester : public tester { const std::string& original_message_id_hex = std::string(64, '0')) { return push_opreg_action(OPREG_ACCOUNT, "depositinle"_n, mvo() ("account", account) - ("chain_code", codename(chain_code)) - ("token_code", codename(token_code)) + ("chain_code", chain_code) + ("token_code", token_code) ("amount", amount) ("actor_chain", actor_chain) ("actor_address", actor_address) @@ -420,8 +414,8 @@ class sysio_opreg_tester : public tester { uint64_t amount) { return push_opreg_action(OPREG_ACCOUNT, "withdrawinle"_n, mvo() ("account", account) - ("chain_code", codename(chain_code)) - ("token_code", codename(token_code)) + ("chain_code", chain_code) + ("token_code", token_code) ("amount", amount)); } @@ -471,8 +465,8 @@ class sysio_opreg_tester : public tester { uint64_t amount) { return push_opreg_action(signer, "releaselock"_n, mvo() ("account", account) - ("chain_code", codename(chain_code)) - ("token_code", codename(token_code)) + ("chain_code", chain_code) + ("token_code", token_code) ("amount", amount)); } diff --git a/contracts/tests/sysio.reserv_tests.cpp b/contracts/tests/sysio.reserv_tests.cpp index 18b02e69fb..a68cacc4de 100644 --- a/contracts/tests/sysio.reserv_tests.cpp +++ b/contracts/tests/sysio.reserv_tests.cpp @@ -135,7 +135,7 @@ class sysio_reserve_tester : public tester { uint32_t external_chain_id) { return push_to(CHAINS_ACCOUNT, chains_abi_ser, CHAINS_ACCOUNT, "regchain"_n, mvo() ("kind", kind) - ("code", codename(code)) + ("code", code) ("external_chain_id", external_chain_id) ("name", std::string("outpost")) ("description", std::string{}) @@ -174,13 +174,13 @@ class sysio_reserve_tester : public tester { std::string_view to_chain, std::string_view to_token, std::string_view to_reserve) { auto trace = tester::push_action(RESERVE_ACCOUNT, "swapquote"_n, RESERVE_ACCOUNT, mvo() - ("from_chain_code", codename(from_chain)) - ("from_token_code", codename(from_token)) - ("from_reserve_code", codename(from_reserve)) + ("from_chain_code", from_chain) + ("from_token_code", from_token) + ("from_reserve_code", from_reserve) ("from_amount", from_amount) - ("to_chain_code", codename(to_chain)) - ("to_token_code", codename(to_token)) - ("to_reserve_code", codename(to_reserve))); + ("to_chain_code", to_chain) + ("to_token_code", to_token) + ("to_reserve_code", to_reserve)); BOOST_REQUIRE(trace && !trace->action_traces.empty()); return fc::raw::unpack(trace->action_traces[0].return_value); } @@ -188,9 +188,6 @@ class sysio_reserve_tester : public tester { // ── SlugName helpers (v6) ── static fc::slug_name cn(std::string_view s) { return fc::slug_name{s}; } - static std::string codename(std::string_view s) { - return std::string{s}; - } /// `regreserve` is the v6 bootstrap-window action for inserting a reserve /// row with `status=ACTIVE` and REAL WIRE backing drained from the @@ -208,9 +205,9 @@ class sysio_reserve_tester : public tester { const std::string& description = "", uint32_t source_token_precision = 9) { return push_action(RESERVE_ACCOUNT, "regreserve"_n, mvo() - ("chain_code", codename(chain_code)) - ("token_code", codename(token_code)) - ("reserve_code", codename(reserve_code)) + ("chain_code", chain_code) + ("token_code", token_code) + ("reserve_code", reserve_code) ("name", name_str) ("description", description) ("initial_chain_amount", initial_chain_amount) @@ -481,9 +478,9 @@ BOOST_FIXTURE_TEST_CASE(regreserve_bounds_metadata, sysio_reserve_tester) { try BOOST_FIXTURE_TEST_CASE(oncrtreserve_requires_msgch_auth, sysio_reserve_tester) { try { BOOST_REQUIRE(push_action(RESERVE_ACCOUNT, "oncrtreserve"_n, mvo() - ("chain_code", codename("ETH")) - ("token_code", codename("ETH")) - ("reserve_code", codename("USERRES")) + ("chain_code", "ETH") + ("token_code", "ETH") + ("reserve_code", "USERRES") ("name", "user reserve") ("description", "") ("external_token_amount", 1000) @@ -504,9 +501,9 @@ BOOST_FIXTURE_TEST_CASE(oncrtreserve_unregistered_chain_soft_skips_before_queueo deploy_msgch(); BOOST_REQUIRE_EQUAL(success(), push_action(MSGCH_ACCOUNT, "oncrtreserve"_n, mvo() - ("chain_code", codename("NOCHAIN")) - ("token_code", codename("ETH")) - ("reserve_code", codename("USERRES")) + ("chain_code", "NOCHAIN") + ("token_code", "ETH") + ("reserve_code", "USERRES") ("name", "user reserve") ("description", "") ("external_token_amount", 1000) @@ -527,9 +524,9 @@ BOOST_FIXTURE_TEST_CASE(oncrtreserve_unlinked_creator_is_cancelled, sysio_reserv // rejected by inserting a CANCELLED row (idempotency + audit) and // queueing RESERVE_CREATE_CANCELLED back. Never throws. BOOST_REQUIRE_EQUAL(success(), push_action(MSGCH_ACCOUNT, "oncrtreserve"_n, mvo() - ("chain_code", codename("ETH")) - ("token_code", codename("ETH")) - ("reserve_code", codename("USERRES")) + ("chain_code", "ETH") + ("token_code", "ETH") + ("reserve_code", "USERRES") ("name", "user reserve") ("description", "") ("external_token_amount", 1000) @@ -564,9 +561,9 @@ BOOST_FIXTURE_TEST_CASE(oncrtreserve_creator_chain_kind_mismatch_is_cancelled, const std::vector creator_address(20, '\x01'); BOOST_REQUIRE_EQUAL(success(), push_action(MSGCH_ACCOUNT, "oncrtreserve"_n, mvo() - ("chain_code", codename("ETH")) - ("token_code", codename("ETH")) - ("reserve_code", codename("USERRES")) + ("chain_code", "ETH") + ("token_code", "ETH") + ("reserve_code", "USERRES") ("name", "mismatched creator") ("description", "") ("external_token_amount", 1000) @@ -606,9 +603,9 @@ BOOST_FIXTURE_TEST_CASE(oncnclrsv_requires_registry_creator_kind, const std::vector creator_address(20, '\x01'); BOOST_REQUIRE_EQUAL(success(), push_action(MSGCH_ACCOUNT, "oncrtreserve"_n, mvo() - ("chain_code", codename("ETH")) - ("token_code", codename("ETH")) - ("reserve_code", codename("CANCEL")) + ("chain_code", "ETH") + ("token_code", "ETH") + ("reserve_code", "CANCEL") ("name", "cancel kind guard") ("description", "") ("external_token_amount", 1000) @@ -625,9 +622,9 @@ BOOST_FIXTURE_TEST_CASE(oncnclrsv_requires_registry_creator_kind, BOOST_REQUIRE_EQUAL("RESERVE_STATUS_PENDING", pending["status"].as_string()); BOOST_REQUIRE_EQUAL(success(), push_action(MSGCH_ACCOUNT, "oncnclrsv"_n, mvo() - ("chain_code", codename("ETH")) - ("token_code", codename("ETH")) - ("reserve_code", codename("CANCEL")) + ("chain_code", "ETH") + ("token_code", "ETH") + ("reserve_code", "CANCEL") ("creator_chain_kind", ChainKind::CHAIN_KIND_SVM) ("creator_chain_addr", creator_address))); @@ -637,9 +634,9 @@ BOOST_FIXTURE_TEST_CASE(oncnclrsv_requires_registry_creator_kind, "attestations"_n, 1).empty()); BOOST_REQUIRE_EQUAL(success(), push_action(MSGCH_ACCOUNT, "oncnclrsv"_n, mvo() - ("chain_code", codename("ETH")) - ("token_code", codename("ETH")) - ("reserve_code", codename("CANCEL")) + ("chain_code", "ETH") + ("token_code", "ETH") + ("reserve_code", "CANCEL") ("creator_chain_kind", ChainKind::CHAIN_KIND_EVM) ("creator_chain_addr", creator_address))); @@ -660,9 +657,9 @@ BOOST_FIXTURE_TEST_CASE(oncrtreserve_invalid_creator_address_is_cancelled, const auto creator_key = em_pubkey_bytes(creator_pub); BOOST_REQUIRE_EQUAL(success(), push_action(MSGCH_ACCOUNT, "oncrtreserve"_n, mvo() - ("chain_code", codename("ETH")) - ("token_code", codename("ETH")) - ("reserve_code", codename("BADADDR")) + ("chain_code", "ETH") + ("token_code", "ETH") + ("reserve_code", "BADADDR") ("name", "malformed creator address") ("description", "") ("external_token_amount", 1000) @@ -687,9 +684,9 @@ BOOST_FIXTURE_TEST_CASE(oncrtreserve_invalid_creator_address_is_cancelled, BOOST_FIXTURE_TEST_CASE(oncrtreserve_cancelled_relay_does_not_double_refund, sysio_reserve_tester) { try { auto crt = [&]() { return push_action(MSGCH_ACCOUNT, "oncrtreserve"_n, mvo() - ("chain_code", codename("ETH")) - ("token_code", codename("ETH")) - ("reserve_code", codename("USERRES")) + ("chain_code", "ETH") + ("token_code", "ETH") + ("reserve_code", "USERRES") ("name", "user reserve") ("description", "") ("external_token_amount", 1000) @@ -729,9 +726,9 @@ BOOST_FIXTURE_TEST_CASE(oncrtreserve_cancelled_is_reclaimable_by_linked_creator, // 1) An UNLINKED creator squats the triple → CANCELLED. BOOST_REQUIRE_EQUAL(success(), push_action(MSGCH_ACCOUNT, "oncrtreserve"_n, mvo() - ("chain_code", codename("ETH")) - ("token_code", codename("ETH")) - ("reserve_code", codename("USERRES")) + ("chain_code", "ETH") + ("token_code", "ETH") + ("reserve_code", "USERRES") ("name", "squatter") ("description", "squat") ("external_token_amount", 1000) @@ -756,9 +753,9 @@ BOOST_FIXTURE_TEST_CASE(oncrtreserve_cancelled_is_reclaimable_by_linked_creator, recordlink("alice"_n, ChainKind::CHAIN_KIND_EVM, creator_pub)); BOOST_REQUIRE_EQUAL(success(), push_action(MSGCH_ACCOUNT, "oncrtreserve"_n, mvo() - ("chain_code", codename("ETH")) - ("token_code", codename("ETH")) - ("reserve_code", codename("USERRES")) + ("chain_code", "ETH") + ("token_code", "ETH") + ("reserve_code", "USERRES") ("name", "rightful owner") ("description", "reclaimed") ("external_token_amount", 5000) @@ -802,9 +799,9 @@ BOOST_FIXTURE_TEST_CASE(oncrtreserve_invalid_amount_is_cancelled, sysio_reserve_ // invalid inbound amount). The link is valid, so the amount alone forces the // cancel/refund — proving the amount path no longer drops silently. BOOST_REQUIRE_EQUAL(success(), push_action(MSGCH_ACCOUNT, "oncrtreserve"_n, mvo() - ("chain_code", codename("ETH")) - ("token_code", codename("ETH")) - ("reserve_code", codename("USERRES")) + ("chain_code", "ETH") + ("token_code", "ETH") + ("reserve_code", "USERRES") ("name", "invalid amount") ("description", "") ("external_token_amount", 0) @@ -849,9 +846,9 @@ BOOST_FIXTURE_TEST_CASE(oncrtreserve_oversized_metadata_is_cancelled, sysio_rese const std::string& name, const std::string& description) { return push_action(MSGCH_ACCOUNT, "oncrtreserve"_n, mvo() - ("chain_code", codename("ETH")) - ("token_code", codename("ETH")) - ("reserve_code", codename(reserve_code)) + ("chain_code", "ETH") + ("token_code", "ETH") + ("reserve_code", reserve_code) ("name", name) ("description", description) ("external_token_amount", 1000) @@ -903,9 +900,9 @@ BOOST_FIXTURE_TEST_CASE(matchreserve_rejects_unknown_reserve, sysio_reserve_test BOOST_REQUIRE_EQUAL( error("assertion failure with message: matchreserve: reserve not found"), push_action("alice"_n, "matchreserve"_n, mvo() - ("chain_code", codename("ETH")) - ("token_code", codename("ETH")) - ("reserve_code", codename("NOPE")) + ("chain_code", "ETH") + ("token_code", "ETH") + ("reserve_code", "NOPE") ("matcher", "alice") ("wire_amount", 100))); } FC_LOG_AND_RETHROW() } @@ -917,9 +914,9 @@ BOOST_FIXTURE_TEST_CASE(matchreserve_rejects_non_pending, sysio_reserve_tester) BOOST_REQUIRE_EQUAL( error("assertion failure with message: matchreserve: reserve is not PENDING"), push_action("alice"_n, "matchreserve"_n, mvo() - ("chain_code", codename("ETH")) - ("token_code", codename("ETH")) - ("reserve_code", codename("PRIMARY")) + ("chain_code", "ETH") + ("token_code", "ETH") + ("reserve_code", "PRIMARY") ("matcher", "alice") ("wire_amount", 1000))); } FC_LOG_AND_RETHROW() } @@ -928,13 +925,13 @@ BOOST_FIXTURE_TEST_CASE(matchreserve_rejects_non_pending, sysio_reserve_tester) BOOST_FIXTURE_TEST_CASE(applyswap_requires_uwrit_auth, sysio_reserve_tester) { try { BOOST_REQUIRE(push_action("alice"_n, "applyswap"_n, mvo() - ("src_chain_code", codename("ETH")) - ("src_token_code", codename("ETH")) - ("src_reserve_code", codename("PRIMARY")) + ("src_chain_code", "ETH") + ("src_token_code", "ETH") + ("src_reserve_code", "PRIMARY") ("src_amount", 100) - ("dst_chain_code", codename("SOLANA")) - ("dst_token_code", codename("SOL")) - ("dst_reserve_code", codename("PRIMARY")) + ("dst_chain_code", "SOLANA") + ("dst_token_code", "SOL") + ("dst_reserve_code", "PRIMARY") ("dst_amount", 50) ("underwriter", "underwriter1") ).find("missing authority of sysio.uwrit") != std::string::npos); @@ -948,13 +945,13 @@ BOOST_FIXTURE_TEST_CASE(applyswap_applies_four_legs, sysio_reserve_tester) { try // w = cp_output(1000, 1000, 100) = 1000*100 / (1000+100) = 90 (floor). BOOST_REQUIRE_EQUAL(success(), push_action(UWRIT_ACCOUNT, "applyswap"_n, mvo() - ("src_chain_code", codename("ETH")) - ("src_token_code", codename("ETH")) - ("src_reserve_code", codename("PRIMARY")) + ("src_chain_code", "ETH") + ("src_token_code", "ETH") + ("src_reserve_code", "PRIMARY") ("src_amount", 100) - ("dst_chain_code", codename("SOLANA")) - ("dst_token_code", codename("SOL")) - ("dst_reserve_code", codename("PRIMARY")) + ("dst_chain_code", "SOLANA") + ("dst_token_code", "SOL") + ("dst_reserve_code", "PRIMARY") ("dst_amount", 50) ("underwriter", "underwriter1"))); @@ -984,13 +981,13 @@ BOOST_FIXTURE_TEST_CASE(applyswap_rejects_debit_above_curve_output, sysio_reserv constexpr int64_t CURVE_OUT = 82; auto apply = [&](int64_t dst_amount) { return push_action(UWRIT_ACCOUNT, "applyswap"_n, mvo() - ("src_chain_code", codename("ETH")) - ("src_token_code", codename("ETH")) - ("src_reserve_code", codename("PRIMARY")) + ("src_chain_code", "ETH") + ("src_token_code", "ETH") + ("src_reserve_code", "PRIMARY") ("src_amount", 100) - ("dst_chain_code", codename("SOLANA")) - ("dst_token_code", codename("SOL")) - ("dst_reserve_code", codename("PRIMARY")) + ("dst_chain_code", "SOLANA") + ("dst_token_code", "SOL") + ("dst_reserve_code", "PRIMARY") ("dst_amount", dst_amount) ("underwriter", "underwriter1")); }; @@ -1022,13 +1019,13 @@ BOOST_FIXTURE_TEST_CASE(applyswap_charges_fee_and_routes_50_50, sysio_reserve_te // fee = 999'000'999 * 10 / 10000 = 999'000 ; underwriter = reward = 499'500 ; // net = 999'000'999 - 999'000 = 998'001'999. BOOST_REQUIRE_EQUAL(success(), push_action(UWRIT_ACCOUNT, "applyswap"_n, mvo() - ("src_chain_code", codename("ETH")) - ("src_token_code", codename("ETH")) - ("src_reserve_code", codename("PRIMARY")) + ("src_chain_code", "ETH") + ("src_token_code", "ETH") + ("src_reserve_code", "PRIMARY") ("src_amount", 1'000'000'000ULL) - ("dst_chain_code", codename("SOLANA")) - ("dst_token_code", codename("SOL")) - ("dst_reserve_code", codename("PRIMARY")) + ("dst_chain_code", "SOLANA") + ("dst_token_code", "SOL") + ("dst_reserve_code", "PRIMARY") ("dst_amount", 100'000'000ULL) ("underwriter", "underwriter1"))); @@ -1081,13 +1078,13 @@ BOOST_FIXTURE_TEST_CASE(setconfig_emissions_share_routes_pool_to_treasury, sysio // Same swap as the 50/50 test: fee 999'000, underwriter 499'500, pool 499'500. BOOST_REQUIRE_EQUAL(success(), push_action(UWRIT_ACCOUNT, "applyswap"_n, mvo() - ("src_chain_code", codename("ETH")) - ("src_token_code", codename("ETH")) - ("src_reserve_code", codename("PRIMARY")) + ("src_chain_code", "ETH") + ("src_token_code", "ETH") + ("src_reserve_code", "PRIMARY") ("src_amount", 1'000'000'000ULL) - ("dst_chain_code", codename("SOLANA")) - ("dst_token_code", codename("SOL")) - ("dst_reserve_code", codename("PRIMARY")) + ("dst_chain_code", "SOLANA") + ("dst_token_code", "SOL") + ("dst_reserve_code", "PRIMARY") ("dst_amount", 100'000'000ULL) ("underwriter", "underwriter1"))); @@ -1109,16 +1106,16 @@ BOOST_FIXTURE_TEST_CASE(setrsvfee_guards_owner_status_and_bounds, sysio_reserve_ BOOST_REQUIRE_EQUAL( error("assertion failure with message: setrsvfee: reserve has no owner"), push_action(RESERVE_ACCOUNT, "setrsvfee"_n, mvo() - ("chain_code", codename("ETH"))("token_code", codename("ETH")) - ("reserve_code", codename("PRIMARY"))("owner_fee_bps", 100))); + ("chain_code", "ETH")("token_code", "ETH") + ("reserve_code", "PRIMARY")("owner_fee_bps", 100))); // An OWNED reserve: only the owner may set the fee. BOOST_REQUIRE_EQUAL(success(), regreserve("SOLANA", "SOL", "PRIMARY", 1000, 1000, 5000, false, "alice"_n)); auto setFee = [&](name signer, uint32_t bps) { return push_action(signer, "setrsvfee"_n, mvo() - ("chain_code", codename("SOLANA"))("token_code", codename("SOL")) - ("reserve_code", codename("PRIMARY"))("owner_fee_bps", bps)); + ("chain_code", "SOLANA")("token_code", "SOL") + ("reserve_code", "PRIMARY")("owner_fee_bps", bps)); }; BOOST_REQUIRE(setFee(UNDERWRITER_ACCOUNT, 100).find("missing authority of alice") != std::string::npos); @@ -1137,8 +1134,8 @@ BOOST_FIXTURE_TEST_CASE(setrsvfee_guards_owner_status_and_bounds, sysio_reserve_ BOOST_REQUIRE_EQUAL( error("assertion failure with message: setrsvfee: reserve not found"), push_action("alice"_n, "setrsvfee"_n, mvo() - ("chain_code", codename("ETH"))("token_code", codename("NOPE")) - ("reserve_code", codename("PRIMARY"))("owner_fee_bps", 10))); + ("chain_code", "ETH")("token_code", "NOPE") + ("reserve_code", "PRIMARY")("owner_fee_bps", 10))); } FC_LOG_AND_RETHROW() } BOOST_FIXTURE_TEST_CASE(applyswap_charges_both_reserve_owner_fees, sysio_reserve_tester) { try { @@ -1153,24 +1150,24 @@ BOOST_FIXTURE_TEST_CASE(applyswap_charges_both_reserve_owner_fees, sysio_reserve // src 100 bps (1%), dst 200 bps (2%) on the same WIRE leg. BOOST_REQUIRE_EQUAL(success(), push_action("alice"_n, "setrsvfee"_n, mvo() - ("chain_code", codename("ETH"))("token_code", codename("ETH")) - ("reserve_code", codename("PRIMARY"))("owner_fee_bps", 100))); + ("chain_code", "ETH")("token_code", "ETH") + ("reserve_code", "PRIMARY")("owner_fee_bps", 100))); BOOST_REQUIRE_EQUAL(success(), push_action(UNDERWRITER_ACCOUNT, "setrsvfee"_n, mvo() - ("chain_code", codename("SOLANA"))("token_code", codename("SOL")) - ("reserve_code", codename("PRIMARY"))("owner_fee_bps", 200))); + ("chain_code", "SOLANA")("token_code", "SOL") + ("reserve_code", "PRIMARY")("owner_fee_bps", 200))); const int64_t resv_before = wire_balance(RESERVE_ACCOUNT); // w_gross = 999'000'999. network 10bps = 999'000; src 1% = 9'990'009; // dst 2% = 19'980'019; net = w_gross - (999'000 + 9'990'009 + 19'980'019). BOOST_REQUIRE_EQUAL(success(), push_action(UWRIT_ACCOUNT, "applyswap"_n, mvo() - ("src_chain_code", codename("ETH")) - ("src_token_code", codename("ETH")) - ("src_reserve_code", codename("PRIMARY")) + ("src_chain_code", "ETH") + ("src_token_code", "ETH") + ("src_reserve_code", "PRIMARY") ("src_amount", 1'000'000'000ULL) - ("dst_chain_code", codename("SOLANA")) - ("dst_token_code", codename("SOL")) - ("dst_reserve_code", codename("PRIMARY")) + ("dst_chain_code", "SOLANA") + ("dst_token_code", "SOL") + ("dst_reserve_code", "PRIMARY") ("dst_amount", 100'000'000ULL) ("underwriter", "underwriter1"))); @@ -1198,13 +1195,13 @@ BOOST_FIXTURE_TEST_CASE(single_reserve_paths_charge_only_their_own_side, sysio_r regreserve("ETH", "ETH", "PRIMARY", 1'000'000'000'000ULL, 1'000'000'000'000ULL, 5000, false, "alice"_n)); BOOST_REQUIRE_EQUAL(success(), push_action("alice"_n, "setrsvfee"_n, mvo() - ("chain_code", codename("ETH"))("token_code", codename("ETH")) - ("reserve_code", codename("PRIMARY"))("owner_fee_bps", 100))); + ("chain_code", "ETH")("token_code", "ETH") + ("reserve_code", "PRIMARY")("owner_fee_bps", 100))); BOOST_REQUIRE_EQUAL(success(), push_action(UWRIT_ACCOUNT, "paywire"_n, mvo() - ("src_chain_code", codename("ETH")) - ("src_token_code", codename("ETH")) - ("src_reserve_code", codename("PRIMARY")) + ("src_chain_code", "ETH") + ("src_token_code", "ETH") + ("src_reserve_code", "PRIMARY") ("src_amount", 1'000'000'000ULL) ("recipient", "alice") ("wire_out", 100'000'000ULL) @@ -1219,12 +1216,12 @@ BOOST_FIXTURE_TEST_CASE(single_reserve_paths_charge_only_their_own_side, sysio_r regreserve("SOLANA", "SOL", "PRIMARY", 1'000'000'000'000ULL, 1'000'000'000'000ULL, 5000, false, UNDERWRITER_ACCOUNT)); BOOST_REQUIRE_EQUAL(success(), push_action(UNDERWRITER_ACCOUNT, "setrsvfee"_n, mvo() - ("chain_code", codename("SOLANA"))("token_code", codename("SOL")) - ("reserve_code", codename("PRIMARY"))("owner_fee_bps", 200))); + ("chain_code", "SOLANA")("token_code", "SOL") + ("reserve_code", "PRIMARY")("owner_fee_bps", 200))); BOOST_REQUIRE_EQUAL(success(), push_action(UWRIT_ACCOUNT, "applyfromwire"_n, mvo() - ("dst_chain_code", codename("SOLANA")) - ("dst_token_code", codename("SOL")) - ("dst_reserve_code", codename("PRIMARY")) + ("dst_chain_code", "SOLANA") + ("dst_token_code", "SOL") + ("dst_reserve_code", "PRIMARY") ("wire_in", 1'000'000'000ULL) ("dst_amount", 100'000'000ULL) ("underwriter", "underwriter1"))); @@ -1243,20 +1240,20 @@ BOOST_FIXTURE_TEST_CASE(claimrsvfee_pays_owner_and_guards_auth, sysio_reserve_te auto claim = [&](name signer) { return push_action(signer, "claimrsvfee"_n, mvo() - ("chain_code", codename("ETH"))("token_code", codename("ETH")) - ("reserve_code", codename("PRIMARY"))); + ("chain_code", "ETH")("token_code", "ETH") + ("reserve_code", "PRIMARY")); }; // Nothing earned yet. BOOST_REQUIRE_EQUAL( error("assertion failure with message: claimrsvfee: no unclaimed balance"), claim("alice"_n)); BOOST_REQUIRE_EQUAL(success(), push_action("alice"_n, "setrsvfee"_n, mvo() - ("chain_code", codename("ETH"))("token_code", codename("ETH")) - ("reserve_code", codename("PRIMARY"))("owner_fee_bps", 100))); + ("chain_code", "ETH")("token_code", "ETH") + ("reserve_code", "PRIMARY")("owner_fee_bps", 100))); BOOST_REQUIRE_EQUAL(success(), push_action(UWRIT_ACCOUNT, "paywire"_n, mvo() - ("src_chain_code", codename("ETH")) - ("src_token_code", codename("ETH")) - ("src_reserve_code", codename("PRIMARY")) + ("src_chain_code", "ETH") + ("src_token_code", "ETH") + ("src_reserve_code", "PRIMARY") ("src_amount", 1'000'000'000ULL) ("recipient", UNDERWRITER_ACCOUNT) ("wire_out", 100'000'000ULL) @@ -1316,11 +1313,11 @@ BOOST_FIXTURE_TEST_CASE(swapquote_prices_the_reserve_owner_fees, sysio_reserve_t BOOST_CHECK_EQUAL(before, expected(0, 0)); BOOST_REQUIRE_EQUAL(success(), push_action("alice"_n, "setrsvfee"_n, mvo() - ("chain_code", codename("ETH"))("token_code", codename("ETH")) - ("reserve_code", codename("PRIMARY"))("owner_fee_bps", OWNER_FEE))); + ("chain_code", "ETH")("token_code", "ETH") + ("reserve_code", "PRIMARY")("owner_fee_bps", OWNER_FEE))); BOOST_REQUIRE_EQUAL(success(), push_action(UNDERWRITER_ACCOUNT, "setrsvfee"_n, mvo() - ("chain_code", codename("SOLANA"))("token_code", codename("SOL")) - ("reserve_code", codename("PRIMARY"))("owner_fee_bps", OWNER_FEE))); + ("chain_code", "SOLANA")("token_code", "SOL") + ("reserve_code", "PRIMARY")("owner_fee_bps", OWNER_FEE))); produce_block(); // Both owner fees are now priced in, off the same gross WIRE leg. @@ -1333,8 +1330,8 @@ BOOST_FIXTURE_TEST_CASE(swapquote_prices_the_reserve_owner_fees, sysio_reserve_t // by only that side's share, which a quote summing the wrong reserve's rate // (or double-counting one) would not reproduce. BOOST_REQUIRE_EQUAL(success(), push_action(UNDERWRITER_ACCOUNT, "setrsvfee"_n, mvo() - ("chain_code", codename("SOLANA"))("token_code", codename("SOL")) - ("reserve_code", codename("PRIMARY"))("owner_fee_bps", 0))); + ("chain_code", "SOLANA")("token_code", "SOL") + ("reserve_code", "PRIMARY")("owner_fee_bps", 0))); produce_block(); const uint64_t source_only = swapquote_value("ETH", "ETH", "PRIMARY", FROM, "SOLANA", "SOL", "PRIMARY"); @@ -1363,13 +1360,13 @@ BOOST_FIXTURE_TEST_CASE(claimuwfee_pays_accrual_and_zeroes_balance, sysio_reserv BOOST_REQUIRE_EQUAL(success(), regreserve("SOLANA", "SOL", "PRIMARY", 1'000'000'000'000ULL, 1'000'000'000'000ULL)); BOOST_REQUIRE_EQUAL(success(), push_action(UWRIT_ACCOUNT, "applyswap"_n, mvo() - ("src_chain_code", codename("ETH")) - ("src_token_code", codename("ETH")) - ("src_reserve_code", codename("PRIMARY")) + ("src_chain_code", "ETH") + ("src_token_code", "ETH") + ("src_reserve_code", "PRIMARY") ("src_amount", 1'000'000'000ULL) - ("dst_chain_code", codename("SOLANA")) - ("dst_token_code", codename("SOL")) - ("dst_reserve_code", codename("PRIMARY")) + ("dst_chain_code", "SOLANA") + ("dst_token_code", "SOL") + ("dst_reserve_code", "PRIMARY") ("dst_amount", 100'000'000ULL) ("underwriter", "underwriter1"))); @@ -1421,13 +1418,13 @@ BOOST_FIXTURE_TEST_CASE(applyswap_accrues_per_underwriter_and_accumulates, sysio auto swap_won_by = [&](const char* underwriter) { return push_action(UWRIT_ACCOUNT, "applyswap"_n, mvo() - ("src_chain_code", codename("ETH")) - ("src_token_code", codename("ETH")) - ("src_reserve_code", codename("PRIMARY")) + ("src_chain_code", "ETH") + ("src_token_code", "ETH") + ("src_reserve_code", "PRIMARY") ("src_amount", 1'000'000'000ULL) - ("dst_chain_code", codename("SOLANA")) - ("dst_token_code", codename("SOL")) - ("dst_reserve_code", codename("PRIMARY")) + ("dst_chain_code", "SOLANA") + ("dst_token_code", "SOL") + ("dst_reserve_code", "PRIMARY") ("dst_amount", 100'000'000ULL) ("underwriter", underwriter)); }; @@ -1465,13 +1462,13 @@ BOOST_FIXTURE_TEST_CASE(drainrewards_sweeps_bucket_to_treasury, sysio_reserve_te BOOST_REQUIRE_EQUAL(success(), regreserve("SOLANA", "SOL", "PRIMARY", 1'000'000'000'000ULL, 1'000'000'000'000ULL)); BOOST_REQUIRE_EQUAL(success(), push_action(UWRIT_ACCOUNT, "applyswap"_n, mvo() - ("src_chain_code", codename("ETH")) - ("src_token_code", codename("ETH")) - ("src_reserve_code", codename("PRIMARY")) + ("src_chain_code", "ETH") + ("src_token_code", "ETH") + ("src_reserve_code", "PRIMARY") ("src_amount", 1'000'000'000ULL) - ("dst_chain_code", codename("SOLANA")) - ("dst_token_code", codename("SOL")) - ("dst_reserve_code", codename("PRIMARY")) + ("dst_chain_code", "SOLANA") + ("dst_token_code", "SOL") + ("dst_reserve_code", "PRIMARY") ("dst_amount", 100'000'000ULL) ("underwriter", "underwriter1"))); @@ -1519,9 +1516,9 @@ BOOST_FIXTURE_TEST_CASE(applyfromwire_credits_wire_and_debits_chain, sysio_reser regreserve("SOLANA", "SOL", "PRIMARY", 1000, 1000)); BOOST_REQUIRE_EQUAL(success(), push_action(UWRIT_ACCOUNT, "applyfromwire"_n, mvo() - ("dst_chain_code", codename("SOLANA")) - ("dst_token_code", codename("SOL")) - ("dst_reserve_code", codename("PRIMARY")) + ("dst_chain_code", "SOLANA") + ("dst_token_code", "SOL") + ("dst_reserve_code", "PRIMARY") ("wire_in", 200) ("dst_amount", 100) ("underwriter", "underwriter1"))); @@ -1544,9 +1541,9 @@ BOOST_FIXTURE_TEST_CASE(applyfromwire_rejects_debit_above_curve_output, constexpr int64_t CURVE_OUT = 166; auto apply = [&](int64_t dst_amount) { return push_action(UWRIT_ACCOUNT, "applyfromwire"_n, mvo() - ("dst_chain_code", codename("SOLANA")) - ("dst_token_code", codename("SOL")) - ("dst_reserve_code", codename("PRIMARY")) + ("dst_chain_code", "SOLANA") + ("dst_token_code", "SOL") + ("dst_reserve_code", "PRIMARY") ("wire_in", 200) ("dst_amount", dst_amount) ("underwriter", "underwriter1")); @@ -1577,9 +1574,9 @@ BOOST_FIXTURE_TEST_CASE(paywire_pays_real_wire_from_custody, sysio_reserve_teste // Swap-to-WIRE settlement: source books move + alice is paid REAL WIRE. BOOST_REQUIRE_EQUAL(success(), push_action(UWRIT_ACCOUNT, "paywire"_n, mvo() - ("src_chain_code", codename("ETH")) - ("src_token_code", codename("ETH")) - ("src_reserve_code", codename("PRIMARY")) + ("src_chain_code", "ETH") + ("src_token_code", "ETH") + ("src_reserve_code", "PRIMARY") ("src_amount", 100) ("recipient", "alice") ("wire_out", CURVE_OUT) @@ -1622,9 +1619,9 @@ BOOST_FIXTURE_TEST_CASE(paywire_rejects_payout_above_curve_output, sysio_reserve BOOST_REQUIRE_EQUAL( error("assertion failure with message: paywire: payout exceeds the post-fee WIRE the source leg produced"), push_action(UWRIT_ACCOUNT, "paywire"_n, mvo() - ("src_chain_code", codename("ETH")) - ("src_token_code", codename("ETH")) - ("src_reserve_code", codename("PRIMARY")) + ("src_chain_code", "ETH") + ("src_token_code", "ETH") + ("src_reserve_code", "PRIMARY") ("src_amount", 100) ("recipient", "alice") ("wire_out", 200) @@ -1632,9 +1629,9 @@ BOOST_FIXTURE_TEST_CASE(paywire_rejects_payout_above_curve_output, sysio_reserve // The curve's own output settles cleanly against the same reserve. BOOST_REQUIRE_EQUAL(success(), push_action(UWRIT_ACCOUNT, "paywire"_n, mvo() - ("src_chain_code", codename("ETH")) - ("src_token_code", codename("ETH")) - ("src_reserve_code", codename("PRIMARY")) + ("src_chain_code", "ETH") + ("src_token_code", "ETH") + ("src_reserve_code", "PRIMARY") ("src_amount", 100) ("recipient", "alice") ("wire_out", 9) diff --git a/contracts/tests/sysio.tokens_tests.cpp b/contracts/tests/sysio.tokens_tests.cpp index 38688a9865..6959d07a14 100644 --- a/contracts/tests/sysio.tokens_tests.cpp +++ b/contracts/tests/sysio.tokens_tests.cpp @@ -59,10 +59,6 @@ class sysio_tokens_tester : public tester { } } - static std::string codename(std::string_view s) { - return std::string{s}; - } - /// `sysio.tokens::regtoken` for a chain-native token; the metadata strings are the /// parameters under test, everything else is a fixed valid value. action_result regtoken(std::string_view code, @@ -70,7 +66,7 @@ class sysio_tokens_tester : public tester { const std::string& description) { return push_action(TOKENS_ACCOUNT, "regtoken"_n, mvo() ("kind", TokenKind::TOKEN_KIND_NATIVE) - ("code", codename(code)) + ("code", code) ("symbol_name", symbol_name) ("description", description) ("precision", 9) @@ -122,8 +118,8 @@ BOOST_AUTO_TEST_SUITE(sysio_tokens_tests) BOOST_FIXTURE_TEST_CASE(regctok_records_binding, sysio_tokens_tester) { try { // Native binding. BOOST_REQUIRE_EQUAL(success(), push_action(TOKENS_ACCOUNT, "regctok"_n, mvo() - ("chain_code", codename("ETH")) - ("token_code", codename("WIRE")) + ("chain_code", "ETH") + ("token_code", "WIRE") ("contract_addr", "") ("is_native", true))); @@ -134,8 +130,8 @@ BOOST_FIXTURE_TEST_CASE(regctok_records_binding, sysio_tokens_tester) { try { // Non-native ERC-20 binding with a contract address. BOOST_REQUIRE_EQUAL(success(), push_action(TOKENS_ACCOUNT, "regctok"_n, mvo() - ("chain_code", codename("ETH")) - ("token_code", codename("USDC")) + ("chain_code", "ETH") + ("token_code", "USDC") ("contract_addr", "01") ("is_native", false))); diff --git a/contracts/tests/sysio.uwrit_tests.cpp b/contracts/tests/sysio.uwrit_tests.cpp index e8ea946100..ecf037a293 100644 --- a/contracts/tests/sysio.uwrit_tests.cpp +++ b/contracts/tests/sysio.uwrit_tests.cpp @@ -20,11 +20,6 @@ using mvo = fc::mutable_variant_object; namespace { -/// SlugName mvo helper for v6 action arguments. -inline std::string codename(std::string_view s) { - return std::string{s}; -} - } // anonymous namespace class sysio_uwrit_tester : public tester { @@ -351,9 +346,9 @@ BOOST_FIXTURE_TEST_CASE(swapfromwire_rejects_zero_wire_amount, sysio_uwrit_teste push_uwrit_action("uwrit.a"_n, "swapfromwire"_n, mvo() ("user", "uwrit.a") ("wire_amount", 0) - ("dst_chain_code", codename("SOLANA")) - ("dst_token_code", codename("SOL")) - ("dst_reserve_code", codename("PRIMARY")) + ("dst_chain_code", "SOLANA") + ("dst_token_code", "SOL") + ("dst_reserve_code", "PRIMARY") ("target_amount", 100) ("target_tolerance_bps", 50) ("recipient_kind", sysio::opp::types::ChainKind::CHAIN_KIND_SVM) @@ -370,9 +365,9 @@ BOOST_FIXTURE_TEST_CASE(swapfromwire_rejects_below_minimum, sysio_uwrit_tester) push_uwrit_action("uwrit.a"_n, "swapfromwire"_n, mvo() ("user", "uwrit.a") ("wire_amount", 4'999'999'999ull) - ("dst_chain_code", codename("SOLANA")) - ("dst_token_code", codename("SOL")) - ("dst_reserve_code", codename("PRIMARY")) + ("dst_chain_code", "SOLANA") + ("dst_token_code", "SOL") + ("dst_reserve_code", "PRIMARY") ("target_amount", 100) ("target_tolerance_bps", 50) ("recipient_kind", sysio::opp::types::ChainKind::CHAIN_KIND_SVM) @@ -391,9 +386,9 @@ BOOST_FIXTURE_TEST_CASE(swapfromwire_rejects_unregistered_target_chain, sysio_uw push_uwrit_action("uwrit.a"_n, "swapfromwire"_n, mvo() ("user", "uwrit.a") ("wire_amount", 5'000'000'000ull) - ("dst_chain_code", codename("SOLANA")) - ("dst_token_code", codename("SOL")) - ("dst_reserve_code", codename("PRIMARY")) + ("dst_chain_code", "SOLANA") + ("dst_token_code", "SOL") + ("dst_reserve_code", "PRIMARY") ("target_amount", 100) ("target_tolerance_bps", 50) ("recipient_kind", sysio::opp::types::ChainKind::CHAIN_KIND_SVM) @@ -412,9 +407,9 @@ BOOST_FIXTURE_TEST_CASE(rcrdcommit_requires_msgch_auth, sysio_uwrit_tester) { tr ("uwreq_id", 1) ("underwriter", "uwrit.a") ("chain_code", 1) - ("from_chain_code", codename("ETH")) - ("from_token_code", codename("ETH")) - ("reserve_code", codename("PRIMARY")) + ("from_chain_code", "ETH") + ("from_token_code", "ETH") + ("reserve_code", "PRIMARY") ("uic_bytes", std::vector{}) ).find("missing authority of sysio.msgch") != std::string::npos); } FC_LOG_AND_RETHROW() } @@ -429,9 +424,9 @@ BOOST_FIXTURE_TEST_CASE(rcrdcommit_rejects_unknown_uwreq, sysio_uwrit_tester) { ("uwreq_id", 42) ("underwriter", "uwrit.a") ("chain_code", 1) - ("from_chain_code", codename("ETH")) - ("from_token_code", codename("ETH")) - ("reserve_code", codename("PRIMARY")) + ("from_chain_code", "ETH") + ("from_token_code", "ETH") + ("reserve_code", "PRIMARY") ("uic_bytes", std::vector{}) ) ); @@ -444,8 +439,8 @@ BOOST_FIXTURE_TEST_CASE(sumlocks_zero_for_unbonded_underwriter, sysio_uwrit_test BOOST_REQUIRE_EQUAL(success(), push_uwrit_action("uwrit.a"_n, "sumlocks"_n, mvo() ("underwriter", "uwrit.a") - ("chain_code", codename("ETH")) - ("token_code", codename("ETH")) + ("chain_code", "ETH") + ("token_code", "ETH") ) ); } FC_LOG_AND_RETHROW() } @@ -466,9 +461,9 @@ BOOST_FIXTURE_TEST_CASE(rcrdcommit_same_chain_swap_auth, sysio_uwrit_tester) { t ("uwreq_id", 7) ("underwriter", "uwrit.a") ("chain_code", 1) - ("from_chain_code", codename("ETH")) // src == dst chain - ("from_token_code", codename("USDC")) // distinguishes legs - ("reserve_code", codename("PRIMARY")) + ("from_chain_code", "ETH") // src == dst chain + ("from_token_code", "USDC") // distinguishes legs + ("reserve_code", "PRIMARY") ("uic_bytes", std::vector{}) ).find("missing authority of sysio.msgch") != std::string::npos); } FC_LOG_AND_RETHROW() } diff --git a/libraries/libfc/include/fc/slug_name.hpp b/libraries/libfc/include/fc/slug_name.hpp index 30c3e34322..9b876d265c 100644 --- a/libraries/libfc/include/fc/slug_name.hpp +++ b/libraries/libfc/include/fc/slug_name.hpp @@ -98,9 +98,14 @@ using slug_name_literals::operator""_s; /// from a forgeable attestation payload; a throwing conversion would let one /// such row make an entire table unreadable over `get_table_rows`. /// -/// A JSON integer cannot collide with a JSON string, which is what makes the -/// two carriers unambiguous. A numeric *string* would not: the slug alphabet -/// contains digits, so `"7"` is itself a valid canonical slug. +/// The two carriers are distinguished by JSON *type* here, and by string +/// LENGTH after a round trip through JSON text — `fc::json` quotes a uint64 +/// above 0xffffffff, so the integer arm comes back as a decimal string that +/// `from_variant` re-routes on length (see there). What is never ambiguous is +/// the spelling: a canonical slug is at most max_len symbols and a stringified +/// uint64 past 0xffffffff is at least 10 digits. The carrier could NOT have +/// been a numeric string chosen freely — the slug alphabet contains digits, so +/// `"7"` is itself a valid canonical slug. inline void to_variant(const slug_name& s, fc::variant& v) { const std::string text = s.to_string(); // `pack` is the non-validating encoder, so this is a pure round-trip test: @@ -125,9 +130,22 @@ inline void to_variant(const slug_name& s, fc::variant& v) { /// object form. inline void from_variant(const fc::variant& v, slug_name& s) { if (v.is_string()) { + const std::string_view text = v.get_string(); + // The integer carrier arrives here as a STRING whenever it crossed JSON + // TEXT: fc::json quotes a uint64 above 0xffffffff (fc/io/json.cpp), and + // `next_key` is json text that a paginating caller feeds back as a bound. + // Length disambiguates exactly — a canonical slug is at most max_len + // symbols, while a stringified uint64 past 0xffffffff is at least 10 + // digits — so no valid spelling is diverted. In particular the 8-digit + // "12345678" stays a slug, keeping the rule that `"7"` is the slug 7 and + // not the integer 7. + if (text.size() > static_cast(slug_name_traits::max_len)) { + s = slug_name{ v.as_uint64() }; + return; + } // Validating: the ctor round-trip-checks and rejects a non-canonical or // out-of-alphabet spelling. `""` is the zero sentinel. - s = slug_name{ v.get_string() }; + s = slug_name{ text }; return; } if (v.is_object()) { diff --git a/libraries/libfc/test/test_slug_name.cpp b/libraries/libfc/test/test_slug_name.cpp index bd477100b3..cb06388707 100644 --- a/libraries/libfc/test/test_slug_name.cpp +++ b/libraries/libfc/test/test_slug_name.cpp @@ -3,6 +3,7 @@ #include #include +#include #include #include #include @@ -445,4 +446,87 @@ BOOST_AUTO_TEST_CASE(variant_rejects_a_non_canonical_string_spelling) { BOOST_CHECK_THROW(fc::from_variant(fc::variant(std::string{"TOOOLONGXX"}), back), fc::exception); } +BOOST_AUTO_TEST_CASE(variant_every_carrier_round_trips_through_json_TEXT) { + // The variant-layer round trip above is NOT sufficient: `next_key` is json + // TEXT (chain_plugin renders it with fc::json::to_string) and a paginating + // caller feeds it straight back as a `lower_bound`, which is re-parsed with + // fc::json::from_string. fc::json QUOTES a uint64 above 0xffffffff, so the + // integer carrier crosses that boundary as a decimal STRING — which the + // validating string arm rejected as "too long" until from_variant learned to + // re-route on length. Values are chosen to straddle every threshold that + // matters: the 0xffffffff quoting cutoff, the 2^42 canonical floor, and 2^48. + const uint64_t values[] = { + 0u, // the zero sentinel -> "" + 7u, // non-canonical, below the quoting cutoff + 0xffffffffu, // last value fc::json emits unquoted + 0x100000000u, // first value fc::json QUOTES + (uint64_t{1} << 42) - 1, // last non-canonical + uint64_t{1} << 42, // first canonical ("A") + slug_name{"ETH"}.value, + slug_name{"12345678"}.value, + (uint64_t{1} << 48) - 1, // symbols past the alphabet -> integer carrier + ~uint64_t{0}, + }; + for (const uint64_t raw : values) { + fc::variant v; + fc::to_variant(slug_name{raw}, v); + const std::string text = + fc::json::to_string(fc::variant(fc::mutable_variant_object("code", v)), + fc::time_point::maximum()); + slug_name back; + BOOST_REQUIRE_NO_THROW( + fc::from_variant(fc::json::from_string(text).get_object()["code"], back)); + BOOST_CHECK_MESSAGE(back.value == raw, + "json text round trip lost " << raw << " via " << text); + } +} + +BOOST_AUTO_TEST_CASE(mvo_accepts_every_spelling_a_caller_can_write) { + // Pins the whole INPUT surface a test or caller may write for a slug_name + // field. mutable_variant_object's templated operator() forwards to + // fc::variant's constructor set (variant_object.hpp:206-211), so every + // spelling below resolves through a different ctor and must still land on + // the same value: + // + // const char* -> variant(const char*) -> string + // std::string -> variant(std::string) -> string + // std::string_view -> variant(std::string_view) -> string + // fc::slug_name -> explicit variant(const T&) -> to_variant -> string + // uint64_t -> variant(uint64_t) -> the integer escape + // + // This is why no `codename()`-style wrapper is needed at a call site: the + // raw literal and the `_s` literal both already work, and a wrapper + // returning std::string is just identity. + const slug_name expected{"LIQSOL"}; + + const char* const as_c_str = "LIQSOL"; + const std::string as_string = "LIQSOL"; + const std::string_view as_view = "LIQSOL"; + + const fc::variant obj{ fc::mutable_variant_object() + ("c_str", as_c_str) + ("string", as_string) + ("view", as_view) + ("literal", "LIQSOL"_s) // the _s literal — validated at compile time + ("slug", expected) }; // an fc::slug_name value + + for (const char* key : {"c_str", "string", "view", "literal", "slug"}) { + const fc::variant& cell = obj.get_object()[key]; + BOOST_REQUIRE_MESSAGE(cell.is_string(), std::string{"not a string: "} + key); + BOOST_CHECK_EQUAL(cell.as_string(), "LIQSOL"); + slug_name back; + fc::from_variant(cell, back); + BOOST_CHECK_MESSAGE(back == expected, std::string{"round trip failed: "} + key); + } + + // The integer escape is the one writable spelling that is NOT a string, and + // it is the only way to write a non-canonical value. + const fc::variant esc{ fc::mutable_variant_object()("code", uint64_t{7}) }; + const fc::variant& cell = esc.get_object()["code"]; + BOOST_CHECK(cell.is_integer()); + slug_name back; + fc::from_variant(cell, back); + BOOST_CHECK_EQUAL(back.value, 7u); +} + BOOST_AUTO_TEST_SUITE_END() diff --git a/tests/get_table_tests.cpp b/tests/get_table_tests.cpp index 7c48ef223d..fc3f3194f3 100644 --- a/tests/get_table_tests.cpp +++ b/tests/get_table_tests.cpp @@ -453,7 +453,8 @@ BOOST_FIXTURE_TEST_CASE( get_table_next_key_test, validating_tester ) try { push_action("test"_n, "addhashobj"_n, "test"_n, mutable_variant_object()("hashinput", "secondinput")); push_action("test"_n, "addhashobj"_n, "test"_n, mutable_variant_object()("hashinput", "thirdinput")); // structobjs: kv::table keyed by the reflected struct composite_key{value} — the - // sysio.chains `chains` key shape. Drives the struct-key path in (sec-10). + // key shape the v6 registry tables used BEFORE `slug_name` became an ABI builtin + // (they now key on a slug_name LEAF). Drives the struct-key path in (sec-10). push_action("test"_n, "addstruct"_n, "test"_n, mutable_variant_object()("code", 10)("payload", 100)); push_action("test"_n, "addstruct"_n, "test"_n, mutable_variant_object()("code", 20)("payload", 200)); push_action("test"_n, "addstruct"_n, "test"_n, mutable_variant_object()("code", 30)("payload", 300)); diff --git a/unittests/abi_tests.cpp b/unittests/abi_tests.cpp index df0c559a7f..3e21b14081 100644 --- a/unittests/abi_tests.cpp +++ b/unittests/abi_tests.cpp @@ -682,6 +682,57 @@ BOOST_AUTO_TEST_CASE(optional_vector) +BOOST_AUTO_TEST_CASE(slug_name_builtin_type) +{ try { + // Guards the `slug_name` entry in configure_built_in_types(). Without it the + // spelling resolves as neither builtin nor struct and set_abi's validate() + // throws invalid_type_inside_abi; with a same-named ABI struct present it + // would instead serialize as {"value":N}. The converted table-read sweep in + // contracts/tests cannot catch either case, because fc::slug_name's + // from_variant accepts the string, the integer AND the object form, so those + // reads pass identically whether or not this registration exists. + const char* test_abi = R"=====( + { + "version": "sysio::abi/1.0", + "types": [], + "structs": [{ + "name": "regrow", + "base": "", + "fields": [{ + "name": "code", + "type": "slug_name" + }] + }], + "actions": [], + "tables": [], + "ricardian_clauses": [] + } + )====="; + + auto abi = fc::json::from_string(test_abi).as(); + abi_serializer abis(sysio_contract_abi(abi), yield_fn()); + + // A canonical slug is carried as its STRING spelling, in 8 bytes. + auto bytes = abis.variant_to_binary( + "regrow", fc::json::from_string(R"({"code":"ETH"})"), yield_fn()); + BOOST_REQUIRE_EQUAL(bytes.size(), 8u); + auto back = abis.binary_to_variant("regrow", bytes, yield_fn()); + BOOST_REQUIRE(back.get_object()["code"].is_string()); + BOOST_CHECK_EQUAL(back.get_object()["code"].as_string(), "ETH"); + + // A planted non-canonical value must render — as a JSON INTEGER, not a + // string, and WITHOUT throwing. A throwing conversion would make one such + // row able to brick get_table_rows for a whole table. + auto planted = abis.variant_to_binary( + "regrow", fc::json::from_string(R"({"code":7})"), yield_fn()); + BOOST_REQUIRE_EQUAL(planted.size(), 8u); + fc::variant rendered; + BOOST_REQUIRE_NO_THROW(rendered = abis.binary_to_variant("regrow", planted, yield_fn())); + BOOST_REQUIRE(rendered.get_object()["code"].is_integer()); + BOOST_CHECK_EQUAL(rendered.get_object()["code"].as_uint64(), 7u); + +} FC_LOG_AND_RETHROW() } + BOOST_AUTO_TEST_CASE(uint_types) { try { diff --git a/unittests/be_key_codec_tests.cpp b/unittests/be_key_codec_tests.cpp index 954224dc9b..d775b64259 100644 --- a/unittests/be_key_codec_tests.cpp +++ b/unittests/be_key_codec_tests.cpp @@ -123,6 +123,74 @@ BOOST_AUTO_TEST_CASE(slug_name_leaf_roundtrips_a_non_canonical_value_as_an_integ BOOST_CHECK(bytes == codec::encode_key(decoded, shapes)); } +BOOST_AUTO_TEST_CASE(slug_name_multi_leaf_keys_preserve_field_order_and_offsets) { + // THREE of the five v6 registry tables key on more than one slug: + // sysio.tokens::chaintokens ["slug_name","slug_name"] + // sysio.reserv::reserves ["slug_name","slug_name","slug_name"] + // sysio.uwrit::locksums ["name","slug_name","slug_name"] + // Every other slug case here builds a SINGLE leaf, and a single leaf cannot + // observe a field-ordering or offset error because there is only one field to + // misplace. decode_key walks the shapes in order consuming a fixed width each, + // and encode_key must reproduce that ordering from the decoded object. + auto abi = make_test_abi(); + + // 2-leaf, mirroring chaintokens. + { + auto shapes = codec::build_key_shapes(abi, {"chain_code", "token_code"}, + {"slug_name", "slug_name"}); + auto bytes = codec::encode_key( + fc::variant(fc::mutable_variant_object("chain_code", "ETH")("token_code", "USDC")), + shapes); + BOOST_REQUIRE_EQUAL(bytes.size(), 16u); + // Ordering is observable: each leaf must occupy its own 8-byte window, in + // declaration order. Swapping the two would keep the size and fail here. + BOOST_CHECK(std::vector(bytes.begin(), bytes.begin() + 8) + == encode_single(abi, "slug_name", fc::variant("ETH"))); + BOOST_CHECK(std::vector(bytes.begin() + 8, bytes.end()) + == encode_single(abi, "slug_name", fc::variant("USDC"))); + auto decoded = codec::decode_key(bytes.data(), bytes.size(), shapes); + BOOST_CHECK_EQUAL(decoded.get_object()["chain_code"].as_string(), "ETH"); + BOOST_CHECK_EQUAL(decoded.get_object()["token_code"].as_string(), "USDC"); + BOOST_CHECK(bytes == codec::encode_key(decoded, shapes)); + } + + // 3-leaf, mirroring reserves — and with a non-canonical middle leaf, so the + // mixed-carrier case (string, integer, string) is covered at its own offset. + { + auto shapes = codec::build_key_shapes(abi, {"chain_code", "token_code", "reserve_code"}, + {"slug_name", "slug_name", "slug_name"}); + auto bytes = codec::encode_key( + fc::variant(fc::mutable_variant_object("chain_code", "ETH") + ("token_code", 7u) + ("reserve_code", "PRIMARY")), + shapes); + BOOST_REQUIRE_EQUAL(bytes.size(), 24u); + auto decoded = codec::decode_key(bytes.data(), bytes.size(), shapes); + BOOST_CHECK_EQUAL(decoded.get_object()["chain_code"].as_string(), "ETH"); + BOOST_CHECK(decoded.get_object()["token_code"].is_integer()); + BOOST_CHECK_EQUAL(decoded.get_object()["token_code"].as_uint64(), 7u); + BOOST_CHECK_EQUAL(decoded.get_object()["reserve_code"].as_string(), "PRIMARY"); + BOOST_CHECK(bytes == codec::encode_key(decoded, shapes)); + } + + // name + 2 slugs, mirroring locksums — a mixed-KIND composite. + { + auto shapes = codec::build_key_shapes(abi, {"underwriter", "chain_code", "token_code"}, + {"name", "slug_name", "slug_name"}); + auto bytes = codec::encode_key( + fc::variant(fc::mutable_variant_object("underwriter", "uw.a") + ("chain_code", "SOLANA") + ("token_code", "SOL")), + shapes); + BOOST_REQUIRE_EQUAL(bytes.size(), 24u); + auto decoded = codec::decode_key(bytes.data(), bytes.size(), shapes); + BOOST_CHECK_EQUAL(decoded.get_object()["underwriter"].as_string(), "uw.a"); + BOOST_CHECK_EQUAL(decoded.get_object()["chain_code"].as_string(), "SOLANA"); + BOOST_CHECK_EQUAL(decoded.get_object()["token_code"].as_string(), "SOL"); + BOOST_CHECK(bytes == codec::encode_key(decoded, shapes)); + } +} + BOOST_AUTO_TEST_CASE(slug_name_leaf_groups_shared_prefixes) { // The property slug_name was designed for: MSB-first 6-bit packing puts // char[0] at bits [42..47], so a shared textual prefix is a shared leading diff --git a/unittests/test-contracts/get_table_test/get_table_test.hpp b/unittests/test-contracts/get_table_test/get_table_test.hpp index 9a28d0755a..1d71f00d5c 100644 --- a/unittests/test-contracts/get_table_test/get_table_test.hpp +++ b/unittests/test-contracts/get_table_test/get_table_test.hpp @@ -102,10 +102,10 @@ class [[sysio::contract]] get_table_test : public sysio::contract { // pagination must round-trip the nested `{ "code": { "value": N } }` key // shape — coverage a flat scalar key cannot provide. // - // Deliberately NOT named `composite_key`: that spelling is an abi_serializer + // Deliberately NOT named `slug_name`: that spelling is an abi_serializer // builtin and a `leaf_key_spellings` entry, and abigen's builtin match is // on the namespace-stripped bare name — so a member struct called - // `composite_key` would be emitted as the builtin, take the leaf branch in + // `slug_name` would be emitted as the builtin, take the leaf branch in // `build_key_shape`, and stop exercising struct expansion at all. The // suite would keep passing while testing nothing it was written for. struct composite_key { From f2cd62e7da7855314aabbfa9f9fe30e060ba27df Mon Sep 17 00:00:00 2001 From: kevin Heifner Date: Wed, 16 Sep 2026 09:16:06 -0500 Subject: [PATCH 03/29] docs: drop the "v6" label, and decouple the struct-key fixture from slug_name MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit "v6" is not a version. wire-sysio is 1.0.0 with only v1.0.0 tags; the label informally named the data-model revision that introduced the registry entities and the slug_name rename, and it reads like a release nobody can resolve. Removed from 88 of 90 mentions — comments, one ilog string, and a python docstring — saying what is meant instead ("the data-model refactor", "the registry tables", or the commit). Two are deliberately kept, and a blind sweep would have broken both: CHANGELOG.md's "Bump snapshot version to v6" is a real snapshot-format version in historical record, and test_http_client.cpp's `tcp::v6()` is IPv6 in live code. Also: - get_table_test's struct-key fixture no longer mentions slug_name. It exists to drive struct-key expansion in the BE key codec and has nothing to do with that type; the comment now states the hazard generally (a key struct named after any ABI builtin is emitted AS the builtin and silently stops testing struct expansion) rather than naming an unrelated type and preserving the fixture's former name. - Remove regchain_code_cell_renders_as_the_decoded_slug. It spent a full contract deploy in the heaviest binary to assert row["code"].is_string(), a property of abi_serializer and to_variant with no contract logic involved; abi_tests::slug_name_builtin_type pins it against a synthetic one-field ABI. - Restructure the be_key_codec key_shape comment. The sentence defining what a key_shape IS had been spliced mid-paragraph into an explanation of to_key routing, leaving the definition unfindable. Change-Id: I7f3d339ad9bade25c4c7838ad64d69f24db0d409 --- .../include/sysio.authex/sysio.authex.hpp | 2 +- contracts/sysio.authex/src/sysio.authex.cpp | 2 +- .../include/sysio.msgch/sysio.msgch.hpp | 2 +- contracts/sysio.msgch/src/sysio.msgch.cpp | 12 ++++----- .../sysio.opp.common/opp_table_types.hpp | 26 +++++++++---------- .../include/sysio.opreg/sysio.opreg.hpp | 2 +- contracts/sysio.opreg/src/sysio.opreg.cpp | 10 +++---- .../include/sysio.reserv/sysio.reserv.hpp | 4 +-- contracts/sysio.reserv/src/sysio.reserv.cpp | 6 ++--- .../include/sysio.uwrit/sysio.uwrit.hpp | 4 +-- contracts/sysio.uwrit/src/sysio.uwrit.cpp | 4 +-- contracts/tests/sysio.chains_tests.cpp | 15 ----------- contracts/tests/sysio.dclaim_tests.cpp | 2 +- contracts/tests/sysio.dispatch_tests.cpp | 6 ++--- .../tests/sysio.epoch_flushwtdw_tests.cpp | 6 ++--- contracts/tests/sysio.epoch_tests.cpp | 4 +-- contracts/tests/sysio.msgch_tests.cpp | 8 +++--- contracts/tests/sysio.opreg_tests.cpp | 10 +++---- contracts/tests/sysio.reserv_tests.cpp | 8 +++--- contracts/tests/sysio.uwrit_tests.cpp | 8 +++--- .../include/sysio/chain/database_utils.hpp | 26 +++++++++---------- libraries/opp/include/sysio/opp/opp.hpp | 4 +-- .../src/batch_operator_plugin.cpp | 8 +++--- .../sysio/outpost_solana_client_plugin.hpp | 2 +- .../underwriter_plugin/routing_detail.hpp | 4 +-- .../src/underwriter_plugin.cpp | 26 +++++++++---------- .../test/test_underwriter_routing.cpp | 2 +- tests/get_table_tests.cpp | 11 +++----- tests/producer_rank_test.py | 2 +- unittests/be_key_codec_tests.cpp | 6 ++--- .../get_table_test/get_table_test.hpp | 10 +++---- 31 files changed, 109 insertions(+), 133 deletions(-) diff --git a/contracts/sysio.authex/include/sysio.authex/sysio.authex.hpp b/contracts/sysio.authex/include/sysio.authex/sysio.authex.hpp index a48db164e4..8fc4184c6e 100644 --- a/contracts/sysio.authex/include/sysio.authex/sysio.authex.hpp +++ b/contracts/sysio.authex/include/sysio.authex/sysio.authex.hpp @@ -200,7 +200,7 @@ namespace sysio { * @param chain_kind The chain identifier from `opp::types::ChainKind` * (CHAIN_KIND_EVM / CHAIN_KIND_SVM). * Wire-side legacy `fc::crypto::chain_kind_t` is host-only. - * TODO @jglanz: SUI variant removed in v6; revisit when + * TODO @jglanz: SUI variant removed in the data-model refactor; revisit when * SUI outpost is added. * @param account The WIRE account name of the user which the address is being linked to. * @param sig A valid signature for the target chain converted to Wire's standard. diff --git a/contracts/sysio.authex/src/sysio.authex.cpp b/contracts/sysio.authex/src/sysio.authex.cpp index 624ec7c899..2b85822b07 100644 --- a/contracts/sysio.authex/src/sysio.authex.cpp +++ b/contracts/sysio.authex/src/sysio.authex.cpp @@ -32,7 +32,7 @@ namespace sysio { require_auth(account); // ——— Chain kind validation ——— - // TODO @jglanz: SUI removed in v6; restore when SUI outpost is added. + // TODO @jglanz: SUI removed in the data-model refactor; restore when SUI outpost is added. check(chain_kind == ChainKind::CHAIN_KIND_EVM || chain_kind == ChainKind::CHAIN_KIND_SVM, "Invalid chain_kind. Supported: CHAIN_KIND_EVM(2), CHAIN_KIND_SVM(3)."); diff --git a/contracts/sysio.msgch/include/sysio.msgch/sysio.msgch.hpp b/contracts/sysio.msgch/include/sysio.msgch/sysio.msgch.hpp index 7a3e8b9aab..79fec990d5 100644 --- a/contracts/sysio.msgch/include/sysio.msgch/sysio.msgch.hpp +++ b/contracts/sysio.msgch/include/sysio.msgch/sysio.msgch.hpp @@ -72,7 +72,7 @@ namespace sysio { /// * `sysio.reserv::oncnclrsv` — `RESERVE_CREATE_CANCELLED` to /// the reserve's owning outpost on race-win cancel. /// * `sysio.opreg::*` — `OPERATOR_ACTION` family (WITHDRAW_REMIT, - /// SLASH) — once the v6 reserve-flow lands the same pattern + /// SLASH) — once the reserve-flow work lands the same pattern /// reaches every depot-authorised outbound. /// /// Gated to the depot's own system contracts (sysio.epoch / .opreg / diff --git a/contracts/sysio.msgch/src/sysio.msgch.cpp b/contracts/sysio.msgch/src/sysio.msgch.cpp index 38ef1d82e4..7fa97b3ebc 100644 --- a/contracts/sysio.msgch/src/sysio.msgch.cpp +++ b/contracts/sysio.msgch/src/sysio.msgch.cpp @@ -447,7 +447,7 @@ std::optional to_checksum256_exact(const std::vector& bytes) /// Decode an OperatorAction sub-message and dispatch to the appropriate /// sysio.opreg action. Called from the inbound dispatch loop in `evalcons`. /// -/// Sub-type routing (post v6 data-model refactor — codenames everywhere): +/// Sub-type routing (post data-model refactor — codenames everywhere): /// * DEPOSIT_REQUEST → opreg::depositinle(account, chain_code, token_code, /// amount, actor_chain, actor_addr, /// msg_id) @@ -561,7 +561,7 @@ void dispatch_operator_action(name self, const std::vector& data, /// covering this leg); the authoritative copy for verification is the /// bytes themselves, stored on `commit_entry.{source,dest}_uic_bytes`. /// -/// Post v6: identity scalars on UIC are codenames (uint64). `chain_code` is the proven source +/// After the refactor: identity scalars on UIC are codenames (uint64). `chain_code` is the proven source /// outpost from `deliver`; `uic.chain_code` is the leg this commit covers. WSA-005 requires the two /// to be identical — each leg's underwrite commit is emitted on, and relayed by, that leg's own /// outpost (a source-leg UIC rides the source outpost's envelope, a dest-leg UIC the dest outpost's; @@ -856,10 +856,10 @@ void dispatch_attestation(name self, uint64_t attestation_id, // longer exists; any stray inbound falls through to the default drop below. case AttestationType::ATTESTATION_TYPE_STAKING_REWARD: - // Per-staker staking reward -> sysio.dclaim claim ledger. The v6 + // Per-staker staking reward -> sysio.dclaim claim ledger. The // staking-reward path does not deposit back to a reserve (the // external-pool credit and native -> WIRE conversion are - // outpost-side), so the pre-v6 reserv::onreward leg is dropped and + // outpost-side), so the pre-refactor reserv::onreward leg is dropped and // reward_amount.amount is forwarded as the WIRE-denominated credit. { opp::attestations::StakingReward sr; @@ -1331,7 +1331,7 @@ void msgch::deliver(name batch_op_name, uint64_t chain_code, std::vector d // Verify outpost exists on the new `sysio.chains::chains` table. // `chain_code` is the originating chain's slug_name value (uint64) per - // the v6 data-model refactor — the chain row's PK is `code.value`. + // the data-model refactor — the chain row's PK is `code.value`. // Reject deliveries from the depot self-row (`is_depot==true`) and // from inactive chains; both are protocol invariants. sysio::chains::chains_t chains_tbl(CHAINS_ACCOUNT); @@ -1577,7 +1577,7 @@ void msgch::chkcons() { // Check all active outposts have consensus for the current epoch. // Outpost set is sourced from `sysio.chains::chains` filtered to - // active && !is_depot per the v6 data-model refactor; outpost ids + // active && !is_depot per the data-model refactor; outpost ids // in `outpcons` are slug_name values (chain_row::code.value). outpost_consensus_t opcons(get_self()); sysio::chains::chains_t chains_tbl(CHAINS_ACCOUNT); diff --git a/contracts/sysio.opp.common/include/sysio.opp.common/opp_table_types.hpp b/contracts/sysio.opp.common/include/sysio.opp.common/opp_table_types.hpp index 4abc09f303..5ad2b85b08 100644 --- a/contracts/sysio.opp.common/include/sysio.opp.common/opp_table_types.hpp +++ b/contracts/sysio.opp.common/include/sysio.opp.common/opp_table_types.hpp @@ -82,7 +82,7 @@ DataStream& operator>>(DataStream& ds, ChainId& t) { return ds >> t.kind >> t.id; } -// TokenAmount: { uint64 token_code; vint64_t amount; } (v6 — codename-keyed) +// TokenAmount: { uint64 token_code; vint64_t amount; } (codename-keyed) template DataStream& operator<<(DataStream& ds, const TokenAmount& t) { return ds << t.token_code << t.amount; @@ -133,7 +133,7 @@ DataStream& operator>>(DataStream& ds, WirePermission& t) { } // --------------------------------------------------------------------------- -// v6 registry-entity messages (Chain / Token / ChainToken / Reserve / ReserveAmount) +// Registry-entity messages (Chain / Token / ChainToken / Reserve / ReserveAmount) // --------------------------------------------------------------------------- template @@ -286,7 +286,7 @@ DataStream& operator>>(DataStream& ds, Envelope& t) { // ───────────────────────────────────────────────────────────────────────────── namespace sysio::opp::attestations { -// ReserveBalanceSheet (v6, renamed from ChainReserveBalanceSheet; the +// ReserveBalanceSheet (renamed from ChainReserveBalanceSheet; the // parallel amounts[]/reserve_codes[] arrays were replaced by // self-describing depot-frame `ReserveAmount` entries). template @@ -348,7 +348,7 @@ DataStream& operator>>(DataStream& ds, WireTokenPurchase& t) { return ds >> t.actor >> t.amounts; } -// OperatorAction — v6: chain_code (codename uint64), and SLASH carries reserve_code. +// OperatorAction — chain_code (codename uint64), and SLASH carries reserve_code. template DataStream& operator<<(DataStream& ds, const OperatorAction& t) { return ds << t.action_type << t.op_address << t.type << t.status @@ -380,7 +380,7 @@ DataStream& operator>>(DataStream& ds, ReserveDisbursement& t) { return ds >> t.actor >> t.amount >> t.signature; } -// ProtocolState — v6: chain_code (codename uint64) replaces ChainId chain_id. +// ProtocolState — chain_code (codename uint64) replaces ChainId chain_id. template DataStream& operator<<(DataStream& ds, const ProtocolState& t) { return ds << t.chain_code << t.current_message_id << t.processed_message_id @@ -392,7 +392,7 @@ DataStream& operator>>(DataStream& ds, ProtocolState& t) { >> t.incoming_messages >> t.outgoing_messages; } -// SwapRequest — v6: full codename triples for source + target. +// SwapRequest — full codename triples for source + target. template DataStream& operator<<(DataStream& ds, const SwapRequest& t) { return ds << t.actor << t.source_amount @@ -412,7 +412,7 @@ DataStream& operator>>(DataStream& ds, SwapRequest& t) { >> t.source_tx_id; } -// UnderwriteIntentCommit — v6: (token_code, chain_code, reserve_code) triple +// UnderwriteIntentCommit — (token_code, chain_code, reserve_code) triple // disambiguates same-chain swap legs. Field order MUST match the generated proto struct // (attestations.proto): uw_account, uw_ext_chain_addr, uw_request_id, signature, // token_code, chain_code, reserve_code. (The legacy v5 `outpost_id` field — a per-outpost @@ -431,7 +431,7 @@ DataStream& operator>>(DataStream& ds, UnderwriteIntentCommit& t) { >> t.token_code >> t.chain_code >> t.reserve_code; } -// SwapRevert — v6 adds source_chain_code + source_reserve_code. +// SwapRevert — adds source_chain_code + source_reserve_code. template DataStream& operator<<(DataStream& ds, const SwapRevert& t) { return ds << t.original_swap_message_id << t.depositor @@ -445,7 +445,7 @@ DataStream& operator>>(DataStream& ds, SwapRevert& t) { >> t.source_chain_code >> t.source_reserve_code; } -// SwapRemit — v6: adds chain_code + reserve_code (destination identity). +// SwapRemit — adds chain_code + reserve_code (destination identity). template DataStream& operator<<(DataStream& ds, const SwapRemit& t) { return ds << t.recipient << t.amount << t.original_message_id @@ -525,11 +525,11 @@ DataStream& operator>>(DataStream& ds, BatchOperatorGroups& t) { return ds >> t.active_group_index >> t.epoch_index >> t.groups; } -// ReserveAmount — v6: (chain_code, reserve_code, TokenAmount), depot-frame. -// (NOTE: ReserveAmount lives in `sysio::opp::types` per v6 types.proto; +// ReserveAmount — (chain_code, reserve_code, TokenAmount), depot-frame. +// (NOTE: ReserveAmount lives in `sysio::opp::types` per types.proto; // the DataStream overloads are above in the types namespace.) -// DepositRevert — v6: adds chain_code. +// DepositRevert — adds chain_code. template DataStream& operator<<(DataStream& ds, const DepositRevert& t) { return ds << t.original_deposit_message_id << t.depositor @@ -569,7 +569,7 @@ DataStream& operator>>(DataStream& ds, StakingReward& t) { } // --------------------------------------------------------------------------- -// v6 reserve-flow attestations +// Reserve-flow attestations // --------------------------------------------------------------------------- // ReserveCreate — reserve identity + custodied amount travel together in diff --git a/contracts/sysio.opreg/include/sysio.opreg/sysio.opreg.hpp b/contracts/sysio.opreg/include/sysio.opreg/sysio.opreg.hpp index 588c948b3b..880e85c50e 100644 --- a/contracts/sysio.opreg/include/sysio.opreg/sysio.opreg.hpp +++ b/contracts/sysio.opreg/include/sysio.opreg/sysio.opreg.hpp @@ -161,7 +161,7 @@ namespace sysio { /// Per-(chain, token) minimum-bond row stored in `opconfig`'s /// per-role requirement vectors and accepted as `setconfig` input. - /// Per the v6 data-model refactor: `chain` / `token` identifiers are + /// Per the data-model refactor: `chain` / `token` identifiers are /// `sysio::slug_name` (uint64-packed) instead of the old enums. struct chain_min_bond { sysio::slug_name chain_code; diff --git a/contracts/sysio.opreg/src/sysio.opreg.cpp b/contracts/sysio.opreg/src/sysio.opreg.cpp index 1ae13ea288..7f11127446 100644 --- a/contracts/sysio.opreg/src/sysio.opreg.cpp +++ b/contracts/sysio.opreg/src/sysio.opreg.cpp @@ -87,7 +87,7 @@ checksum256 make_account_chain_token_key(name account, /// for handling that case — typically by skipping the queueout for chains /// without an outpost, e.g. WIRE-direct flows). /// -/// Post v6 cross-contract realignment: chain rows live in +/// After the cross-contract realignment: chain rows live in /// `sysio.chains::chains` keyed by `code` (slug_name); the legacy /// `sysio.epoch::outposts` table is gone. The "outpost id" returned here is /// the chain's `code.value` (uint64) — callers that still expect a small @@ -333,7 +333,7 @@ void opreg::regoperator(name account, // Verify authex links exist for all active outpost chains. // Skip when: bootstrapped OR privileged caller (sysio.opreg registering on behalf) // - // Post v6 refactor: the outpost set lives in `sysio.chains::chains` keyed + // After the refactor: the outpost set lives in `sysio.chains::chains` keyed // by slug_name. The depot self-row (`is_depot == true`) is skipped; only // active outpost chains require an authex link. `authex::links.bynamechain` // is still keyed by ChainKind (uint128 of (account, ChainKind)), so we @@ -411,7 +411,7 @@ uint64_t sum_locks_inline(name account, sysio::slug_name chain_code, sysio::slug /// (op, chain, token). Subtracted by `available()` so a queued withdraw /// effectively reserves the funds for its 2-epoch wait. /// -/// Per v6 plan §B.2 (split-index design): `wtdwqueue_t` exposes only uint64 +/// Per the split-index design: `wtdwqueue_t` exposes only uint64 /// secondary indexes. `byaccount` keys on `account.value`; rows are filtered /// on `(chain_code, token_code)` in memory. Per-account pending-withdraw /// counts are bounded by the operator's collateral-bucket count. @@ -659,7 +659,7 @@ namespace { /// fails gracefully (the depot's `dispatch_operator_action` rejects empty /// `op_address.address`). /// -/// Post v6: `authex::links.bynamechain` is still keyed by `(name, ChainKind)` +/// After the refactor: `authex::links.bynamechain` is still keyed by `(name, ChainKind)` /// and `ChainAddress.kind` is still `ChainKind`. opreg now stores chains by /// slug_name; resolve via `chain_kind_for_code` first. opp::types::ChainAddress operator_chain_address(name account, sysio::slug_name chain_code) { @@ -1214,7 +1214,7 @@ void opreg::deposit(name account, uint64_t amount) { // `actor_chain` is retained as `opp::types::ChainKind` per the // ChainAddress flattening pattern — the depositor's source-chain // `ChainAddress.kind` field is still ChainKind on the wire and is not -// part of the v6 slug_name refactor. +// part of the slug_name refactor. void opreg::depositinle(name account, sysio::slug_name chain_code, sysio::slug_name token_code, diff --git a/contracts/sysio.reserv/include/sysio.reserv/sysio.reserv.hpp b/contracts/sysio.reserv/include/sysio.reserv/sysio.reserv.hpp index 607e32c1d5..df477f6d86 100644 --- a/contracts/sysio.reserv/include/sysio.reserv/sysio.reserv.hpp +++ b/contracts/sysio.reserv/include/sysio.reserv/sysio.reserv.hpp @@ -22,7 +22,7 @@ namespace sysio { /** * @brief sysio.reserv — reserve registry with create→match→ready handshake. * - * Per the v6 data-model refactor: + * Per the data-model refactor: * * - Reserve primary key is the triple `(chain_code, token_code, code)` * (all codenames). Composite stored as `checksum256(chain || token || code)`. @@ -297,7 +297,7 @@ namespace sysio { // depot-initiated REMIT is paid by the destination outpost (no rejection, // so no reserve-ledger reconciliation is needed). - // onreward was removed: the v6 STAKING_REWARD path credits the per-staker + // onreward was removed: the STAKING_REWARD path credits the per-staker // reward to sysio.dclaim directly (already WIRE-denominated), so there is // no reserve leg and no reserve-side reward crediting. diff --git a/contracts/sysio.reserv/src/sysio.reserv.cpp b/contracts/sysio.reserv/src/sysio.reserv.cpp index 1351bbd141..e7b67820a2 100644 --- a/contracts/sysio.reserv/src/sysio.reserv.cpp +++ b/contracts/sysio.reserv/src/sysio.reserv.cpp @@ -183,7 +183,7 @@ std::optional pubkey_from_raw(opp::types::ChainKind kind, /// (the default `zpp::bits::data_out` form prepends a 4-byte LE length /// prefix that corrupts the first field tag on the receiving side). /// * The destination `chain_code` is the reserve's `chain_code.value` -/// itself (per the v6 convention recorded in `sysio.msgch.hpp`: +/// itself (per the convention recorded in `sysio.msgch.hpp`: /// "the outpost id IS the chain's slug_name value"). template void queue_attestation_out(name self, @@ -669,7 +669,7 @@ void reserve::matchreserve(sysio::slug_name chain_code, // Reserve is now ACTIVE on the depot. Notify the owning outpost so its // local reserve record can flip to ACTIVE and become usable for swap // routing. The destination `chain_code` is the reserve's `chain_code` - // (per the v6 `sysio.msgch::queueout` convention — the outpost id is + // (per the `sysio.msgch::queueout` convention — the outpost id is // the chain slug_name's packed uint64 value). opp::attestations::ReserveReady ready; ready.chain_code = chain_code.value; @@ -839,7 +839,7 @@ void reserve::debit(sysio::slug_name chain_code, // onreject was removed — no SwapRejected attestation exists (every depot-initiated // REMIT is paid by the destination outpost; reserves need no rejection reconciliation). -// onreward was removed: the v6 STAKING_REWARD path credits the per-staker reward to +// onreward was removed: the STAKING_REWARD path credits the per-staker reward to // sysio.dclaim directly (already WIRE-denominated), so there is no reserve leg. // --------------------------------------------------------------------------- diff --git a/contracts/sysio.uwrit/include/sysio.uwrit/sysio.uwrit.hpp b/contracts/sysio.uwrit/include/sysio.uwrit/sysio.uwrit.hpp index 6a6c620d39..e8a489024e 100644 --- a/contracts/sysio.uwrit/include/sysio.uwrit/sysio.uwrit.hpp +++ b/contracts/sysio.uwrit/include/sysio.uwrit/sysio.uwrit.hpp @@ -16,7 +16,7 @@ namespace sysio { /** * @brief sysio.uwrit — underwriter race resolver + flat lock vector. * - * Per the v6 data-model refactor (`load-context-and-follow-smooth-flame.md` + * Per the data-model refactor (`load-context-and-follow-smooth-flame.md` * §3.13, §4.5, §4.6): * * - opreg owns the bond ledger (per-(operator, chain_code, token_code) aggregate @@ -771,7 +771,7 @@ namespace sysio { /// src/dst pairs, the underwriter race, and the eventual settlement. /// /// Each side of the swap carries a full `(chain_code, token_code, - /// reserve_code)` triple per the v6 data-model refactor: identity + /// reserve_code)` triple per the data-model refactor: identity /// is slug_name-keyed throughout, and `reserve_code` lets a same- /// `(chain, token)` swap target a specific reserve when multiple /// reserves exist for that pair. diff --git a/contracts/sysio.uwrit/src/sysio.uwrit.cpp b/contracts/sysio.uwrit/src/sysio.uwrit.cpp index 17d466564f..86c5290fbd 100644 --- a/contracts/sysio.uwrit/src/sysio.uwrit.cpp +++ b/contracts/sysio.uwrit/src/sysio.uwrit.cpp @@ -123,7 +123,7 @@ uint32_t get_current_epoch() { } /// Sum the underwriter's pending withdraws on opreg for the given -/// `(chain_code, token_code)`. Per v6 plan §B.2 (split-index design): +/// `(chain_code, token_code)`. Per the split-index design: /// `opreg::wtdwqueue_t` exposes only uint64 secondary indexes. The `byaccount` /// index keys on `account.value`; rows are filtered on `(chain_code, /// token_code)` in memory. Per-account pending-withdraw counts are O(1)-ish @@ -467,7 +467,7 @@ std::optional chain_kind_for_code(sysio::slug_name chain_code) { /// `feedback_no_zero_sentinels` — outpost id 0 is a real id, so 0 must not /// double as "missing"). /// -/// Post v6 cross-contract realignment: chain rows live in +/// After the cross-contract realignment: chain rows live in /// `sysio.chains::chains` keyed by `code` (slug_name); the legacy /// `sysio.epoch::outposts` table is gone. The "outpost id" returned here is /// the chain's `code.value` (uint64). The depot-self row is filtered out so diff --git a/contracts/tests/sysio.chains_tests.cpp b/contracts/tests/sysio.chains_tests.cpp index 26cf8c1900..8fe264bcd9 100644 --- a/contracts/tests/sysio.chains_tests.cpp +++ b/contracts/tests/sysio.chains_tests.cpp @@ -117,21 +117,6 @@ class sysio_chains_tester : public tester { BOOST_AUTO_TEST_SUITE(sysio_chains_tests) // ── EVM: all four role addresses are accepted and stored verbatim ── -// ── The `code` CELL renders as its decoded slug, not a `{value}` wrapper ───── -// `sysio.chains::chains` is keyed on `code`, so this table is the one whose -// `next_key` spelling the slug_name ABI builtin changes — and before this case -// the suite only ever used `code` as a KEY ARGUMENT (get_chain's get_row_by_id), -// never asserting the rendered cell. A wrong carrier here had no coverage. -BOOST_FIXTURE_TEST_CASE(regchain_code_cell_renders_as_the_decoded_slug, sysio_chains_tester) { try { - BOOST_REQUIRE_EQUAL(success(), regchain(ChainKind::CHAIN_KIND_EVM, "ETH", 1, - evm_outpost_mvo(EVM_OPP, EVM_INBOUND, EVM_OPREG, EVM_DEPOSIT))); - auto row = get_chain("ETH"); - BOOST_REQUIRE(!row.is_null()); - BOOST_REQUIRE_MESSAGE(row["code"].is_string(), - "a slug_name cell must render as its decoded string, not a wrapper"); - BOOST_REQUIRE_EQUAL("ETH", row["code"].as_string()); -} FC_LOG_AND_RETHROW() } - BOOST_FIXTURE_TEST_CASE(regchain_evm_addresses_stored, sysio_chains_tester) { try { BOOST_REQUIRE_EQUAL(success(), regchain(ChainKind::CHAIN_KIND_EVM, "ETH", 1, evm_outpost_mvo(EVM_OPP, EVM_INBOUND, EVM_OPREG, EVM_DEPOSIT))); diff --git a/contracts/tests/sysio.dclaim_tests.cpp b/contracts/tests/sysio.dclaim_tests.cpp index 8c69c68286..3d428c4827 100644 --- a/contracts/tests/sysio.dclaim_tests.cpp +++ b/contracts/tests/sysio.dclaim_tests.cpp @@ -17,7 +17,7 @@ using namespace sysio::opp::types; using mvo = fc::mutable_variant_object; /// Test fixture for sysio.dclaim. Deploys sysio.dclaim and creates sysio.msgch / -/// sysio.authex as the authorized inbound callers. The v6 staking-reward path +/// sysio.authex as the authorized inbound callers. The staking-reward path /// credits a WIRE-denominated amount directly (native -> WIRE conversion is /// outpost-side), so no sysio.reserv deployment is needed. class sysio_dclaim_tester : public tester { diff --git a/contracts/tests/sysio.dispatch_tests.cpp b/contracts/tests/sysio.dispatch_tests.cpp index 5b8cf72a9d..cf53924880 100644 --- a/contracts/tests/sysio.dispatch_tests.cpp +++ b/contracts/tests/sysio.dispatch_tests.cpp @@ -1,7 +1,7 @@ /// Cross-contract dispatch tests for sysio.msgch's per-attestation-type /// routing (Task 4 of the operator-collateral plan). /// -/// v6 data-model: identity moved to slug_name-keyed registries. The dispatch +/// Data model: identity moved to slug_name-keyed registries. The dispatch /// surface still routes `OPERATOR_ACTION` payloads into opreg, but the /// payload schema now carries `chain_code` (slug_name uint64) instead of a /// `ChainKind chain` field, and `TokenAmount.token_code` (slug_name uint64) @@ -228,7 +228,7 @@ std::vector em_pubkey_bytes(const fc::crypto::public_key& pk) { return std::vector(compressed.begin(), compressed.end()); } -/// Encode an OperatorAction attestation payload (v6 schema). +/// Encode an OperatorAction attestation payload (schema). /// `chain_code` and `amount.token_code` are slug_name-packed uint64 values. std::string encode_operator_action( sysio::opp::attestations::OperatorAction_ActionType action_type, @@ -527,7 +527,7 @@ class sysio_dispatch_tester : public tester { create_uwrit_op_eth_authex_link(); - // v6: chains are first-class registry rows. + // Chains are first-class registry rows. BOOST_REQUIRE_EQUAL(success(), push(CHAINS_ACCOUNT, chains_abi, CHAINS_ACCOUNT, "regchain"_n, mvo() ("kind", outpost_kind) diff --git a/contracts/tests/sysio.epoch_flushwtdw_tests.cpp b/contracts/tests/sysio.epoch_flushwtdw_tests.cpp index e0fe9ff674..3fc1c889e5 100644 --- a/contracts/tests/sysio.epoch_flushwtdw_tests.cpp +++ b/contracts/tests/sysio.epoch_flushwtdw_tests.cpp @@ -1,7 +1,7 @@ /// Cross-contract tests for the `sysio.epoch::advance` ↔ `sysio.opreg:: /// flushwtdw` integration (Task 9 of the operator-collateral plan). /// -/// v6 data-model: identity is now slug_name-keyed across opreg / chains. +/// Data model: identity is now slug_name-keyed across opreg / chains. /// The fixture deploys `sysio.chains` so the chain-of-record exists and /// uses `regchain` (replacing the v5 `regoutpost`). @@ -289,7 +289,7 @@ class sysio_epoch_flushwtdw_tester : public tester { } /// Direct opreg::depositinle, signed as opreg itself. - /// v6 signature: codenames for chain and token, plus the actor identity. + /// Signature: codenames for chain and token, plus the actor identity. action_result depositinle(name account, std::string_view chain_code, std::string_view token_code, uint64_t amount) { return push(OPREG_ACCOUNT, opreg_abi, OPREG_ACCOUNT, "depositinle"_n, mvo() @@ -485,7 +485,7 @@ BOOST_FIXTURE_TEST_CASE(slashed_operator_withdraw_drops_silently, /// emitted by `sysio.opreg::emit_*` parse as a standard protobuf /// `OperatorAction` message. They were originally written against the v5 /// OperatorAction proto (with a `chain` ChainKind field and a -/// `TokenAmount.kind` TokenKind field). The v6 proto carries +/// `TokenAmount.kind` TokenKind field). The proto carries /// `chain_code` (uint64) and `amount.token_code` (uint64) instead — same /// shape, different field semantics — so the parse + field-1-tag-byte /// invariant still holds. diff --git a/contracts/tests/sysio.epoch_tests.cpp b/contracts/tests/sysio.epoch_tests.cpp index 95a3797c3d..7852733129 100644 --- a/contracts/tests/sysio.epoch_tests.cpp +++ b/contracts/tests/sysio.epoch_tests.cpp @@ -17,7 +17,7 @@ using namespace sysio::opp::types; using mvo = fc::mutable_variant_object; -/// v6: `regoutpost` is gone; `sysio.chains::regchain` is its replacement. The +/// `regoutpost` is gone; `sysio.chains::regchain` is its replacement. The /// tests still focus on epoch lifecycle, so they only depend on a `sysio.chains` /// row existing for downstream epoch lookups. class sysio_epoch_tester : public tester { @@ -98,7 +98,7 @@ class sysio_epoch_tester : public tester { return push_epoch_action(EPOCH_ACCOUNT, "schbatchgps"_n, mvo()); } - /// v6 replacement for `regoutpost`: register a chain row in `sysio.chains`. + /// Replacement for `regoutpost`: register a chain row in `sysio.chains`. /// Codenames stand in for the old `ChainKind` per-chain identity. action_result regchain(ChainKind kind, const std::string& code_str, uint32_t external_chain_id, diff --git a/contracts/tests/sysio.msgch_tests.cpp b/contracts/tests/sysio.msgch_tests.cpp index a5c198d9f2..6b3c1c6d03 100644 --- a/contracts/tests/sysio.msgch_tests.cpp +++ b/contracts/tests/sysio.msgch_tests.cpp @@ -158,7 +158,7 @@ BOOST_FIXTURE_TEST_CASE(deliver_invalid_request, sysio_msgch_tester) { try { } FC_LOG_AND_RETHROW() } BOOST_FIXTURE_TEST_CASE(queueout_basic, sysio_msgch_tester) { try { - // v6: chain_code is the chain's slug_name value. queueout requires a + // Chain_code is the chain's slug_name value. queueout requires a // registered chain row -- a pure registration/ops gate (the terminal // account estimator that once lived behind it is deleted): an unregistered // code would otherwise create a READY row no epoch fan-out ever drains. @@ -175,7 +175,7 @@ BOOST_FIXTURE_TEST_CASE(queueout_basic, sysio_msgch_tester) { try { } FC_LOG_AND_RETHROW() } BOOST_FIXTURE_TEST_CASE(buildenv_basic, sysio_msgch_tester) { try { - // v6 buildenv looks up the chain row in sysio.chains before doing any + // buildenv looks up the chain row in sysio.chains before doing any // packing work; without a registered chain it would fail with "key not // found". The empty-queue early-return happens before that lookup, so // an unregistered chain_code still returns success when there are no @@ -251,7 +251,7 @@ class sysio_msgch_envlog_tester : public tester { )); } - /// v6 replacement for `regoutpost` — register a chain row in sysio.chains. + /// Replacement for `regoutpost` — register a chain row in sysio.chains. /// The slug_name `code` carries the per-chain identity that used to come /// from `ChainKind`. Tests pass a deterministic spelling derived from the /// `kind` so successive callers don't collide. @@ -348,7 +348,7 @@ namespace { using fc::slug_name_literals::operator""_s; -/// In v6, `chain_code` is the chain's slug_name value (uint64). All envlog +/// `chain_code` is the chain's slug_name value (uint64). All envlog /// tests register one EVM-class chain via `register_outpost(...)` which uses /// the spelling `"ETH"`. ETH_OUTPOST_ID is the slug_name's packed value. constexpr uint64_t ETH_OUTPOST_ID = "ETH"_s.value; diff --git a/contracts/tests/sysio.opreg_tests.cpp b/contracts/tests/sysio.opreg_tests.cpp index d919adbadb..b8e3480e7f 100644 --- a/contracts/tests/sysio.opreg_tests.cpp +++ b/contracts/tests/sysio.opreg_tests.cpp @@ -191,7 +191,7 @@ constexpr uint32_t kDellogPrunePerCrank = 64; } // namespace -/// v6 data-model: per-chain identity has moved from `ChainKind` enums to +/// Data model: per-chain identity has moved from `ChainKind` enums to /// `sysio::slug_name`-keyed registries (`sysio.chains`, `sysio.tokens`, /// `sysio.reserv`). The test fixture treats the codenames as opaque uint64 /// values; per-chain spelling ("ETH", "SOL", "WIRE", "LIQETH", ...) maps to @@ -243,7 +243,7 @@ class sysio_opreg_tester : public tester { epoch_abi_ser.set_abi(std::move(epoch_abi), abi_serializer::create_yield_function(abi_serializer_max_time)); } - // ── SlugName helpers (v6) ── + // ── SlugName helpers ── // // Codenames are 8-byte packed identifiers (`fc::slug_name`). The contract's // `sysio::slug_name` and the host-side `fc::slug_name` use the same packing @@ -285,7 +285,7 @@ class sysio_opreg_tester : public tester { } /// Build a single `chain_min_bond` entry as an fc::variant suitable for - /// `setconfig`'s `req_*_collat` vector arguments. v6: identity is by + /// `setconfig`'s `req_*_collat` vector arguments. identity is by /// (chain_code, token_code) codenames rather than the old enums. static fc::variant make_chain_min_bond(std::string_view chain_code, std::string_view token_code, @@ -386,10 +386,10 @@ class sysio_opreg_tester : public tester { BOOST_REQUIRE_EQUAL(0, op["is_bootstrapped"].as_uint64()); } - // ── Collateral-action helpers (msgch-dispatched paths, v6 codenames) ── + // ── Collateral-action helpers (msgch-dispatched paths, codenames) ── /// `depositinle`: dispatched from sysio.msgch. - /// v6 signature: `(account, chain_code, token_code, amount, + /// Signature: `(account, chain_code, token_code, amount, /// actor_chain ChainKind, actor_address bytes, /// original_message_id checksum256)`. action_result depositinle(name account, diff --git a/contracts/tests/sysio.reserv_tests.cpp b/contracts/tests/sysio.reserv_tests.cpp index a68cacc4de..39ceb6009a 100644 --- a/contracts/tests/sysio.reserv_tests.cpp +++ b/contracts/tests/sysio.reserv_tests.cpp @@ -33,7 +33,7 @@ std::vector em_pubkey_bytes(const fc::crypto::public_key& pk) { } // anonymous namespace -/// v6 data-model: reserves are keyed by the triple `(chain_code, token_code, +/// Data model: reserves are keyed by the triple `(chain_code, token_code, /// reserve_code)` (each a `sysio::slug_name` packed uint64). The legacy /// `setreserve` action is gone; `regreserve` is the bootstrap-window /// equivalent (it works only while `current_epoch_index == 0`, which is the @@ -185,11 +185,11 @@ class sysio_reserve_tester : public tester { return fc::raw::unpack(trace->action_traces[0].return_value); } - // ── SlugName helpers (v6) ── + // ── SlugName helpers ── static fc::slug_name cn(std::string_view s) { return fc::slug_name{s}; } - /// `regreserve` is the v6 bootstrap-window action for inserting a reserve + /// `regreserve` is the bootstrap-window action for inserting a reserve /// row with `status=ACTIVE` and REAL WIRE backing drained from the /// treasury. Triple-slug_name PK is `(chain_code, token_code, /// reserve_code)`; `is_private`/`owner` seed privately-owned reserves. @@ -358,7 +358,7 @@ class sysio_reserve_tester : public tester { BOOST_AUTO_TEST_SUITE(sysio_reserve_tests) -// ── regreserve (v6 bootstrap-window action; real-WIRE treasury drain) ── +// ── regreserve (bootstrap-window action; real-WIRE treasury drain) ── BOOST_FIXTURE_TEST_CASE(regreserve_creates_reserve_row, sysio_reserve_tester) { try { const int64_t treasury_before = wire_balance(SYSIO_ACCOUNT); diff --git a/contracts/tests/sysio.uwrit_tests.cpp b/contracts/tests/sysio.uwrit_tests.cpp index ecf037a293..7dcb88f57a 100644 --- a/contracts/tests/sysio.uwrit_tests.cpp +++ b/contracts/tests/sysio.uwrit_tests.cpp @@ -401,7 +401,7 @@ BOOST_FIXTURE_TEST_CASE(swapfromwire_rejects_unregistered_target_chain, sysio_uw BOOST_FIXTURE_TEST_CASE(rcrdcommit_requires_msgch_auth, sysio_uwrit_tester) { try { // rcrdcommit is invoked inline from sysio.msgch on UNDERWRITE_INTENT_COMMIT - // dispatch. v6 signature carries (from_chain_code, from_token_code, + // dispatch. the signature carries (from_chain_code, from_token_code, // reserve_code) slug_name triples in place of the old enum pair. BOOST_REQUIRE(push_uwrit_action("uwrit.a"_n, "rcrdcommit"_n, mvo() ("uwreq_id", 1) @@ -415,7 +415,7 @@ BOOST_FIXTURE_TEST_CASE(rcrdcommit_requires_msgch_auth, sysio_uwrit_tester) { tr } FC_LOG_AND_RETHROW() } BOOST_FIXTURE_TEST_CASE(rcrdcommit_rejects_unknown_uwreq, sysio_uwrit_tester) { try { - // v6: OPP handlers MUST NEVER throw (feedback_opp_handlers_never_throw.md + // OPP handlers MUST NEVER throw (feedback_opp_handlers_never_throw.md // — a `check()` in dispatch halts consensus). The previous error-based // assertion is gone; the action logs + skips on unknown uwreq and // returns success. The test now pins THAT invariant. @@ -435,7 +435,7 @@ BOOST_FIXTURE_TEST_CASE(rcrdcommit_rejects_unknown_uwreq, sysio_uwrit_tester) { // ── sumlocks (Task 3: read-only per-(underwriter, chain, token) lock total) ── BOOST_FIXTURE_TEST_CASE(sumlocks_zero_for_unbonded_underwriter, sysio_uwrit_tester) { try { - // v6 sumlocks signature: slug_name pair (chain_code, token_code). + // sumlocks signature: slug_name pair (chain_code, token_code). BOOST_REQUIRE_EQUAL(success(), push_uwrit_action("uwrit.a"_n, "sumlocks"_n, mvo() ("underwriter", "uwrit.a") @@ -456,7 +456,7 @@ BOOST_FIXTURE_TEST_CASE(sumlocks_zero_for_unbonded_underwriter, sysio_uwrit_test // legs to the source slot. This case verifies the dispatch still // auth-checks correctly when the two chains coincide. BOOST_FIXTURE_TEST_CASE(rcrdcommit_same_chain_swap_auth, sysio_uwrit_tester) { try { - // v6: slug_name triple disambiguates same-chain swap legs. + // Slug_name triple disambiguates same-chain swap legs. BOOST_REQUIRE(push_uwrit_action("uwrit.a"_n, "rcrdcommit"_n, mvo() ("uwreq_id", 7) ("underwriter", "uwrit.a") diff --git a/libraries/chain/include/sysio/chain/database_utils.hpp b/libraries/chain/include/sysio/chain/database_utils.hpp index 978e637378..e167080b16 100644 --- a/libraries/chain/include/sysio/chain/database_utils.hpp +++ b/libraries/chain/include/sysio/chain/database_utils.hpp @@ -500,20 +500,18 @@ inline void encode_field(writer& w, key_leaf_kind kind, const fc::variant& val) } // ── ABI-aware key shapes ──────────────────────────────────────────────────── -// kv/multi_index keys are not limited to the builtin leaf types above: a key -// may be a struct whose fields encode in declaration order. Note `sysio::kv` -// does NOT route through CDT's `to_key` — `make_key` writes into a -// `be_key_stream` whose operator<< set is closed (the integrals, name, the -// floats, bool, string, vector), so a struct key resolves through -// SYSLIB_SERIALIZE's generic-DataStream friend template and recurses to -// write_be64 per member. `slug_name` is the primary key of the v6 registry -// tables (sysio.chains chains, sysio.tokens tokens/chaintokens, sysio.reserv -// reserves) and is now a LEAF above, not a struct key; the byte encoding is -// unchanged either way. A key_shape is the resolved encode/decode plan for one key -// field: a leaf with a codec-supported type, or a struct node whose children -// encode in declaration order (matching to_key's reflected-field walk). Leaf -// types and their kinds are defined above, with the codec (key_leaf_kind / -// leaf_key_spellings / leaf_kind_of). +// A key_shape is the resolved encode/decode plan for one key field: either a +// leaf of a codec-supported type (key_leaf_kind / leaf_key_spellings / +// leaf_kind_of, above), or a struct node whose children encode in declaration +// order — matching to_key's reflected-field walk. +// +// Struct nodes exist because a kv/multi_index key may itself be a struct, and +// they encode the same bytes a leaf does: `sysio::kv` never routes through +// CDT's `to_key`. `make_key` writes into a `be_key_stream` whose operator<< +// set is closed, so a struct key resolves through SYSLIB_SERIALIZE's generic +// friend template and recurses to write_be64 per member. That is why promoting +// `slug_name` from a struct key to a leaf leaves every stored key +// byte-identical. /// Canonicalize abigen template spellings and chase ABI typedefs to a fixpoint. /// A visited set of resolved typedef names makes an alias cycle (a -> b -> ... diff --git a/libraries/opp/include/sysio/opp/opp.hpp b/libraries/opp/include/sysio/opp/opp.hpp index 7224652467..8a66c45fa3 100644 --- a/libraries/opp/include/sysio/opp/opp.hpp +++ b/libraries/opp/include/sysio/opp/opp.hpp @@ -68,7 +68,7 @@ FC_REFLECT_ENUM(sysio::opp::types::TokenKind, (TOKEN_KIND_SPL_NFT) (TOKEN_KIND_LIQ)) -// v6 — Reserve lifecycle +// Reserve lifecycle FC_REFLECT_ENUM(sysio::opp::types::ReserveStatus, (RESERVE_STATUS_UNKNOWN) (RESERVE_STATUS_PENDING) @@ -126,7 +126,7 @@ FC_REFLECT_ENUM(sysio::opp::attestations::OperatorAction_ActionType, (OperatorAction_ActionType_ACTION_TYPE_WITHDRAW_REMIT) (OperatorAction_ActionType_ACTION_TYPE_SLASH)) -// ReserveTarget_Kind removed in v6 — the carrier (renamed `ReserveAmount`) +// ReserveTarget_Kind removed in the data-model refactor — the carrier (renamed `ReserveAmount`) // is now (chain_code, reserve_code, TokenAmount) in the depot 9-dec frame. FC_REFLECT_ENUM(sysio::opp::types::AttestationStatus, diff --git a/plugins/batch_operator_plugin/src/batch_operator_plugin.cpp b/plugins/batch_operator_plugin/src/batch_operator_plugin.cpp index 2a6296ea0e..6ab094645d 100644 --- a/plugins/batch_operator_plugin/src/batch_operator_plugin.cpp +++ b/plugins/batch_operator_plugin/src/batch_operator_plugin.cpp @@ -115,7 +115,7 @@ namespace { } } - /// v6: chain registry was split out of `sysio.epoch` onto its own + /// Chain registry was split out of `sysio.epoch` onto its own /// `sysio.chains` contract. The `outposts` table was replaced by the /// `chains` KV table, keyed by slug_name (uint64 packed). Field spellings /// are shared with underwriter_plugin, which reads the same rows. @@ -445,7 +445,7 @@ struct batch_operator_plugin::impl { * in the batch-op log without grep'ing every poll. */ void poll_own_status() { - // v6: `sysio.opreg::operators` is a KV table whose PK is a struct + // `sysio.opreg::operators` is a KV table whose PK is a struct // `{account: name}`; the chain_plugin's `lower_bound` / `upper_bound` // expects JSON-shaped key bounds for KV tables, not the bare name // string the v5 multi_index path accepted. Easiest robust fix: scan @@ -573,7 +573,7 @@ struct batch_operator_plugin::impl { // ----------------------------------------------------------------------- void refresh_outposts() { - // v6: chain registry lives on `sysio.chains::chains` (replaces the + // Chain registry lives on `sysio.chains::chains` (replaces the // removed `sysio.epoch::outposts` table). Each row carries the // chain's slug_name + kind + external_chain_id + is_depot + active. // Outposts are the non-depot, active rows; the single is_depot=true @@ -640,7 +640,7 @@ struct batch_operator_plugin::impl { } outposts.push_back(std::move(od)); } - ilog("batch_operator: loaded {} outposts (v6 sysio.chains)", outposts.size()); + ilog("batch_operator: loaded {} outposts (sysio.chains)", outposts.size()); prune_stale_opp_jobs(); build_opp_jobs(); schedule_opp_jobs(); diff --git a/plugins/outpost_solana_client_plugin/include/sysio/outpost_solana_client_plugin.hpp b/plugins/outpost_solana_client_plugin/include/sysio/outpost_solana_client_plugin.hpp index 3bf9d0072f..a9021bb36b 100644 --- a/plugins/outpost_solana_client_plugin/include/sysio/outpost_solana_client_plugin.hpp +++ b/plugins/outpost_solana_client_plugin/include/sysio/outpost_solana_client_plugin.hpp @@ -199,7 +199,7 @@ struct opp_solana_outpost_client : fc::network::solana::solana_program_client { , vault_pda(fc::network::solana::system::find_program_address( {std::vector{'o','u','t','p','o','s','t','_','v','a','u','l','t'}}, prog_id).first) - // v6: SOL outpost reserve aggregate is seeded with b"reserve_aggregate" + // SOL outpost reserve aggregate is seeded with b"reserve_aggregate" // (see `RESERVE_AGGREGATE_SEED` in programs/opp-outpost/src/state/reserve.rs). // Previously this used b"outpost_reserve" which derived to a non-existent // PDA → epoch_in's `reserve_aggregate` account validation failed with diff --git a/plugins/underwriter_plugin/include/sysio/underwriter_plugin/routing_detail.hpp b/plugins/underwriter_plugin/include/sysio/underwriter_plugin/routing_detail.hpp index 2a9a060bcf..7e61c41544 100644 --- a/plugins/underwriter_plugin/include/sysio/underwriter_plugin/routing_detail.hpp +++ b/plugins/underwriter_plugin/include/sysio/underwriter_plugin/routing_detail.hpp @@ -5,7 +5,7 @@ * plugin, lifted out of the `.cpp`-private `impl` so they are unit-testable * without standing up a chain. * - * SEC-13 / WSA-027: the v6 depot identifies every value-bearing underwrite leg by + * SEC-13 / WSA-027: the depot identifies every value-bearing underwrite leg by * its EXACT slug codes — `chain_code`, `token_code`, `reserve_code` — and an * underwriter's collateral is posted per exact `(chain_code, token_code)` on every * registered outpost. Two active chains of the same VM family (e.g. two EVM @@ -115,7 +115,7 @@ inline void reserve_buckets(credit_buckets& remaining, reserve(dst); } -/// Local commit de-dup key — one CONFIRMED leg. Keyed by the exact v6 leg +/// Local commit de-dup key — one CONFIRMED leg. Keyed by the exact leg /// identity `(uwreq_id, chain_code, token_code, reserve_code)` so two legs that /// differ only by chain OR reserve (e.g. a same-`(chain, token)` swap with two /// reserves) are tracked independently and never suppress each other. diff --git a/plugins/underwriter_plugin/src/underwriter_plugin.cpp b/plugins/underwriter_plugin/src/underwriter_plugin.cpp index b8dfab6787..8726edd612 100644 --- a/plugins/underwriter_plugin/src/underwriter_plugin.cpp +++ b/plugins/underwriter_plugin/src/underwriter_plugin.cpp @@ -144,7 +144,7 @@ struct uw_request { /// `SwapDeposit` correlation hash, so it is the value the source-deposit /// verifier must reproduce. uint64_t target_amount; - /// Per-leg slug_name triples (v6 data-model). These are the authoritative + /// Per-leg slug_name triples (data-model). These are the authoritative /// identifiers for the depot's `rcrdcommit` routing and the /// `UnderwriteIntentCommit` (`chain_code` / `token_code` / `reserve_code`) /// payload populated in `create_signed_uic_bytes`. The `ChainKind` / @@ -196,7 +196,7 @@ struct uw_request { // Credit line — per-(chain_code, token_code) bond from sysio.opreg::operators // // Reads the `balances` field (one aggregate balance per EXACT -// (chain_code, token_code) — SEC-13/WSA-027: keyed by the v6 slug codes, NOT +// (chain_code, token_code) — SEC-13/WSA-027: keyed by the slug codes, NOT // the coarse (ChainKind, TokenKind) family, so two same-family chains hold // independent collateral). `read_credit_lines` starts from this raw balance // and subtracts active locks plus pending withdrawals to mirror the depot's @@ -378,7 +378,7 @@ struct underwriter_plugin::impl { /// The `commit_addr` each entry of `outpost_by_chain` was built against, so /// a `sysio.chains::setoutpost` redeploy is noticed and the handle rebuilt. std::map wired_commit_addrs; - /// v6 cross-walk: token slug_name → TokenKind enum. Refreshed each + /// cross-walk: token slug_name → TokenKind enum. Refreshed each /// scan cycle by `read_credit_lines` (which reads `sysio.tokens::tokens` /// for the lookup); used by `scan_pending_requests` to translate the /// uwreq row's `src/dst_token_code` slug into the `TokenKind` the @@ -388,7 +388,7 @@ struct underwriter_plugin::impl { // ── Outstanding commit tracking (one entry per CONFIRMED leg) ─────── // Per `feedback`: an underwriter that confirmed a commit tx for a leg // should NOT resubmit on the next scan cycle. The de-dup key is the EXACT - // v6 leg identity `(uwreq_id, chain_code, token_code, reserve_code)` + // leg identity `(uwreq_id, chain_code, token_code, reserve_code)` // (`underwriter_detail::commit_key`) — NOT the coarse (ChainKind, // TokenKind), so two legs differing only by chain or reserve are tracked // independently (SEC-13/WSA-027). The set is pruned at the end of each scan @@ -1191,7 +1191,7 @@ struct underwriter_plugin::impl { outpost_chain_kinds.clear(); outpost_external_chain_ids.clear(); outpost_endpoints.clear(); - // v6 refactor: chain rows moved from `sysio.epoch::outposts` to + // refactor: chain rows moved from `sysio.epoch::outposts` to // `sysio.chains::chains`. Each row is a `Chain` with fields: // `code` — slug_name (the universal chain identifier; the // v5 `outpost_id` was just this slug's uint64). @@ -1367,7 +1367,7 @@ struct underwriter_plugin::impl { void read_credit_lines() { credit_lines.clear(); - // v6 schema: balance / lock / withdraw rows carry `chain_code` + // schema: balance / lock / withdraw rows carry `chain_code` // and `token_code` slug_names (read via fc::slug_name's from_variant) // — not the v5 `chain` (ChainKind enum) / `token_kind` (TokenKind // enum). Translation: @@ -1392,7 +1392,7 @@ struct underwriter_plugin::impl { } } - // Local helper: read `chain_code`/`token_code` slug fields (v6 shape + // Local helper: read `chain_code`/`token_code` slug fields (shape // `{"value": }`) as their EXACT packed slug values. Returns nullopt // (row skipped) when the chain isn't a registered non-depot outpost or // the token is unknown — neither can back a leg. SEC-13/WSA-027: key by @@ -1503,10 +1503,10 @@ struct underwriter_plugin::impl { std::vector scan_pending_requests() { std::vector requests; - // v6: `sysio.uwrit::uwreqs` is now a KV table. The legacy + // `sysio.uwrit::uwreqs` is now a KV table. The legacy // multi_index-style `{"bystatus": }` lower_bound format // doesn't traverse the KV secondary index — it returns 0 rows. - // Until a v6 KV-index query path lands here, scan by primary + // Until a KV-index query path lands here, scan by primary // key and filter PENDING in C++. uwreqs is small (one row per // in-flight swap; race-resolved rows transition to other // statuses within an epoch), so this is cheap. @@ -1543,7 +1543,7 @@ struct underwriter_plugin::impl { AttestationType::ATTESTATION_TYPE_SWAP_REQUEST) continue; req.attestation_type = *attestation_type; - // v6 data-model schema: src/dst identity lives on the uwreq row as + // Data-model schema: src/dst identity lives on the uwreq row as // `(chain_code, token_code, reserve_code)` slug_name triples plus a // `*_amount`. Populated by `sysio.uwrit::createuwreq` from the // originating SwapRequest. Each is read through fc::slug_name's own @@ -1855,7 +1855,7 @@ struct underwriter_plugin::impl { /// provider, serialize failure, etc.). /// /// The slug_name triple `(chain_code, token_code, reserve_code)` is the - /// v6 routing scalar set the depot's `rcrdcommit` uses to disambiguate + /// routing scalar set the depot's `rcrdcommit` uses to disambiguate /// src vs dst legs — same-chain swaps with multiple reserves on a single /// `(chain, token)` pair are still resolvable because `reserve_code` /// breaks the tie. `chain_code` carries the chain identity at the OPP @@ -1875,7 +1875,7 @@ struct underwriter_plugin::impl { opp_att::UnderwriteIntentCommit uic; uic.mutable_uw_account()->set_name(underwriter_account.to_string()); uic.set_uw_request_id(uwreq_id); - // v6 data-model: leg identity is the slug_name triple. The wire format + // Data model: leg identity is the slug_name triple. The wire format // for each field is the packed uint64 slug_name value (alphabet // `[A-Z0-9_]+`, ≤8 chars). The depot decodes these back to // `sysio::slug_name` via `sysio::slug_name{uic.chain_code}` etc. in @@ -2621,7 +2621,7 @@ struct underwriter_plugin::impl { req.dst_token_code.to_string(), req.dst_reserve_code.to_string()); - // Per-leg dispatch keyed on the v6 slug_name triple + // Per-leg dispatch keyed on the slug_name triple // `(chain_code, token_code, reserve_code)`. Same-chain swaps (e.g. // ERC20 → native on one outpost) share `chain_code` between the two // legs but differ on `token_code`/`reserve_code`; the UIC payload diff --git a/plugins/underwriter_plugin/test/test_underwriter_routing.cpp b/plugins/underwriter_plugin/test/test_underwriter_routing.cpp index bb79ff3b75..6569bf46b1 100644 --- a/plugins/underwriter_plugin/test/test_underwriter_routing.cpp +++ b/plugins/underwriter_plugin/test/test_underwriter_routing.cpp @@ -11,7 +11,7 @@ * routing/accounting keys. * * The plugin must key its credit buckets and its local commit de-dup by the - * EXACT v6 slug codes — `(chain_code, token_code[, reserve_code])` — NOT the + * EXACT slug codes — `(chain_code, token_code[, reserve_code])` — NOT the * coarse `(ChainKind, TokenKind)` VM family. Otherwise two active chains of the * same family (e.g. two EVM outposts) collapse onto one key: their collateral * merges into a single bucket and a confirmed commit on one suppresses the diff --git a/tests/get_table_tests.cpp b/tests/get_table_tests.cpp index fc3f3194f3..2f6204264f 100644 --- a/tests/get_table_tests.cpp +++ b/tests/get_table_tests.cpp @@ -452,9 +452,8 @@ BOOST_FIXTURE_TEST_CASE( get_table_next_key_test, validating_tester ) try { push_action("test"_n, "addhashobj"_n, "test"_n, mutable_variant_object()("hashinput", "firstinput")); push_action("test"_n, "addhashobj"_n, "test"_n, mutable_variant_object()("hashinput", "secondinput")); push_action("test"_n, "addhashobj"_n, "test"_n, mutable_variant_object()("hashinput", "thirdinput")); - // structobjs: kv::table keyed by the reflected struct composite_key{value} — the - // key shape the v6 registry tables used BEFORE `slug_name` became an ABI builtin - // (they now key on a slug_name LEAF). Drives the struct-key path in (sec-10). + // structobjs: kv::table keyed by the reflected struct composite_key{value}. + // Drives the struct-key expansion path in (sec-10). push_action("test"_n, "addstruct"_n, "test"_n, mutable_variant_object()("code", 10)("payload", 100)); push_action("test"_n, "addstruct"_n, "test"_n, mutable_variant_object()("code", 20)("payload", 200)); push_action("test"_n, "addstruct"_n, "test"_n, mutable_variant_object()("code", 30)("payload", 300)); @@ -748,11 +747,7 @@ BOOST_FIXTURE_TEST_CASE( get_table_next_key_test, validating_tester ) try { } // (sec-10) structobjs primary key — kv::table keyed by the reflected struct - // `composite_key { value: uint64 }` — the struct key shape the v6 - // registry tables used before `slug_name` became an ABI builtin. - // Deliberately not named `slug_name`: that spelling now resolves as - // a codec LEAF, which would bypass struct expansion entirely and - // leave this case passing while testing nothing. + // `composite_key { value: uint64 }`. // Exercises the ABI-aware BE key codec's struct-key expansion on the // live get_table_rows path: a JSON bound of the documented nested // `{ "code": { "value": N } }` form (encode_key), and a `next_key` diff --git a/tests/producer_rank_test.py b/tests/producer_rank_test.py index f75249ee19..edcf442d47 100755 --- a/tests/producer_rank_test.py +++ b/tests/producer_rank_test.py @@ -258,7 +258,7 @@ def producerRow(name): """The producer's `sysio.system::producers` row, or None if it has none. - v6 promotes the table to KV, so each row arrives as {"key": ..., "value": ...} and the + The KV promotion means, so each row arrives as {"key": ..., "value": ...} and the fields live under `value`; the fallback keeps this working if that ever flattens. """ resp = node0.processUrllibRequest("chain", "get_table_rows", { diff --git a/unittests/be_key_codec_tests.cpp b/unittests/be_key_codec_tests.cpp index d775b64259..c8b0870b47 100644 --- a/unittests/be_key_codec_tests.cpp +++ b/unittests/be_key_codec_tests.cpp @@ -73,7 +73,7 @@ BOOST_AUTO_TEST_CASE(composite_key_struct_roundtrip) { // ── slug_name as a codec LEAF ─────────────────────────────────────────────── // `slug_name` is an abi_serializer builtin and a leaf_key_spellings entry, so // it needs no abi.structs entry here — build_key_shapes resolves it through -// leaf_kind_of. These pin the leaf's carrier and the two properties the v6 +// leaf_kind_of. These pin the leaf's carrier and the two properties the registry // registry tables depend on: byte compatibility with the struct-key encoding it // replaced, and prefix grouping. @@ -124,7 +124,7 @@ BOOST_AUTO_TEST_CASE(slug_name_leaf_roundtrips_a_non_canonical_value_as_an_integ } BOOST_AUTO_TEST_CASE(slug_name_multi_leaf_keys_preserve_field_order_and_offsets) { - // THREE of the five v6 registry tables key on more than one slug: + // THREE of the five registry tables key on more than one slug: // sysio.tokens::chaintokens ["slug_name","slug_name"] // sysio.reserv::reserves ["slug_name","slug_name","slug_name"] // sysio.uwrit::locksums ["name","slug_name","slug_name"] @@ -412,7 +412,7 @@ BOOST_AUTO_TEST_CASE(typedef_cycle_is_rejected) { } // Scoped table whose within-scope primary key is a struct (composite_key). The real -// v6 registry tables are unscoped, but chain_plugin supports scoped tables by +// registry tables are unscoped, but chain_plugin supports scoped tables by // stripping the leading scope field's shape from the bound shapes and encoding // only the within-scope portion (see get_table_rows' scope_key_count erase). // This pins that slice-then-encode path for a struct-typed within-scope key: diff --git a/unittests/test-contracts/get_table_test/get_table_test.hpp b/unittests/test-contracts/get_table_test/get_table_test.hpp index 1d71f00d5c..414d574b4f 100644 --- a/unittests/test-contracts/get_table_test/get_table_test.hpp +++ b/unittests/test-contracts/get_table_test/get_table_test.hpp @@ -102,12 +102,10 @@ class [[sysio::contract]] get_table_test : public sysio::contract { // pagination must round-trip the nested `{ "code": { "value": N } }` key // shape — coverage a flat scalar key cannot provide. // - // Deliberately NOT named `slug_name`: that spelling is an abi_serializer - // builtin and a `leaf_key_spellings` entry, and abigen's builtin match is - // on the namespace-stripped bare name — so a member struct called - // `slug_name` would be emitted as the builtin, take the leaf branch in - // `build_key_shape`, and stop exercising struct expansion at all. The - // suite would keep passing while testing nothing it was written for. + // The name must not collide with an ABI builtin. abigen matches builtins on + // the namespace-stripped bare name, so a key struct named after one is + // emitted AS that builtin, takes the leaf branch in `build_key_shape`, and + // stops exercising struct expansion — while the suite keeps passing. struct composite_key { uint64_t value = 0; SYSLIB_SERIALIZE(composite_key, (value)) From e66cc36b92f1aacc17e0dd9fa7815875201bb3f8 Mon Sep 17 00:00:00 2001 From: kevin Heifner Date: Wed, 16 Sep 2026 13:48:32 -0500 Subject: [PATCH 04/29] fix(chain): reject a signed slug spelling, and pin builtin-over-struct precedence MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit From a six-lane review of the previous two commits. from_variant's over-long string arm routed everything to as_uint64(), which goes through boost::lexical_cast — and that does NOT reject a sign for an unsigned target, it WRAPS. So "-12345678" was admitted as 18446744073697205938, and a JSON bound of that shape paged silently from the far end of a table where it had previously been a clean 500. Guarded with an all-digits check, which also restores the diagnostic: a non-numeric over-long string now falls through to the validating parse and reports `invalid slug_name '...': too long` again rather than `Couldn't parse uint64_t`. The number spelling had the same hole and the first fix missed it, leaving the two arms disagreeing — "-1" rejected as a string, accepted as a number. Both reject now. null/false/true still coerce through as_uint64 exactly as they do for every other uint64 key leaf; that is deliberate and now documented in the test, since 0 is a legitimate slug (the absent sentinel). Precedence: every shipped registry ABI carries a `slug_name` struct_def alongside the field, and `slug_name` is the ONLY builtin name so shadowed — symbol, symbol_code, name, asset and checksum256 appear in no structs[] entry. set_abi has no collision check, so those ABIs are genuinely ambiguous and are resolved only by lookup order. That was untested at BOTH resolution sites: - the action/row data path (built_in_types before structs) — abi_tests now declares the shadowing struct and asserts the serializer's own post-set_abi view, is_builtin_type && is_struct, which proves set_abi kept the struct rather than dropping it. Behavioural assertions alone could not discriminate: if the struct won, a string input would not render differently, it would fail to encode at all, since the struct branch throws pack_exception for a non-object input. - the key codec (leaf_kind_of before abi.structs) — its own case, on a fresh abi_def rather than make_test_abi(), whose avoidance of the name `slug_name` is deliberate and load-bearing for the struct-expansion and typedef-chain paths. `is_leaf` is the discriminator there: if the struct won, encode_key would demand the nested form. Also: two v6 mentions the earlier sweep missed, in .proto files its grep never covered (the same sweep removed the C++ mirrors of those exact section headers), and a sentence that sweep mangled in a docstring. contracts_unit_test 761, unit_test 1536, plugin_test 295, codename_tests 38. Change-Id: I70d61911aa5f39d34c64b143265fd21c8f6d4d22 --- libraries/libfc/include/fc/slug_name.hpp | 17 ++++++++- libraries/libfc/test/test_slug_name.cpp | 38 +++++++++++++++++++ .../sysio/opp/attestations/attestations.proto | 2 +- .../opp/proto/sysio/opp/types/types.proto | 2 +- tests/producer_rank_test.py | 2 +- unittests/abi_tests.cpp | 26 +++++++++++++ unittests/be_key_codec_tests.cpp | 34 +++++++++++++++++ 7 files changed, 117 insertions(+), 4 deletions(-) diff --git a/libraries/libfc/include/fc/slug_name.hpp b/libraries/libfc/include/fc/slug_name.hpp index 9b876d265c..566d56b60e 100644 --- a/libraries/libfc/include/fc/slug_name.hpp +++ b/libraries/libfc/include/fc/slug_name.hpp @@ -20,6 +20,7 @@ #include #include +#include #include #include #include @@ -139,7 +140,14 @@ inline void from_variant(const fc::variant& v, slug_name& s) { // digits — so no valid spelling is diverted. In particular the 8-digit // "12345678" stays a slug, keeping the rule that `"7"` is the slug 7 and // not the integer 7. - if (text.size() > static_cast(slug_name_traits::max_len)) { + // The all-digits guard is load-bearing: `as_uint64` goes through + // boost::lexical_cast, which does NOT reject a sign for an unsigned + // target — it WRAPS, so "-12345678" would be admitted as + // 18446744073697205938. Anything over-long that is not a plain decimal + // falls through to the validating parse below, which rejects it. + if (text.size() > static_cast(slug_name_traits::max_len) + && std::all_of(text.begin(), text.end(), + [](char c) { return c >= '0' && c <= '9'; })) { s = slug_name{ v.as_uint64() }; return; } @@ -152,6 +160,13 @@ inline void from_variant(const fc::variant& v, slug_name& s) { s = slug_name{ v.get_object()["value"].as_uint64() }; return; } + // A negative number is never a slug, and `as_uint64` would WRAP it rather + // than reject it (boost::lexical_cast does not reject a sign for an unsigned + // target), so a JSON bound of `-1` would silently page from the far end of + // the table. Rejecting it here also keeps the two arms consistent: the string + // arm above already rejects "-12345678". + if (v.is_int64() && v.as_int64() < 0) + slug_name_traits::throw_invalid(std::to_string(v.as_int64()), "negative"); s = slug_name{ v.as_uint64() }; } diff --git a/libraries/libfc/test/test_slug_name.cpp b/libraries/libfc/test/test_slug_name.cpp index cb06388707..9ac8d27aab 100644 --- a/libraries/libfc/test/test_slug_name.cpp +++ b/libraries/libfc/test/test_slug_name.cpp @@ -529,4 +529,42 @@ BOOST_AUTO_TEST_CASE(mvo_accepts_every_spelling_a_caller_can_write) { BOOST_CHECK_EQUAL(back.value, 7u); } +BOOST_AUTO_TEST_CASE(variant_numeric_string_arm_rejects_a_signed_spelling) { + // The over-long string arm routes to as_uint64() for the integer carrier that + // crossed JSON text. as_uint64 goes through boost::lexical_cast, which does + // NOT reject a sign for an unsigned target — it WRAPS. Without the all-digits + // guard, "-12345678" (9 chars, so over max_len) was admitted as + // 18446744073697205938 instead of being rejected. + slug_name back; + for (const char* spelling : {"-12345678", "-123456789", "+123456789", "-1"}) { + BOOST_CHECK_THROW(fc::from_variant(fc::variant(std::string{spelling}), back), + fc::exception); + } + + // A plain decimal over max_len is still the integer carrier, which is the + // whole point of the arm. + fc::from_variant(fc::variant(std::string{"4294967296"}), back); + BOOST_CHECK_EQUAL(back.value, 4294967296u); + + // Zero-padded is all-digits, so it is accepted and means what it says. + fc::from_variant(fc::variant(std::string{"000000000000000042"}), back); + BOOST_CHECK_EQUAL(back.value, 42u); + + // The NUMBER spelling must agree with the string spelling. `as_uint64` + // wraps a negative rather than rejecting it, so without a guard a JSON + // bound of `-1` would silently page from the far end of the table while + // `"-1"` threw — the two arms disagreeing is worse than either choice. + BOOST_CHECK_THROW(fc::from_variant(fc::variant(int64_t{-1}), back), fc::exception); + BOOST_CHECK_THROW(fc::from_variant(fc::variant(int64_t{-12345678}), back), fc::exception); + + // NOT changed, and documented so the next reader knows it is deliberate: + // null / false / true coerce through fc::variant::as_uint64 exactly as they + // do for every other uint64 key leaf. Making slug_name alone strict here + // would diverge from the rest of the ABI for no gain — `0` is a legitimate + // slug value (the absent sentinel). + fc::from_variant(fc::variant(), back); BOOST_CHECK_EQUAL(back.value, 0u); + fc::from_variant(fc::variant(false), back); BOOST_CHECK_EQUAL(back.value, 0u); + fc::from_variant(fc::variant(true), back); BOOST_CHECK_EQUAL(back.value, 1u); +} + BOOST_AUTO_TEST_SUITE_END() diff --git a/libraries/opp/proto/sysio/opp/attestations/attestations.proto b/libraries/opp/proto/sysio/opp/attestations/attestations.proto index 69ce3d8721..b412b226f1 100644 --- a/libraries/opp/proto/sysio/opp/attestations/attestations.proto +++ b/libraries/opp/proto/sysio/opp/attestations/attestations.proto @@ -426,7 +426,7 @@ message AttestationProcessingError { } // --------------------------------------------------------------------------- -// Reserve creation flow (v6 — outpost-initiated, depot-matched) +// Reserve creation flow (outpost-initiated, depot-matched) // // Lifecycle: // 1. User calls outpost `create_reserve(...)`. Outpost takes external_token_amount diff --git a/libraries/opp/proto/sysio/opp/types/types.proto b/libraries/opp/proto/sysio/opp/types/types.proto index da05cce45b..cbbda0d2db 100644 --- a/libraries/opp/proto/sysio/opp/types/types.proto +++ b/libraries/opp/proto/sysio/opp/types/types.proto @@ -269,7 +269,7 @@ enum AttestationType { // always pay; there is no post-underwriting rejection path. // --------------------------------------------------------------------------- - // Reserve-flow attestations (v6 data-model refactor, 2026-05-19) + // Reserve-flow attestations (data-model refactor, 2026-05-19) // Outpost-initiated reserve creation with depot-side matching handshake. // --------------------------------------------------------------------------- ATTESTATION_TYPE_RESERVE_CREATE = 60958; // 0xEE1E outpost → depot diff --git a/tests/producer_rank_test.py b/tests/producer_rank_test.py index edcf442d47..a7d429826d 100755 --- a/tests/producer_rank_test.py +++ b/tests/producer_rank_test.py @@ -258,7 +258,7 @@ def producerRow(name): """The producer's `sysio.system::producers` row, or None if it has none. - The KV promotion means, so each row arrives as {"key": ..., "value": ...} and the + The table is a KV table, so each row arrives as {"key": ..., "value": ...} and the fields live under `value`; the fallback keeps this working if that ever flattens. """ resp = node0.processUrllibRequest("chain", "get_table_rows", { diff --git a/unittests/abi_tests.cpp b/unittests/abi_tests.cpp index 3e21b14081..506e852b4e 100644 --- a/unittests/abi_tests.cpp +++ b/unittests/abi_tests.cpp @@ -691,11 +691,27 @@ BOOST_AUTO_TEST_CASE(slug_name_builtin_type) // contracts/tests cannot catch either case, because fc::slug_name's // from_variant accepts the string, the integer AND the object form, so those // reads pass identically whether or not this registration exists. + // The `slug_name` struct_def below is NOT filler: every one of the five + // registry ABIs shipped today carries exactly this shadowed definition, and + // `slug_name` is the ONLY builtin name so shadowed (`symbol`, `name`, + // `asset` appear in no `structs[]`). `set_abi` has no collision check, so + // the ABI is genuinely ambiguous and is resolved only by LOOKUP ORDER — + // `built_in_types` at abi_serializer.cpp:706 before `structs` at :778. If + // the struct ever won, the field would still encode to 8 bytes but + // `binary_to_variant` would yield `{"value":N}`, so the `is_string()` + // assertions below are what pin the precedence. const char* test_abi = R"=====( { "version": "sysio::abi/1.0", "types": [], "structs": [{ + "name": "slug_name", + "base": "", + "fields": [{ + "name": "value", + "type": "uint64" + }] + },{ "name": "regrow", "base": "", "fields": [{ @@ -712,6 +728,16 @@ BOOST_AUTO_TEST_CASE(slug_name_builtin_type) auto abi = fc::json::from_string(test_abi).as(); abi_serializer abis(sysio_contract_abi(abi), yield_fn()); + // Both lookups resolve, and the BUILTIN is the one that wins. Asserting the + // serializer's own post-`set_abi` view is stronger than inspecting the input + // `abi_def`: it proves `set_abi` KEPT the struct_def rather than dropping or + // rejecting it, which is what makes the ambiguity real. Note that if the + // struct won instead, the inputs below would not merely render differently — + // they would not encode at all, because the struct branch throws + // `pack_exception` for a non-object/array input (abi_serializer.cpp:831). + BOOST_REQUIRE( abis.is_builtin_type("slug_name") ); + BOOST_REQUIRE( abis.is_struct("slug_name") ); + // A canonical slug is carried as its STRING spelling, in 8 bytes. auto bytes = abis.variant_to_binary( "regrow", fc::json::from_string(R"({"code":"ETH"})"), yield_fn()); diff --git a/unittests/be_key_codec_tests.cpp b/unittests/be_key_codec_tests.cpp index c8b0870b47..6ad79d916e 100644 --- a/unittests/be_key_codec_tests.cpp +++ b/unittests/be_key_codec_tests.cpp @@ -123,6 +123,40 @@ BOOST_AUTO_TEST_CASE(slug_name_leaf_roundtrips_a_non_canonical_value_as_an_integ BOOST_CHECK(bytes == codec::encode_key(decoded, shapes)); } +BOOST_AUTO_TEST_CASE(slug_name_leaf_wins_over_a_shadowing_struct_def) { + // The collision spans TWO independent resolution sites. abi_tests covers the + // action/row data path (built_in_types at abi_serializer.cpp:706 before + // structs at :778); the key codec is its own lookup (leaf_kind_of at + // database_utils.hpp:558 before the struct table), and it needs its own pin. + // + // Every shipped registry ABI carries a `slug_name` struct_def alongside the + // field, and `slug_name` is the ONLY builtin name so shadowed. `set_abi` has + // no collision check, so the ABI is genuinely ambiguous and resolved only by + // lookup order. If the struct won here, `encode_key` would demand the nested + // `{"code":{"value":N}}` form and `decode_key` would emit it — so `is_leaf` + // is the exact discriminator for the bounds and `next_key` path. + // + // Deliberately NOT built on make_test_abi(): that fixture avoids the name + // `slug_name` on purpose, and its avoidance is load-bearing for the + // struct-expansion and typedef-chain cases. + abi_def abi; + abi.structs.emplace_back(struct_def{"slug_name", "", {field_def{"value", "uint64"}}}); + + auto shapes = codec::build_key_shapes(abi, {"code"}, {"slug_name"}); + BOOST_REQUIRE_EQUAL(shapes.size(), 1u); + BOOST_REQUIRE(shapes[0].is_leaf); + BOOST_CHECK(shapes[0].kind == codec::key_leaf_kind::slug_name); + BOOST_CHECK(shapes[0].children.empty()); + + // And it round-trips as the leaf carrier, not as a nested object. + auto bytes = codec::encode_key( + fc::variant(fc::mutable_variant_object("code", "LIQSOL")), shapes); + BOOST_REQUIRE_EQUAL(bytes.size(), 8u); + auto decoded = codec::decode_key(bytes.data(), bytes.size(), shapes); + BOOST_CHECK(decoded.get_object()["code"].is_string()); + BOOST_CHECK_EQUAL(decoded.get_object()["code"].as_string(), "LIQSOL"); +} + BOOST_AUTO_TEST_CASE(slug_name_multi_leaf_keys_preserve_field_order_and_offsets) { // THREE of the five registry tables key on more than one slug: // sysio.tokens::chaintokens ["slug_name","slug_name"] From f831d61227b74060aec769753fce0db506fdced4 Mon Sep 17 00:00:00 2001 From: kevin Heifner Date: Wed, 16 Sep 2026 17:53:41 -0500 Subject: [PATCH 05/29] feat(chain): give slug_name one canonical JSON carrier MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit to_variant renders the canonical spelling ("" for zero) and throws for a value below 2^42, which zero_terminates leaves with no spelling at all; from_variant takes that string plus the transitional {value} object and refuses everything else. The integer arm, its JSON-text length re-route, and the all-digits and negative guards that patched it are all deleted. One carrier means a caller writes a slug field exactly one way and a reader never branches on the JSON type. The throw is safe because get_table_rows already wraps each row's key decode and value render in its own try/catch, so a planted non-canonical row costs one cell. Adds the slug_name builtin to generate-sysio-contract-types.py, which had no entry and resolved only through the struct_def the ABIs still ship — every slug field would have become `unknown` once abigen stops emitting it. Adds get_table_tests (sec-11): a kv table keyed on slug_name, named after the builtin on purpose so the leaf branch runs end to end. Change-Id: I9351990c0aad6127139f23c09f5ce3b50b85bbcd --- .../tools/generate-sysio-contract-types.py | 8 +- .../include/sysio/chain/database_utils.hpp | 17 +- libraries/libfc/include/fc/slug_name.hpp | 95 +++------- libraries/libfc/test/test_slug_name.cpp | 165 ++++++++---------- .../sysio/opp/depot/chains_registry.hpp | 2 +- .../src/batch_operator_plugin.cpp | 7 +- .../src/underwriter_plugin.cpp | 8 +- tests/get_table_tests.cpp | 65 +++++++ unittests/abi_tests.cpp | 24 +-- unittests/be_key_codec_tests.cpp | 43 +++-- .../get_table_test/get_table_test.abi | 61 +++++++ .../get_table_test/get_table_test.cpp | 5 + .../get_table_test/get_table_test.hpp | 38 ++++ .../get_table_test/get_table_test.wasm | Bin 22208 -> 22662 bytes 14 files changed, 328 insertions(+), 210 deletions(-) diff --git a/contracts/tools/generate-sysio-contract-types.py b/contracts/tools/generate-sysio-contract-types.py index eb6cb5fc9a..e6129095aa 100755 --- a/contracts/tools/generate-sysio-contract-types.py +++ b/contracts/tools/generate-sysio-contract-types.py @@ -110,6 +110,12 @@ def emit_contract_interface(abi: dict, contract_name: str, contract_prefix: str) 'int128': {'type': 'string'}, 'uint128': {'type': 'string'}, 'float32': {'type': 'number'}, 'float64': {'type': 'number'}, 'float128': {'type': 'string'}, 'name': {'type': 'string', 'pattern': '^[a-z1-5.]{1,13}$'}, + # slug_name is an abi_serializer builtin whose JSON carrier is its canonical + # spelling — up to 8 symbols over [A-Z0-9_], with "" the zero sentinel. The + # entry must precede the structs lookup: every registry ABI still ships a + # `slug_name` struct_def, and without this the field would resolve to that + # `{value: uint64}` shape (or, once abigen stops emitting it, to `unknown`). + 'slug_name': {'type': 'string', 'pattern': '^[A-Z0-9_]{0,8}$'}, 'string': {'type': 'string'}, 'bytes': {'type': 'string', 'description': 'hex-encoded bytes'}, 'checksum256': {'type': 'string', 'pattern': '^[a-f0-9]{64}$'}, @@ -163,7 +169,7 @@ def abi_type_to_schema(abi_type: str, structs_map: dict, type_aliases: dict, 'int64': 'number | string', 'uint64': 'number | string', 'int128': 'string', 'uint128': 'string', 'float32': 'number', 'float64': 'number', 'float128': 'string', - 'name': 'string', 'string': 'string', 'bytes': 'string', + 'name': 'string', 'slug_name': 'string', 'string': 'string', 'bytes': 'string', 'checksum256': 'string', 'checksum160': 'string', 'checksum512': 'string', 'public_key': 'string', 'signature': 'string', 'symbol': 'string', 'symbol_code': 'string', diff --git a/libraries/chain/include/sysio/chain/database_utils.hpp b/libraries/chain/include/sysio/chain/database_utils.hpp index e167080b16..9b417fc8ea 100644 --- a/libraries/chain/include/sysio/chain/database_utils.hpp +++ b/libraries/chain/include/sysio/chain/database_utils.hpp @@ -365,11 +365,10 @@ inline fc::variant decode_field(reader& r, key_leaf_kind kind) { } case key_leaf_kind::name: return fc::variant(name(r.read_be64()).to_string()); case key_leaf_kind::slug_name: { - // Delegates to fc::slug_name's to_variant, so next_key inherits the same - // total, injective carrier: a canonical slug decodes to its string, zero - // to "", and a non-canonical value to the raw integer. A string-only - // decode would send every sub-2^42 key to "" and re-encode it to 0, - // restarting pagination at the top of the table. + // Delegates to fc::slug_name's to_variant, so next_key carries the same + // canonical string the row's key field does, and feeding it back as a + // bound re-encodes the identical bytes. A stored key with no spelling + // throws; get_table_rows catches per row and falls back to hex. const fc::slug_name s{ r.read_be64() }; fc::variant v; fc::to_variant(s, v); @@ -452,10 +451,10 @@ inline void encode_field(writer& w, key_leaf_kind kind, const fc::variant& val) } case key_leaf_kind::name: w.write_be64(name(val.as_string()).to_uint64_t()); return; case key_leaf_kind::slug_name: { - // Delegates to fc::slug_name's from_variant so the dual carrier (string / - // "" / integer, plus the transitional object) is implemented exactly once. - // Byte-identical to the struct-node path it replaces: that recursed one - // uint64 child to write_be64, and so does this. + // Delegates to fc::slug_name's from_variant so the carrier — the + // canonical string, plus the transitional object — is implemented exactly + // once. Byte-identical to the struct-node path it replaces: that recursed + // one uint64 child to write_be64, and so does this. fc::slug_name s; fc::from_variant(val, s); w.write_be64(s.value); diff --git a/libraries/libfc/include/fc/slug_name.hpp b/libraries/libfc/include/fc/slug_name.hpp index 566d56b60e..08709d21ad 100644 --- a/libraries/libfc/include/fc/slug_name.hpp +++ b/libraries/libfc/include/fc/slug_name.hpp @@ -20,8 +20,6 @@ #include #include -#include -#include #include #include #include @@ -85,89 +83,52 @@ inline constexpr slug_name operator""_s() { using slug_name_literals::operator""_s; -/// JSON carrier for a slug_name — a DUAL carrier, and deliberately so. +/// JSON carrier for a slug_name: the canonical string spelling, and nothing +/// else. A slug renders as its text (`"LIQSOL"`), the zero sentinel as `""`. /// -/// A canonical slug renders as its string spelling (`"LIQSOL"`), and the zero -/// sentinel as the empty string. A value below 2^42 renders as the **raw -/// integer**, because `to_string()` cannot represent it: `zero_terminates` is -/// true, so decoding stops at the first zero symbol slot, and every such value -/// collapses to `""`. Emitting the integer instead keeps this conversion TOTAL -/// and INJECTIVE over all 2^64 — `""` means exactly zero and nothing else. +/// A value below 2^42 has no spelling — `zero_terminates` is true, so decoding +/// stops at the first zero symbol slot and every such value collapses to `""`. +/// Those THROW rather than acquire a second, numeric carrier. One type, one +/// JSON shape: a caller writes a slug field exactly one way, and a reader never +/// branches on the JSON type. /// -/// The integer arm must not be replaced by a throw. Only `chain_code` is bound -/// to the proven source outpost, so a non-canonical `token_code` is plantable -/// from a forgeable attestation payload; a throwing conversion would let one -/// such row make an entire table unreadable over `get_table_rows`. -/// -/// The two carriers are distinguished by JSON *type* here, and by string -/// LENGTH after a round trip through JSON text — `fc::json` quotes a uint64 -/// above 0xffffffff, so the integer arm comes back as a decimal string that -/// `from_variant` re-routes on length (see there). What is never ambiguous is -/// the spelling: a canonical slug is at most max_len symbols and a stringified -/// uint64 past 0xffffffff is at least 10 digits. The carrier could NOT have -/// been a numeric string chosen freely — the slug alphabet contains digits, so -/// `"7"` is itself a valid canonical slug. +/// The throw is contained by construction: `get_table_rows` wraps each row's +/// key decode and each row's value render in its own try/catch and falls back +/// to hex (`plugins/chain_plugin/src/chain_plugin.cpp`), so a row holding a +/// non-canonical code degrades that one cell instead of failing the table. inline void to_variant(const slug_name& s, fc::variant& v) { const std::string text = s.to_string(); // `pack` is the non-validating encoder, so this is a pure round-trip test: - // the string spelling is used only when it recovers the value exactly. - if (slug_name::pack(text) == s.value) { - v = text; - return; - } - v = s.value; + // the value is canonical exactly when its own spelling recovers it. + FC_ASSERT(slug_name::pack(text) == s.value, + "slug_name {} is not canonical and has no string spelling", s.value); + v = text; } -/// Accepts every carrier `to_variant` can emit, plus — TRANSITIONALLY — the +/// Accepts the string carrier `to_variant` emits, plus — TRANSITIONALLY — the /// `{"value": }` object that abigen's reflected struct emitted before /// `slug_name` became an ABI builtin. /// /// The object arm is what makes the cross-repo landing window survivable: with /// no variant conversions, a slug converts through /// `FC_REFLECT_TEMPLATE(basic_name, (value))` and is therefore -/// object-only, while a string/integer-only reader rejects that object. There -/// is no value both spellings accept, so a JSON *writer* cannot straddle the -/// window the way a reader can. Delete this arm once no writer emits the -/// object form. +/// object-only, while a string-only reader rejects that object. There is no +/// value both spellings accept, so a JSON *writer* cannot straddle the window +/// the way a reader can. Delete this arm once no writer emits the object form. inline void from_variant(const fc::variant& v, slug_name& s) { - if (v.is_string()) { - const std::string_view text = v.get_string(); - // The integer carrier arrives here as a STRING whenever it crossed JSON - // TEXT: fc::json quotes a uint64 above 0xffffffff (fc/io/json.cpp), and - // `next_key` is json text that a paginating caller feeds back as a bound. - // Length disambiguates exactly — a canonical slug is at most max_len - // symbols, while a stringified uint64 past 0xffffffff is at least 10 - // digits — so no valid spelling is diverted. In particular the 8-digit - // "12345678" stays a slug, keeping the rule that `"7"` is the slug 7 and - // not the integer 7. - // The all-digits guard is load-bearing: `as_uint64` goes through - // boost::lexical_cast, which does NOT reject a sign for an unsigned - // target — it WRAPS, so "-12345678" would be admitted as - // 18446744073697205938. Anything over-long that is not a plain decimal - // falls through to the validating parse below, which rejects it. - if (text.size() > static_cast(slug_name_traits::max_len) - && std::all_of(text.begin(), text.end(), - [](char c) { return c >= '0' && c <= '9'; })) { - s = slug_name{ v.as_uint64() }; - return; - } - // Validating: the ctor round-trip-checks and rejects a non-canonical or - // out-of-alphabet spelling. `""` is the zero sentinel. - s = slug_name{ text }; - return; - } if (v.is_object()) { s = slug_name{ v.get_object()["value"].as_uint64() }; return; } - // A negative number is never a slug, and `as_uint64` would WRAP it rather - // than reject it (boost::lexical_cast does not reject a sign for an unsigned - // target), so a JSON bound of `-1` would silently page from the far end of - // the table. Rejecting it here also keeps the two arms consistent: the string - // arm above already rejects "-12345678". - if (v.is_int64() && v.as_int64() < 0) - slug_name_traits::throw_invalid(std::to_string(v.as_int64()), "negative"); - s = slug_name{ v.as_uint64() }; + // A number is REJECTED, never coerced. The slug alphabet contains digits, so + // `"123"` is itself a canonical slug whose packed value is nothing like 123 + // — reading the JSON number 123 as either one would be a silent mis-decode. + // Same for null/bool, which `as_uint64` would quietly turn into 0/1. + FC_ASSERT(v.is_string(), "slug_name must be a string, got {}", + fc::reflector::to_string(v.get_type())); + // Validating: the ctor round-trip-checks and rejects a non-canonical or + // out-of-alphabet spelling. `""` is the zero sentinel. + s = slug_name{ std::string_view{ v.get_string() } }; } } // namespace fc diff --git a/libraries/libfc/test/test_slug_name.cpp b/libraries/libfc/test/test_slug_name.cpp index 9ac8d27aab..19393b0b61 100644 --- a/libraries/libfc/test/test_slug_name.cpp +++ b/libraries/libfc/test/test_slug_name.cpp @@ -348,12 +348,10 @@ BOOST_AUTO_TEST_CASE(non_zero_terminator_trait_accepts_alphabet_zero) { } // ── variant carrier ──────────────────────────────────────────────────────── -// to_variant is a DUAL carrier: a canonical slug renders as its string, zero as -// "", and a value with no string spelling as the raw integer. It must be TOTAL -// and INJECTIVE over all 2^64 — a non-canonical code is plantable from a -// forgeable attestation payload (only chain_code is bound to the proven source -// outpost), so a throwing conversion would let one row make a whole table -// unreadable over get_table_rows. +// ONE carrier: the canonical string spelling. A slug renders as its text, zero +// as "", and a value with no spelling throws. The cases below pin that single +// shape from both directions — every writable spelling lands on a string, and +// every non-string is refused rather than coerced. BOOST_AUTO_TEST_CASE(variant_canonical_slug_is_a_string) { fc::variant v; @@ -377,38 +375,30 @@ BOOST_AUTO_TEST_CASE(variant_zero_is_the_empty_string_both_ways) { BOOST_CHECK_EQUAL(back.value, 0u); } -BOOST_AUTO_TEST_CASE(variant_non_canonical_uses_the_integer_carrier) { +BOOST_AUTO_TEST_CASE(variant_non_canonical_has_no_spelling_and_throws) { // Every value below 1<<42 has a zero in the char[0] slot, so to_string() - // truncates it to "" and the string spelling cannot recover it. The carrier - // must therefore be the integer, or the conversion stops being injective. + // truncates it to "" and no string recovers it. Rather than grow a second + // carrier for those, the conversion refuses them — which is what keeps "" + // meaning exactly zero. The throw is contained: get_table_rows catches per + // row and renders that cell as hex. for (uint64_t raw : {uint64_t{1}, uint64_t{7}, uint64_t{42}, - uint64_t{(uint64_t{1} << 42) - 1}}) { + uint64_t{(uint64_t{1} << 42) - 1}, + (uint64_t{1} << 48) - 1, // symbols past the alphabet + ~uint64_t{0}}) { fc::variant v; - fc::to_variant(slug_name{raw}, v); - BOOST_REQUIRE_MESSAGE(v.is_integer(), "raw=" << raw << " must use the integer carrier"); - slug_name back; - fc::from_variant(v, back); - BOOST_CHECK_EQUAL(back.value, raw); - } -} - -BOOST_AUTO_TEST_CASE(variant_never_throws_on_any_value) { - // The anti-DoS property. If this is ever "tidied" into a throw, one planted - // row makes get_table_rows fail for an entire table. - for (uint64_t raw : {uint64_t{0}, uint64_t{1}, uint64_t{9}, uint64_t{1} << 41, - uint64_t{1} << 42, ~uint64_t{0}}) { - fc::variant v; - BOOST_CHECK_NO_THROW(fc::to_variant(slug_name{raw}, v)); + BOOST_CHECK_THROW(fc::to_variant(slug_name{raw}, v), fc::exception); } } -BOOST_AUTO_TEST_CASE(variant_every_carrier_round_trips_exactly) { - // Totality + injectivity across the boundary, including the canonical floor. - for (uint64_t raw : {uint64_t{0}, uint64_t{1}, uint64_t{(uint64_t{1} << 42) - 1}, - uint64_t{1} << 42, fc::slug_name{"A"}.value, - fc::slug_name{"LIQSOL"}.value, fc::slug_name{"12345678"}.value}) { +BOOST_AUTO_TEST_CASE(variant_every_canonical_value_round_trips_exactly) { + // Injectivity across the boundary for the whole canonical range, including + // its floor (1<<42 is "A") and the zero sentinel. + for (uint64_t raw : {uint64_t{0}, uint64_t{1} << 42, fc::slug_name{"A"}.value, + fc::slug_name{"LIQSOL"}.value, fc::slug_name{"12345678"}.value, + fc::slug_name{"________"}.value}) { fc::variant v; - fc::to_variant(slug_name{raw}, v); + BOOST_REQUIRE_NO_THROW(fc::to_variant(slug_name{raw}, v)); + BOOST_REQUIRE(v.is_string()); slug_name back; fc::from_variant(v, back); BOOST_CHECK_EQUAL(back.value, raw); @@ -416,14 +406,17 @@ BOOST_AUTO_TEST_CASE(variant_every_carrier_round_trips_exactly) { } BOOST_AUTO_TEST_CASE(variant_an_all_digit_slug_is_a_string_not_its_own_decimal) { - // The slug alphabet contains digits, so "7" is itself a valid canonical slug - // — which is exactly why the non-canonical carrier must be a JSON integer - // and not a numeric string. A numeric string would be ambiguous. + // The slug alphabet contains digits, so "7" is itself a canonical slug whose + // packed value is nothing like 7. That ambiguity is why a JSON number is + // rejected outright rather than read as either one. fc::variant v; fc::to_variant(slug_name{"7"}, v); BOOST_REQUIRE(v.is_string()); BOOST_CHECK_EQUAL(v.as_string(), "7"); BOOST_CHECK_NE(slug_name{"7"}.value, 7u); + + slug_name back; + BOOST_CHECK_THROW(fc::from_variant(fc::variant(uint64_t{7}), back), fc::exception); } BOOST_AUTO_TEST_CASE(variant_accepts_the_transitional_object_carrier) { @@ -446,26 +439,20 @@ BOOST_AUTO_TEST_CASE(variant_rejects_a_non_canonical_string_spelling) { BOOST_CHECK_THROW(fc::from_variant(fc::variant(std::string{"TOOOLONGXX"}), back), fc::exception); } -BOOST_AUTO_TEST_CASE(variant_every_carrier_round_trips_through_json_TEXT) { - // The variant-layer round trip above is NOT sufficient: `next_key` is json - // TEXT (chain_plugin renders it with fc::json::to_string) and a paginating - // caller feeds it straight back as a `lower_bound`, which is re-parsed with - // fc::json::from_string. fc::json QUOTES a uint64 above 0xffffffff, so the - // integer carrier crosses that boundary as a decimal STRING — which the - // validating string arm rejected as "too long" until from_variant learned to - // re-route on length. Values are chosen to straddle every threshold that - // matters: the 0xffffffff quoting cutoff, the 2^42 canonical floor, and 2^48. +BOOST_AUTO_TEST_CASE(variant_carrier_round_trips_through_json_TEXT) { + // The variant-layer round trip above is NOT sufficient on its own: `next_key` + // is json TEXT (chain_plugin renders it with fc::json::to_string) and a + // paginating caller feeds it straight back as a `lower_bound`, re-parsed with + // fc::json::from_string. A single string carrier is what makes that crossing + // uneventful — fc::json quotes a uint64 above 0xffffffff, so a numeric + // carrier would change JSON TYPE mid-flight and land on the string arm as a + // decimal nobody asked for. const uint64_t values[] = { - 0u, // the zero sentinel -> "" - 7u, // non-canonical, below the quoting cutoff - 0xffffffffu, // last value fc::json emits unquoted - 0x100000000u, // first value fc::json QUOTES - (uint64_t{1} << 42) - 1, // last non-canonical - uint64_t{1} << 42, // first canonical ("A") + 0u, // the zero sentinel -> "" + uint64_t{1} << 42, // the canonical floor ("A") slug_name{"ETH"}.value, - slug_name{"12345678"}.value, - (uint64_t{1} << 48) - 1, // symbols past the alphabet -> integer carrier - ~uint64_t{0}, + slug_name{"12345678"}.value, // all digits, and still a string + slug_name{"________"}.value, }; for (const uint64_t raw : values) { fc::variant v; @@ -492,11 +479,11 @@ BOOST_AUTO_TEST_CASE(mvo_accepts_every_spelling_a_caller_can_write) { // std::string -> variant(std::string) -> string // std::string_view -> variant(std::string_view) -> string // fc::slug_name -> explicit variant(const T&) -> to_variant -> string - // uint64_t -> variant(uint64_t) -> the integer escape // - // This is why no `codename()`-style wrapper is needed at a call site: the - // raw literal and the `_s` literal both already work, and a wrapper - // returning std::string is just identity. + // Every one lands on a string, because the string IS the carrier. This is + // why no `codename()`-style wrapper is needed at a call site: the raw + // literal and the `_s` literal both already work, and a wrapper returning + // std::string is just identity. const slug_name expected{"LIQSOL"}; const char* const as_c_str = "LIQSOL"; @@ -518,53 +505,37 @@ BOOST_AUTO_TEST_CASE(mvo_accepts_every_spelling_a_caller_can_write) { fc::from_variant(cell, back); BOOST_CHECK_MESSAGE(back == expected, std::string{"round trip failed: "} + key); } - - // The integer escape is the one writable spelling that is NOT a string, and - // it is the only way to write a non-canonical value. - const fc::variant esc{ fc::mutable_variant_object()("code", uint64_t{7}) }; - const fc::variant& cell = esc.get_object()["code"]; - BOOST_CHECK(cell.is_integer()); - slug_name back; - fc::from_variant(cell, back); - BOOST_CHECK_EQUAL(back.value, 7u); } -BOOST_AUTO_TEST_CASE(variant_numeric_string_arm_rejects_a_signed_spelling) { - // The over-long string arm routes to as_uint64() for the integer carrier that - // crossed JSON text. as_uint64 goes through boost::lexical_cast, which does - // NOT reject a sign for an unsigned target — it WRAPS. Without the all-digits - // guard, "-12345678" (9 chars, so over max_len) was admitted as - // 18446744073697205938 instead of being rejected. +BOOST_AUTO_TEST_CASE(variant_rejects_every_non_string_carrier) { + // Nothing but a string (and the transitional object) is read. Each value + // below would otherwise be COERCED by fc::variant::as_uint64 — which is the + // failure mode a single carrier removes, because none of these coercions is + // the value the writer meant: + // + // 7 -> the slug "7" is 0x1F0000000000, not 7 + // -1 -> lexical_cast does not reject a sign for an unsigned + // target, it WRAPS; a bound of -1 would page from the far + // end of the table + // null/false -> 0, the absent sentinel, silently + // true -> 1, a value with no spelling at all slug_name back; - for (const char* spelling : {"-12345678", "-123456789", "+123456789", "-1"}) { + BOOST_CHECK_THROW(fc::from_variant(fc::variant(uint64_t{7}), back), fc::exception); + BOOST_CHECK_THROW(fc::from_variant(fc::variant(uint64_t{1} << 42), back), fc::exception); + BOOST_CHECK_THROW(fc::from_variant(fc::variant(int64_t{-1}), back), fc::exception); + BOOST_CHECK_THROW(fc::from_variant(fc::variant(int64_t{-12345678}), back), fc::exception); + BOOST_CHECK_THROW(fc::from_variant(fc::variant(), back), fc::exception); + BOOST_CHECK_THROW(fc::from_variant(fc::variant(false), back), fc::exception); + BOOST_CHECK_THROW(fc::from_variant(fc::variant(true), back), fc::exception); + BOOST_CHECK_THROW(fc::from_variant(fc::variant(1.5), back), fc::exception); + + // A numeric STRING is not an escape either: it is parsed as a slug like any + // other spelling, so a signed or over-long decimal is simply invalid text. + for (const char* spelling : {"-1", "-12345678", "+123456789", "4294967296", + "000000000000000042"}) { BOOST_CHECK_THROW(fc::from_variant(fc::variant(std::string{spelling}), back), fc::exception); } - - // A plain decimal over max_len is still the integer carrier, which is the - // whole point of the arm. - fc::from_variant(fc::variant(std::string{"4294967296"}), back); - BOOST_CHECK_EQUAL(back.value, 4294967296u); - - // Zero-padded is all-digits, so it is accepted and means what it says. - fc::from_variant(fc::variant(std::string{"000000000000000042"}), back); - BOOST_CHECK_EQUAL(back.value, 42u); - - // The NUMBER spelling must agree with the string spelling. `as_uint64` - // wraps a negative rather than rejecting it, so without a guard a JSON - // bound of `-1` would silently page from the far end of the table while - // `"-1"` threw — the two arms disagreeing is worse than either choice. - BOOST_CHECK_THROW(fc::from_variant(fc::variant(int64_t{-1}), back), fc::exception); - BOOST_CHECK_THROW(fc::from_variant(fc::variant(int64_t{-12345678}), back), fc::exception); - - // NOT changed, and documented so the next reader knows it is deliberate: - // null / false / true coerce through fc::variant::as_uint64 exactly as they - // do for every other uint64 key leaf. Making slug_name alone strict here - // would diverge from the rest of the ABI for no gain — `0` is a legitimate - // slug value (the absent sentinel). - fc::from_variant(fc::variant(), back); BOOST_CHECK_EQUAL(back.value, 0u); - fc::from_variant(fc::variant(false), back); BOOST_CHECK_EQUAL(back.value, 0u); - fc::from_variant(fc::variant(true), back); BOOST_CHECK_EQUAL(back.value, 1u); } BOOST_AUTO_TEST_SUITE_END() diff --git a/libraries/opp/include/sysio/opp/depot/chains_registry.hpp b/libraries/opp/include/sysio/opp/depot/chains_registry.hpp index 6b88f6f8df..a383df47b1 100644 --- a/libraries/opp/include/sysio/opp/depot/chains_registry.hpp +++ b/libraries/opp/include/sysio/opp/depot/chains_registry.hpp @@ -23,7 +23,7 @@ inline constexpr auto table_chains = "chains"; /// Field names on a `chain_row` as they surface through the ABI serializer. namespace field { - inline constexpr auto code = "code"; // {value: uint64} slug_name + inline constexpr auto code = "code"; // slug_name (canonical string) inline constexpr auto kind = "kind"; // ChainKind enum (string spelling) inline constexpr auto external_chain_id = "external_chain_id"; // uint32 inline constexpr auto is_depot = "is_depot"; // bool — the single WIRE-self row diff --git a/plugins/batch_operator_plugin/src/batch_operator_plugin.cpp b/plugins/batch_operator_plugin/src/batch_operator_plugin.cpp index 6ab094645d..6934ea589b 100644 --- a/plugins/batch_operator_plugin/src/batch_operator_plugin.cpp +++ b/plugins/batch_operator_plugin/src/batch_operator_plugin.cpp @@ -612,10 +612,9 @@ struct batch_operator_plugin::impl { outposts.clear(); for (auto& row : rows.rows) { auto obj = row.get_object(); - // `code` is a `slug_name`. fc::slug_name's own from_variant accepts - // every carrier — the decoded slug string, "" for zero, a raw integer, - // and the legacy `{value: }` object — so the shape does not - // have to be probed here. + // `code` is a `slug_name`. fc::slug_name's own from_variant reads the + // decoded slug string ("" for zero) and the transitional + // `{value: }` object, so the shape is not probed here. uint64_t code_val = 0; if (auto code_obj = obj.find(chains::field::code); code_obj != obj.end()) { code_val = code_obj->value().as().value; diff --git a/plugins/underwriter_plugin/src/underwriter_plugin.cpp b/plugins/underwriter_plugin/src/underwriter_plugin.cpp index 8726edd612..ae6efe4ed9 100644 --- a/plugins/underwriter_plugin/src/underwriter_plugin.cpp +++ b/plugins/underwriter_plugin/src/underwriter_plugin.cpp @@ -1209,9 +1209,8 @@ struct underwriter_plugin::impl { for (auto& row : rows.rows) { auto obj = row.get_object(); // `code` is a `slug_name`. Read it through fc::slug_name's own - // from_variant, which accepts every carrier the depot emits — the - // decoded slug string, "" for the zero sentinel, and a raw integer for - // a non-canonical value — plus the legacy `{"value": }` object. + // from_variant, which takes the decoded slug string ("" for the zero + // sentinel) plus the transitional `{"value": }` object. uint64_t chain_code = obj["code"].as().value; if (obj.contains("is_depot") && obj["is_depot"].as_bool()) { // Record the depot's own code for exact per-leg depot @@ -1547,8 +1546,7 @@ struct underwriter_plugin::impl { // `(chain_code, token_code, reserve_code)` slug_name triples plus a // `*_amount`. Populated by `sysio.uwrit::createuwreq` from the // originating SwapRequest. Each is read through fc::slug_name's own - // from_variant, so every carrier the depot emits is accepted without - // this plugin knowing which one it is. + // from_variant, so the carrier is decoded in exactly one place. if (!obj.contains(uwrit::request_field::source_chain_code) || !obj.contains(uwrit::request_field::source_amount) || !obj.contains(uwrit::request_field::destination_chain_code) || diff --git a/tests/get_table_tests.cpp b/tests/get_table_tests.cpp index 2f6204264f..72c8aeee04 100644 --- a/tests/get_table_tests.cpp +++ b/tests/get_table_tests.cpp @@ -457,6 +457,12 @@ BOOST_FIXTURE_TEST_CASE( get_table_next_key_test, validating_tester ) try { push_action("test"_n, "addstruct"_n, "test"_n, mutable_variant_object()("code", 10)("payload", 100)); push_action("test"_n, "addstruct"_n, "test"_n, mutable_variant_object()("code", 20)("payload", 200)); push_action("test"_n, "addstruct"_n, "test"_n, mutable_variant_object()("code", 30)("payload", 300)); + // slugobjs: kv::table keyed on `slug_name`, the shape every registry table + // ships. The code is written as its canonical string — `slug_name` is an ABI + // builtin, so that string IS the action's wire form. Drives (sec-11). + push_action("test"_n, "addslug"_n, "test"_n, mutable_variant_object()("code", "ETH")("payload", 100)); + push_action("test"_n, "addslug"_n, "test"_n, mutable_variant_object()("code", "SOL")("payload", 200)); + push_action("test"_n, "addslug"_n, "test"_n, mutable_variant_object()("code", "WIRE")("payload", 300)); produce_block(); // The result of the init will populate @@ -797,6 +803,65 @@ BOOST_FIXTURE_TEST_CASE( get_table_next_key_test, validating_tester ) try { BOOST_CHECK_EQUAL(page2.rows[0].get_object()["value"].get_object()["payload"].as_uint64(), 300u); } + // (sec-11) slugobjs primary key — kv::table keyed on `slug_name`, the shape + // every registry table ships. `slug_name` is an ABI builtin, so + // build_key_shape takes the LEAF branch and the carrier is the + // canonical STRING at every boundary: the decoded key, the row + // value, a JSON bound, and the `next_key` cursor. (sec-10) covers + // the struct-node path; the leaf resolves through a different lookup + // (leaf_kind_of, before the struct table) and needs its own + // end-to-end pin. + { + chain_apis::read_only::get_table_rows_params p; + p.json = true; + p.code = "test"_n; + // slugobjs is unscoped, exactly like structobjs above — no `scope` is set. + p.table = "slugobjs"; + + // (a) Both the decoded key and the row value render the canonical string. + // A hex `key` here means the per-row decode threw and fell back. + auto all = get_table_rows_full(plugin, p, fc::time_point::maximum()); + BOOST_REQUIRE_EQUAL(all.rows.size(), 3u); + BOOST_REQUIRE(all.rows[0].get_object()["key"].get_object()["code"].is_string()); + BOOST_CHECK_EQUAL(all.rows[0].get_object()["key"].get_object()["code"].as_string(), "ETH"); + BOOST_CHECK_EQUAL(all.rows[0].get_object()["value"].get_object()["code"].as_string(), "ETH"); + BOOST_CHECK_EQUAL(all.rows[1].get_object()["value"].get_object()["code"].as_string(), "SOL"); + BOOST_CHECK_EQUAL(all.rows[2].get_object()["value"].get_object()["code"].as_string(), "WIRE"); + + // (b) A bare-string JSON bound filters inclusively — no nested object, no + // packed integer. MSB-first packing makes key order follow the + // alphabet, so ETH < SOL < WIRE holds in the stored bytes too. + p.lower_bound = R"({"code":"SOL"})"; + auto bounded = get_table_rows_full(plugin, p, fc::time_point::maximum()); + BOOST_REQUIRE_EQUAL(bounded.rows.size(), 2u); // SOL, WIRE + BOOST_CHECK_EQUAL(bounded.rows[0].get_object()["value"].get_object()["code"].as_string(), "SOL"); + BOOST_CHECK_EQUAL(bounded.rows[0].get_object()["value"].get_object()["payload"].as_uint64(), 200u); + + // (c) Pagination: `next_key` carries the same string spelling and is fed + // back verbatim as the next bound. + p.lower_bound.clear(); + p.limit = 2; + auto page1 = get_table_rows_full(plugin, p, fc::time_point::maximum()); + BOOST_REQUIRE_EQUAL(page1.rows.size(), 2u); + BOOST_REQUIRE_EQUAL(page1.more, true); + BOOST_REQUIRE(!page1.next_key.empty()); + BOOST_CHECK_EQUAL(page1.rows[0].get_object()["value"].get_object()["code"].as_string(), "ETH"); + BOOST_CHECK_EQUAL(page1.rows[1].get_object()["value"].get_object()["code"].as_string(), "SOL"); + + auto nk = fc::json::from_string(page1.next_key); + BOOST_REQUIRE(nk.is_object()); + BOOST_REQUIRE(nk.get_object()["code"].is_string()); + BOOST_CHECK_EQUAL(nk.get_object()["code"].as_string(), "WIRE"); + + p.lower_bound = page1.next_key; + p.limit = 50; + auto page2 = get_table_rows_full(plugin, p, fc::time_point::maximum()); + BOOST_REQUIRE_EQUAL(page2.rows.size(), 1u); + BOOST_REQUIRE_EQUAL(page2.more, false); + BOOST_CHECK_EQUAL(page2.rows[0].get_object()["value"].get_object()["code"].as_string(), "WIRE"); + BOOST_CHECK_EQUAL(page2.rows[0].get_object()["value"].get_object()["payload"].as_uint64(), 300u); + } + // (sec-5) Invalid index name on multi_index — should throw, not silently // return primary rows. { diff --git a/unittests/abi_tests.cpp b/unittests/abi_tests.cpp index 506e852b4e..3e4d675e34 100644 --- a/unittests/abi_tests.cpp +++ b/unittests/abi_tests.cpp @@ -746,16 +746,20 @@ BOOST_AUTO_TEST_CASE(slug_name_builtin_type) BOOST_REQUIRE(back.get_object()["code"].is_string()); BOOST_CHECK_EQUAL(back.get_object()["code"].as_string(), "ETH"); - // A planted non-canonical value must render — as a JSON INTEGER, not a - // string, and WITHOUT throwing. A throwing conversion would make one such - // row able to brick get_table_rows for a whole table. - auto planted = abis.variant_to_binary( - "regrow", fc::json::from_string(R"({"code":7})"), yield_fn()); - BOOST_REQUIRE_EQUAL(planted.size(), 8u); - fc::variant rendered; - BOOST_REQUIRE_NO_THROW(rendered = abis.binary_to_variant("regrow", planted, yield_fn())); - BOOST_REQUIRE(rendered.get_object()["code"].is_integer()); - BOOST_CHECK_EQUAL(rendered.get_object()["code"].as_uint64(), 7u); + // The string is the ONLY carrier, in both directions. A JSON number is not a + // second spelling of a slug — `"7"` is itself a canonical slug whose packed + // value is nothing like 7 — so it is refused rather than read as either one. + BOOST_CHECK_THROW( + abis.variant_to_binary("regrow", fc::json::from_string(R"({"code":7})"), yield_fn()), + fc::exception); + + // And a value with no spelling does not render. Every value below 2^42 has a + // zero in the leading symbol slot, so `to_string` truncates it to "" and no + // string recovers it. get_table_rows wraps each row's render in its own + // try/catch and falls back to hex, so a planted row costs that one cell + // rather than the query (plugins/chain_plugin/src/chain_plugin.cpp). + const std::vector planted{ 7, 0, 0, 0, 0, 0, 0, 0 }; // packed LE uint64 7 + BOOST_CHECK_THROW(abis.binary_to_variant("regrow", planted, yield_fn()), fc::exception); } FC_LOG_AND_RETHROW() } diff --git a/unittests/be_key_codec_tests.cpp b/unittests/be_key_codec_tests.cpp index 6ad79d916e..994244ffd3 100644 --- a/unittests/be_key_codec_tests.cpp +++ b/unittests/be_key_codec_tests.cpp @@ -107,20 +107,28 @@ BOOST_AUTO_TEST_CASE(slug_name_leaf_roundtrips_the_zero_sentinel_as_empty) { BOOST_CHECK(bytes == encode_single(abi, "composite_key", slug(0))); } -BOOST_AUTO_TEST_CASE(slug_name_leaf_roundtrips_a_non_canonical_value_as_an_integer) { +BOOST_AUTO_TEST_CASE(slug_name_leaf_refuses_a_non_canonical_value_both_ways) { // A value below 2^42 has no string spelling (to_string truncates at the first - // zero symbol slot), so the carrier is the raw integer. Without this, decode - // would emit "" and re-encode to 0 — restarting pagination at the top of the - // table for any row holding a plantable non-canonical code. + // zero symbol slot), so it is not writable as a bound and not renderable as a + // key. Both directions throw rather than silently collapsing to "" — which + // would re-encode to 0 and restart pagination at the top of the table. + // get_table_rows catches per row and falls back to hex, so a stored key like + // this costs that one cell, not the query. auto abi = make_test_abi(); auto shapes = codec::build_key_shapes(abi, {"code"}, {"slug_name"}); - auto bytes = codec::encode_key( - fc::variant(fc::mutable_variant_object("code", 7u)), shapes); - auto decoded = codec::decode_key(bytes.data(), bytes.size(), shapes); - BOOST_CHECK(decoded.get_object()["code"].is_integer()); - BOOST_CHECK_EQUAL(decoded.get_object()["code"].as_uint64(), 7u); - // And it re-encodes to the same key — the round trip pagination relies on. - BOOST_CHECK(bytes == codec::encode_key(decoded, shapes)); + + // No bound can name it: the integer is refused, and there is no spelling. + BOOST_CHECK_THROW( + codec::encode_key(fc::variant(fc::mutable_variant_object("code", 7u)), shapes), + fc::exception); + + // And a key already holding one does not decode. Reach past the carrier to + // build those bytes — the transitional object arm is the only writer left + // that can express a raw value. + auto bytes = codec::encode_key( + fc::variant(fc::mutable_variant_object("code", slug(7))), shapes); + BOOST_REQUIRE_EQUAL(bytes.size(), 8u); + BOOST_CHECK_THROW(codec::decode_key(bytes.data(), bytes.size(), shapes), fc::exception); } BOOST_AUTO_TEST_CASE(slug_name_leaf_wins_over_a_shadowing_struct_def) { @@ -188,21 +196,23 @@ BOOST_AUTO_TEST_CASE(slug_name_multi_leaf_keys_preserve_field_order_and_offsets) BOOST_CHECK(bytes == codec::encode_key(decoded, shapes)); } - // 3-leaf, mirroring reserves — and with a non-canonical middle leaf, so the - // mixed-carrier case (string, integer, string) is covered at its own offset. + // 3-leaf, mirroring reserves. The middle leaf is a DIFFERENT length from its + // neighbours, so a decoder that mis-tracked its offset would land inside an + // adjacent slug and still produce a string. { auto shapes = codec::build_key_shapes(abi, {"chain_code", "token_code", "reserve_code"}, {"slug_name", "slug_name", "slug_name"}); auto bytes = codec::encode_key( fc::variant(fc::mutable_variant_object("chain_code", "ETH") - ("token_code", 7u) + ("token_code", "USDC") ("reserve_code", "PRIMARY")), shapes); BOOST_REQUIRE_EQUAL(bytes.size(), 24u); + BOOST_CHECK(std::vector(bytes.begin() + 8, bytes.begin() + 16) + == encode_single(abi, "slug_name", fc::variant("USDC"))); auto decoded = codec::decode_key(bytes.data(), bytes.size(), shapes); BOOST_CHECK_EQUAL(decoded.get_object()["chain_code"].as_string(), "ETH"); - BOOST_CHECK(decoded.get_object()["token_code"].is_integer()); - BOOST_CHECK_EQUAL(decoded.get_object()["token_code"].as_uint64(), 7u); + BOOST_CHECK_EQUAL(decoded.get_object()["token_code"].as_string(), "USDC"); BOOST_CHECK_EQUAL(decoded.get_object()["reserve_code"].as_string(), "PRIMARY"); BOOST_CHECK(bytes == codec::encode_key(decoded, shapes)); } @@ -356,6 +366,7 @@ BOOST_AUTO_TEST_CASE(leaf_support_list_roundtrips) { auto sample = [](std::string_view t) -> fc::variant { if (t == "checksum256") return fc::variant(fc::sha256::hash(std::string("x")).str()); if (t == "name") return fc::variant(std::string("alice")); + if (t == "slug_name") return fc::variant(std::string("LIQSOL")); if (t == "bool") return fc::variant(true); if (t == "string") return fc::variant(std::string("hi")); if (t == "float128" || t == "long double") { diff --git a/unittests/test-contracts/get_table_test/get_table_test.abi b/unittests/test-contracts/get_table_test/get_table_test.abi index f7a1e4a528..fa7a912fe3 100644 --- a/unittests/test-contracts/get_table_test/get_table_test.abi +++ b/unittests/test-contracts/get_table_test/get_table_test.abi @@ -23,6 +23,20 @@ } ] }, + { + "name": "addslug", + "base": "", + "fields": [ + { + "name": "code", + "type": "slug_name" + }, + { + "name": "payload", + "type": "uint64" + } + ] + }, { "name": "addstruct", "base": "", @@ -115,6 +129,40 @@ } ] }, + { + "name": "slug_name", + "base": "", + "fields": [ + { + "name": "value", + "type": "uint64" + } + ] + }, + { + "name": "slugobj", + "base": "", + "fields": [ + { + "name": "code", + "type": "slug_name" + }, + { + "name": "payload", + "type": "uint64" + } + ] + }, + { + "name": "slugobj_key", + "base": "", + "fields": [ + { + "name": "code", + "type": "slug_name" + } + ] + }, { "name": "structobj", "base": "", @@ -151,6 +199,11 @@ "type": "addnumobj", "ricardian_contract": "" }, + { + "name": "addslug", + "type": "addslug", + "ricardian_contract": "" + }, { "name": "addstruct", "type": "addstruct", @@ -234,6 +287,14 @@ } ] }, + { + "name": "slugobjs", + "type": "slugobj", + "index_type": "i64", + "key_names": ["code"], + "key_types": ["slug_name"], + "table_id": 41308 + }, { "name": "structobjs", "type": "structobj", diff --git a/unittests/test-contracts/get_table_test/get_table_test.cpp b/unittests/test-contracts/get_table_test/get_table_test.cpp index 15d7e41190..2db7d1c7cc 100644 --- a/unittests/test-contracts/get_table_test/get_table_test.cpp +++ b/unittests/test-contracts/get_table_test/get_table_test.cpp @@ -44,3 +44,8 @@ void get_table_test::addstruct(uint64_t code, uint64_t payload) { structobjs structobjs_table( get_self() ); structobjs_table.emplace( get_self(), { composite_key{code} }, { composite_key{code}, payload } ); } + +void get_table_test::addslug(slug_name code, uint64_t payload) { + slugobjs slugobjs_table( get_self() ); + slugobjs_table.emplace( get_self(), { code }, { code, payload } ); +} diff --git a/unittests/test-contracts/get_table_test/get_table_test.hpp b/unittests/test-contracts/get_table_test/get_table_test.hpp index 414d574b4f..bbeaf90aec 100644 --- a/unittests/test-contracts/get_table_test/get_table_test.hpp +++ b/unittests/test-contracts/get_table_test/get_table_test.hpp @@ -125,6 +125,37 @@ class [[sysio::contract]] get_table_test : public sysio::contract { typedef sysio::kv::table< "structobjs"_n, structobj_key, structobj > structobjs; + // Slug-keyed kv::table — the shape every registry table ships + // (sysio.chains::chains, sysio.tokens::tokens, sysio.reserv::reserves). + // + // This one is named after a builtin ON PURPOSE, the exact hazard + // `composite_key` above exists to avoid: abigen matches builtins on the + // namespace-stripped bare name, so the field reaches the ABI as the bare + // `slug_name` and `build_key_shape` takes the LEAF branch. That is what puts + // the slug carrier on the live get_table_rows path — a bound and a + // `next_key` are the canonical STRING, never a nested object. + // + // Declared here rather than included from the contract library so the + // fixture stays self-contained: the layout is all the ABI sees. + struct slug_name { + uint64_t value = 0; + SYSLIB_SERIALIZE(slug_name, (value)) + }; + + struct slugobj_key { + slug_name code; + uint64_t primary_key() const { return code.value; } + SYSLIB_SERIALIZE(slugobj_key, (code)) + }; + + struct [[sysio::table("slugobjs")]] slugobj { + slug_name code; + uint64_t payload = 0; + SYSLIB_SERIALIZE(slugobj, (code)(payload)) + }; + + typedef sysio::kv::table< "slugobjs"_n, slugobj_key, slugobj > slugobjs; + [[sysio::action]] void addnumobj(uint64_t input); @@ -144,5 +175,12 @@ class [[sysio::contract]] get_table_test : public sysio::contract { [[sysio::action]] void addstruct(uint64_t code, uint64_t payload); + /// Insert a row into the slug-keyed kv::table `slugobjs`. + /// @param code the slug forming the primary key — written as its + /// canonical string, since `slug_name` is an ABI builtin + /// @param payload arbitrary row payload + [[sysio::action]] + void addslug(slug_name code, uint64_t payload); + }; diff --git a/unittests/test-contracts/get_table_test/get_table_test.wasm b/unittests/test-contracts/get_table_test/get_table_test.wasm index 7a27b01039ff5f839960e0b3f5cd687b587ab3b1..4621c65a094d2f34e3217becb0b744d3937b1cec 100755 GIT binary patch delta 1103 zcmZ8fZ){Ul6o0?_UZGwyv_x;n>(=WVarB8^^{S)v1-A|_Q*_14 zG@b$MygBW0@YQL+`HQrkFG33q@;a=cZ}=!WMgQDu8a7aAj#tW#oOg0tbWK+ms`j92 z*EN&JbVy~7@fLErde9@byY8vjOl^6&*dqG#dTe-|zSsAmkJ|Iwu!k<^N3fS>6nunz zVxpi5=oc_rK~W>5PF-g-XcW$`)otbp7BiIWUAq;b&MyBi{395GxiIFvkF7pE~n02R}l6i*L-1xOLU^#H5haBUJ0_ z#v}S-@s=kmm7vNW`?&P||6&blXl|iPT=0Diw90mhXsF1AHcA#vVy&EA30*37qn&OQ zKR^c!tt_JZ{<~O5UzT)ZJv|*bg)VV3aFx}+lr8J{tuB`-|2>vsRZPEJ2C(y&(;f%k zmq*h;UphIjc zXOM3`6)L9jiYL%ZS1PvRReCBEmPs~+OuRxDLVg)O6{^NoGQt%yv?JV$*XXP8+t?=7 zJv-Be?bK}^3%{W=Q@0ulWHywseH!24wpP-v!vx=HnU0jXi+q*uBQ7Q@JAvIIT$QWJ zku_JJlk)Fsl6y0>@(l0NQwwc35i4W%?E7L_O*>cuG*)|2D$!K88aPYmBd4C*4>{Zv zmuE7$WXx0Jt1`G8K$^@!iq~(zAsUF9WJEi0SPVo@GrTDZ8#Lf3`5JG=zlNm3kAW?f zi={L=6}B{ebXNI~{7#PkoD4KO6$u}W-oAbA+U29e@dl?RaU$_>Y~;5~$v}e>Dfr_j z?~1C36b%m-dKCp+~!k$Bfi^7z*2&;J;?Yr!D1+!IHJ|4jb$>m6BNONB*cm=*r~ Vv~HT#jnlfGPBs}B6cbIF`Y&ncA!+~s delta 1006 zcmZ8fZA_C_6n>xkzJ|B9=v5O~p&=K7v-p8f+@!P^J0VJ`Al0hC5YY*&&M6KjYzmca zD8?+t6SqWTf|#f^nn*6j7(d{bx*vYTpi#4BGkzo{BpS2%%c-}Otj%(h=bR@u=iKK! z_w){8WVz*gmPr5O6Y0XijGEBx3Sl z;>O08CkW>&Xg!~a5dF+6v63G0BUmFlrVJZcOT}qUg-vO{*h9h)dY*31*G*ygcvwIM zGt5K8(_67YcBVhk(MoGF?f6o5WVTMg7J4MQ&_-*s>(NPzEIH@!y&TRd1GY*d zw+{4lMkoB#=q#kmAKxF(uFd#0p1nd91@lg!TK=1t!)!l5sTR!1Z2U)>HT(J`e}>R~ zj<5nH#7~&hBJ1)8fmLKHD936F7Cb<$8mo{R3Nuhg9fi+PPbL0*+V6Ud&#Aqr1r79j z(IGU-?eng%K$BJVDhTIl)Uqt2AbQ8X&ZKVgWS(*z{ zCg2<5i}rfj1@np6ysqSVllI2o+cILyWD5Aezm27~c)Fc_v!BKe*;Cd8bjT;=c3nC8 zcF75a$16zvc!__2H;VWwnm&X-Wv2D19P>8-JE<}-h@J9nU?ng>p{hgPE@gaNNKvwo zB(Du#l_aD)AtVC2T|J$G)uq@)`>O}gqtGYYmo+l{Bu9e=&`S@N-|QZO(!zIxEmT{i z1yeP)Fm?p{?A^ORFP@Cvin>CnnxZd{jE Date: Thu, 17 Sep 2026 10:14:22 -0500 Subject: [PATCH 06/29] fix(fc): validate the slug_name object arm's packed value MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit as_uint64 coerces where this needs to validate: it wraps a negative int64 to UINT64_MAX, truncates a double, turns null/bool into 0/1, and its string path ignores a sign. from_variant feeds encode_field, so a bound of {"value": -1} encoded be64(UINT64_MAX) and paged from the far end of the table. Canonicality stays unrequired there — a value with no spelling is the one thing the string carrier cannot express, so that arm is the only way to name such a key as a bound. Addresses the P2 on #619. Change-Id: Ie1c552d2d9a480d3faf961bf02e4f3281845bd78 --- libraries/libfc/include/fc/slug_name.hpp | 49 +++++++++++++++++++++++- libraries/libfc/test/test_slug_name.cpp | 40 +++++++++++++++++++ 2 files changed, 88 insertions(+), 1 deletion(-) diff --git a/libraries/libfc/include/fc/slug_name.hpp b/libraries/libfc/include/fc/slug_name.hpp index 08709d21ad..9480d2895e 100644 --- a/libraries/libfc/include/fc/slug_name.hpp +++ b/libraries/libfc/include/fc/slug_name.hpp @@ -20,6 +20,7 @@ #include #include +#include #include #include #include @@ -105,6 +106,52 @@ inline void to_variant(const slug_name& s, fc::variant& v) { v = text; } +namespace detail { + +/// Checked unsigned decode for the transitional object arm's `value`. +/// +/// `fc::variant::as_uint64` COERCES where this needs to validate: it wraps a +/// negative `int64` to `UINT64_MAX`, truncates a `double`, turns null/bool into +/// 0/1, and its string path goes through a `lexical_cast` that does not reject a +/// sign (`variant.cpp`, `as_uint64`). `from_variant` feeds `encode_field`, so a +/// bound of `{"value": -1}` would otherwise encode `be64(UINT64_MAX)` and page +/// from the far end of the table. Every shape that is not an exact non-negative +/// integer is rejected here instead. +/// +/// Canonicality is deliberately NOT required: a packed value with no spelling is +/// the one thing the string carrier cannot express, so this arm is the only way +/// to name such a key as a bound. +inline uint64_t checked_packed_value(const fc::variant& v) { + if (v.is_uint64()) + return v.as_uint64(); + if (v.is_int64()) { + const int64_t i = v.as_int64(); + FC_ASSERT(i >= 0, "slug_name value must not be negative, got {}", i); + return static_cast(i); + } + if (v.is_string()) { + const std::string_view text = v.get_string(); + // All-digits only: no sign, no decimal point, no exponent. `as_uint64` + // then throws on overflow rather than wrapping. + // + // The predicate is bound to a local on purpose: FC_ASSERT stringizes its + // condition INTO the fmt format string (`#TEST ": " FORMAT`), so a lambda + // inline here would feed fmt's compile-time checker the lambda's own + // braces as malformed replacement fields. + const bool all_digits = + !text.empty() && std::all_of(text.begin(), text.end(), + [](char c) { return c >= '0' && c <= '9'; }); + FC_ASSERT(all_digits, "slug_name value must be an unsigned decimal, got '{}'", + std::string(text)); + return v.as_uint64(); + } + FC_ASSERT(false, "slug_name value must be an unsigned integer, got {}", + fc::reflector::to_string(v.get_type())); + __builtin_unreachable(); +} + +} // namespace detail + /// Accepts the string carrier `to_variant` emits, plus — TRANSITIONALLY — the /// `{"value": }` object that abigen's reflected struct emitted before /// `slug_name` became an ABI builtin. @@ -117,7 +164,7 @@ inline void to_variant(const slug_name& s, fc::variant& v) { /// the way a reader can. Delete this arm once no writer emits the object form. inline void from_variant(const fc::variant& v, slug_name& s) { if (v.is_object()) { - s = slug_name{ v.get_object()["value"].as_uint64() }; + s = slug_name{ detail::checked_packed_value(v.get_object()["value"]) }; return; } // A number is REJECTED, never coerced. The slug alphabet contains digits, so diff --git a/libraries/libfc/test/test_slug_name.cpp b/libraries/libfc/test/test_slug_name.cpp index 19393b0b61..4468f3dc87 100644 --- a/libraries/libfc/test/test_slug_name.cpp +++ b/libraries/libfc/test/test_slug_name.cpp @@ -538,4 +538,44 @@ BOOST_AUTO_TEST_CASE(variant_rejects_every_non_string_carrier) { } } +BOOST_AUTO_TEST_CASE(variant_object_arm_rejects_every_coercible_value_shape) { + // The transitional object arm reaches a RAW uint64, so it is the one place a + // malformed number can still land on a key. `fc::variant::as_uint64` coerces + // rather than validates — it wraps a negative int64 to UINT64_MAX, truncates + // a double, and turns null/bool into 0/1 — and since from_variant feeds + // encode_field, `{"value": -1}` would encode be64(UINT64_MAX) and page from + // the far end of the table. Each shape below must be refused, not coerced. + slug_name back; + const auto obj = [](const fc::variant& value) { + return fc::variant(fc::mutable_variant_object("value", value)); + }; + + BOOST_CHECK_THROW(fc::from_variant(obj(fc::variant(int64_t{-1})), back), fc::exception); + BOOST_CHECK_THROW(fc::from_variant(obj(fc::variant(int64_t{-12345678})), back), fc::exception); + BOOST_CHECK_THROW(fc::from_variant(obj(fc::variant(std::string{"-1"})), back), fc::exception); + BOOST_CHECK_THROW(fc::from_variant(obj(fc::variant(std::string{"+1"})), back), fc::exception); + BOOST_CHECK_THROW(fc::from_variant(obj(fc::variant(1.5)), back), fc::exception); + BOOST_CHECK_THROW(fc::from_variant(obj(fc::variant(-1.0)), back), fc::exception); + BOOST_CHECK_THROW(fc::from_variant(obj(fc::variant(std::string{"1.5"})), back), fc::exception); + BOOST_CHECK_THROW(fc::from_variant(obj(fc::variant(std::string{"1E3"})), back), fc::exception); + BOOST_CHECK_THROW(fc::from_variant(obj(fc::variant()), back), fc::exception); + BOOST_CHECK_THROW(fc::from_variant(obj(fc::variant(true)), back), fc::exception); + BOOST_CHECK_THROW(fc::from_variant(obj(fc::variant(std::string{})), back), fc::exception); + // Out of range: all digits, but past 2^64. boost::lexical_cast throws rather + // than saturating, so the parse error surfaces instead of a wrong value. + BOOST_CHECK_THROW( + fc::from_variant(obj(fc::variant(std::string{"1234567890123456789012345"})), back), + fc::exception); + + // And the shapes a real pre-builtin writer emits still work. Canonicality is + // NOT required here: a value with no spelling is exactly what the string + // carrier cannot express, so this arm is the only way to name such a bound. + fc::from_variant(obj(fc::variant(uint64_t{7})), back); + BOOST_CHECK_EQUAL(back.value, 7u); + fc::from_variant(obj(fc::variant(std::string{"7"})), back); + BOOST_CHECK_EQUAL(back.value, 7u); + fc::from_variant(obj(fc::variant(slug_name{"LIQSOL"}.value)), back); + BOOST_CHECK(back == slug_name{"LIQSOL"}); +} + BOOST_AUTO_TEST_SUITE_END() From 5415b86f7b839157530e82a10bbd853d90d828f5 Mon Sep 17 00:00:00 2001 From: kevin Heifner Date: Thu, 17 Sep 2026 13:06:42 -0500 Subject: [PATCH 07/29] feat(fc): a slug_name code must start with a letter Makes the string carrier unambiguous by construction: no legal code can be spelled like a number, so a bare JSON string is always a code and never a decimal. Digits and '_' stay legal in every position after the first. leading_alphabet is an OPTIONAL traits member, so sysio::name is unaffected. Change-Id: I0b19b790af92ed20b48b7a8c17a71ce9c583ada5 --- libraries/libfc/include/fc/basic_name.hpp | 21 ++++++++ libraries/libfc/include/fc/slug_name.hpp | 43 ++++++++++----- libraries/libfc/test/test_slug_name.cpp | 64 ++++++++++++----------- 3 files changed, 84 insertions(+), 44 deletions(-) diff --git a/libraries/libfc/include/fc/basic_name.hpp b/libraries/libfc/include/fc/basic_name.hpp index faf8f0f462..73f8b7d9ca 100644 --- a/libraries/libfc/include/fc/basic_name.hpp +++ b/libraries/libfc/include/fc/basic_name.hpp @@ -64,6 +64,15 @@ concept basic_name_traits = && Traits::max_len > 0 && std::string_view{ Traits::alphabet }.size() > 0; +/// OPTIONAL traits member: the symbols a spelling may START with. Traits that +/// omit it accept any alphabet character in the leading position, which is what +/// `sysio::name` wants. `slug_name` supplies it so that no legal code can be +/// confused with a decimal number — see `slug_name_traits::leading_alphabet`. +template +concept basic_name_has_leading_alphabet = requires { + { Traits::leading_alphabet } -> std::convertible_to; +}; + template struct basic_name { uint64_t value = 0; @@ -102,6 +111,12 @@ struct basic_name { static constexpr bool is_valid_literal(std::string_view str) { if (str.size() > static_cast(Traits::max_len)) return false; + if constexpr (basic_name_has_leading_alphabet) { + if (!str.empty() + && std::string_view{ Traits::leading_alphabet }.find(str[0]) + == std::string_view::npos) + return false; + } for (char c : str) { if (Traits::alphabet.find(c) == std::string_view::npos) return false; @@ -199,6 +214,12 @@ struct basic_name { static uint64_t encode(std::string_view str) { if (static_cast(str.size()) > Traits::max_len) Traits::throw_invalid(str, "too long"); + if constexpr (basic_name_has_leading_alphabet) { + if (!str.empty() + && std::string_view{ Traits::leading_alphabet }.find(str[0]) + == std::string_view::npos) + Traits::throw_invalid(str, "first character is not allowed to lead"); + } const basic_name packed{ pack(str) }; if (packed.to_string() != str) Traits::throw_invalid(str, "not properly normalized"); diff --git a/libraries/libfc/include/fc/slug_name.hpp b/libraries/libfc/include/fc/slug_name.hpp index 9480d2895e..b4c13eb752 100644 --- a/libraries/libfc/include/fc/slug_name.hpp +++ b/libraries/libfc/include/fc/slug_name.hpp @@ -42,6 +42,16 @@ struct slug_name_traits { static constexpr std::string_view alphabet{ alphabet_storage, sizeof(alphabet_storage) - 1 }; + // A code must START with a letter. This is what makes the string carrier + // unambiguous: no legal code can be spelled like a number, so a bare JSON + // string is always a code and never a decimal. Without it the alphabet's + // digits make "7" both a valid code (packed 149533581377536) and a valid + // decimal, and "1E3" / "0X10" additionally collide with JS numeric syntax — + // an ambiguity no reader can resolve from the value alone. Digits and '_' + // remain legal in every position after the first ("V1", "USDC", "TRAIL_"). + // The empty string is unaffected: it is the zero sentinel, not a spelling. + static constexpr std::string_view leading_alphabet{ "ABCDEFGHIJKLMNOPQRSTUVWXYZ" }; + // A symbol-0 ('\0') slot terminates the string — to_string() stops there, so // a raw value with an interior zero decodes identically to the contract-side // sysio::slug_name, which also stops at the first zero. @@ -87,22 +97,29 @@ using slug_name_literals::operator""_s; /// JSON carrier for a slug_name: the canonical string spelling, and nothing /// else. A slug renders as its text (`"LIQSOL"`), the zero sentinel as `""`. /// -/// A value below 2^42 has no spelling — `zero_terminates` is true, so decoding -/// stops at the first zero symbol slot and every such value collapses to `""`. -/// Those THROW rather than acquire a second, numeric carrier. One type, one -/// JSON shape: a caller writes a slug field exactly one way, and a reader never -/// branches on the JSON type. +/// The string is unambiguous because `leading_alphabet` forbids a code from +/// starting with a digit: no legal spelling can be read as a number, so a bare +/// JSON string is always a code. That is what removes the need for a second, +/// type-disjoint carrier — and why the carrier could not have been a numeric +/// string before the rule existed. /// -/// The throw is contained by construction: `get_table_rows` wraps each row's -/// key decode and each row's value render in its own try/catch and falls back -/// to hex (`plugins/chain_plugin/src/chain_plugin.cpp`), so a row holding a -/// non-canonical code degrades that one cell instead of failing the table. +/// A packed value whose leading symbol slot is 0 or a digit is not a code and +/// has no spelling; rendering one THROWS. That is an invariant assertion, not a +/// carrier decision: such a value must never be persisted in the first place, +/// which is the job of the validation at the proto boundary where a raw +/// `uint64` becomes a slug (`sysio.msgch`'s dispatch path and the opreg/uwrit +/// /reserv writers). Until that lands, a stored one is a defect that surfaces +/// here rather than being silently rendered as something it is not. inline void to_variant(const slug_name& s, fc::variant& v) { const std::string text = s.to_string(); - // `pack` is the non-validating encoder, so this is a pure round-trip test: - // the value is canonical exactly when its own spelling recovers it. - FC_ASSERT(slug_name::pack(text) == s.value, - "slug_name {} is not canonical and has no string spelling", s.value); + // Two checks, because a round trip alone is not enough. `is_valid_literal` + // is the static spelling predicate (length, alphabet, pad, leading letter); + // `pack` is the NON-validating encoder, so comparing it to `value` is the + // canonicality test. A value packed from an illegal spelling — say + // `pack("0")` — round-trips through `pack`/`to_string` yet is not a code, so + // emitting it would produce a string `from_variant` then refuses. + FC_ASSERT(slug_name::is_valid_literal(text) && slug_name::pack(text) == s.value, + "slug_name {} is not a code and has no string spelling", s.value); v = text; } diff --git a/libraries/libfc/test/test_slug_name.cpp b/libraries/libfc/test/test_slug_name.cpp index 4468f3dc87..5eb78fa5f5 100644 --- a/libraries/libfc/test/test_slug_name.cpp +++ b/libraries/libfc/test/test_slug_name.cpp @@ -62,15 +62,12 @@ BOOST_AUTO_TEST_CASE(roundtrip_simple_strings) { BOOST_AUTO_TEST_CASE(roundtrip_with_underscores) { BOOST_CHECK_EQUAL(slug_name{"A_B"}.to_string(), "A_B"); BOOST_CHECK_EQUAL(slug_name{"X_Y_Z"}.to_string(), "X_Y_Z"); - BOOST_CHECK_EQUAL(slug_name{"_LEAD"}.to_string(), "_LEAD"); BOOST_CHECK_EQUAL(slug_name{"TRAIL_"}.to_string(), "TRAIL_"); } BOOST_AUTO_TEST_CASE(roundtrip_with_digits) { BOOST_CHECK_EQUAL(slug_name{"V1"}.to_string(), "V1"); - BOOST_CHECK_EQUAL(slug_name{"0"}.to_string(), "0"); BOOST_CHECK_EQUAL(slug_name{"X12345"}.to_string(), "X12345"); - BOOST_CHECK_EQUAL(slug_name{"01234567"}.to_string(), "01234567"); // 8 digits } BOOST_AUTO_TEST_CASE(empty_codename) { @@ -115,12 +112,12 @@ BOOST_AUTO_TEST_CASE(values_under_js_safe_integer_limit) { BOOST_CHECK_LT("ETHEREUM"_s.value, JS_SAFE_LIMIT); BOOST_CHECK_LT("ZZZZZZZZ"_s.value, JS_SAFE_LIMIT); - BOOST_CHECK_LT("12345678"_s.value, JS_SAFE_LIMIT); - BOOST_CHECK_LT("________"_s.value, JS_SAFE_LIMIT); + BOOST_CHECK_LT("Z1234567"_s.value, JS_SAFE_LIMIT); + BOOST_CHECK_LT("Z_______"_s.value, JS_SAFE_LIMIT); - // The theoretical maximum: all slots = 37 (the `_` char). - // Value = 37 * (1 + 2^6 + 2^12 + ... + 2^42) = 37 * ((2^48 - 1) / 63). - const uint64_t max_codename = "________"_s.value; + // The largest legal code: 'Z' (26) leading, then '_' (37) in every slot the + // leading rule leaves free. + const uint64_t max_codename = "Z_______"_s.value; BOOST_CHECK_LT(max_codename, JS_SAFE_LIMIT); BOOST_CHECK_LT(max_codename, (1ULL << 48)); } @@ -159,13 +156,32 @@ BOOST_AUTO_TEST_CASE(accepts_full_alphabet) { // Alphabet // --------------------------------------------------------------------------- -BOOST_AUTO_TEST_CASE(every_alphabet_char_roundtrips) { - // every non-pad symbol of the alphabet round-trips as a single-char slug +BOOST_AUTO_TEST_CASE(every_alphabet_char_roundtrips_after_the_first_slot) { + // Every non-pad symbol round-trips — but only a letter may LEAD, so the + // sweep puts each symbol in the SECOND slot behind a fixed letter. const std::string_view alphabet = fc::slug_name_traits::alphabet; for (std::size_t s = 1; s < alphabet.size(); ++s) { - const std::string one(1, alphabet[s]); - BOOST_CHECK_EQUAL(slug_name{one}.to_string(), one); + const std::string two = std::string("A") + alphabet[s]; + BOOST_CHECK_EQUAL(slug_name{two}.to_string(), two); + } +} + +BOOST_AUTO_TEST_CASE(a_code_must_start_with_a_letter) { + // The rule that makes the string carrier unambiguous: no legal code can be + // spelled like a number, so a bare JSON string is never a decimal. + for (const char* bad : {"0", "7", "101", "1E3", "0X10", "12345678", + "_LEAD", "________", "01234567"}) { + BOOST_CHECK_THROW(slug_name{bad}, fc::exception); + BOOST_CHECK_MESSAGE(!slug_name::is_valid_literal(bad), + std::string{"must be rejected as a literal: "} + bad); + } + // Digits and '_' stay legal everywhere after the first symbol. + for (const char* good : {"V1", "X12345", "AZ09_", "TRAIL_", "A_B", "Z_______"}) { + BOOST_CHECK_EQUAL(slug_name{good}.to_string(), good); + BOOST_CHECK(slug_name::is_valid_literal(good)); } + // The zero sentinel is not a spelling and is unaffected. + BOOST_CHECK_EQUAL(slug_name{""}.value, 0u); } BOOST_AUTO_TEST_CASE(symbol_zero_is_the_nul_pad) { @@ -394,8 +410,8 @@ BOOST_AUTO_TEST_CASE(variant_every_canonical_value_round_trips_exactly) { // Injectivity across the boundary for the whole canonical range, including // its floor (1<<42 is "A") and the zero sentinel. for (uint64_t raw : {uint64_t{0}, uint64_t{1} << 42, fc::slug_name{"A"}.value, - fc::slug_name{"LIQSOL"}.value, fc::slug_name{"12345678"}.value, - fc::slug_name{"________"}.value}) { + fc::slug_name{"LIQSOL"}.value, fc::slug_name{"Z1234567"}.value, + fc::slug_name{"Z_______"}.value}) { fc::variant v; BOOST_REQUIRE_NO_THROW(fc::to_variant(slug_name{raw}, v)); BOOST_REQUIRE(v.is_string()); @@ -405,20 +421,6 @@ BOOST_AUTO_TEST_CASE(variant_every_canonical_value_round_trips_exactly) { } } -BOOST_AUTO_TEST_CASE(variant_an_all_digit_slug_is_a_string_not_its_own_decimal) { - // The slug alphabet contains digits, so "7" is itself a canonical slug whose - // packed value is nothing like 7. That ambiguity is why a JSON number is - // rejected outright rather than read as either one. - fc::variant v; - fc::to_variant(slug_name{"7"}, v); - BOOST_REQUIRE(v.is_string()); - BOOST_CHECK_EQUAL(v.as_string(), "7"); - BOOST_CHECK_NE(slug_name{"7"}.value, 7u); - - slug_name back; - BOOST_CHECK_THROW(fc::from_variant(fc::variant(uint64_t{7}), back), fc::exception); -} - BOOST_AUTO_TEST_CASE(variant_accepts_the_transitional_object_carrier) { // TRANSITIONAL: the shape abigen's reflected struct emitted before slug_name // became an ABI builtin. Deleted once no writer emits it. @@ -451,8 +453,8 @@ BOOST_AUTO_TEST_CASE(variant_carrier_round_trips_through_json_TEXT) { 0u, // the zero sentinel -> "" uint64_t{1} << 42, // the canonical floor ("A") slug_name{"ETH"}.value, - slug_name{"12345678"}.value, // all digits, and still a string - slug_name{"________"}.value, + slug_name{"Z1234567"}.value, // digits after the leading letter + slug_name{"Z_______"}.value, }; for (const uint64_t raw : values) { fc::variant v; @@ -513,7 +515,7 @@ BOOST_AUTO_TEST_CASE(variant_rejects_every_non_string_carrier) { // failure mode a single carrier removes, because none of these coercions is // the value the writer meant: // - // 7 -> the slug "7" is 0x1F0000000000, not 7 + // 7 -> not a code at all now; a code must start with a letter // -1 -> lexical_cast does not reject a sign for an unsigned // target, it WRAPS; a bound of -1 would page from the far // end of the table From 9f899338c8e577aaaae70cc57869d9ee4fa4e80b Mon Sep 17 00:00:00 2001 From: kevin Heifner Date: Mon, 21 Sep 2026 08:02:51 -0500 Subject: [PATCH 08/29] feat(contracts): use CDT's slug_name and validate payload codes The contracts carried their OWN slug_name -- a fourth implementation beside the host, CDT and sdk-core -- so the leading-letter rule reached three of four. It is deleted; the ten includes point at , which carries the rule. Same type name, so no call site changes. A code arriving in a forgeable payload field reaches slug_name through the non-validating raw constructor. chain_code is proven by source_chain_binding_ok, but token_code / reserve_code are not, so msgch now drops an attestation whose codes have no canonical spelling -- never check(), which would halt evalcons. basic_name gets one validation algorithm shared with CDT: validity_error() backs both is_valid_literal() and the constructor, which previously disagreed. That disagreement was real -- "abcdefghijklm"_n compiled as abcdefghijkl2 while name{"abcdefghijklm"} threw, and the same for a trailing pad. Both paths now agree, pinned by name_tests::literal_and_runtime_validation_agree. parse_wire_account_name asks name::is_valid_literal before constructing: it built the name first and checked after, so a stricter constructor aborted the whole evalcons delivery. is_valid_name_string, a hand-rolled mirror of the same rules, is deleted for the same reason. The generated-schema pattern gains the leading rule so it agrees with the ABI serializer. Change-Id: I617dcb6abcf2ba2d7c79863a73fd5a6d758a9b50 --- .../include/sysio.chains/sysio.chains.hpp | 2 +- contracts/sysio.chains/sysio.chains.abi | 10 -- contracts/sysio.chains/sysio.chains.wasm | Bin 35739 -> 35745 bytes contracts/sysio.chalg/sysio.chalg.wasm | Bin 67572 -> 67973 bytes contracts/sysio.dclaim/src/sysio.dclaim.cpp | 10 +- contracts/sysio.dclaim/sysio.dclaim.wasm | Bin 30325 -> 30963 bytes .../include/sysio.msgch/sysio.msgch.hpp | 2 +- contracts/sysio.msgch/src/sysio.msgch.cpp | 59 ++++++- contracts/sysio.msgch/sysio.msgch.wasm | Bin 157530 -> 161994 bytes .../include/sysio.opp.common/name_ops.hpp | 33 ++-- .../include/sysio.opp.common/safe_ops.hpp | 39 +---- .../include/sysio.opp.common/slug_name.hpp | 145 ------------------ .../include/sysio.opp.common/wire_asset.hpp | 5 +- .../include/sysio.opreg/sysio.opreg.hpp | 2 +- contracts/sysio.opreg/src/sysio.opreg.cpp | 2 +- contracts/sysio.opreg/sysio.opreg.abi | 10 -- contracts/sysio.opreg/sysio.opreg.wasm | Bin 92467 -> 93586 bytes .../include/sysio.reserv/sysio.reserv.hpp | 2 +- contracts/sysio.reserv/sysio.reserv.abi | 10 -- contracts/sysio.reserv/sysio.reserv.wasm | Bin 85200 -> 85221 bytes contracts/sysio.roa/sysio.roa.wasm | Bin 50583 -> 51237 bytes contracts/sysio.system/sysio.system.wasm | Bin 202534 -> 203199 bytes .../include/sysio.tokens/sysio.tokens.hpp | 2 +- contracts/sysio.tokens/sysio.tokens.abi | 10 -- contracts/sysio.tokens/sysio.tokens.wasm | Bin 26737 -> 26755 bytes .../include/sysio.uwrit/sysio.uwrit.hpp | 2 +- contracts/sysio.uwrit/src/sysio.uwrit.cpp | 2 +- contracts/sysio.uwrit/sysio.uwrit.abi | 10 -- contracts/sysio.uwrit/sysio.uwrit.wasm | Bin 159688 -> 161218 bytes contracts/tests/safe_ops_tests.cpp | 46 +----- contracts/tests/sysio.dispatch_tests.cpp | 43 ++++++ .../tools/generate-sysio-contract-types.py | 6 +- libraries/chain/include/sysio/chain/name.hpp | 12 ++ libraries/libfc/include/fc/basic_name.hpp | 131 +++++++++++----- libraries/libfc/include/fc/slug_name.hpp | 29 ++-- libraries/libfc/test/test_slug_name.cpp | 13 ++ unittests/name_tests.cpp | 51 +++++- unittests/test_contract_action_match.cpp | 14 +- 38 files changed, 339 insertions(+), 363 deletions(-) delete mode 100644 contracts/sysio.opp.common/include/sysio.opp.common/slug_name.hpp diff --git a/contracts/sysio.chains/include/sysio.chains/sysio.chains.hpp b/contracts/sysio.chains/include/sysio.chains/sysio.chains.hpp index 0b581cf632..1f25837573 100644 --- a/contracts/sysio.chains/include/sysio.chains/sysio.chains.hpp +++ b/contracts/sysio.chains/include/sysio.chains/sysio.chains.hpp @@ -6,7 +6,7 @@ #include #include #include -#include +#include #include namespace sysio { diff --git a/contracts/sysio.chains/sysio.chains.abi b/contracts/sysio.chains/sysio.chains.abi index dd2675ef8a..c86cb25143 100644 --- a/contracts/sysio.chains/sysio.chains.abi +++ b/contracts/sysio.chains/sysio.chains.abi @@ -134,16 +134,6 @@ "type": "outpost_addrs" } ] - }, - { - "name": "slug_name", - "base": "", - "fields": [ - { - "name": "value", - "type": "uint64" - } - ] } ], "actions": [ diff --git a/contracts/sysio.chains/sysio.chains.wasm b/contracts/sysio.chains/sysio.chains.wasm index d3409e3fd52be3e679f7b7235dc4cbf6756c06d6..d129935798185dd2ebb3a67cbef8afbedda2a5f3 100755 GIT binary patch delta 173 zcmbO|ooV58rVSgJ8CPxI#B6P*$>>;b#>Aq)punifEMU$opupg$kfp!~Bs7^B%$XU0 zdFS#NCP7yW4$tiqd>M2lM2v41xA6W&CiNHuwj#_+x)h2 Giy#0#jK@itYdRJ16gQsjrGe2En?`J?dw zPt*{PhNw{!?}K=tqw!z`qJrWHB9Y*t2zZ1jBBFq2dfrOVm_KiNdZxR-`TCoA^-Hz+ zsAa5B)pWKU)6@evOw02<#ooGCr*0QP7F!!=z^K6f&?qJcUbMgq5o6PQp$X&91;b+z zXAgqr3C7uniDE~jrBxzxFz#;pvjL7`VPdTLjS#U(O8~Bl z$~h_i@H-|fe-2N@loe-!tko*>nGLhLDz5dos7v>D9*xE5%thE;<(lEjk%mXZ+Fijx z9?ikXG4gFiS6sIh!`y;Av15owGj|!hz3t@^)4|Cou4=ope>qKb)|YwYsChRye5eTJ z>d=>!m`u4!n88m&Kb2D|g}gFl3B&T;y@D>(gYIiisixDrRDkQNLxag#KtLET0?HdK zAr&ZGvti*LJ^IZsm39W&2!%BM6QZl!X{Gq}7O{87R*QeJ%;=OEsUd%$JtG#~dt%>o z&-7mj0JDie{-r|=RdC()_tBlv`W+TyGFhMum4und@Y~yJ~GpskH(w>8cxYP57%*^JBDsaqI;-ch0NI6;FVb5wnG&juW$yL zRn95~zX5gi5&@B*jiC$eYeiO~Oa3_KSfpXO@wV<)Da)4VTVJ0A;w47vqpoEwbS z&d*?p498zcgJt;O!p4yAnZr-_b27yRMqj4reuRwr&r@)B!63f{bloJ-h4`c(Jvv#= zPSK)i(52yvq{ljBzCsAx-3#Yym=KDvGZX-;~}0_zA~) zvSE$rP?qPNbix-`H)?B`u43o1#Uir&8L+hsW8ClInBiS#}VYv)-a>^YWNmNyg3V(Jf{Uo!)fg-HP7u3py&R*?K0G2OF?Q)rYW=9?u}0 zQ7sa>1}&8MMzh(LMC=|GKOb7|Iz z|GqrUcR5+Gdl3Kvd)7KwhEz+Km{B`SVyLNWFFg0#k|}EHrb9@Bd}S0VpU$Rym!6m?IT^&hEr#{U2S delta 1881 zcmaJ?Yfx2H6u#fS=kmG;2ZF$>l+bLb2Llcz~uBVtsMT34j4=|X#@qq65MR*>8$;@nuX4b3Ce6HVF10L~L zOp2@Nq1e#b%$gatVx6kUMWap&W@-zMw)o>2`xI>%Ps7fHaiu3Sunv%$bqf0nG{YK# zr8F)kTrNJ3=^@QI>oUaixBK`ic*tjmxxrO_*PvQyPFM=L_OC|@HTRFhI{w)JlPA_w z>agBeM*9{{!*cQ)o{w~XZumwZjpmLVj5NMulSJ9U3ij zjbhV>ZH>Y-Ku4Ey7^EDtuiOD-*{BM#<(#TC~$WLIf1yoT%s! zKU>^{E;e_=9HUN1&26|yp5tnFzY8vd1MW2hEKI9rWlQu$L!t6uOJe)XV8n4#7RmCTA-C#s{? z)4qaGOOqs`Xlj|XLb^<8{WR4{Fi;TW4+=hJXrK>I%tIqDIq@D)&R;s^3EZPmg%fd~ zCl@9%eJg^gI(0IARP+Jb=u}ZJJfgayrD&IF(8|-J@Fy=it?HkB!~MzqOheHB3<-nU z0<(c0{x+7T6)%z_psY9@Yw45Uw_p|doDINg>T`CCM}ibXq>#vHKzU}QL-bpcgns?U zDHhHOiCh-WSJJ5v&PwUj3MYY2I_D3T%qVYNS6WiiiZ%R;^RA#*OT8%hLLOG~38lw@ zRB|gXMiP~m&jj&N7c=3Wz{I5xI)BaUFWm&bp&1oHbnnU*WrG+1WWK(N_q;Z=6Srdn z54ygJ;ake8jO|khSw^T^AJ<`{WFs16leDVViHmw{6|G{Wj%*{XxX~Bi5x;Speb49( zy9qzg(W=qdOdVB8_>mH;>#&6u+??&URZfs?%tTF_La04Ij0V+gLnd9UnTPFs;w^tY zvS@c*5Ra?v2W0Wxb^C#xG@~KHv`c>ciSBQefWq5h{pA6JuU&hRLQFXFhSYI@3(z)EEvWTg*;6T-*bDskGS2yWAZw7*O2(03LhK zN0N!VzYtv?AXuETGM()O(c#Vjwi5Bl)#TeeWKzy!lFgEdxT1-2El3`cc8D0I5|>Tj zbe?JgXYFMqXYFMc351r>{8Doe+>grhLir>CrHAG*lTnOjRSVG>yFOj2jY9stYiS5xi!K>RhF`VSfJDBlru$P9v diff --git a/contracts/sysio.dclaim/src/sysio.dclaim.cpp b/contracts/sysio.dclaim/src/sysio.dclaim.cpp index 8391a56965..7bc37fd659 100644 --- a/contracts/sysio.dclaim/src/sysio.dclaim.cpp +++ b/contracts/sysio.dclaim/src/sysio.dclaim.cpp @@ -57,7 +57,6 @@ uint64_t next_id(name self, Pick pick) { /// Non-throwing validation of a string destined for `name(std::string_view)`. Shared with every /// other OPP inbound handler via `sysio.opp.common/safe_ops.hpp` so the never-throw name domain is /// defined and audited in exactly one place. -using sysio::opp::safe::is_valid_name_string; /// Saturating WIRE credit. `asset::operator+=` aborts on overflow past `asset::max_amount` /// (2^62-1); credit_wire runs inside the never-throw OPP inbound path (via onreward), so cap at @@ -259,10 +258,11 @@ void dclaim::onreward(uint64_t chain_code, } name wacct; // value 0 == not yet AuthX-linked - // Validate the cross-chain-supplied account string before constructing name(): an invalid or - // oversized string is treated as unlinked (credit parked by native address) rather than - // aborting the inbound dispatch via name()'s internal check(). See is_valid_name_string. - if (!staker_wire_account.empty() && is_valid_name_string(staker_wire_account)) { + // Validate the cross-chain-supplied account string before constructing name(): an invalid + // string is treated as unlinked (credit parked by native address) rather than aborting the + // inbound dispatch via name()'s internal check(). `is_valid_literal` is name's OWN predicate -- + // the one its constructor uses -- so it cannot drift from what the constructor accepts. + if (!staker_wire_account.empty() && sysio::name::is_valid_literal(staker_wire_account)) { wacct = name(staker_wire_account); } diff --git a/contracts/sysio.dclaim/sysio.dclaim.wasm b/contracts/sysio.dclaim/sysio.dclaim.wasm index 9e653a09feebbe50659da0ea95af15ea326db000..cc21018237de2b1a1dfb486d4f1faf4a3de729fe 100755 GIT binary patch delta 5622 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
B@drui(PCj96>91`NG`u^u3;kEDeNOu|(Kn zw_KWI<6%t+cqX2IKc{|2$GB>9M{A{o2SylJg<2Y zb#i-i7mPP$(Su||$l-i$Q7+xeLl-v=8z`w&kF#$k;BmMu_VDh-pVKY8ami#otnsPl zEPigueA>(YrNcc~tlqQjJZWhq?d{sO)UWp=>NbAl^6P=6QOhKv7A>0xM0G9;)9w7` zvL!%N?-lKQ{EAAtqpSCdYY3a{72)ciBk4%ZksS-62f)1vQXmAjphLCZiVo%eekofR zuJUdmz5gMs;~^|J?B>6$cpA^Gy(_Db9A95Kk{0l1D`OMn!vncQK9!OG+I6Jad*wF@ zIGlE6@K! zT0Yg1#mCop@Dh1$O&%T^P8ij6;h}MA&8Xmj(eIZELBBn+$42$8fr2HlcP3B2Qa);= zAdg*Hr{6(*=h~$_Z*2vvxNmKnLslZ(yRvfVPpEj9SFO8Id7*$)*ZVNaUVpwP0{rOu z`{);3ZGVS0*3y>OZ*bWG1=8-gC#2m?1PcGXr3hEc1)3CZ7}R6{K1w6(R2pGxJ8$0* zQCf84pyeCiM_X}K6VKlC+w8<|9QZjyDlQ{mlZPE+bMs{2Y~kh(81K`~(s&MOJSTpu z$k-AgdW0)lV_A;^gW@si3mw}E@G}*D3kh>aYdd~V8n$&gJ;B#*{gxuUV_P#_&2MZg zq!vEAZ5r+5iQ7kouEBa(U0l`Qw^9FKbAP{qcWp07e-dx #include #include -#include +#include #include #include diff --git a/contracts/sysio.msgch/src/sysio.msgch.cpp b/contracts/sysio.msgch/src/sysio.msgch.cpp index 7fa97b3ebc..9cafecd84c 100644 --- a/contracts/sysio.msgch/src/sysio.msgch.cpp +++ b/contracts/sysio.msgch/src/sysio.msgch.cpp @@ -5,7 +5,7 @@ #include // dispute trigger + open-dispute gate (disputes table) #include // operator-status delivery gate (operators table) #include // authoritative Tier-1 electorate preflight -#include +#include #include // to_depot_amount — WSA-028 fail-closed TokenAmount gate #include // parse_wire_account_name — never-throw account-name parse #include // canonical envelope encoding + keccak epoch digest @@ -49,7 +49,7 @@ constexpr name ram_payer = "sysio"_n; /// always WIRE. constexpr uint32_t WIRE_CHAIN_ID = 1; -using sysio::slug_name_literals::operator""_s; +// `operator""_s` is declared at global scope by . /// Codename of the Ethereum outpost — the sole source of node-owner NFT (ERC1155) deposits, which /// occur on Ethereum mainnet only. This is the `ChainSpec.code` the launch and dev bootstrap configs @@ -332,6 +332,33 @@ name resolve_account_from_op_address(const opp::types::ChainAddress& op_address) return false; } +/// Are a payload's FORGEABLE code fields canonical slug_names? +/// +/// `chain_code` is proven — `source_chain_binding_ok` binds it to the delivering +/// outpost. `token_code` / `reserve_code` are NOT: they arrive as raw protobuf +/// uint64s and reach a slug_name through the non-validating raw constructor, so a +/// forged payload can carry a value no spelling produces. Such a value can never +/// have been registered, and persisting it makes every later render of that row +/// throw — in a `values_only` scan the underwriter's unconditional +/// `row.get_object()` then drops the WHOLE cycle, not one cell. +/// +/// Drop the attestation instead; never check(), per +/// feedback_opp_handlers_never_throw — a check() here halts evalcons and stalls +/// consensus. +/// +/// `path` labels the dispatch path in the diagnostic. True iff every code is canonical. +[[nodiscard]] bool payload_codes_canonical(std::initializer_list codes, + const char* path) { + for (const sysio::slug_name code : codes) { + if (!code.is_canonical()) { + sysio::print("msgch::", path, ": DROP attestation -- payload code ", code.value, + " has no canonical slug_name spelling\n"); + return false; + } + } + return true; +} + /// Reinterpret an exactly-32-byte protobuf `bytes` field as a checksum256. Returns std::nullopt /// for any other length; chain and header verification treat a malformed hash as a mismatch, /// never as a match or a wildcard. @@ -504,6 +531,7 @@ void dispatch_operator_action(name self, const std::vector& data, // no-proto-messages-in-actions rule. const sysio::slug_name chain_code_slug{chain_code}; const sysio::slug_name token_code{oa.amount.token_code}; + if (!payload_codes_canonical({token_code}, "dispatch_operator_action")) return; // WSA-028: TokenAmount.amount is signed on the wire. Gate it through the // shared fail-closed parser before any unsigned use — a negative or // out-of-range amount is dropped here, never wrapped into a huge collateral @@ -595,6 +623,11 @@ void dispatch_underwrite_commit(name self, const std::vector& data, uint64 // commit is recorded against a swap leg. if (!source_chain_binding_ok(chain_code, uic.chain_code, "dispatch_underwrite_commit")) return; + const sysio::slug_name uic_token_code{uic.token_code}; + const sysio::slug_name uic_reserve_code{uic.reserve_code}; + if (!payload_codes_canonical({uic_token_code, uic_reserve_code}, + "dispatch_underwrite_commit")) return; + // Route with the proven `chain_code` (equal to `uic.chain_code`, enforced above) so the leg slot // is keyed off provenance, not the payload's self-asserted chain. action( @@ -602,8 +635,8 @@ void dispatch_underwrite_commit(name self, const std::vector& data, uint64 UWRIT_ACCOUNT, "rcrdcommit"_n, std::make_tuple(uic.uw_request_id, *underwriter, chain_code, sysio::slug_name{chain_code}, - sysio::slug_name{uic.token_code}, - sysio::slug_name{uic.reserve_code}, + uic_token_code, + uic_reserve_code, data) ).send(); } @@ -636,6 +669,11 @@ void dispatch_reserve_create(name self, const std::vector& data, uint64_t // reserve whose external custody is claimed against a different chain B. if (!source_chain_binding_ok(chain_code, ext.chain_code, "dispatch_reserve_create")) return; + const sysio::slug_name ext_token_code{ext.amount.token_code}; + const sysio::slug_name ext_reserve_code{ext.reserve_code}; + if (!payload_codes_canonical({ext_token_code, ext_reserve_code}, + "dispatch_reserve_create")) return; + const uint64_t ext_amount = sysio::opp::safe::to_depot_amount(static_cast(ext.amount.amount)).value_or(0); @@ -643,8 +681,8 @@ void dispatch_reserve_create(name self, const std::vector& data, uint64_t permission_level{self, "active"_n}, RESERV_ACCOUNT, "oncrtreserve"_n, std::make_tuple(sysio::slug_name{ext.chain_code}, - sysio::slug_name{ext.amount.token_code}, - sysio::slug_name{ext.reserve_code}, + ext_token_code, + ext_reserve_code, rc.name, rc.description, ext_amount, @@ -675,12 +713,17 @@ void dispatch_reserve_create_cancel(name self, const std::vector& data, ui // delivering outpost so an envelope proven from outpost A cannot cancel a reserve on chain B. if (!source_chain_binding_ok(chain_code, cancel.chain_code, "dispatch_reserve_create_cancel")) return; + const sysio::slug_name cancel_token_code{cancel.token_code}; + const sysio::slug_name cancel_reserve_code{cancel.reserve_code}; + if (!payload_codes_canonical({cancel_token_code, cancel_reserve_code}, + "dispatch_reserve_create_cancel")) return; + action( permission_level{self, "active"_n}, RESERV_ACCOUNT, "oncnclrsv"_n, std::make_tuple(sysio::slug_name{cancel.chain_code}, - sysio::slug_name{cancel.token_code}, - sysio::slug_name{cancel.reserve_code}, + cancel_token_code, + cancel_reserve_code, cancel.creator_addr.kind, cancel.creator_addr.address) ).send(); diff --git a/contracts/sysio.msgch/sysio.msgch.wasm b/contracts/sysio.msgch/sysio.msgch.wasm index b581f28a4f6c2d039c3f9a103e3d3b01ddea705b..5bd67a00c13514907ef6e84c6f165e096778b046 100755 GIT binary patch delta 45100 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 delta 40207 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 -#include // is_valid_name_string #include #include @@ -32,23 +31,29 @@ namespace sysio::opp::safe { /// Parse an inbound account string into a validated `sysio::name`, or /// `std::nullopt` when the string is empty or not a canonical account name. /// -/// Validation is delegated to `is_valid_name_string`, which mirrors CDT -/// `basic_name`'s charset, length, and final-symbol rules exactly, so the returned -/// `name{s}` is guaranteed to construct without aborting — in particular a -/// legitimate 13-byte name is accepted where a naive `size() > 12` cap would -/// wrongly reject it. Callers on the OPP dispatch path MUST treat `std::nullopt` -/// as "drop this message" and `return`, never as a reason to `check()`-abort. +/// Validation is delegated to `name::is_valid_literal` — the type's OWN predicate, +/// the one its constructor and its `_n` literal use — so it cannot drift from what +/// the constructor accepts. A legitimate 13-byte name whose final symbol fits the +/// 4-bit final slot is accepted, where a naive `size() > 12` cap would wrongly +/// reject it. Callers on the OPP dispatch path MUST treat `std::nullopt` as "drop +/// this message" and `return`, never as a reason to `check()`-abort. /// /// @param s the candidate account string (no leading/trailing trimming). /// @return the constructed `name` iff `s` is a nonempty canonical account name. inline std::optional parse_wire_account_name(std::string_view s) { - if (s.empty() || !is_valid_name_string(s)) return std::nullopt; - const sysio::name parsed{s}; - // CDT's numeric name encoding discards trailing dots. Require the exact - // round trip so aliases such as `underwriter.` cannot name the same roster - // principal differently on the depot and on different outpost runtimes. - if (parsed.to_string() != s) return std::nullopt; - return parsed; + // ASK FIRST, CONSTRUCT SECOND. `name`'s constructor aborts on any spelling it + // will not accept, and an abort on this path reverts the whole + // evalcons/apply_consensus delivery — so the candidate is never handed to the + // constructor until it is known good. + // + // `is_valid_literal` is the type's OWN validation predicate, the same one its + // constructor and its `_n` literal use, so this cannot drift from what the + // constructor accepts. It subsumes both checks this helper used to make by + // hand: the charset/length mirror, AND the round trip that rejected aliases + // like `underwriter.` (a trailing dot is discarded by the encoding, so it + // would otherwise name the same roster principal two ways). + if (s.empty() || !sysio::name::is_valid_literal(s)) return std::nullopt; + return sysio::name{s}; } } // namespace sysio::opp::safe diff --git a/contracts/sysio.opp.common/include/sysio.opp.common/safe_ops.hpp b/contracts/sysio.opp.common/include/sysio.opp.common/safe_ops.hpp index fee09ecab2..68cf5b4f50 100644 --- a/contracts/sysio.opp.common/include/sysio.opp.common/safe_ops.hpp +++ b/contracts/sysio.opp.common/include/sysio.opp.common/safe_ops.hpp @@ -16,9 +16,14 @@ * `subtract_balance` abort slip through into settlement. * * The helpers below were previously copied into individual contracts - * (`sysio.dclaim`'s `is_valid_name_string`, `sysio.reserv`'s `add_capped_u64`). - * They live here so every OPP contract validates names and saturates the same - * way — one place to change, one place to audit. + * (`sysio.reserv`'s `add_capped_u64`). They live here so every OPP contract + * saturates the same way — one place to change, one place to audit. + * + * Name VALIDATION is deliberately not here: it belongs to `sysio::name` itself + * (`name::is_valid_literal`, the predicate its own constructor uses). A copy in + * this header went stale the moment the type gained a rule it did not know + * about, and this header is included host-side, where the CDT name header is + * not available — so the copy could not be made to delegate. See name_ops.hpp. */ #include @@ -67,34 +72,6 @@ inline std::optional to_depot_amount(int64_t amount) { return static_cast(amount); } -/// Non-throwing validation of a string destined for `name(std::string_view)`. -/// -/// CDT's `name` constructor `check(false, ...)`-aborts on a string longer than -/// 13 characters, one containing a character outside ".12345abcdefghijklmnopqrstuvwxyz", -/// or one whose 13th character exceeds the 4-bit final symbol (value > 15). A -/// cross-chain-supplied account string must therefore be validated here and -/// soft-skipped, never fed blindly into `name()` from inside the dispatch -/// chain. Mirrors CDT `basic_name`'s `char_to_value` + length rules exactly so -/// the *full* CDT name domain is accepted — in particular a legitimate 13-byte -/// name (final symbol in `.`/`1`-`5`/`a`-`j`) passes, where a naive -/// `size() > 12` length cap would wrongly reject it. -/// -/// @param s the candidate account string (no leading/trailing trimming). -/// @return true iff `name(s)` would construct without aborting. -inline bool is_valid_name_string(std::string_view s) { - if (s.size() > 13) return false; - for (std::size_t i = 0; i < s.size(); ++i) { - const char c = s[i]; - uint8_t v; - if (c == '.') v = 0; - else if (c >= '1' && c <= '5') v = static_cast(c - '1' + 1); - else if (c >= 'a' && c <= 'z') v = static_cast(c - 'a' + 6); - else return false; // character outside the name alphabet - if (i == 12 && v > 15) return false; // 13th character encodes only 4 bits - } - return true; -} - /// Saturating unsigned 64-bit addition. Returns `a + b`, clamped to /// `UINT64_MAX` on overflow instead of wrapping. /// diff --git a/contracts/sysio.opp.common/include/sysio.opp.common/slug_name.hpp b/contracts/sysio.opp.common/include/sysio.opp.common/slug_name.hpp deleted file mode 100644 index cfd1a1ab76..0000000000 --- a/contracts/sysio.opp.common/include/sysio.opp.common/slug_name.hpp +++ /dev/null @@ -1,145 +0,0 @@ -#pragma once -/** - * @file slug_name.hpp - * @brief 8-byte packed identifier for Chain/Token/Reserve `code` fields. - * - * `sysio::slug_name` is the contract-side type for slug_name-keyed entities - * (Chain.code, Token.code, ChainToken.{chain,token}_code, Reserve.code, - * ReserveAmount.{chain,reserve}_code, TokenAmount.token_code). - * - * Wire format: `uint64`. Protobuf fields are plain uint64; this type is the - * C++ wrapper. Mirrored host-side as `fc::slug_name` (libfc/include/fc/slug_name.hpp) - * with identical packing semantics — byte identity guaranteed by the same - * encoding algorithm in both implementations. - * - * ## Alphabet + packing - * - * Alphabet: `[A-Z0-9_]+`, max 8 chars. 38-value alphabet (A-Z = 1..26, - * 0-9 = 27..36, `_` = 37; value 0 reserved for terminator/padding) packed - * most-significant-symbol-first in 6-bit slots: bits [42..47] = char[0], bits [36..41] = char[1], …, - * bits [0..5] = char[7]. Bits [48..63] are unused (always 0). - * - * Encoded values therefore live in [0, 2^48) — comfortably under JS Number's - * 2^53 safe-integer limit, so TS code can use plain `number` (not `bigint`). - * - * ## Usage - * - * - Compile-time: `"ETH"_s`, `"USDC"_s`, `"PRIMARY"_s` (literal suffix). - * Invalid characters or >8 chars trigger a compile error (constexpr-throw). - * - Runtime: `sysio::slug_name{"ETH"}` (validates + throws via sysio::check on - * invalid input; never silently produces a wrong value). - * - * @see fc::slug_name in libfc/include/fc/slug_name.hpp (host-side mirror) - * @see project_codename_type.md memory + the data-model-refactor plan §3.1 - */ - -#include -#include -#include -#include -#include -#include - -namespace sysio { - -/// 8-byte packed identifier, alphabet `[A-Z0-9_]+`, max 8 chars. -/// See file-level docs for the packing format. -struct slug_name { - uint64_t value = 0; - - constexpr slug_name() = default; - constexpr explicit slug_name(uint64_t raw) : value(raw) {} - - /// Construct from a string (compile-time-or-runtime validated). - /// Throws via `sysio::check` on >8 chars or invalid alphabet. - explicit slug_name(std::string_view s) { - sysio::check(s.size() <= 8, "slug_name: max 8 characters"); - uint64_t out = 0; - for (std::size_t i = 0; i < s.size(); ++i) { - const auto v = char_to_slot(s[i]); - sysio::check(v != INVALID_SLOT, "slug_name: invalid character (alphabet is [A-Z0-9_])"); - out |= (v << (42 - i * 6)); // most-significant-symbol-first - } - value = out; - } - - /// Unpack to a string. Stops at the first null (zero) slot. - std::string to_string() const { - std::string out; - out.reserve(8); - for (std::size_t i = 0; i < 8; ++i) { - const auto slot = (value >> (42 - i * 6)) & 0x3F; - if (slot == 0) break; - out.push_back(slot_to_char(slot)); - } - return out; - } - - operator std::string() const { return to_string(); } - - friend bool operator==(slug_name a, slug_name b) { return a.value == b.value; } - friend bool operator!=(slug_name a, slug_name b) { return a.value != b.value; } - friend bool operator<(slug_name a, slug_name b) { return a.value < b.value; } - friend bool operator<=(slug_name a, slug_name b) { return a.value <= b.value; } - friend bool operator>(slug_name a, slug_name b) { return a.value > b.value; } - friend bool operator>=(slug_name a, slug_name b) { return a.value >= b.value; } - - SYSLIB_SERIALIZE(slug_name, (value)) - - /// Sentinel for invalid characters. Public so tests / parsers can detect. - static constexpr uint64_t INVALID_SLOT = static_cast(-1); - - /// Map alphabet character to its 6-bit slot value. - /// Returns INVALID_SLOT for out-of-alphabet input. - static constexpr uint64_t char_to_slot(char c) { - if (c >= 'A' && c <= 'Z') return static_cast(1 + (c - 'A')); // 1..26 - if (c >= '0' && c <= '9') return static_cast(27 + (c - '0')); // 27..36 - if (c == '_') return 37; - return INVALID_SLOT; - } - - /// Inverse of char_to_slot. Returns '\0' for slot==0 (terminator). - static constexpr char slot_to_char(uint64_t slot) { - if (slot == 0) return '\0'; - if (slot >= 1 && slot <= 26) return static_cast('A' + (slot - 1)); - if (slot >= 27 && slot <= 36) return static_cast('0' + (slot - 27)); - if (slot == 37) return '_'; - return '\0'; - } -}; - -namespace slug_name_literals { - -/// Internal: invalid-input sentinel for the compile-time literal. CDT compiles -/// without exceptions, so we can't `throw` in a constexpr context. Instead the -/// literal calls this non-constexpr function which causes any compile-time -/// evaluation to fail (constexpr cannot invoke a non-constexpr function). At -/// runtime — should never be reached — the function calls sysio::check. -[[noreturn]] inline void codename_literal_failed(const char* msg) { - sysio::check(false, msg); - __builtin_unreachable(); -} - -/// Compile-time slug_name literal: `"ETH"_s`, `"USDC"_s`, `"PRIMARY"_s`. -/// Bad characters or >8 chars cause a compile error (the call to -/// `codename_literal_failed` is not constant-evaluable). -constexpr slug_name operator""_s(const char* s, std::size_t n) { - if (n > 8) { - codename_literal_failed("slug_name literal: max 8 characters"); - } - uint64_t out = 0; - for (std::size_t i = 0; i < n; ++i) { - const auto slot = slug_name::char_to_slot(s[i]); - if (slot == slug_name::INVALID_SLOT) { - codename_literal_failed("slug_name literal: invalid character (alphabet is [A-Z0-9_])"); - } - out |= (slot << (42 - i * 6)); - } - return slug_name{out}; -} - -} // namespace slug_name_literals - -using slug_name_literals::operator""_s; - -} // namespace sysio diff --git a/contracts/sysio.opp.common/include/sysio.opp.common/wire_asset.hpp b/contracts/sysio.opp.common/include/sysio.opp.common/wire_asset.hpp index 1bc0df39a6..93d3e52b3c 100644 --- a/contracts/sysio.opp.common/include/sysio.opp.common/wire_asset.hpp +++ b/contracts/sysio.opp.common/include/sysio.opp.common/wire_asset.hpp @@ -9,11 +9,12 @@ */ #include -#include +#include namespace sysio::opp::wire { -using sysio::slug_name_literals::operator""_s; +// `operator""_s` is declared at global scope by , so it +// needs no using-declaration here. /// Native WIRE token symbol and its system-wide nine-decimal precision. inline constexpr sysio::symbol asset_symbol{"WIRE", 9}; diff --git a/contracts/sysio.opreg/include/sysio.opreg/sysio.opreg.hpp b/contracts/sysio.opreg/include/sysio.opreg/sysio.opreg.hpp index 880e85c50e..020a4d5c02 100644 --- a/contracts/sysio.opreg/include/sysio.opreg/sysio.opreg.hpp +++ b/contracts/sysio.opreg/include/sysio.opreg/sysio.opreg.hpp @@ -7,7 +7,7 @@ #include #include #include -#include +#include #include #include #include diff --git a/contracts/sysio.opreg/src/sysio.opreg.cpp b/contracts/sysio.opreg/src/sysio.opreg.cpp index 7f11127446..490c4c30f9 100644 --- a/contracts/sysio.opreg/src/sysio.opreg.cpp +++ b/contracts/sysio.opreg/src/sysio.opreg.cpp @@ -3,7 +3,7 @@ #include #include #include -#include +#include #include #include #include diff --git a/contracts/sysio.opreg/sysio.opreg.abi b/contracts/sysio.opreg/sysio.opreg.abi index ab3bef11c9..11fbb230aa 100644 --- a/contracts/sysio.opreg/sysio.opreg.abi +++ b/contracts/sysio.opreg/sysio.opreg.abi @@ -609,16 +609,6 @@ } ] }, - { - "name": "slug_name", - "base": "", - "fields": [ - { - "name": "value", - "type": "uint64" - } - ] - }, { "name": "termcheck", "base": "", diff --git a/contracts/sysio.opreg/sysio.opreg.wasm b/contracts/sysio.opreg/sysio.opreg.wasm index 1cdf33ba6bad6c0b45cc8852d05d25b7f0f0cdcb..a6ceec00a21054ad09fd5faf92c6da98ae4df011 100755 GIT binary patch delta 3403 zcmds3YjjlA6~5oOck-G%m`5BhpKxre*3?dRijUR~<*EtH{^Al+6I3+;YC>89eT>u@EKdHAmcWZ0 z1$>rcRpBBvHV;Yw&crWq8CP%>*Ki#J{>{!a?`Q0Dc7c7tF0n7!Wfr$+r{k2F7q!yG z_AxOn4H^8M^dZ>C<1-c}9$*Fn@j=~|ZjyX`hC8r7thFU+ZJ86)S4+JoEDOTY7t~LI z^rhi`IZ5++#(v$|&J4v+12IAUAa5^xsu%SgdjGq1UcT5j8f*AL-z@x!4=T#XT61bq zqlOPf{V*h%nZ->iPVpl{hu~u#UABeVW*N23b7eJvW%|o!0w3{X!&h)$MI|imFCS=b zs2D;@^Vo0$xME)OuVmO}K37!*+~DmalJS*!cEoV71&n`ha6XS6HQHQTJ>stZk7>w! za+D`NZ81ww0*b+OIGw2q^BanXDc?y@3`OT=>w`~$En;Tb_njJkZB|dYqT&@^KJ7KE zZbm zmQKa6yDVs^p3sml7+Apc-USHT(JW>noK3|Y-`3&J5fYitO(o65% z#X)nDQw-IR4VoNrhEHeqGACW3O*7BTtdGL`eCg~Wd|)=u?gMP%?ZI+fGvn%B0h*{& z1d|_sY#z3oHFFwM@K;k?wl>xIiOQnLI5KSkPbkexZqW!bMgW4g4F=j|qeTnqZc#rJ zNxd~PrcD`ZTeDXQ%2^zp>9i~|FNN%pBSbq1gLpm#o?&g&GZ-Nzr52iz9YyERUa7^b zS&|NcW){@HK#8hTo8WDhdp7g?h7Rs4afuGiklWfLKK@Z>Uhf_d-SRdBOA7av8723w zP(w(9unfcAy~0rS{!UuGz`=WM`W&tN_ND;+{hRmWb?$7Kfj9V^hVf`KTNmAlllTe7h0@c64@! zDNVM+)Lsz6QKdWQs|12f1G=Pu-qVi>Q6e&KsG*iLew!w(ulqWf;=^Hx)+D47y6V84 zyMW4%5W@y4!V3aJ?*Yn&@BcOOXZ??nFL8eI>o`kFx{&qo zl@e$uJ_&i-3AQ`F>4Q-4+7zaF#NYjrwp66|!?`LPK`*hoYzI{wN5EIiUSND~Ly}7i z8k(Kz0@7H@tOoPQ+IYCZ&EN7DVwHF`6+74x`{^(VOX%6)7W?QGk-SvC2Iv~LBp!6g z1Ji=~v)j}0EwE)wtoI-V&xyheRN{H@lMF0j%R^exI}>~Gf;gUu(x?@TV*VGxWuOEv zSrZHdffucvSzy3Q(VC5cc-cCi4VCe#TYZJE1O?(k4qVy4Wdwz+$pIr+%xsvb3Iz2{ z@@8*SW3%w&BCX$VWS?F+>sc06a|2#QFH?FEsw~hesMpKUK0$qhsLO>XZewKetJb<) z++p~gSm!|+{vZ}SK(T$)10Sgz`B)bj`MuSaPl*5xLLUGZHVMxF%q6jA0Jcoo(tT!a znG$WE87lBSIiwESK;K{p>a5I;+*r$R#Y(yPQuJmirQ4{>rJ zn(&%q%=P~TC zemDf3aIfgea6{Ad_4X|F7v0&iKR<#g|euilnkoFA!q_du#w zpcRL#gB5rjI4t~rWZ@rTrXNM@2op74%HuV+yy9a&`muk834Sa@41a)h9bw&3L6k@p z>jUuM9kDln8oVovO4@CysI0^&ocyn~S6G!4GRMV@N=(6f;-MZ&mgA6U)RNY>D49T|ft;IueC7K?F4OdE$~_(Yr?hc!4W7JL`GaZZdF zkJs@pp-!Me^{FVHfNH8~^Cn0QD-5#u8rYtBT9Ra~?NPr?HXUDo3dQW4C# z9`+QaoQ>ou&H01wj2CflqqXZHEC*V}_!>gM76Zl68my*vuofdpG}U6KYjt;!^tn4Q ltwocOLoahxJXVdd>>1VCQH^p~gQj9FV+&R5;50lM_a8UEZ5#jq delta 3138 zcmd5;YjjlA6~5n@nJ{5y7B@DKC&|q)z@SW+ftZ9ClH33ZqVit61c9)y<+50{U8OGS zBhgZ&S_|X=2MAKM22w!;E^_4&g(4!477#1JgbE^+wm?zdFX=w_PB3fP{^-BXnsxU% zd!M`S-uv6%Ij6SSV}9&eU*UEgXU+UU_rWY?|JnyK0xo0*=*I$#WwG}1flS`d)06+m zv#8f?me>u74qwvO_y!ko372sN_nl^cW09}e8TNPf4|bNFW9M1>H#c~i&7KJhQm~7^ z*Lfs8@5y@J*v&NPenYios+2)!2s1?w8Tm}n;2WW6B-9V$3vEdYZ5cAuZBieO%bsx= zHPn;jMlX!di%FW_X6;jbdl-3h>0U$qh);}0YNJCskjkUp+gAN^wt(>oSuvgvi=mbm z#(IF8Rk5{h9ILJBWOmKp=t47Z>YIla9w=Hv<1IxrHqRH00oL=`CAsF$it~WacuVOk zJinwI>&@9ERirRmN;TkL=Iyct41X}^_3w|2YZw@CVxquyC z1v9r|vy?oy;-`Gx{;~^;c===RU@^b;Sa;0h+NANAZ$33?4#S1os%Tat7^?AX7e&9oW&~X!8J3by z2$DRjh$$1G@h7Uh)n{dtVMiY|^r)JqR}Yqvdao`QF|+AsuL%t;Li_u(Rc{KyDg;#-bn<1#;X zWEQ_OCB>Zf$qhHQ^U24iqLF`aY*x~HRCZ8}!?O2?8tlP!FB%ht&F4`hp+o3|H#*VnCos`_v1~g zArW_c{%dy;Sm_|p@*M&vS`lcG2&5&mpj|%M&iWY}h!ZvtnQ1MubS4Pe1I2_m6l8A$#aJ7PCdE+OV9_KUHYo%~pT3u%wX5|(-l6vEHxm9^!F0{>Q!+n4Ugi6C{4#u+!XWD@pRX(;F6p}HL>ncTh<+r ztUDl6Q*Vf927=XcLbS6DnxSj|g?7{_K_8lz+w2r+-&=ViW1ud{0N#4l@V?A8< zdkVd$;qJt1Xr3OfUXDp~IePs&f)cwac1h6xV}kYn9|GfpNoTOD_PL3xn&-ScEE1(NOV_M2(uS{j1{6w6l?H?I2c98lBMyudblPR zxV&~;v_sX)DC9eJEQfY*0*2CpFTRhp=5`Vy978B)m0ayM)YVeb+YM4uR(9@$pdd{p{L6~Du%8!S?HW7ue2E5s?Vmmtm# zL7KQ;gsCva#0RML-my1)MPk)JC#{R?H#>@9JrNNX9>6$kwFVdCeqfuZ=|`8n@|(%F zk(cJDTATVI2<#9?OEA+Bx_F_KQf(C5N~!PuNH-*rScd1sgT?S$i_4HL)3=R1|AP4)!wgHIYfH*e*vvAOEc^r~%bo|Mg{aLm|e;k(7 z7=w?+>&8pNLJB)MtMYrz?@I?UMvx&cP)QMcNR( z1um_%S8A-#Ak;G)75fI`VKiAO4&F&s;9QhYuf zyZm3!ZARX5Xjf?zJBQ*2V&@2~#3@lV620-ISTqt9xGGvkVqv?hpgy7F;>l4M(mJsY zj3TDvpH}T?^kcYc9U6-WLf^!p)MWpr>u%rU-=k-}l`|eM1DnNLk5Y+jkt_O4z# #include #include -#include +#include #include #include diff --git a/contracts/sysio.reserv/sysio.reserv.abi b/contracts/sysio.reserv/sysio.reserv.abi index dcda328aa3..1336ae2214 100644 --- a/contracts/sysio.reserv/sysio.reserv.abi +++ b/contracts/sysio.reserv/sysio.reserv.abi @@ -562,16 +562,6 @@ } ] }, - { - "name": "slug_name", - "base": "", - "fields": [ - { - "name": "value", - "type": "uint64" - } - ] - }, { "name": "swapquote", "base": "", diff --git a/contracts/sysio.reserv/sysio.reserv.wasm b/contracts/sysio.reserv/sysio.reserv.wasm index 300949cfbd67b09132b985574be367175515c095..233c77b8a46e80599405b09de482a880d114cd3e 100755 GIT binary patch delta 143 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 delta 6799 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 bTyZ9q*PKCoU3WpU&g0!@9yPZlO49!T2CVH= diff --git a/contracts/sysio.system/sysio.system.wasm b/contracts/sysio.system/sysio.system.wasm index f6399eed110447585a9a4c1a8a1bbfd81b5bf3fb..8e896406a3e0f7a305fa8ea6ddfcd3fca6e58435 100755 GIT binary patch delta 28926 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

i572`DuP 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 V$-DCyp0qo8j2D!%V`GQo{{fk@RfPZm delta 28507 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 #include #include -#include +#include #include namespace sysio { diff --git a/contracts/sysio.tokens/sysio.tokens.abi b/contracts/sysio.tokens/sysio.tokens.abi index 21eaef13ed..92d6fdb4ac 100644 --- a/contracts/sysio.tokens/sysio.tokens.abi +++ b/contracts/sysio.tokens/sysio.tokens.abi @@ -141,16 +141,6 @@ } ] }, - { - "name": "slug_name", - "base": "", - "fields": [ - { - "name": "value", - "type": "uint64" - } - ] - }, { "name": "token_key", "base": "", diff --git a/contracts/sysio.tokens/sysio.tokens.wasm b/contracts/sysio.tokens/sysio.tokens.wasm index ffb6e6168a5d1a7cca11227042b5c0c07d45ca48..4e1e2b7516fee5e50f30ef924f23b90f0cf24618 100755 GIT binary patch delta 147 zcmex(fwB1_H_wugu^mkgdd|!l1yQz$g$drNFRxMvxPuSTTlN r4M^^7a5B3>0frn%p8}&JM;6$G$yw2F6ml^Xfee5s0vlBj!^i^wX+U l!O84WSr`)G3XF~%Spwmc%c9>%rDI6fK%{FnH^ngW005ZB9$5eY diff --git a/contracts/sysio.uwrit/include/sysio.uwrit/sysio.uwrit.hpp b/contracts/sysio.uwrit/include/sysio.uwrit/sysio.uwrit.hpp index e8a489024e..f8b85cccae 100644 --- a/contracts/sysio.uwrit/include/sysio.uwrit/sysio.uwrit.hpp +++ b/contracts/sysio.uwrit/include/sysio.uwrit/sysio.uwrit.hpp @@ -8,7 +8,7 @@ #include #include #include -#include +#include #include namespace sysio { diff --git a/contracts/sysio.uwrit/src/sysio.uwrit.cpp b/contracts/sysio.uwrit/src/sysio.uwrit.cpp index 86c5290fbd..b18d569bad 100644 --- a/contracts/sysio.uwrit/src/sysio.uwrit.cpp +++ b/contracts/sysio.uwrit/src/sysio.uwrit.cpp @@ -4,7 +4,7 @@ #include #include #include -#include +#include #include #include #include diff --git a/contracts/sysio.uwrit/sysio.uwrit.abi b/contracts/sysio.uwrit/sysio.uwrit.abi index c9d953a937..857deac56a 100644 --- a/contracts/sysio.uwrit/sysio.uwrit.abi +++ b/contracts/sysio.uwrit/sysio.uwrit.abi @@ -354,16 +354,6 @@ } ] }, - { - "name": "slug_name", - "base": "", - "fields": [ - { - "name": "value", - "type": "uint64" - } - ] - }, { "name": "sumlocks", "base": "", diff --git a/contracts/sysio.uwrit/sysio.uwrit.wasm b/contracts/sysio.uwrit/sysio.uwrit.wasm index 1e7e3ce5ca307e4326f4227cd3f419e4353aea65..608270bb6fce7f804b0c4a1534a08837e5b33fce 100755 GIT binary patch delta 18657 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 delta 17785 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 diff --git a/contracts/tests/safe_ops_tests.cpp b/contracts/tests/safe_ops_tests.cpp index 0719f5898a..629c178176 100644 --- a/contracts/tests/safe_ops_tests.cpp +++ b/contracts/tests/safe_ops_tests.cpp @@ -5,11 +5,11 @@ * * These helpers are pure C++ (no contract intrinsics), so they are exercised * directly on the host. Coverage: - * - `is_valid_name_string` accepts the FULL CDT name domain — including a - * legitimate 13-byte name whose final symbol fits the 4-bit final slot - * ('.'/'1'-'5'/'a'-'j') — and rejects every string `name()` would abort on - * (length > 13, an out-of-alphabet character, or a 13th symbol > 15). This - * pins the parse_wire_name fix (PR #417, r3444212148): the old `size() > 12` + * - name-string validation now lives on the type itself + * (`name::is_valid_literal`, covered by name_tests); the hand-rolled mirror + * that used to be pinned here is gone. The domain it guarded — a legitimate + * 13-byte name whose final symbol fits the 4-bit final slot — is pinned in + * name_tests::literal_and_runtime_validation_agree. Historical note: the old * cap wrongly rejected every 13-character WIRE depositor/recipient. * - `add_sat_u64` clamps at UINT64_MAX instead of wrapping (r3444213199). */ @@ -23,46 +23,10 @@ using sysio::opp::safe::add_sat_i64; using sysio::opp::safe::add_sat_u64; using sysio::opp::safe::depot_amount_max; -using sysio::opp::safe::is_valid_name_string; using sysio::opp::safe::to_depot_amount; BOOST_AUTO_TEST_SUITE(safe_ops_tests) -BOOST_AUTO_TEST_CASE(is_valid_name_string_accepts_full_cdt_domain) { - // Empty + short names within the alphabet. - BOOST_CHECK(is_valid_name_string("")); - BOOST_CHECK(is_valid_name_string("a")); - BOOST_CHECK(is_valid_name_string("uwrit.alice")); // 11, with a dot - BOOST_CHECK(is_valid_name_string("aaaaaaaaaaaa")); // 12 chars - - // The regressed case: a legitimate 13-character name whose final symbol fits - // the 4-bit final slot. 'a' == 6 and 'j' == 15 are both <= 15. - BOOST_CHECK(is_valid_name_string("aaaaaaaaaaaaa")); // 13 chars, final 'a' (6) - BOOST_CHECK(is_valid_name_string("aaaaaaaaaaaaj")); // 13 chars, final 'j' (15) - BOOST_CHECK(is_valid_name_string("abcdefghij.15")); // 13 chars, mixed, final '5' - - // Full alphabet members are accepted in non-final positions. - BOOST_CHECK(is_valid_name_string(".12345")); - BOOST_CHECK(is_valid_name_string("zzzzzzzzzzzz")); // 12 'z' (z == 31, fine pre-final) -} - -BOOST_AUTO_TEST_CASE(is_valid_name_string_rejects_abortable_strings) { - // A 13th symbol that exceeds the 4-bit final slot (value > 15) — name() aborts. - // 'k' == 16, 'z' == 31. - BOOST_CHECK(!is_valid_name_string("aaaaaaaaaaaak")); // final 'k' (16) - BOOST_CHECK(!is_valid_name_string("wirerecipient")); // 13 chars, final 't' (25) - BOOST_CHECK(!is_valid_name_string("aaaaaaaaaaaaz")); // final 'z' (31) - - // Longer than 13 characters. - BOOST_CHECK(!is_valid_name_string("aaaaaaaaaaaaaa")); // 14 chars - - // Characters outside ".12345abcdefghijklmnopqrstuvwxyz". - BOOST_CHECK(!is_valid_name_string("Alice")); // uppercase - BOOST_CHECK(!is_valid_name_string("a6")); // '6'..'9'/'0' not in alphabet - BOOST_CHECK(!is_valid_name_string("a-b")); // '-' - BOOST_CHECK(!is_valid_name_string("hello world")); // space -} - BOOST_AUTO_TEST_CASE(add_sat_u64_saturates_instead_of_wrapping) { constexpr uint64_t MAX = ~uint64_t{0}; diff --git a/contracts/tests/sysio.dispatch_tests.cpp b/contracts/tests/sysio.dispatch_tests.cpp index cf53924880..e345b95771 100644 --- a/contracts/tests/sysio.dispatch_tests.cpp +++ b/contracts/tests/sysio.dispatch_tests.cpp @@ -1421,6 +1421,49 @@ BOOST_FIXTURE_TEST_CASE(dispatch_routes_deposit_to_opreg, sysio_dispatch_tester) bal["balance"].as_uint64()); } FC_LOG_AND_RETHROW() } +// A payload `token_code` with no canonical spelling is DROPPED, never stored. +// +// `chain_code` is proven -- source_chain_binding_ok binds it to the delivering +// outpost -- but `token_code` rides the FORGEABLE payload and reaches slug_name +// through the non-validating raw constructor. Persisting one would make every +// later render of that balance row throw; under `values_only` the underwriter's +// unconditional `row.get_object()` then drops the ENTIRE scan cycle rather than +// one cell. So the dispatcher drops the attestation -- a check() here would halt +// evalcons and stall consensus (feedback_opp_handlers_never_throw). +BOOST_FIXTURE_TEST_CASE(dispatch_drops_uncanonical_token_code, sysio_dispatch_tester) { try { + bootstrap_for_dispatch(); + + const auto eth_code = fc::slug_name{"ETH"}.value; + // Any value below the leading symbol's floor decodes to "" and packs back to + // 0, so it is not a code and has no spelling. + constexpr uint64_t uncanonical_token = 7; + BOOST_REQUIRE(!fc::slug_name{uncanonical_token}.is_canonical()); + + const auto before = get_operator(UWRIT_OP); + const size_t balances_before = before.is_null() ? 0 : before["balances"].get_array().size(); + + auto payload = encode_operator_action( + sysio::opp::attestations::OperatorAction::ACTION_TYPE_DEPOSIT_REQUEST, + sysio::opp::types::CHAIN_KIND_EVM, + uwrit_op_eth_pubkey, + /*chain_code_v=*/ eth_code, + /*token_code_v=*/ uncanonical_token, + 1'000'000); + + auto envelope = encode_envelope_with_one_attestation( + current_epoch(), + sysio::opp::types::ATTESTATION_TYPE_OPERATOR_ACTION, + payload); + + // The delivery SUCCEEDS -- the attestation is dropped inside dispatch, not reverted. + BOOST_REQUIRE_EQUAL(success(), deliver(/*chain_code=*/eth_code, envelope)); + + // ...and nothing was persisted, so no stored row can later fail to render. + const auto after = get_operator(UWRIT_OP); + const size_t balances_after = after.is_null() ? 0 : after["balances"].get_array().size(); + BOOST_CHECK_EQUAL(balances_before, balances_after); +} FC_LOG_AND_RETHROW() } + BOOST_FIXTURE_TEST_CASE(dispatch_routes_withdraw_request_to_opreg, sysio_dispatch_tester) { try { bootstrap_for_dispatch(); diff --git a/contracts/tools/generate-sysio-contract-types.py b/contracts/tools/generate-sysio-contract-types.py index e6129095aa..80c49adf48 100755 --- a/contracts/tools/generate-sysio-contract-types.py +++ b/contracts/tools/generate-sysio-contract-types.py @@ -115,7 +115,11 @@ def emit_contract_interface(abi: dict, contract_name: str, contract_prefix: str) # entry must precede the structs lookup: every registry ABI still ships a # `slug_name` struct_def, and without this the field would resolve to that # `{value: uint64}` shape (or, once abigen stops emitting it, to `unknown`). - 'slug_name': {'type': 'string', 'pattern': '^[A-Z0-9_]{0,8}$'}, + # A code must START with a letter, so the pattern is NOT [A-Z0-9_]{0,8}: that + # would accept "7" and "_LEAD", which fc::slug_name now rejects, leaving the + # generated schema disagreeing with the ABI serializer. The outer group is + # optional so the empty string — the zero sentinel — still validates. + 'slug_name': {'type': 'string', 'pattern': '^(?:[A-Z][A-Z0-9_]{0,7})?$'}, 'string': {'type': 'string'}, 'bytes': {'type': 'string', 'description': 'hex-encoded bytes'}, 'checksum256': {'type': 'string', 'pattern': '^[a-f0-9]{64}$'}, diff --git a/libraries/chain/include/sysio/chain/name.hpp b/libraries/chain/include/sysio/chain/name.hpp index 826f8fa6eb..cdf9fd35c0 100644 --- a/libraries/chain/include/sysio/chain/name.hpp +++ b/libraries/chain/include/sysio/chain/name.hpp @@ -32,6 +32,18 @@ namespace sysio::chain { // Pinned in name_tests.cpp::encoding_golden_values. static constexpr fc::basic_name_endianness packing = fc::basic_name_endianness::MSB; + // The rejection messages. bad_final_symbol_message restates the classic + // SYSIO rule: 13 symbols x 5 bits exceeds 64, so the thirteenth slot holds + // only 4 bits — symbols 0-15, i.e. '.', '1'-'5', 'a'-'j'. + static constexpr const char* bad_char_message = + "character is not in allowed character set for names ([.1-5a-z])"; + static constexpr const char* too_long_message = "string is too long to be a valid name"; + static constexpr const char* bad_final_symbol_message = + "thirteenth character in name cannot be a letter that comes after j"; + // Spring's wording: a legal character in an illegal position (a trailing + // pad) leaves a spelling that does not round-trip through to_string(). + static constexpr const char* not_normalized_message = "name is not properly normalized"; + // Declared here, defined in name.cpp — keeps // and the SYS_ASSERT machinery out of this very widely-included header. [[noreturn]] static void throw_invalid( std::string_view in, const char* why ); diff --git a/libraries/libfc/include/fc/basic_name.hpp b/libraries/libfc/include/fc/basic_name.hpp index 73f8b7d9ca..87a062c853 100644 --- a/libraries/libfc/include/fc/basic_name.hpp +++ b/libraries/libfc/include/fc/basic_name.hpp @@ -60,6 +60,10 @@ concept basic_name_traits = { Traits::zero_terminates } -> std::convertible_to; { Traits::packing } -> std::convertible_to; { Traits::throw_invalid(in, why) } -> std::same_as; + { Traits::bad_char_message } -> std::convertible_to; + { Traits::too_long_message } -> std::convertible_to; + { Traits::bad_final_symbol_message } -> std::convertible_to; + { Traits::not_normalized_message } -> std::convertible_to; } && Traits::max_len > 0 && std::string_view{ Traits::alphabet }.size() > 0; @@ -70,7 +74,8 @@ concept basic_name_traits = /// confused with a decimal number — see `slug_name_traits::leading_alphabet`. template concept basic_name_has_leading_alphabet = requires { - { Traits::leading_alphabet } -> std::convertible_to; + { Traits::leading_alphabet } -> std::convertible_to; + { Traits::bad_leading_char_message } -> std::convertible_to; }; template @@ -80,15 +85,20 @@ struct basic_name { constexpr basic_name() = default; constexpr explicit basic_name(uint64_t v) : value(v) {} - /// Construct from a string: checks length, then requires the input to be the - /// canonical spelling of its own encoding (round-trip check). Throws via - /// Traits::throw_invalid on bad or non-canonical input. - explicit basic_name(std::string_view str) : value(encode(str)) {} - - constexpr uint64_t to_uint64_t() const { return value; } - constexpr bool empty() const { return value == 0; } - constexpr bool good() const { return value != 0; } - constexpr explicit operator bool() const { return value != 0; } + /// Construct from a string. Rejects via Traits::throw_invalid unless the + /// input is the canonical spelling of its own encoding — see + /// validity_error(), which is the SINGLE validation algorithm this type + /// has, shared with is_valid_literal() and byte-for-byte the same rules as + /// the contract-side sysio::basic_name. + /// + /// constexpr: Traits::throw_invalid is not constexpr, so it is reached only + /// on the failure path. A valid literal therefore constant-evaluates, and an + /// invalid one is a compile error rather than a silent mis-encoding. + constexpr explicit basic_name(std::string_view str) : value(0) { + if (const char* why = validity_error(str)) + Traits::throw_invalid(str, why); + value = pack(str); + } /// Non-validating encode — the constexpr path used by literals and by /// string_to_name. Characters outside the alphabet pack as symbol 0. @@ -100,31 +110,86 @@ struct basic_name { return v; } - /// Compile-time literal check: length within bounds and every character in - /// the alphabet. For zero_terminates traits the pad symbol (alphabet[0]) is - /// additionally rejected; accepting it would let a literal like "A\0B"_s - /// compile to the same packed value as "A"_s, while the runtime constructor - /// fed the same bytes would throw on the canonical round-trip check. The - /// literal path bypasses that constructor, so the check has to live here. - /// Canonicality (trailing pads, an over-wide final symbol) is still left to - /// the validating constructor's round-trip check. + /// Is `str` a valid, canonical spelling? The literal path's gate — and the + /// predicate a caller uses to ask whether a raw packed value has a spelling + /// at all. Delegates to validity_error so the literal path and the throwing + /// constructor can never disagree. static constexpr bool is_valid_literal(std::string_view str) { + return validity_error(str) == nullptr; + } + + /// THE validation algorithm. Returns nullptr when `str` is a valid, canonical + /// spelling; otherwise the traits' message for the FIRST rule it breaks. + /// Identical, rule for rule and in the same order, to the contract-side + /// sysio::basic_name — the two are meant to be diffable. + /// + /// Rules 4-6 make pack() lossless, which is what lets the constructor drop + /// the old `to_string() != str` round-trip: given 1-6, to_string(pack(str)) + /// IS str, so the round trip can be a test assertion instead of a runtime + /// one. Rules 5 and 6 were previously enforced ONLY by that round-trip and + /// were absent from is_valid_literal, so the literal path accepted spellings + /// the constructor rejected — `"abcdefghijklm"_n` compiled and packed as + /// `abcdefghijkl2` while `name{"abcdefghijklm"}` threw. + static constexpr const char* validity_error(std::string_view str) { + // 1. length if (str.size() > static_cast(Traits::max_len)) - return false; + return Traits::too_long_message; + + // 2. leading symbol, for traits that restrict it if constexpr (basic_name_has_leading_alphabet) { if (!str.empty() && std::string_view{ Traits::leading_alphabet }.find(str[0]) == std::string_view::npos) - return false; + return Traits::bad_leading_char_message; } - for (char c : str) { - if (Traits::alphabet.find(c) == std::string_view::npos) - return false; + + for (std::size_t i = 0; i < str.size(); ++i) { + const std::size_t sym = Traits::alphabet.find(str[i]); + + // 3. in the alphabet + if (sym == std::string_view::npos) + return Traits::bad_char_message; + + // 4. a zero-terminated alphabet has no INTERIOR pad: to_string() stops + // at the first symbol-0 slot, so such a spelling cannot round-trip. if constexpr (Traits::zero_terminates) { - if (c == Traits::alphabet[0]) return false; + if (sym == 0) + return Traits::bad_char_message; } + + // 5. the final slot may be narrower than `bits` (13 x 5 > 64 for name, + // leaving 4 bits), and pack() would silently truncate a symbol too + // wide for it. + if (static_cast(sym) > width_mask(static_cast(i))) + return Traits::bad_final_symbol_message; + } + + // 6. a non-zero-terminated alphabet strips TRAILING pads in to_string(), + // so a trailing pad cannot round-trip either. + if constexpr (!Traits::zero_terminates) { + if (!str.empty() && str.back() == Traits::alphabet[0]) + return Traits::not_normalized_message; } - return true; + + return nullptr; + } + + constexpr uint64_t to_uint64_t() const { return value; } + constexpr bool empty() const { return value == 0; } + constexpr bool good() const { return value != 0; } + constexpr explicit operator bool() const { return value != 0; } + + + /// Does this value have a canonical spelling? A basic_name built from a RAW + /// uint64 bypasses the validating constructor, so it can hold a value no + /// spelling produces — for zero_terminates traits, anything whose leading + /// symbol slot is empty. Such a value cannot round-trip: to_string() yields a + /// text that packs to something else. Persisting one makes every later render + /// of that row throw, so writers that accept a raw uint64 off the wire gate on + /// this before storing it. + bool is_canonical() const { + const std::string text = to_string(); + return is_valid_literal(text) && pack(text) == value; } std::string to_string() const { @@ -209,22 +274,6 @@ struct basic_name { return (static_cast(1) << w) - 1; } - /// Validating encode — mirrors sysio::chain::name::set(): length check, then - /// require the string to round-trip (rejects non-canonical input). - static uint64_t encode(std::string_view str) { - if (static_cast(str.size()) > Traits::max_len) - Traits::throw_invalid(str, "too long"); - if constexpr (basic_name_has_leading_alphabet) { - if (!str.empty() - && std::string_view{ Traits::leading_alphabet }.find(str[0]) - == std::string_view::npos) - Traits::throw_invalid(str, "first character is not allowed to lead"); - } - const basic_name packed{ pack(str) }; - if (packed.to_string() != str) - Traits::throw_invalid(str, "not properly normalized"); - return packed.value; - } }; /// fmtlib hook — `format_as` is found by ADL for basic_name and its derivations. diff --git a/libraries/libfc/include/fc/slug_name.hpp b/libraries/libfc/include/fc/slug_name.hpp index b4c13eb752..5cc1b102a9 100644 --- a/libraries/libfc/include/fc/slug_name.hpp +++ b/libraries/libfc/include/fc/slug_name.hpp @@ -61,6 +61,19 @@ struct slug_name_traits { // [0..5]. Byte-identical with the contract-side sysio::slug_name. static constexpr basic_name_endianness packing = basic_name_endianness::MSB; + // The rejection messages. Identical to the contract-side + // sysio::slug_name_traits so a failure reads the same on both sides of the + // wire; the host additionally names the offending input via throw_invalid. + static constexpr const char* bad_char_message = + "character is not in allowed character set for slug_names ([A-Z0-9_])"; + static constexpr const char* too_long_message = "string is too long to be a valid slug_name"; + static constexpr const char* bad_leading_char_message = + "slug_name must start with a letter ([A-Z])"; + static constexpr const char* bad_final_symbol_message = + "final character in slug_name does not fit its packed slot"; + static constexpr const char* not_normalized_message = + "slug_name is not properly normalized"; + [[noreturn]] static void throw_invalid( std::string_view in, const char* why ) { FC_ASSERT( false, "invalid slug_name '{}': {}", std::string(in), why ); __builtin_unreachable(); @@ -111,16 +124,14 @@ using slug_name_literals::operator""_s; /// /reserv writers). Until that lands, a stored one is a defect that surfaces /// here rather than being silently rendered as something it is not. inline void to_variant(const slug_name& s, fc::variant& v) { - const std::string text = s.to_string(); - // Two checks, because a round trip alone is not enough. `is_valid_literal` - // is the static spelling predicate (length, alphabet, pad, leading letter); - // `pack` is the NON-validating encoder, so comparing it to `value` is the - // canonicality test. A value packed from an illegal spelling — say - // `pack("0")` — round-trips through `pack`/`to_string` yet is not a code, so - // emitting it would produce a string `from_variant` then refuses. - FC_ASSERT(slug_name::is_valid_literal(text) && slug_name::pack(text) == s.value, + // is_canonical() is the shared predicate: the spelling must be a valid + // literal AND pack back to this exact value. A round trip alone is not + // enough — a value packed from an illegal spelling (say pack("0")) round- + // trips yet is not a code, so emitting it would produce a string + // from_variant then refuses. + FC_ASSERT(s.is_canonical(), "slug_name {} is not a code and has no string spelling", s.value); - v = text; + v = s.to_string(); } namespace detail { diff --git a/libraries/libfc/test/test_slug_name.cpp b/libraries/libfc/test/test_slug_name.cpp index 5eb78fa5f5..113ae2dbfc 100644 --- a/libraries/libfc/test/test_slug_name.cpp +++ b/libraries/libfc/test/test_slug_name.cpp @@ -30,6 +30,13 @@ struct slug_name_lsb_traits { static constexpr bool zero_terminates = true; static constexpr fc::basic_name_endianness packing = fc::basic_name_endianness::LSB; + static constexpr const char* bad_char_message = + "character is not in allowed character set for slug_names ([A-Z0-9_])"; + static constexpr const char* too_long_message = "string is too long to be a valid slug_name"; + static constexpr const char* bad_final_symbol_message = + "final character in slug_name does not fit its packed slot"; + static constexpr const char* not_normalized_message = "slug_name is not properly normalized"; + [[noreturn]] static void throw_invalid( std::string_view in, const char* why ) { FC_ASSERT( false, "invalid slug_name_lsb '{}': {}", std::string(in), why ); __builtin_unreachable(); @@ -341,6 +348,12 @@ struct name_like_traits { static constexpr std::string_view alphabet = ".12345abcdefghijklmnopqrstuvwxyz"; static constexpr bool zero_terminates = false; static constexpr fc::basic_name_endianness packing = fc::basic_name_endianness::MSB; + static constexpr const char* bad_char_message = + "character is not in allowed character set for names ([.1-5a-z])"; + static constexpr const char* too_long_message = "string is too long to be a valid name"; + static constexpr const char* bad_final_symbol_message = + "thirteenth character in name cannot be a letter that comes after j"; + static constexpr const char* not_normalized_message = "name is not properly normalized"; [[noreturn]] static void throw_invalid( std::string_view in, const char* why ) { FC_ASSERT( false, "invalid name '{}': {}", std::string(in), why ); __builtin_unreachable(); diff --git a/unittests/name_tests.cpp b/unittests/name_tests.cpp index 73a3c1dd73..e79aa46b38 100644 --- a/unittests/name_tests.cpp +++ b/unittests/name_tests.cpp @@ -281,20 +281,65 @@ BOOST_AUTO_TEST_CASE(name_suffix_additional_tests) { BOOST_CHECK_EQUAL( name{"sys.ioa.cco"}.suffix(), name{"cco"} ); } +// The literal path and the runtime constructor share ONE algorithm +// (fc::basic_name::validity_error), so they must agree on every input. Before +// they were unified they did not: "abcdefghijklm"_n compiled and packed as +// "abcdefghijkl2" while name{"abcdefghijklm"} threw, and "sysio."_n compiled as +// "sysio" while name{"sysio."} threw. This pins the agreement. +BOOST_AUTO_TEST_CASE(literal_and_runtime_validation_agree) +{ + // Rejected by BOTH paths. (The literal rejection is a compile error, so it + // cannot be asserted here — these pin the predicate the literal's + // static_assert uses, alongside the runtime throw.) + for (std::string_view bad : { "abcdefghijklm", // 13th symbol past 'j' + "abcdefghijklmn", // too long + "sysio.", // trailing pad + ".", // pad only + "SYSIO", // out of alphabet + "sysio6", // '6' is not in .12345a-z + "a-b", // punctuation + "hello world", // space + "aaaaaaaaaaaak" }) { // 13th symbol 'k' (16) > 15 + BOOST_CHECK_MESSAGE(!name::is_valid_literal(bad), + std::string{"should not be a valid literal: "} + std::string{bad}); + BOOST_CHECK_THROW(name{bad}, name_type_exception); + } + + // Accepted by BOTH paths, and pack() is lossless for every one of them — + // which is what lets the constructor drop its round-trip check. + for (std::string_view good : { "", "e", "sysio", "abc.xyz", "abc.xyz.qrt", + "abcdefghijkl", "abcdefghijklj", ".sysio", + "uwrit.alice", "zzzzzzzzzzzz", ".12345", + "abcdefghij.15" }) { + BOOST_CHECK_MESSAGE(name::is_valid_literal(good), + std::string{"should be a valid literal: "} + std::string{good}); + BOOST_CHECK_NO_THROW(name{good}); + BOOST_CHECK_EQUAL(name{good}.to_string(), std::string{good}); + } + + // The spellings that collapse still pack the way they always did — the + // encoding is unchanged, only the spellings we accept are. + BOOST_CHECK_EQUAL(name{name::pack("sysio.")}, "sysio"_n); + BOOST_CHECK_EQUAL(name{name::pack(".")}, ""_n); + BOOST_CHECK_EQUAL(name{name::pack("abcdefghijklm")}, "abcdefghijkl2"_n); +} + BOOST_AUTO_TEST_CASE(name_prefix_tests) { BOOST_CHECK_EQUAL("e"_n.prefix(), "e"_n); BOOST_CHECK_EQUAL(""_n.prefix(), ""_n); - BOOST_CHECK_EQUAL("abcdefghijklm"_n.prefix(), "abcdefghijklm"_n); + BOOST_CHECK_EQUAL("abcdefghijklj"_n.prefix(), "abcdefghijklj"_n); BOOST_CHECK_EQUAL("abcdefghijkl"_n.prefix(), "abcdefghijkl"_n); BOOST_CHECK_EQUAL("abc.xyz"_n.prefix(), "abc"_n); BOOST_CHECK_EQUAL("abc.xyz.qrt"_n.prefix(), "abc.xyz"_n); - BOOST_CHECK_EQUAL("."_n.prefix(), ""_n); + // "." is not a valid literal: it spells the pad symbol, which packs to 0. + // Construct it from the packed value so the collapse is stated, not implied. + BOOST_CHECK_EQUAL(name{name::pack(".")}.prefix(), ""_n); BOOST_CHECK_EQUAL("sysio.any"_n.prefix(), "sysio"_n); BOOST_CHECK_EQUAL("sysio"_n.prefix(), "sysio"_n); BOOST_CHECK_EQUAL("sysio"_n.prefix(), config::system_account_name); - BOOST_CHECK_EQUAL("sysio."_n.prefix(), "sysio"_n); + BOOST_CHECK_EQUAL(name{name::pack("sysio.")}.prefix(), "sysio"_n); BOOST_CHECK_EQUAL("sysio.evm"_n.prefix(), "sysio"_n); BOOST_CHECK_EQUAL(".sysio"_n.prefix(), ""_n); BOOST_CHECK_NE("sysi"_n.prefix(), "sysio"_n); diff --git a/unittests/test_contract_action_match.cpp b/unittests/test_contract_action_match.cpp index b9bbf1a1aa..f3ccca3513 100644 --- a/unittests/test_contract_action_match.cpp +++ b/unittests/test_contract_action_match.cpp @@ -15,8 +15,11 @@ BOOST_AUTO_TEST_CASE(exact_match) { contract_action_match matcher("s"_n, "test"_n, contract_action_match::match_type::exact); BOOST_CHECK(matcher.is_contract_match("test"_n)); - BOOST_CHECK_EQUAL("test."_n, "test"_n); // any '.' at the end of a name is really a non-character - BOOST_CHECK(matcher.is_contract_match("test."_n)); // which is why this also passes + // A trailing '.' is a pad symbol, so "test." PACKS to "test" - but it is not + // a valid spelling, so the literal is rejected at compile time. Build it from + // the packed value to pin the collapse without a literal that lies. + BOOST_CHECK_EQUAL(name{name::pack("test.")}, "test"_n); + BOOST_CHECK(matcher.is_contract_match(name{name::pack("test.")})); BOOST_CHECK(!matcher.is_contract_match(""_n)); BOOST_CHECK(!matcher.is_contract_match("est"_n)); @@ -37,8 +40,9 @@ BOOST_AUTO_TEST_CASE(suffix_match) { BOOST_CHECK(matcher.is_contract_match("fun.test"_n)); BOOST_CHECK(matcher.is_contract_match("fun.fun.test"_n)); BOOST_CHECK(matcher.is_contract_match("fun...test"_n)); - BOOST_CHECK_EQUAL("test."_n, "test"_n); // any '.' at the end of a name is really a non-character - BOOST_CHECK(matcher.is_contract_match("test."_n)); // which is why this also passes + // See exact_match: "test." packs to "test" but is not a valid spelling. + BOOST_CHECK_EQUAL(name{name::pack("test.")}, "test"_n); + BOOST_CHECK(matcher.is_contract_match(name{name::pack("test.")})); BOOST_CHECK(!matcher.is_contract_match(""_n)); BOOST_CHECK(!matcher.is_contract_match("est"_n)); @@ -51,7 +55,7 @@ BOOST_AUTO_TEST_CASE(prefix_match) { contract_action_match matcher("s"_n, "test"_n, contract_action_match::match_type::prefix); BOOST_CHECK(matcher.is_contract_match("test"_n)); - BOOST_CHECK(matcher.is_contract_match("test."_n)); + BOOST_CHECK(matcher.is_contract_match(name{name::pack("test.")})); BOOST_CHECK(matcher.is_contract_match("test.fun"_n)); BOOST_CHECK(matcher.is_contract_match("test...fun"_n)); //passes because "test.."_n is just "test"_n From 5ebfd5e8a0a8705eef7504cec62e94f716af891a Mon Sep 17 00:00:00 2001 From: kevin Heifner Date: Mon, 21 Sep 2026 08:02:51 -0500 Subject: [PATCH 09/29] chore(contracts): rebuild sysio.epoch against wire-cdt #119 Artifact-only. sysio.epoch has no source change in this branch; its wasm moves because the contracts now build against the CDT carrying the shared basic_name, reached via . Separated from the source commit so the toolchain delta is visible rather than folded into an unrelated diff. Change-Id: Ia9e62c39869993c53299d7287ca54fd08e77d6bb --- contracts/sysio.epoch/sysio.epoch.wasm | Bin 79824 -> 79839 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/contracts/sysio.epoch/sysio.epoch.wasm b/contracts/sysio.epoch/sysio.epoch.wasm index cac4b434e0091a4d77bf83b2648c6bef1b696782..3583ddc36b03401edcaecef5ff887efea340647a 100755 GIT binary patch delta 105 zcmcccp5^{~mJPR97&mUd%~JPCT%AFIL4i@=HKP=WcI3!{FgIs?*JhXKWn^TmS7vY& Z$W~%fK~lJR)xY&TxU_E1;$}?K0RVN_8#Vv{ delta 76 zcmccrp5?-OmJPR97#D56%~JPCP?14_L4i@=HKPKfBS#jH-dylqo1LkPak{)TqvYnD Q|5ox~s@Y!2&6uVG02E{w_y7O^ From 6f148db8c8ff8e53e44a1531671c8e63d350ceb9 Mon Sep 17 00:00:00 2001 From: kevin Heifner Date: Mon, 21 Sep 2026 10:58:29 -0500 Subject: [PATCH 10/29] chore(contracts): rebuild reenter_deposit against wire-cdt #119 No source change here: it includes , which #119 moves onto the shared basic_name. Change-Id: Ie441e50995c45d7d7d2b706347e442f66ad38b72 --- .../reenter_deposit/reenter_deposit.wasm | Bin 4870 -> 5525 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/contracts/test_contracts/reenter_deposit/reenter_deposit.wasm b/contracts/test_contracts/reenter_deposit/reenter_deposit.wasm index c74bc39e3b39dc20feea7155768fa7d9c36cb1dd..a8d27f48b70e3bced3ff2e595ca5873ea0dc8933 100755 GIT binary patch delta 1460 zcmY*Z-)~e!6rM9bmfg<1?A@Xjy3o$O&~{5B!Pt~Uh;){88!$0Es4>xy*4QYwE$p^N z3u158N_g;rGSL`~sL#rS@j(-Lp!CJW|3DKY@qq_Q(#Iy1;5XY+vYXvGGiS~@bIy0Z zvzL3n7|?8dcA6MtbUawn&xwrYmtMGds;`%+9KK5CM2>a{A@cZ9j#PmJVe3C_T1!QB zB2t1F%YU_=J4e>SCpblDhS7@}f3M1onkjEj{-CgRX|J)9KFS~|ZGQ%w!jRha(!w4=K zBOx;!i$6Zgm?8U!8lzCMqK0jjCUVOG=14?s`AF}mwmoVghxL4mHE*@fTFeE^KWZ@- zt>!J(5|7I?MymD_z-VR^2yJA{jfz>opKNJ+t5K79Qd-thTN40o-SVQi^qrrO{*UyPKC|+n0B7`IP%XT6)_3 zE2pUFPOCoZaC7QwddBU_&C~8*uH^)$o$ky0tF*_h<_}>l7Y4A77Tzgz8fE`Lkh1%= z@RhD~8Zx}XgQ(=L7rWeC+w)^*IDzB)nKhFMKLAqr^w0%E{hTG@h}exiL>^Fcn@^*C z2?23%kRk?9{GB(4hzvGSSMzL5>VxSw-Laso!11Wt)q{ftnF2vXG48(ZqP?yyctaJddVHFe5}ed@`P%zK^j0&XNy{VY|2tOZ&}j zbj8RN-LWPL6EzYF-~ja`$eqi zag#1Q#jN<4hiV{#g&u6sH%WQ`{U62V#Q7x2(#s)`jD08+3__3%unA66hDJsG z?yb^bxn1jkMM&Q>F*>OAL(W2;}8Q*=+r{;S;y)*tJiJ2Jm>zzomAc7F04007r23jFO;kYn~ zR>>iVYTG7=7A@ME8v_e!SM)>CqUZ<2gl{daixnjQp#F>(M6QVL3(9#x#f?|*)OZ060jSqByr$WPwuB_&w$RV z^C%NBV!#Ird1cx5h*yyTtLH+DLov)>|IimvC=J9mAKRJP5QfEzhzo_)7Y!Uv%Q?oF z^XUeT6`paG*-WyqkNqq9f?fEQ^5QdH&wRrcnscbBST-_(9dgD0a{X){=la-w%ALtJ8^=Cn zsxlo@w^V1@pbDf?paJhwHn)GuHBf5nWzq77d=CLv{Gu6hr zO252TtWdLmQni)&JeDrQ(s5$eq=`E20P^!cu$6x2hcHhAx{7z{h+fBbDi@A6R{s;R z)3FZNwZuB4g+ecO(EUQCw#uvCJn4C1FGF6jlj{eIZ`jt=PvIKP)F01&h}3TGR(H|M s`k_ieDG#*E?eH#jw^Hwf0K2dPYMo5!D!nf4r+cL#+TAdbnF#>?0?VkM!~g&Q From 444962ccca03e23a09fab13b8be0a1b90f51a3bf Mon Sep 17 00:00:00 2001 From: kevin Heifner Date: Mon, 21 Sep 2026 12:31:30 -0500 Subject: [PATCH 11/29] fix(contracts): validate slug_name codes at the writers that persist them A code with no canonical spelling can be written but not read back: to_variant asserts is_canonical(), so the stored row throws on every later render. Guard where the state originates -- the five privileged registry writers check(), while createuwreq reverts, since dropping a SwapRequest would strand the user's escrowed deposit. Change-Id: I0e527bb29afb129d3e755bba8dff6ffd0eccc42e --- contracts/sysio.chains/src/sysio.chains.cpp | 4 ++ contracts/sysio.chains/sysio.chains.wasm | Bin 35745 -> 39121 bytes .../sysio.opp.common/registry_codes.hpp | 62 ++++++++++++++++++ contracts/sysio.opreg/src/sysio.opreg.cpp | 17 +++++ contracts/sysio.opreg/sysio.opreg.wasm | Bin 93586 -> 96991 bytes contracts/sysio.reserv/src/sysio.reserv.cpp | 4 ++ contracts/sysio.reserv/sysio.reserv.wasm | Bin 85221 -> 88217 bytes contracts/sysio.tokens/src/sysio.tokens.cpp | 8 +++ contracts/sysio.tokens/sysio.tokens.wasm | Bin 26755 -> 30245 bytes contracts/sysio.uwrit/src/sysio.uwrit.cpp | 25 +++++++ contracts/sysio.uwrit/sysio.uwrit.wasm | Bin 161218 -> 162350 bytes contracts/tests/sysio.chains_tests.cpp | 26 ++++++++ contracts/tests/sysio.dispatch_tests.cpp | 60 +++++++++++++++++ contracts/tests/sysio.opreg_tests.cpp | 32 +++++++++ contracts/tests/sysio.reserv_tests.cpp | 40 +++++++++++ contracts/tests/sysio.tokens_tests.cpp | 44 +++++++++++++ 16 files changed, 322 insertions(+) create mode 100644 contracts/sysio.opp.common/include/sysio.opp.common/registry_codes.hpp diff --git a/contracts/sysio.chains/src/sysio.chains.cpp b/contracts/sysio.chains/src/sysio.chains.cpp index 995a725ee4..74882474dd 100644 --- a/contracts/sysio.chains/src/sysio.chains.cpp +++ b/contracts/sysio.chains/src/sysio.chains.cpp @@ -1,5 +1,6 @@ #include #include +#include #include #include @@ -142,6 +143,9 @@ void chains::regchain(opp::types::ChainKind kind, sysio::check(kind != opp::types::CHAIN_KIND_UNKNOWN, "sysio.chains: kind must not be UNKNOWN"); + // The code is this row's PRIMARY KEY and is rendered as a string by every reader -- + // refuse one with no spelling before it becomes a permanent, unrenderable row. + opp::registry::check_codes({code}, "sysio.chains"); // Both strings persist into a `sysio`-billed row -- bound them before emplace. opp::registry::check_metadata(name, description, "sysio.chains"); validate_outpost_addrs(kind, outpost); diff --git a/contracts/sysio.chains/sysio.chains.wasm b/contracts/sysio.chains/sysio.chains.wasm index d129935798185dd2ebb3a67cbef8afbedda2a5f3..4602f132924a8c8565d7aa465739918eccc7e517 100755 GIT binary patch delta 8617 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 WUYM<2!ug;r{G&UEOIz(QA^r{=CI&13 diff --git a/contracts/sysio.opp.common/include/sysio.opp.common/registry_codes.hpp b/contracts/sysio.opp.common/include/sysio.opp.common/registry_codes.hpp new file mode 100644 index 0000000000..e3b4f3bd76 --- /dev/null +++ b/contracts/sysio.opp.common/include/sysio.opp.common/registry_codes.hpp @@ -0,0 +1,62 @@ +#pragma once +/** + * @file registry_codes.hpp + * @brief The spelling guard every depot registry writer applies to a `slug_name` code. + * + * A `slug_name` is carried on the wire and in chain state as a packed `uint64`, but its + * JSON carrier is the canonical STRING spelling: `fc::slug_name`'s `to_variant` renders + * `to_string()` and asserts the value round-trips. Not every 64-bit value has such a + * spelling -- `7` does not -- and the raw `slug_name{uint64}` constructor does not check, + * by design, because the OPP dispatch surfaces must never throw on operator-relayed data. + * + * The consequence is asymmetric: a code with no spelling can be WRITTEN into a registry + * row, and every later attempt to RENDER that row throws. A depot registry has no erase + * action, so such a row is permanent, and the readers that trip over it are not the writer + * -- `get_table_rows` falls back to hex per cell, while the underwriter plugin's scan + * reaches an unconditional `get_object()` and drops its entire cycle. + * + * Registry writers are privileged, top-level actions rather than dispatch handlers, so + * unlike the OPP surfaces they CAN refuse: `sysio::check` here reverts one admin + * transaction and writes nothing. That is the whole point of this guard -- it keeps the + * "a registered code is a renderable code" invariant true by construction, which is what + * lets every downstream registry LOOKUP stand in for a spelling check. + * + * Mirrors the shape of `registry_metadata.hpp`: a throwing helper for the admin writers, + * while a never-throw dispatch handler asks `slug_name::is_canonical()` directly and + * routes the failure into its own drop/refund path. + */ + +#include +#include + +#include +#include +#include + +namespace sysio::opp::registry { + +/** + * @brief Refuse a code with no canonical string spelling before it reaches chain state. + * + * Call before the row is emplaced/modified, alongside `check_metadata`. + * + * The zero code passes deliberately: it spells as `""`, which is a valid literal that + * packs back to zero, so it renders and round-trips like any other code. Whether an + * EMPTY code belongs in a given registry row is that registry's own question -- this + * guard is only about values that cannot be rendered at all. + * + * @param codes The row's `slug_name` columns. + * @param context Contract-scoped message prefix, e.g. `"sysio.tokens"`. + */ +inline void check_codes(std::initializer_list codes, std::string_view context) { + for (const sysio::slug_name code : codes) { + // Build the message only on failure -- `sysio::check(bool, const std::string&)` + // would otherwise construct it on every passing call. + if (!code.is_canonical()) { + sysio::check(false, std::string(context) + ": code " + std::to_string(code.value) + + " has no canonical slug_name spelling"); + } + } +} + +} // namespace sysio::opp::registry diff --git a/contracts/sysio.opreg/src/sysio.opreg.cpp b/contracts/sysio.opreg/src/sysio.opreg.cpp index 490c4c30f9..2ca13e9f02 100644 --- a/contracts/sysio.opreg/src/sysio.opreg.cpp +++ b/contracts/sysio.opreg/src/sysio.opreg.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include #include @@ -148,6 +149,18 @@ void require_positive_min_bond(const std::vector& v, } } +/// Reject a collateral-requirement entry whose codes have no canonical string +/// spelling. These entries persist on the config row and are rendered by every +/// reader of it, so an unspellable code makes the whole row unreadable — see +/// `registry_codes.hpp`. `setconfig` is a privileged top-level action, so it +/// refuses rather than absorbing the value the way a dispatch handler must. +void require_canonical_codes(const std::vector& v, + const char* role_label) { + for (const auto& entry : v) { + opp::registry::check_codes({entry.chain_code, entry.token_code}, role_label); + } +} + /// True iff `sysio.uwrit::locks` still holds ANY row for `account`, on any /// `(chain_code, token_code)` pair. Existence-only, so it stops at the first row /// instead of summing like `sum_locks_inline` — the settlement gate needs to know @@ -238,6 +251,10 @@ void opreg::setconfig(uint32_t max_available_producers, } } + require_canonical_codes(req_prod_collat, "req_prod_collat"); + require_canonical_codes(req_batchop_collat, "req_batchop_collat"); + require_canonical_codes(req_uw_collat, "req_uw_collat"); + require_no_duplicate_chain_token(req_prod_collat, "req_prod_collat"); require_no_duplicate_chain_token(req_batchop_collat, "req_batchop_collat"); require_no_duplicate_chain_token(req_uw_collat, "req_uw_collat"); diff --git a/contracts/sysio.opreg/sysio.opreg.wasm b/contracts/sysio.opreg/sysio.opreg.wasm index a6ceec00a21054ad09fd5faf92c6da98ae4df011..63a851477f0563b9fe86dcceceb307fade661e1b 100755 GIT binary patch delta 15143 zcmbtb2Ygh;*1u=&ZrWzy0tu;*yPE(BNgzN-LJ@L9FVZ_A2}NK-NgxU;k|3x^QDBgZ z1;GNSfPz6HiV9XxDG>#v35tR~EZ814`2J^hLxMiv_xbw*nVCEF^mC?U`ElnbKRLs- zf*Dgt(==MDXDtg~?i6!MsdVe=r}ffr#08;+kroz46h>}mm^0Gtu1{`vl`(8V3wAW?S; zJwb#u7DDxl^(ChwP+x8 zcY_F>;lhk=-5r)7$W56Hfp81tnfTR3CjSV5FMM;7nHlC5VXaBmTkAl8f5P1HZe4d1 zW(*7$fJumia6w!Hs5b@{$1l2DQrq%hk0jk4Dk9Rk~ zRB35)qcbM-sUw=JRI|1mk^7t(&F|Ag8{tIlP*LSn=byQ`K5pN93qJ71T(q8d~lQE~e7ufVb2Xme$Rqi(HY574+gRZp{=( z_%mJ8OtP*}aW_hn4R1JE7NI0IuGJkWV{L|3;!?>X1>7T5MJy{5a4?sr_% zUB9;bHN72J+~M;8RBJXUq96*_U8ch$Os7W+7u7|&T66&?y&ARBYj9Hp5SF6{&`>yA zbMKrVGTGzQL5O4l+-7)nt8|8`TUY-0ZKki-Dno}^#lZ7gQ1o7I%>`0*absmQlSkJT zb(uJN6)82VKKT&_9|I_1qFcT%hJOCh^N=~<_sIeIDJ*7C0GKGJ*$17~U* zj8?T78B|`GcX|D(Gj#$Dd1bF3X^Q>cnLcWeOYOZRJ2!MiJ0^kVBQicX74 ztb@BCJ;D&NfK>QQhv`JyYr(#4j^|}p(vPyLdNvA8YIc1gK3uV=&G_N zXyMF32AQQ4rE|glp}HEH!|WmegC0fKpk(QolX(D^)toPYDu9o%EQbeDgC0Jn*H!ZU z-^nFk3yuEYNYs`|^iR1FrnnK9KKws%pjLnbI$1{fcGpUxobv148ANU6oLZ9FmDjC( z5_R%BV@GKY802~!HVU#-^-t+Hbcc%Fnib~J@#}y-MRau&GP*@^gk|5sMg}Ti%_&y; z-^~s6(m-*r4=F5>(N0aqQ-R=+awfR&fw9U+c#<_{gsS&t2`F!}2=QpD#^=TOwi>8! zQd>=KsuN8uO>W}#Okv8V_m6Vjo9mKJOo%bS)hGy$D9g*;OyvDE38 zl-kq1L68WeiN}da9cn!@2;iterZY=~u<{5~?+OtVES>;3xY)}k4BC!S&{T#UD&pfw zIAeU-`1zIF)C;Xy5JP|W6iv&945NRDzkdj@QmPI^;AHf$j%E6iU_%XEb2&9m!+^M{ zW$SB)x*VDjV|307@*U9grzQtFt zRdEnsK?Udl^M%#G>j0#B33FOlIX;V~dxB6x7OcqQ zP~_IrL)<}-dE7Bj%2@U%(+CK*4HH^f@E4_Ls~g!nKmODEG-Fm2%rnARXhm9WDK zGm1)2J&Ev6o4v{y@&{%Nh`|79&kkr~8G zZT>rebN=fj7BEW&JwT>(t!Yk8=-OX}E{qA5uH^u7rZf+bt4~MU7wb`UJ2XUBH<(6> z%M1aY)m0H(0z0lgE?d zWG(Y{8ZH-_Su{fKHe1rj@*mAL)b7kjEZd-@BV9YH`(16iwiYFrX@4U(eH;jbP`uhE z_j^<62KlS^cCoEe&P?pj*}I8NLq^4hLm!P!^7q8Up*PlmqDRnZd~I>c+EbcPzHP-) zguJOy4i(5Xjk?en`FW%6ky)&Z@57ckI5oz`nhs-}%xnBmo$=5>vd$vvAnTIw88^wV z8+WFWvSpK*5rdiL%DxyAkbQ`e!{jSXI)D_vH)$>IB>Bwuaq{|PmoX6mpK=bcAho*~ zlgcM1M~JX|41$_DjC^^fYkm1h)79jYZBsL-P!^^FR(W}Fefe~1Q<^M4OkGMn<@mHA zG+6FTyN#yE?DV4{Qvt8xGp5O?W;rxW_G`9LTs|oOXm%6uoZFhB3?oblnXO@h$n=+pz<*XMSMWFQG%6H%u#S#WCYpci;#Nr>syA~z%?^gyi0$%GyNZbIW6sl{D zcJ^tz;1T}XD(B^Ss6?*Id!4?p7lu$m2s;jk&v;qh-zkAs$mcpur9rY`=lb;qBOC%0 z#uRu4zqk*R-8wIdzl--%fEHF%w_pR1!j`gr-!0@fo!f!~XS(V{i)4D&4vNhqt62On zv-lCk;)ne#ElxwPvsW|v$l{)Hko_e+ljA=Ft@^|wiUI|lOk6)Yrej24z>*Jm zz*jxnfeTJ_VJ=x$CS6rxxEL`YFL z9|}$R>H3GLR4(mf#XNz1IeY9~&O)yPKbA0yn*_HVm0a(feiV{h-aAWPJZ;#9Wf^^!Ks> z`l^3(F^^>IfU%9{lh?t%*J;dC5i!cvB@O-~#0-P<-i1ozZnjc`ylK zf&pRA10@)9u2f(;t)&pJd!PfT;Nk+AJun*xPaD_<>C*!{ktM$#_)`4+*fz3j<=8C_ zSgYuSDXSTe$~Oir5fz8zfWaG;xyz;dWXB=H;~$_JvbaYH))ojB3*$<;ZOG4bL;1#` zy$PyZ{>!jX@_s@*tHXpG@-7i`^Op|f94_AV8J0{M(S+8Q_a4zpr^oGvjVM7@jEZ5} zRE{cwH0Rxz1Dy_HycY=M&;nky4&PWTdle)_DyOO{IEVtNAr7PE z`~oj6ksAuS)dS(tzlJHCt4kVF544ILlUHTri^g;dg`Gs0(N@c_D{r6%^5mEVS}gw< zGaV{ZG`17cJvwQTH=^KztPhN;!C?miaPwJj|{f zJl?}>)~yAK4{M#w>bu7?t6v?@tZsH7M`qjQ- znot5w*f=49mdp1hw0HLLh8VMC_{0KnPyG2sgpD&t)ugD`=l21r93~WKWh3f zs~4=cX5ix>Ul;N0hw@=xgHVZ(6#>I1AIjancHvtT<2jTx9$^#MIO-hXz`$Pb)1dJQx1Tp9j&aGIUCMRh-akN;}#u@0rrRj*1ZCIY7W>C*Egd$q%Pk z*)`$@RCDN{YL2Q%0yQZ3YtB~HpkQQ`_nz90Vz80Pg*rI$G}v=&h9DuCUUen##`8ThW7Z*7O9hD+oY5GCd`1G{RbI zG4`&*KvJtEyV zXCdu;^E*hxZz(`J^_F2s-?=3pY4S`i&z<=u(yp_HAYDD{ZKUIFEk%0v);&l!-^TTh z+qvhc+j-Qhw{J!|dA7eiNoL(K5#MX>_!8;8bGY~EIY*GbSUL`AgR&cu&MtcmY5Tbz zS}k`!-bCItH#hWA0tzNe?EbkQfuv8~$!7S+I}4#S`SVz5Zky-1zDj8p^FC`a$A^AP zsWb^z5+_8}rW3J%GM&0IojB$4n@$c9{HF8#Jk}?3KI_wn`DOHgJU+iEtt=0^>k!c= z^4Q(i!J0+b1-nvo4#$`|e#3gF!f8q?b{bHNl^D<5C5tL0;mdSPPIdY)x} zDOh0unA&D7vySn&U*v?f7Cuc_=fOkvvV9gs)rXNnZ&m#r#t3$Muu{A4jRgr#+}j%b zuM7UyUzisCpSx1Hym)U+xaqS_y$c~$y-OFS{q4jHX9T8fvnVa?Z>9toS0VM=M+5e8 z*&>E%(;^FO`DT%ok>#m}O`Fpg>%rdL+7%2N%lH~kFiNm%JF(rgc4Er{KY5;D(nl|j zgkigRu`+B5Q~x*^w)Kmd5vLZnLRxo8|L7-)4Nra0Q0)z?2)Rz)zQhc`zjjGli+=(? zc7VpP@;{b5O|)KaxIYKp=k)!t$ub4z%g|tVVO2Lrckqr;w;PV2u ziH*gJAIV#nCBQ&Fxao40OU82&xDs`^l`t~b! zBfY$WRb%i2+mME>EJM0z}f_n8H0%Z)+aggfA`7i$UkgdC-}TR>v|)7c-;yxIAeVg z?U9eIw~&6eelu#8ZHTA6^4SeNquH}w6Tc(ELzw=qG@j~1@5#|mO+&bR;Hd;-Fh@OU zD%`sO+dWKPe(E|@ zVnv?ouPX*)#r#bT|Ivyp_rGlNuhK{k+dLO59oyUrxYl}hG;J)O`fNLb5qjpi253My z0mP%XOaTa?uwv}=8EM_R6hJxzAgtZurEK}emS*&X{C-PfeYGRZ5=)icgONd;F$)w( z-5Q86*jTB#K!O{#GC(V~GC=#cPOiPluM2E3VYN2P69?i#-{COB8w#^D<@wR{tlal} z9D0BGyu>E#j`Dk<2Ep4p0D`XDu7hIC-f+3WvCI_nKqjgxP% z;&gcPQ>31^5}+E^TjN+Y)Vbj_`RrTY;~XIH@zmSPA-3(_i2;iSzf*?v@H@bsEawYUksN3Nu7vIfPYt@BQFD^{FUu&s z9nS*hMek+i_1nwLTe$Zo6+3K_KkfC>W*Pl%RV>l|-N}vp_7%I56y0D=i}u5iry1*D z^_aL7GMy?xU<9KIF|aY~s}SSWx%B_ZLB(wT}fg z>^}-B@L#^4d6O)w(yf`u0f%I`D zuPb5F`*_2QS<4UJ0C634kj3@ZgZJUEShKdjuNluvqp}^9%ifi-;><^KVr4VWHpo3J zRR|ySyx`9Ppc!I@GR-t&sjR4Mj>zO(z;7)@2B$R5cDS`W$@~W}DB!5oV3zusj3A%bM zp!4>c&uK%{4&ld-Xq?tcU? zf{5WoVRV$+PPL-;^5Q2E^7m6M={l5J$ox-gxm8E*wUc|@{z>yduh%|lp?YUH)^o7e1{EH2B=@=TB2mW=#jX&fJIQMz^sccU)Q9{0~zkGABlFB$=fFTNPY$1J($)S=6m zb4^scwOY*!5_a(#yf#>V6{E+)pJpp?eMvgNL!xoZtNx8j#Po9 z0i1>BbJ6tP`AJAye>sUN%GZ3E%~vo-zG@C>!l|W9`j_{*w=glTGt|bC!*!gen*!&P z?~<3Wa4fJOYYXwMhqu}p<1m;_Qu(}ptrH-6$~R2}#B0DeU14S){HAYa7SuvlCRV7P zV-Y$ChS9%(&U-?fQcN)UNCc~3Q#4G*e|rc)$9LbJruSv#ce#+px))p0e%a;XaipH_ z2Viplk#hMn-`7GYg!9iRdGH55yZqM=90|4mv6{KFf9y_2<=&qU%ZGm|!;I#owxGnw zOR=)}=b?18eDBXgs7Ccmr3V3qZmm$>m-lrwzp?AT<{PHpsI1ZdcTl5de;g@Y^ZgyZ z{rwS%Pd0VF4jp%OHKyeU?bUQ>hKO_Xu&d3yfBvAVb&$Wc_tjSTG2;Vy@Rx$beqL<2 z_&i3D*XZXBLJ4A|7=h1Vqq7|J>mIS>kZkx{M`U&#FXg%qVQN^MWD#zBNOndu@cbjP z%eqjEJo8(Uxc86@{=Gyj)a4z&d$Je_7E&0Fsv=BsB}*p)@=9{$5b+xbvdYTJc4_=V z1Lsb5&!&_lFa16{)=z{?1}e#C993BHm+`T@?~hy>C=dLR;|DNFreBWcVzgT9VAe}xDv(Hz2gxwC%-T!_1Pv7iVPdpG$4d~

_+astETJ7$FmWdOG?c;(* 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 bTswp8PClAwZ}!poiq(a5ksMo6HSK=@KCf?c delta 12490 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 M(RbuGqJ^gY4?Ed-r~m)} diff --git a/contracts/sysio.reserv/src/sysio.reserv.cpp b/contracts/sysio.reserv/src/sysio.reserv.cpp index e7b67820a2..7a2cb16c63 100644 --- a/contracts/sysio.reserv/src/sysio.reserv.cpp +++ b/contracts/sysio.reserv/src/sysio.reserv.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include @@ -384,6 +385,9 @@ void reserve::regreserve(sysio::slug_name chain_code, "bootstrap reserve must seed both chain_amount and wire_amount > 0"); sysio::check(!is_private || owner != sysio::name{}, "a private bootstrap reserve must name an owner"); + // The three codes form this row's COMPOSITE PRIMARY KEY and are rendered as strings by + // every reader -- refuse any with no spelling before the row becomes permanent. + opp::registry::check_codes({chain_code, token_code, reserve_code}, "sysio.reserv"); // Both strings persist into a `sysio`-billed row -- bound them before emplace. opp::registry::check_metadata(name, description, "sysio.reserv"); diff --git a/contracts/sysio.reserv/sysio.reserv.wasm b/contracts/sysio.reserv/sysio.reserv.wasm index 233c77b8a46e80599405b09de482a880d114cd3e..8bc28c34cf42d84c45a4badcb225ced7815b05b8 100755 GIT binary patch delta 13337 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%0DRc**n<;p^;M883 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 diff --git a/contracts/sysio.tokens/src/sysio.tokens.cpp b/contracts/sysio.tokens/src/sysio.tokens.cpp index d2725292dd..3b161a0649 100644 --- a/contracts/sysio.tokens/src/sysio.tokens.cpp +++ b/contracts/sysio.tokens/src/sysio.tokens.cpp @@ -1,5 +1,6 @@ #include #include +#include #include namespace sysio { @@ -51,6 +52,9 @@ void tokens::regtoken(opp::types::TokenKind kind, "sysio.tokens: token kind must not be UNKNOWN"); sysio::check(precision <= MAX_TOKEN_PRECISION, "sysio.tokens: precision exceeds the depot frame maximum (9)"); + // The code is this row's PRIMARY KEY and is rendered as a string by every reader -- + // refuse one with no spelling before it becomes a permanent, unrenderable row. + opp::registry::check_codes({code}, "sysio.tokens"); // Both strings persist into a `sysio`-billed row -- bound them before emplace. opp::registry::check_metadata(symbol_name, description, "sysio.tokens"); @@ -96,6 +100,10 @@ void tokens::regctok(sysio::slug_name chain_code, bool is_native) { require_priv_caller(); + // Both codes form this row's COMPOSITE PRIMARY KEY and are rendered as strings by + // every reader -- refuse either with no spelling before the row becomes permanent. + opp::registry::check_codes({chain_code, token_code}, "sysio.tokens"); + chaintokens_t tbl(get_self()); chain_token_key pk{chain_code, token_code}; sysio::check(tbl.find(pk) == tbl.end(), diff --git a/contracts/sysio.tokens/sysio.tokens.wasm b/contracts/sysio.tokens/sysio.tokens.wasm index 4e1e2b7516fee5e50f30ef924f23b90f0cf24618..f69d2e89d80703fa2caf8d42ecc45ae123454617 100755 GIT binary patch delta 7529 zcmb7I3v|@gnZNh`XEHOH4EY0rgiL@te*z?d5fVZYBAB@Ypqa#kJOrzl zi9)G%QKMI_TV16+-Ktf$w6VLiwKXb>pl+qR-Br)-adG7+dsY|M<9a;n((LcM|Hu^`zMvWog|3pJoE%>`9OgU{@p$98_Ph?o@0?vboM1Q7Ijg5dQRcHD4SZ-AWlVB4#5-#+$ufh0O(q;q3IX6?&YQyV5TbVk zqdFu2-8&9ijE7E+1GfNv+ctECB6Qrt5Rl<0!wO1IL`ZLhWr)!(AL#~h zCTsL)$#G{h0uahmhhvyCtK085>Tip9EeN7O;B4KFrIOv!QbSk1IF#sAeE_(*6XzVy ziUmCcA*z)j>})7SEXzojgkuehlBucOJpe>12t~1oHz>XO@4a{MxdWTi21W9sSMXjd z3BFl;=*2e|Y$4_(d%~WZNr%DY(2?K&@SS%jrWWFTFTJ^-&k+D{04If+=)LU;u-^Y0 z)(+hb4OyM)BD!A1d(`Ui`Q zY17iUN&~e1S6?oS*`I)Ez(C@CeMb#9;9#WRds~FLvV8)(zeiRI&OIuCj^yYp?5bq2 zq=*HjDG{;E~BKlLck#4k+BA@Y98kmsMfW?(PFgfrt552~@C#gCj zJ`qzL`rUve%i-$%p`js?nSxbHxU7#P*5e52lcGNC_*7;WNzkW)b>WCFDAT212Eq}) z^w~2bf@E%j0%kq16yrtGD?JG(Pz@al^dzhSs|o2PMgy%WFrKT@3B+$l$UqRs{AhfN zca;hKM*3g#dHPuJ=gE0k41S*@7?Gj4|0ejrv3V!>PLEjldEo3>)gg|CA2jAdj_IL* z2>(>QohCTdgy8!O<^bEm7a`G}Z-@~oSOZ*;tU)ATb?{vHpkibtW*(6fk)=TRBh1Vs z9NSk!$1utZ*9N>WWyYPOT$wVQLt`B;c?F~NW7a}QoahCH| ztK-XOc9~9wW^I?E+`7P_zu&!tTrz0EethZzrJJ+pD_$mErxvSrxJ;Zj>xEb=w+aZf z5GduA1VjNDn@rf{E_9iAQa=^S%X*$LEJ&YJ1cL&RQ~K>tUh+9w@X!L(>Moodv(q!F zolb##j=R%IbZxi$B)TwVl|!4_^zkR`3h9fpa_at4j<%Phb0L{oI6mlXqGPn5o0a zmzeF4o_*>EVsz4{ojbHV)~8%dICdAju*@;BW93Z`EMjZzN4pyZdEk zFEgTXkK@V)wj~7${kKtvz)!il;)W)-DEW zL5c+ISC9)pO)v@QmUlWP_wxeG~xnL~i1%`XW6A0h`b=*{p;9QxUQ-?ib zGL81F7`yM%k$nSnY^K7+2=20{ASCgaOOR4n%{qdv)z!2*4 z7>v36>)=1(G5z*dnMVn?KpYo#y78Y%auJgzxUF>NlxaLZP&j2Tm&=WPxPGpVon!V2 zlZ+x9Q+Ww{rG9$qVvu}yYAjiea0`Ed4W6Mi4O?5->Tt>;fP`hkLJCAa>Q_!3DS4FT zL7k*nSJh==KGv`fc-S3TBqC%1!54}G1Ucx8vNcs3b}RTEOGs=aO0d^XM*u6wUCc+0 zq+ymzB#T^`+ifN!ZionqLBvK%Tf`r!h9eY$Z=s8_AK-=hh3s7M*|~v#&R)xTuP&PQ zeC$T|CPIFd%Rqzjvk{bio9Pn~7Cgu~Nugh~I$n_TQl06s>(v*(&?k$ddQQa?^{o`?^VEX$S+qnL-?xxGyy~n- z*lPr8n_w1SO;qP`rNnYp~? zj#^J8d!1UaHi(uiWK5l4Y{fmisS`HP2@3$3G7LgH%}tR2DA8x>!C3_%s-O|h;mzb) z|2^wzzEOWK|2W^IubN%VV*`6;FXp}mNX@c0>qqCbP9p@=#gEXv@ll+?Qjef8;r5pC zpgRg$c!NGuP=X!2V|I9V>Qo&rEaF>rbzzkMQuh?@<4N5=Z>qjrn2TO|(TYiZ9Jd)0 zG`NCd>^`n{75#|ofxx^>&b6L4|3Utk{_*@nDa>V2!rT^JLzpOrDBN&$3MLB7pGB|Y zTlB@~&w&5ktMY(9@HY$Y;rszzUF;w->qK#!Z`aQjSMUe*`^63~(}9x3e2uOu2~%X; zQ8JgBZy5YtTc8_cP^UF*Xw&1y~3~6TT9Ckknb(6LqLAJv@nwtVYn%@o}@rLaq+Co>o9lL zD1`#?cD;P@zwkfTvTP|Zcb4@!<8%R~k`+PwFq7jr0P@Fo^_8+8@Q3vG%ab`fbEQS$ z>P}Mi1BU9IIPosM1eMHgeQSlXcLOA~bPHK7r6zr(``uE1&bUVWva-Y!Do zwn#0ODfx@^wk0#B)uBejD+O*5;6u=@8bc)K#robQFN;UdX}+u?U~cA2VH8=BuYX>d z&-V_zQ)vlg*3K`d?ZNli!T{lYF3nD43AklvRIn1_tJ4)P&;8a>-MUe^g6s0AWPNQtMv<2 z&9u*)SgrWvoIViixHbfFMTB|BB5bZ;OUhRl z2FYh5?$bxa0}$k05~I2<0-;Fn6XxECJQLwqNFx!}f{*thC>&C9dPhl%^TV)E22u_q zSpzVL*{UPR;SJM~k`6>rx6vAmS*DzYLr#;m(^admE5H(6wc z-cX&TPpsH4&YaW!mD~Ajy>DeL@6pe${KgoJn+O9Qu)6wXeo)&r<=KZ2)o;$F`-4Pj zM9-9V2@6)~!C2NrW!>ahYe+p3{@-ffGH|4(h4XI=d{oOhzg1tZi}J&I`l>iD8Q8OG zJ%_FTq<%P-&tFXuxn^~>Km@*LbxhpO2i{nnA<*hKR!0W{>$VA@U)4<;*7M)$=Qg|| z?%?|H#&9{D1pAS=cB08h*t^j{T-4+vz@RBe*o|mVFz$6=rg-d!C;nz*c3>A~>2A48 z|L4Xit`&Kk=HXkrX=&hYjwMjJTi?BDeexE!vQE{D*v>wN@-VgSw+IybBIp?u(_-hl zXpF7DO)d8RGY(6_Z3EZ3N|p(1!^m-3r6_WYI>gK|9zh&3rKjY<7`E0AHP`{)DigP! zepRDLctP zl=vy*|-qY&6wX|Q)hX2&B&#>T29t>4{`HGd*HMMoC>Q}E>yKeo4jhh-aZ`r!- z+Uph;mnTvlGOWH;;X=>ocJ>6Sem9kQ(h{hkiFw^=sIgUQCWmVKj}vC(aBys5iK zcK0-P^~gglJvRWjt+}VCxl0yoSJCTs7P6)r8oL^sXap0ZBPmZp2 zH}}Z>9bKbabh`u=Mwjdck%s*(?KtfyJcx|)u&<-pCB44|*_!9w%5>TToA!1ClY1GAN zA{t{bM-y9_3CWll(_}_Eo6cxz8(K5UU2zMr#7*jW+rLVY0{)k&wX!EtesAG zX79N_=bm%!chAp#FaH^K{{rGz*OoqDjKPPXABx+}?cpE3xPpgw?qCjwBL51X;dCm_ zMD7GdwasLj=5WR(Ij1|49hxI)dWtg@(lnY(B37rCl%A3V8BX(;K@_Zk2GmOihinu@ z%ha4n7LVDI#8js<%bAp554h0|ZB zZMw;2uo|K?-LB^Fw;FIj?Z-XZF363<*vEL}HRsO&E5%9h;h)6m44*@1jv)@Va5fpy z>*gM&1Bq5cA_vz_{~A{&#;1=rIBZO)j<+?=!+a-kjw-!w&BH_yh7+?jK>)8otRTX)Ck$wLt?J31zA8Dk~^7Cq3)pU@_;fR1y7KA%@|Z)EyCfp9W z+c-RBQZtTn+Crjf-DRy0@Q|M|12)TDWRb}}x%dU#viJ>11<^$>zX)!+>IPGr;t-Ak zSF9*=NhGeJD|trZWinJActTRe$60`1!G`3bl+Wn!HUf!+Nm~ecxu{&nL&=N6WRN~B zpvY;pjDI2b#s-uxCAC+wK~9up4w*Dac%%oEqco6M1_$JC?2XIyG#)Uql{h_pmWhA~ zhIIf7A-a|_KEQ7v;b+%@VDMWwkcl^s>StHvlC^Sea)oq^;@*@@Jd&0lTkf_Q`_C~k z8~ZXU6cG9NN=7N%iNDP-)MMs+PG$s4GYW{48!tsU=fQvD`wLW{zD~#Ttc1K09SEX0 zaOfZ^-JJY3E+jqp02hul-3kW~^CAysb%S1I-n``##%a<~ty8cv7tmBjNZf^=%_=3? zxq5AEjjkF)Jmg^*a2sBc`Rl`PCD;8j(Cpf3Vw9lMUy?hU+g-0jVIzG~@upZFNxTauz(c_OT zrs$?C6D4@bQw*i($a$Xc8^_~0bHTt%IRWruLhejzb8=k}#QNN3D8T1)y>zNEnyu-C z{l0SO!ux$IW>)Ai2DuD*U<~cn)e>$O6?nn7(K`q(2J@V`(%NlJ{Ut41{5){HydQ7_Uyks6#-x%teu|$|N$ubv2qq#$}iMe@s$hIz`kN zp?Fe_aS9yOIK@L^Azhd1XD5K}ixT!XS!lWt;@jcS=i2^6SjnptlQo7P?? zJL^q`n`2o-OuOB*mGYfC_8}=Hw$3$8XLYVvk}<(Tp&|CY(`l02bp&K@n6$J}4*!?QQ~95lb+>s5ez^ zVwLO|5S2cm7{@4igJ`~$_5Ha$a5t70&m}FY%!W}d+lp(U1y2?G;WU0+ybd10rjkNx zx0fssTU|CQ?!g$Na=8^hDER^GLs5}}M@s)5?Y&&K1BNj>u#Gky3&^eS2JWVNo$w#hGYSWfGNgMjBLj!CpMr_-Ap<0P<09) z9>-U!vm|+^I!%&KsxzSx)w|BaGJNl@0Odxtrjc@EbB!-S0&5EMDJOvLV{c7%!g87$ zH$j#;as{5QxdPANbG22p^Ov7>=35HWvrs;~z zQ-kvA4x3U3$>3Hz88n~`KMxwBjaZ^f*XRb?F*rXveihAKq-Ufp58nf;ta$L{`)(oNCy=x{`6UKE`4x z$CLG%)%D<&`V2>B8Xbard7QWpd%Cmi&E_qT9ACq$(2BomD1ut-S)7TU#&B|L^h9)D z(n4yb-?ZWzjp+#;@+j7%2#Ga#t+9SukV-REhk8+mrHj@}wZn@V_{-<<<3*bRv8~Aq ze%#gc0x5DWo7z{Sf0iG38I52iV&YUxz95~%76KR&-a5Bei-F7E-@ z6!F~y0Go04N>j_BamP(An;f zG<39ae(pTJw7LZ*@UN@S!*}q^n)Co&0lA%oVkMEdkZ7}@ki2rjxBfc{^wv}eb%?q2;>8Dr0y`8gTDroM~UVO7B17GP(kKGM4CS-Txdz~#Cw^+}Z z=r;!G729v9-$9I|ey7DR38@bgA@u`N3hCbkE;2v|KP#_f31qlw%;R!ZI?i^Y(w^;j z;J!Tm(RuvQeKnF~c6%_bs}S~JQ`bT{fnD{{`DeS+tzILoZs^XHgJ-%;rc2$wm!mUl ztKcjAYHcnZP+!-jZ6mdc*o#@~gZR$6ddD_h6>e)TlC|E>Z+pNLv2w}=^EPbFx`}nP zN1kUv8ZRNeupw#I8Z&(SEP=edl4X)yAb$`NX~LluZ5wxlE9hNyBOyx;YfRr&GHHmX z<=A|5I{N+x-cjVqgqx^lRI+#y$PKdRahSw9^H!{#rbQBKvS>Tr(Ic;e4>Qco_RPCMLQh diff --git a/contracts/sysio.uwrit/src/sysio.uwrit.cpp b/contracts/sysio.uwrit/src/sysio.uwrit.cpp index b18d569bad..0d72d3f81e 100644 --- a/contracts/sysio.uwrit/src/sysio.uwrit.cpp +++ b/contracts/sysio.uwrit/src/sysio.uwrit.cpp @@ -1059,6 +1059,31 @@ void uwrit::createuwreq(uint64_t attestation_id, return; } + // The four payload-controlled codes must have a canonical string spelling, because a + // uwreq row IS created from them below on a path that no registry lookup gates: the + // zero-quote guard fails closed only when `required_reserves_active` holds, so a + // MISSING reserve (the unprovisioned-LP case) falls through to `reqs.emplace` with + // these codes stored verbatim. A stored code with no spelling makes the row + // unrenderable — `get_table_rows` degrades it to hex, and the underwriter plugin's + // scan reaches an unconditional `get_object()` and drops its whole cycle, stalling + // every commit. Refund rather than drop: the user's deposit is escrowed on the source + // outpost, so a silent skip would strand it. Never `check()` — we are inside the + // evalcons dispatch chain (`feedback_opp_handlers_never_throw`). + // + // The two CHAIN codes are deliberately absent: `src_chain_code` was just proven equal + // to the delivering outpost's `chain_code`, and `dst_chain_code` must pass + // `chain_registered_active` below. Both therefore name a `sysio.chains` row, and + // `sysio.chains::regchain` refuses a code with no spelling — so the registry itself + // carries that guarantee. + if (!src_token_code.is_canonical() || !src_reserve_code.is_canonical() || + !dst_token_code.is_canonical() || !dst_reserve_code.is_canonical()) { + emit_swap_revert(get_self(), chain_code, attestation_id, sr, + src_chain_code, src_reserve_code, + "SwapRequest rejected: a token or reserve code has no canonical " + "slug_name spelling"); + return; + } + // An exact source/destination reserve identity has only one outpost leg. // Admitting it would leave the ordinary two-leg request waiting forever // for a second distinct commitment. Refund on the proven source outpost diff --git a/contracts/sysio.uwrit/sysio.uwrit.wasm b/contracts/sysio.uwrit/sysio.uwrit.wasm index 608270bb6fce7f804b0c4a1534a08837e5b33fce..7cf1d2bc874a2058a952d8f04ba5e3d57ff49722 100755 GIT binary patch delta 17763 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 delta 16708 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 diff --git a/contracts/tests/sysio.chains_tests.cpp b/contracts/tests/sysio.chains_tests.cpp index 8fe264bcd9..53b32e52d6 100644 --- a/contracts/tests/sysio.chains_tests.cpp +++ b/contracts/tests/sysio.chains_tests.cpp @@ -146,6 +146,32 @@ BOOST_FIXTURE_TEST_CASE(regchain_evm_bad_hex_rejected, sysio_chains_tester) { tr BOOST_REQUIRE(get_chain("ETH").is_null()); // nothing registered on reject } FC_LOG_AND_RETHROW() } +// ── A code with no string spelling cannot reach chain state ── +// `slug_name`'s canonical carrier is the STRING, but the transitional object form +// `{"value": N}` packs a raw uint64 with no spelling check — that is how an unspellable +// code reaches action JSON at all. The registry has no erase action, so such a row would +// be permanently unrenderable (and `to_variant` throws on every later read of it). +// `regchain` is a privileged top-level action, so unlike an OPP dispatch handler it can +// simply refuse. +BOOST_FIXTURE_TEST_CASE(regchain_uncanonical_code_rejected, sysio_chains_tester) { try { + // Below the leading symbol's floor: decodes to "" and packs back to 0, so it is not a + // code and has no spelling. + constexpr uint64_t uncanonical = 7; + BOOST_REQUIRE(!fc::slug_name{uncanonical}.is_canonical()); + + BOOST_REQUIRE(push_chains("regchain"_n, mvo() + ("kind", ChainKind::CHAIN_KIND_EVM) + ("code", mvo()("value", uncanonical)) + ("external_chain_id", 1) + ("name", std::string("bad")) + ("description", std::string{}) + ("outpost", no_outpost_mvo())) + .find("has no canonical slug_name spelling") != std::string::npos); + + // Nothing was registered, and a spellable code on the same shape still succeeds. + BOOST_REQUIRE_EQUAL(success(), regchain(ChainKind::CHAIN_KIND_EVM, "ETH", 1, no_outpost_mvo())); +} FC_LOG_AND_RETHROW() } + // ── EVM: empty addresses are allowed (register now, deploy and configure later) ── BOOST_FIXTURE_TEST_CASE(regchain_evm_empty_addresses_allowed, sysio_chains_tester) { try { BOOST_REQUIRE_EQUAL(success(), regchain(ChainKind::CHAIN_KIND_EVM, "ETH", 1, no_outpost_mvo())); diff --git a/contracts/tests/sysio.dispatch_tests.cpp b/contracts/tests/sysio.dispatch_tests.cpp index 5f6289ca84..91cd7f0041 100644 --- a/contracts/tests/sysio.dispatch_tests.cpp +++ b/contracts/tests/sysio.dispatch_tests.cpp @@ -1617,6 +1617,66 @@ BOOST_FIXTURE_TEST_CASE(swap_request_mismatched_source_chain_is_refunded, BOOST_REQUIRE(!get_uwreq(9002).is_null()); } FC_LOG_AND_RETHROW() } +// A SwapRequest's token/reserve codes are payload-controlled and reach `slug_name` +// through the non-validating raw constructor. Nothing downstream gates them: the +// zero-quote guard fails closed only when `required_reserves_active` holds, so a code +// naming NO reserve leaves the quote at zero, skips that guard, and reaches +// `reqs.emplace` — persisting a uwreq row no reader can render. `get_table_rows` +// degrades such a row to hex; the underwriter plugin's scan hits an unconditional +// `get_object()` and drops its whole cycle, stalling every commit. The request must be +// REFUNDED rather than dropped: the user's deposit is escrowed on the source outpost. +BOOST_FIXTURE_TEST_CASE(swap_request_uncanonical_code_is_refunded, + sysio_dispatch_tester) { try { + bootstrap_for_dispatch(); // ETH source outpost + BOOST_REQUIRE_EQUAL(success(), push(CHAINS_ACCOUNT, chains_abi, CHAINS_ACCOUNT, "regchain"_n, mvo() + ("kind", ChainKind::CHAIN_KIND_SVM)("code", "SOLANA") + ("external_chain_id", 900)("name", std::string("solana-test"))("description", std::string{}) + ("outpost", sysio_system::test_support::no_outpost_mvo()))); + setup_wire_token_and_reserves(); + BOOST_REQUIRE_EQUAL(success(), depositinle_credit(UWRIT_OP, "ETH", "ETH", 1'000'000'000)); + BOOST_REQUIRE_EQUAL(success(), depositinle_credit(UWRIT_OP, "SOLANA", "SOL", 1'000'000'000)); + + const auto eth = fc::slug_name{"ETH"}.value; + const auto sol_chain = fc::slug_name{"SOLANA"}.value; + const auto sol_token = fc::slug_name{"SOL"}.value; + const auto primary = fc::slug_name{"PRIMARY"}.value; + + // Below the leading symbol's floor: decodes to "" and packs back to 0, so it is not a + // code and has no spelling. The TARGET CHAIN stays valid — the point is that a + // registered chain does not imply a renderable token/reserve code. + constexpr uint64_t uncanonical = 7; + BOOST_REQUIRE(!fc::slug_name{uncanonical}.is_canonical()); + + const auto bad_target_token = encode_swap_request( + ChainKind::CHAIN_KIND_EVM, std::vector(20, '\x0a'), + eth, eth, primary, /*src_amount*/ 100, + sol_chain, uncanonical, primary, /*target*/ 100, + 5000, ChainKind::CHAIN_KIND_SVM, std::vector(32, '\x0b')); + + // Delivery SUCCEEDS (refund path, never a throw) and NO row is persisted. + BOOST_REQUIRE_EQUAL(success(), createuwreq_direct(/*att_id*/ 9101, /*proven=*/ eth, bad_target_token)); + BOOST_REQUIRE(get_uwreq(9101).is_null()); + + // Same for an unspellable RESERVE code on the source leg. + const auto bad_source_reserve = encode_swap_request( + ChainKind::CHAIN_KIND_EVM, std::vector(20, '\x0a'), + eth, eth, uncanonical, /*src_amount*/ 100, + sol_chain, sol_token, primary, /*target*/ 100, + 5000, ChainKind::CHAIN_KIND_SVM, std::vector(32, '\x0b')); + BOOST_REQUIRE_EQUAL(success(), createuwreq_direct(/*att_id*/ 9102, /*proven=*/ eth, bad_source_reserve)); + BOOST_REQUIRE(get_uwreq(9102).is_null()); + + // Control: every code spellable -> the uwreq is created, so the guard rejects the + // unspellable code rather than the shape of the request. + const auto good = encode_swap_request( + ChainKind::CHAIN_KIND_EVM, std::vector(20, '\x0a'), + eth, eth, primary, /*src_amount*/ 100, + sol_chain, sol_token, primary, /*target*/ 100, + 5000, ChainKind::CHAIN_KIND_SVM, std::vector(32, '\x0b')); + BOOST_REQUIRE_EQUAL(success(), createuwreq_direct(/*att_id*/ 9103, /*proven=*/ eth, good)); + BOOST_REQUIRE(!get_uwreq(9103).is_null()); +} FC_LOG_AND_RETHROW() } + // An exact `(chain, token, reserve)` self-route has only one outpost leg and // can never produce the two distinct commitments an ordinary external-token // swap requires. Depot ingestion must therefore refund it without creating a diff --git a/contracts/tests/sysio.opreg_tests.cpp b/contracts/tests/sysio.opreg_tests.cpp index b8e3480e7f..01f3dcac8c 100644 --- a/contracts/tests/sysio.opreg_tests.cpp +++ b/contracts/tests/sysio.opreg_tests.cpp @@ -622,6 +622,38 @@ BOOST_FIXTURE_TEST_CASE(setconfig_rejects_zero_min_bond, sysio_opreg_tester) { t ); } FC_LOG_AND_RETHROW() } +BOOST_FIXTURE_TEST_CASE(setconfig_rejects_uncanonical_collateral_code, sysio_opreg_tester) { try { + // A `slug_name` reaches action JSON either as its canonical STRING or through the + // transitional object form `{"value": N}`, and only the string arm validates. These + // entries persist on the config row, so an unspellable code makes the whole row + // unrenderable — `to_variant` throws on every later read of it. `setconfig` is a + // privileged top-level action and refuses. + constexpr uint64_t uncanonical = 7; // decodes to "", packs back to 0 — not a code + BOOST_REQUIRE(!fc::slug_name{uncanonical}.is_canonical()); + + const auto bad_chain = fc::variant(mvo() + ("chain_code", mvo()("value", uncanonical)) + ("token_code", "ETH") + ("min_bond", kTestMinBond) + ("config_timestamp_ms", uint64_t{0})); + + BOOST_REQUIRE_EQUAL( + error("assertion failure with message: req_uw_collat: code 7 has no canonical " + "slug_name spelling"), + setconfig(21, 63, 21, kDefaultPruneDelayMs, + kDefaultMaxConsecutiveMisses, kDefaultMaxPctMisses24h, kTerminateWindowMs, + {}, {}, { bad_chain }) + ); + + // The identical shape with a spellable code is accepted. + BOOST_REQUIRE_EQUAL( + success(), + setconfig(21, 63, 21, kDefaultPruneDelayMs, + kDefaultMaxConsecutiveMisses, kDefaultMaxPctMisses24h, kTerminateWindowMs, + {}, {}, { make_chain_min_bond("ETH", "ETH", kTestMinBond) }) + ); +} FC_LOG_AND_RETHROW() } + BOOST_FIXTURE_TEST_CASE(setconfig_rejects_window_narrower_than_consecutive_run, sysio_opreg_tester) { try { BOOST_REQUIRE_EQUAL(success(), set_epoch_config(kWindowBoundEpochDurationSec)); produce_blocks(); diff --git a/contracts/tests/sysio.reserv_tests.cpp b/contracts/tests/sysio.reserv_tests.cpp index 39ceb6009a..d462b3459e 100644 --- a/contracts/tests/sysio.reserv_tests.cpp +++ b/contracts/tests/sysio.reserv_tests.cpp @@ -383,6 +383,46 @@ BOOST_FIXTURE_TEST_CASE(regreserve_creates_reserve_row, sysio_reserve_tester) { BOOST_REQUIRE_EQUAL(treasury_before - 2'000'000, wire_balance(SYSIO_ACCOUNT)); } FC_LOG_AND_RETHROW() } +// A `slug_name` reaches action JSON either as its canonical STRING or through the +// transitional object form `{"value": N}`, and only the string arm validates. A reserve +// row is keyed on all three codes and there is no erase action, so an unspellable code +// would make the row permanently unrenderable — `to_variant` throws on every later read. +// `regreserve` is a privileged bootstrap-window action and refuses. +BOOST_FIXTURE_TEST_CASE(regreserve_uncanonical_code_rejected, sysio_reserve_tester) { try { + // Below the leading symbol's floor: decodes to "" and packs back to 0, so it is not a + // code and has no spelling. + constexpr uint64_t uncanonical = 7; + BOOST_REQUIRE(!fc::slug_name{uncanonical}.is_canonical()); + + // Any of the three key columns is enough to refuse the row. + for (const auto& which : {std::string("chain_code"), + std::string("token_code"), + std::string("reserve_code")}) { + auto data = mvo() + ("chain_code", which == "chain_code" ? fc::variant(mvo()("value", uncanonical)) + : fc::variant(std::string("ETH"))) + ("token_code", which == "token_code" ? fc::variant(mvo()("value", uncanonical)) + : fc::variant(std::string("ETH"))) + ("reserve_code", which == "reserve_code" ? fc::variant(mvo()("value", uncanonical)) + : fc::variant(std::string("PRIMARY"))) + ("name", std::string("bad reserve")) + ("description", std::string{}) + ("initial_chain_amount", uint64_t{1'000'000}) + ("initial_wire_amount", uint64_t{2'000'000}) + ("source_token_precision", 9u) + ("connector_weight_bps", 5000u) + ("is_private", false) + ("owner", name{}); + BOOST_REQUIRE(push_action(RESERVE_ACCOUNT, "regreserve"_n, data) + .find("has no canonical slug_name spelling") != std::string::npos); + } + + // Control: spellable codes on the same shape still register. + BOOST_REQUIRE_EQUAL(success(), + regreserve("ETH", "ETH", "PRIMARY", + /*chain_amount*/ 1'000'000, /*wire_amount*/ 2'000'000)); +} FC_LOG_AND_RETHROW() } + // A token whose depot-frame precision is below 9 (e.g. a 6-decimal stablecoin) // is recorded as-is on the reserve: precision is carried, never assumed to be 9. BOOST_FIXTURE_TEST_CASE(regreserve_records_non_default_precision, sysio_reserve_tester) { try { diff --git a/contracts/tests/sysio.tokens_tests.cpp b/contracts/tests/sysio.tokens_tests.cpp index 6959d07a14..4f85d2d863 100644 --- a/contracts/tests/sysio.tokens_tests.cpp +++ b/contracts/tests/sysio.tokens_tests.cpp @@ -140,6 +140,50 @@ BOOST_FIXTURE_TEST_CASE(regctok_records_binding, sysio_tokens_tester) { try { BOOST_REQUIRE_EQUAL(false, erc20["is_native"].as()); } FC_LOG_AND_RETHROW() } +// A `slug_name` reaches action JSON either as its canonical STRING or through the +// transitional object form `{"value": N}`, and only the string arm validates. Neither +// registry has an erase action, so a code with no spelling would become a permanently +// unrenderable row — `to_variant` throws on every later read. Both writers are +// privileged top-level actions and refuse. +BOOST_FIXTURE_TEST_CASE(regtoken_regctok_uncanonical_code_rejected, sysio_tokens_tester) { try { + // Below the leading symbol's floor: decodes to "" and packs back to 0, so it is not a + // code and has no spelling. + constexpr uint64_t uncanonical = 7; + BOOST_REQUIRE(!fc::slug_name{uncanonical}.is_canonical()); + + BOOST_REQUIRE(push_action(TOKENS_ACCOUNT, "regtoken"_n, mvo() + ("kind", TokenKind::TOKEN_KIND_NATIVE) + ("code", mvo()("value", uncanonical)) + ("symbol_name", std::string("bad")) + ("description", std::string{}) + ("precision", 9) + ("address", mvo()("kind", ChainKind::CHAIN_KIND_UNKNOWN)("address", ""))) + .find("has no canonical slug_name spelling") != std::string::npos); + + // Either half of regctok's composite key is enough to refuse the binding. + BOOST_REQUIRE(push_action(TOKENS_ACCOUNT, "regctok"_n, mvo() + ("chain_code", mvo()("value", uncanonical)) + ("token_code", "WIRE") + ("contract_addr", "") + ("is_native", true)) + .find("has no canonical slug_name spelling") != std::string::npos); + + BOOST_REQUIRE(push_action(TOKENS_ACCOUNT, "regctok"_n, mvo() + ("chain_code", "ETH") + ("token_code", mvo()("value", uncanonical)) + ("contract_addr", "") + ("is_native", true)) + .find("has no canonical slug_name spelling") != std::string::npos); + + // Control: spellable codes on the same shapes still register. + BOOST_REQUIRE_EQUAL(success(), regtoken("WIRE", "Wire", "ok")); + BOOST_REQUIRE_EQUAL(success(), push_action(TOKENS_ACCOUNT, "regctok"_n, mvo() + ("chain_code", "ETH") + ("token_code", "WIRE") + ("contract_addr", "") + ("is_native", true))); +} FC_LOG_AND_RETHROW() } + // `symbol_name` and `description` are moved into a persisted `token_row` billed to // `ram_payer = sysio` — the shared system pool — so an unbounded string lets each unique // `code` consume up to the KV/action ceiling of system-owned state. regtoken validated From 7e1a5bd5a34ec625415923775145dde771eb455e Mon Sep 17 00:00:00 2001 From: kevin Heifner Date: Tue, 22 Sep 2026 10:20:42 -0500 Subject: [PATCH 12/29] fix(libfc): bind Traits::alphabet to a view before using it Mirrors wire-cdt 9b3bbeb8, keeping the two basic_name implementations diffable. The traits concept requires only convertible-to-string_view, but validity_error used find()/operator[] on the traits member directly and the symbol-width derivation used size(). One private binding now serves every use. test_slug_name adds a policy whose alphabet is only convertible. Change-Id: Ibb3f77f4ddc447964f72dc05976183c4f6e0c2fd --- libraries/libfc/include/fc/basic_name.hpp | 20 ++++++---- libraries/libfc/test/test_slug_name.cpp | 45 +++++++++++++++++++++++ 2 files changed, 57 insertions(+), 8 deletions(-) diff --git a/libraries/libfc/include/fc/basic_name.hpp b/libraries/libfc/include/fc/basic_name.hpp index 87a062c853..6a2e581d9c 100644 --- a/libraries/libfc/include/fc/basic_name.hpp +++ b/libraries/libfc/include/fc/basic_name.hpp @@ -144,7 +144,7 @@ struct basic_name { } for (std::size_t i = 0; i < str.size(); ++i) { - const std::size_t sym = Traits::alphabet.find(str[i]); + const std::size_t sym = alphabet.find(str[i]); // 3. in the alphabet if (sym == std::string_view::npos) @@ -167,7 +167,7 @@ struct basic_name { // 6. a non-zero-terminated alphabet strips TRAILING pads in to_string(), // so a trailing pad cannot round-trip either. if constexpr (!Traits::zero_terminates) { - if (!str.empty() && str.back() == Traits::alphabet[0]) + if (!str.empty() && str.back() == alphabet[0]) return Traits::not_normalized_message; } @@ -227,13 +227,19 @@ struct basic_name { } private: + /// `Traits::alphabet` as a view. The traits concept requires only that the + /// member be CONVERTIBLE to string_view, so every use binds here rather than + /// calling find/size/operator[] on the traits member and silently demanding + /// more of a policy than the concept declares. Mirrors sysio::basic_name. + static constexpr std::string_view alphabet = Traits::alphabet; + // --- symbol width: minimal bits to index the alphabet --- static constexpr int symbol_bits(std::size_t alphabet_size) { int b = 0; while ((std::size_t{1} << b) < alphabet_size) ++b; return b; } - static constexpr int bits = symbol_bits(Traits::alphabet.size()); + static constexpr int bits = symbol_bits(alphabet.size()); static constexpr int total_bits = Traits::max_len * bits < 64 ? Traits::max_len * bits : 64; static_assert((Traits::max_len - 1) * bits < 64, @@ -243,14 +249,12 @@ struct basic_name { /// symbol value -> character. Out-of-range symbols decode as the pad /// character (alphabet[0]). static constexpr char char_of(uint64_t s) { - const std::string_view a = Traits::alphabet; - return s < a.size() ? a[s] : a[0]; + return s < alphabet.size() ? alphabet[s] : alphabet[0]; } /// character -> symbol value. Any character not in the alphabet maps to 0. static constexpr uint64_t sym_of(char c) { - const std::string_view a = Traits::alphabet; - for (std::size_t s = 0; s < a.size(); ++s) - if (a[s] == c) return static_cast(s); + for (std::size_t s = 0; s < alphabet.size(); ++s) + if (alphabet[s] == c) return static_cast(s); return 0; } diff --git a/libraries/libfc/test/test_slug_name.cpp b/libraries/libfc/test/test_slug_name.cpp index 113ae2dbfc..2e2b6c401c 100644 --- a/libraries/libfc/test/test_slug_name.cpp +++ b/libraries/libfc/test/test_slug_name.cpp @@ -362,8 +362,53 @@ struct name_like_traits { using name_like = fc::basic_name; +// An alphabet member that is only CONVERTIBLE to std::string_view: it has no +// find(), no size(), no operator[] of its own. basic_name_traits asks for +// exactly this much, so basic_name must bind a view before using it -- a direct +// member call on the traits' alphabet would not compile against this policy. +struct convertible_alphabet { + static constexpr char storage[] = ".12345abcdefghijklmnopqrstuvwxyz"; + constexpr operator std::string_view() const { + return { storage, sizeof(storage) - 1 }; + } +}; + +// Name-style (zero_terminates = false) so ONE instantiation reaches all three +// sites: the alphabet scan in validity_error, rule 6's trailing-pad test (which +// indexes alphabet[0]), and the symbol-width derivation (which takes size()). +struct convertible_alphabet_traits { + static constexpr int max_len = 13; + static constexpr convertible_alphabet alphabet{}; + static constexpr bool zero_terminates = false; + static constexpr fc::basic_name_endianness packing = fc::basic_name_endianness::MSB; + static constexpr const char* bad_char_message = "conv: character is not in the alphabet"; + static constexpr const char* too_long_message = "conv: string is too long"; + static constexpr const char* bad_final_symbol_message = "conv: final symbol does not fit its slot"; + static constexpr const char* not_normalized_message = "conv: spelling is not properly normalized"; + [[noreturn]] static void throw_invalid( std::string_view in, const char* why ) { + FC_ASSERT( false, "invalid conv '{}': {}", std::string(in), why ); + __builtin_unreachable(); + } +}; + +using conv_name = fc::basic_name; + } // anonymous namespace +// A policy whose alphabet is only CONVERTIBLE to string_view is usable. The +// concept promises no more than that, so the implementation must not demand +// more; this whole case is a compile-time assertion as much as a runtime one. +BOOST_AUTO_TEST_CASE(convertible_alphabet_traits_are_usable) { + static_assert(fc::basic_name_traits); + // Round trip: the alphabet scan and the symbol-width derivation both ran. + BOOST_CHECK_EQUAL(conv_name{"sysio"}.to_string(), "sysio"); + BOOST_CHECK_EQUAL(conv_name{"a.b"}.to_string(), "a.b"); + // Rule 6 (!zero_terminates): a trailing pad is not normalized. This is the + // check that indexes alphabet[0]. + BOOST_CHECK_THROW(conv_name{std::string_view{"a."}}, fc::exception); + BOOST_CHECK_THROW(conv_name{std::string_view{"A"}}, fc::exception); +} + BOOST_AUTO_TEST_CASE(non_zero_terminator_trait_accepts_alphabet_zero) { // For name-style traits the pad symbol is '.', and '.' is ALSO an ordinary // interior character: literals like "sysio.token" must validate. Make sure From db999fb4bc5a25959317a2b18df4e2a4b7594276 Mon Sep 17 00:00:00 2001 From: kevin Heifner Date: Tue, 22 Sep 2026 11:00:10 -0500 Subject: [PATCH 13/29] chore(contracts): rebuild chalg/opreg/uwrit against wire-cdt #119 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Toolchain-only: slug_name's exact-match serializer (wire-cdt 9b3bbeb8) replaces the generic overload at every slug_name action parameter. No contract source changed, and all 17 ABIs reproduce byte-for-byte. sysio.bios is excluded — its +11 bytes are wire-cdt master's BLS fix, which wire-sysio master is already stale against. contracts_unit_test: 789 cases, no errors. Change-Id: I2296cdac64032d3c3e5ffedee2591967e90dd232 --- contracts/sysio.chalg/sysio.chalg.wasm | Bin 67973 -> 67609 bytes contracts/sysio.opreg/sysio.opreg.wasm | Bin 96991 -> 95899 bytes contracts/sysio.uwrit/sysio.uwrit.wasm | Bin 162350 -> 161011 bytes 3 files changed, 0 insertions(+), 0 deletions(-) diff --git a/contracts/sysio.chalg/sysio.chalg.wasm b/contracts/sysio.chalg/sysio.chalg.wasm index 1714baf126cf13c1943aa984b014fa67b880b607..c9ba212d2748cc8b046cf55ebbd0b170fabc8a73 100755 GIT binary patch delta 1767 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 E15Z$n-2eap diff --git a/contracts/sysio.opreg/sysio.opreg.wasm b/contracts/sysio.opreg/sysio.opreg.wasm index 63a851477f0563b9fe86dcceceb307fade661e1b..78ae2b7568e9fc39c60326dc08fdf35e8bcdecda 100755 GIT binary patch delta 3027 zcmd5;YjhRW6`t?Ry?Ncl3>Zjq^I&ob;YLVu10*CNflMNgL;{}H^^G9{^-BnANQNR z&uh=OzrD|#`O4jV#oaR8rCw$i`AF9Xsm%FB#U#h5NQt2zi(#xAi;{mx;UnC=`7hi{ zbLOd$8Bh#(I(?6uxP^Aqvu9ba>+Exu^#%Kq-C$p_f3a`azgg7g1MZ7fN!Sn_hsBEC z=*CxO^uu9(GNYmH2-9Iio0@|O5`EC&XNr+-<}*cyr&`eow0A=j9i)j4rkmP6>Gy}Q zcL)Qfc8Rtz3PN^q2zM)Tz_T)%h1cQN-4(++)pNV%ImYM+)rc^)fAHl6&%~1L*6z8} zm&L0JEAS$3E}V*&czaGFX4m6)=r}mT1&f%1F|PXG1Ac zlxTQ#wM^-+`h%bcAt4A_5F&y=2hi5eKFsUnB%m>`fi&hJixnvW8otiVbj8!`k$JVQ z)-DUfIo>!U7w0W|MjWt)A9|vgeb20@nJ)s{$;u74X*XtQ{CZX~PkM4dB|H?nTR!qQ7J;Ecnj#DvxdJ(RG)c%ZOSSyO~MZp@&^e-1H;yF6Lw8 zxKFZd>{o6Dbj;I&_xLip8Q#bnzDGatBiDcj&Kb^hk{UMGCus zce~k#f3zVYr@)kD;W#@84U!ol$yAI48Yilm*F#8Q&fa*{vxmvCaLu2la%{@ztkQF$ zS9co=8D)r!J$&n?xA8Y#*YY6V$IU3vSr#mAfHu{^}ysrrI-ueUY zXFGUrb$CyRYrT{B%OpVfL zqvTQ!lk(YkCo?ChZ`_fZlXAn8B=(zq#aSOBa^-1$=D3e2A-VrJJ2Vtb&kEI^*BvgrtZ#EbAm&< z8uS}K6ed+Q#V!l;|Db^C{>K86VULBw|3c#5O^wd2yPOI$cAckkB@r<{tEa1ioE}+K zRJ77`n9!z3;gI2W=D0NcUR>;j*YLc&0Y?+u=#BiuCaEON)EuN+?UcN=l1=Tmc6k;$ zgUx4Rb1A&|otTl0GAt0=veC#E2EAfz4)$V^NDiPNY%%FV<6=Sp{jt%*O{;1U*vtNBREuTcYUbNhgWNZ|J3QW@uQjB%+n^+q4cO6;E_#!2o=wwE?sjU)ciqR#i zMII~HtQNP5F)VGfv|XmQMI!YfOCm+-1W&}S6g4Ht_Oyid%v1y7@e3m&iyTrjVED=wYVQ4hDLLV5~E}PsL46oZ?+>3`9-Vo!4k|cIJ zeN~aXTXfs0A~z3Z;?tq{3HI9e4nrPX`WT>I(5@8rzT@bc!QLH)*xn1!H5pSJ}`yK4PV;}9zqF2gZ<9K$U^$f zP^NU!B4x_unHKCCTCI;!)1a!-TI|$mXae?#wKW(QglrL*j-`=%s9s>KNvbM4H>u-)??I7Ss7 delta 3421 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 delta 3670 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 wAB*p`_j}QO<8hT Date: Tue, 22 Sep 2026 14:13:41 -0500 Subject: [PATCH 14/29] fix(contracts): refund value-bearing messages with an unspellable code DEPOSIT_REQUEST and RESERVE_CREATE both arrive after the outpost has taken custody, and the reserve code is caller-chosen (ETH create_reserve and SOL create_reserve_handler validate only that it is non-zero). The msgch canonicality guard dropped both, stranding the funds: an outpost cannot release escrow on its own authority. Deposits now reach opreg::depositinle and reserve creates reach reserv::oncrtreserve, which reject through the DEPOSIT_REVERT and RESERVE_CREATE_CANCELLED paths they already own, before persisting anything. The cancel carries no CANCELLED tombstone: the row's key IS the unspellable triple, and such a code can be neither squatted nor reclaimed. Every other action type keeps the drop -- a state transition has no escrow to return. Both regressions assert the OUTBOUND refund, not just the absent row. contracts_unit_test: 810 cases, no errors. Change-Id: I818c59109ca1978a4eca12133dcb2d4b868181e3 --- contracts/sysio.msgch/src/sysio.msgch.cpp | 12 +++- contracts/sysio.msgch/sysio.msgch.wasm | Bin 162844 -> 162797 bytes contracts/sysio.opreg/src/sysio.opreg.cpp | 11 ++++ contracts/sysio.opreg/sysio.opreg.wasm | Bin 95899 -> 96098 bytes contracts/sysio.reserv/src/sysio.reserv.cpp | 18 ++++++ contracts/sysio.reserv/sysio.reserv.wasm | Bin 88217 -> 88684 bytes contracts/tests/sysio.dispatch_tests.cpp | 37 ++++++++++- contracts/tests/sysio.reserv_tests.cpp | 64 ++++++++++++++++++++ 8 files changed, 136 insertions(+), 6 deletions(-) diff --git a/contracts/sysio.msgch/src/sysio.msgch.cpp b/contracts/sysio.msgch/src/sysio.msgch.cpp index dfcc3b33f7..26c58416e0 100644 --- a/contracts/sysio.msgch/src/sysio.msgch.cpp +++ b/contracts/sysio.msgch/src/sysio.msgch.cpp @@ -532,7 +532,12 @@ void dispatch_operator_action(name self, const std::vector& data, // no-proto-messages-in-actions rule. const sysio::slug_name chain_code_slug{chain_code}; const sysio::slug_name token_code{oa.amount.token_code}; - if (!payload_codes_canonical({token_code}, "dispatch_operator_action")) return; + // A DEPOSIT_REQUEST carries outpost custody, so an unspellable token code must be + // REFUNDED, not dropped: opreg::depositinle rejects it with DEPOSIT_REVERT before + // touching the balance map. Every other action type is a state transition with no + // escrow to return, so dropping stays correct there. + if (oa.action_type != AT::ACTION_TYPE_DEPOSIT_REQUEST && + !payload_codes_canonical({token_code}, "dispatch_operator_action")) return; // WSA-028: TokenAmount.amount is signed on the wire. Gate it through the // shared fail-closed parser before any unsigned use — a negative or // out-of-range amount is dropped here, never wrapped into a huge collateral @@ -668,10 +673,11 @@ void dispatch_reserve_create(name self, const std::vector& data, uint64_t // reserve whose external custody is claimed against a different chain B. if (!source_chain_binding_ok(chain_code, ext.chain_code, "dispatch_reserve_create")) return; + // No canonicality drop here: the creator's escrow is already in outpost custody, so + // an unspellable token/reserve code must be REFUNDED. reserv::oncrtreserve rejects it + // with RESERVE_CREATE_CANCELLED before persisting anything. const sysio::slug_name ext_token_code{ext.amount.token_code}; const sysio::slug_name ext_reserve_code{ext.reserve_code}; - if (!payload_codes_canonical({ext_token_code, ext_reserve_code}, - "dispatch_reserve_create")) return; const uint64_t ext_amount = sysio::opp::safe::to_depot_amount(static_cast(ext.amount.amount)).value_or(0); diff --git a/contracts/sysio.msgch/sysio.msgch.wasm b/contracts/sysio.msgch/sysio.msgch.wasm index beedd3a584ce4ee6c262201fd81a7756364a71ae..f1fa74a8b16a0d8ee6cecb28a26f1fbf378e5407 100755 GIT binary patch delta 5912 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 delta 6029 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 diff --git a/contracts/sysio.opreg/src/sysio.opreg.cpp b/contracts/sysio.opreg/src/sysio.opreg.cpp index 2ca13e9f02..32caefd184 100644 --- a/contracts/sysio.opreg/src/sysio.opreg.cpp +++ b/contracts/sysio.opreg/src/sysio.opreg.cpp @@ -1253,6 +1253,17 @@ void opreg::depositinle(name account, auto deposit_action = build_deposit_action(actor, chain_code, token_code, amount); + // The balance map is keyed by (chain_code, token_code), so an unspellable token + // code cannot be persisted — every later render of that row would throw, and the + // underwriter's values_only scan drops the whole cycle rather than one cell. The + // outpost has already taken custody, so refund rather than drop. + if (!token_code.is_canonical()) { + const std::string err = "token code has no canonical slug_name spelling"; + emit_deposit_revert(get_self(), chain_code, actor, token_code, amount, + original_message_id, err); + append_action_log(ops, op_pk, deposit_action, false, err); + return; + } if (amount == 0) { const std::string err = "amount must be positive"; emit_deposit_revert(get_self(), chain_code, actor, token_code, amount, diff --git a/contracts/sysio.opreg/sysio.opreg.wasm b/contracts/sysio.opreg/sysio.opreg.wasm index 78ae2b7568e9fc39c60326dc08fdf35e8bcdecda..c8ef62528e407fd0930e5b5b246a27e1138065da 100755 GIT binary patch delta 5873 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 KJ&&R|MfpDsjlNO< diff --git a/contracts/sysio.reserv/src/sysio.reserv.cpp b/contracts/sysio.reserv/src/sysio.reserv.cpp index 7a2cb16c63..290e9cafc1 100644 --- a/contracts/sysio.reserv/src/sysio.reserv.cpp +++ b/contracts/sysio.reserv/src/sysio.reserv.cpp @@ -449,6 +449,24 @@ void reserve::oncrtreserve(sysio::slug_name chain_code, registered_chain_kind_or_skip(chain_code, "oncrtreserve"); if (!expected_chain_kind.has_value()) return; + // The reserve row is keyed by (chain, token, reserve) codes, so an unspellable one + // cannot be persisted. The creator's escrow is already in outpost custody, so refund + // rather than drop. No CANCELLED tombstone (unlike the rejections below): the row's + // key IS the unspellable triple, and a code with no spelling can be neither squatted + // nor reclaimed, so the tombstone has nothing to protect. + if (!token_code.is_canonical() || !reserve_code.is_canonical()) { + sysio::print("oncrtreserve: rejecting with RESERVE_CREATE_CANCELLED " + "(token or reserve code has no canonical slug_name spelling)\n"); + opp::attestations::ReserveCreateCancelled cancelled; + cancelled.chain_code = chain_code.value; + cancelled.token_code = token_code.value; + cancelled.reserve_code = reserve_code.value; + queue_attestation_out(get_self(), chain_code, + opp::types::AttestationType::ATTESTATION_TYPE_RESERVE_CREATE_CANCELLED, + cancelled); + return; + } + // Soft-validate; silent skip per feedback_opp_handlers_never_throw. if (connector_weight_bps == 0 || connector_weight_bps > MAX_CONNECTOR_WEIGHT_BPS) { sysio::print("oncrtreserve: bad connector_weight_bps; skipping\n"); diff --git a/contracts/sysio.reserv/sysio.reserv.wasm b/contracts/sysio.reserv/sysio.reserv.wasm index 8bc28c34cf42d84c45a4badcb225ced7815b05b8..25fd69285fd720720682b07188442f77691b8b5d 100755 GIT binary patch delta 11198 zcmcIq30zgx)<0{Xd*Q;p!V%$uC~)ot6;u=z!FZM)%(-$-v$WD|xMq$o(_UQ?Gc4zw z9V-WNC=*odn(*qSn5M?lL_?)=z%d+9E1y|eerxS>4x;wH_kQ2+_wmy`tUayazt$ek zeS3rb!bW?M6Kvpzmw6~G3#@|};MFXS?_{2UO&$*NO!gWdW$WaRh)+;GOpai3B#ZDe z7n6Z`T!}16W-d=8G+|9$&0LYm?i5#ZInL9fWvc9v<61?r9gsb^fpoC&Z2{f)b$44FB;2d(_~>IF}1tvR5Xm3uJ6TiuotYvWxx$ z0A}=Gj&LQWrrPW<4k9A#GDr~Qmi?gRluAy&q~%;RGyaA>t@Rof(i9{Oob{{VIGli! za0*UC>bq<)YrTXmWy@GTTh3Onl`Qn=ueR6tIeT^I%VShYQ=a#N&&x8{l6}EV*p)GX z`N0K|?AjQ9Pvcln0uMF52yAr~?-`%U_E!a7h<_ls@t0uCr^rrcBMDc*<)yCma6YAy zpJ+0XeSVw|Zn}<@9S;OIa{#+^oYzm@#Bxqx7R{@Y_p{fi*pXjOnR=7DEZ}Xima(NL z_`cLcw)X_Tn);>d2E?k!c`~ypG}(|0cZ!Te6b}wNLiv6Q49TQ(iN%Lx?3gY> zA;PpAWU)WyMa-uR@P-@o3}y_S} zq~0DkG40lFz<3&<<<>|DG+MyUcef9u-?@%4&KqN`g2Q8QD0tkc0329pMrt|Gd%W5}jnHyf@9`5n4OAy$*b|Nnx>cu!4nf*q&3r_+c_XNfC5ITJ_8ZH*5RR|ikC{JT8*+&zRQr%dSvLppFe+F}@VaYg$ zhh$Ipp$a{UJy!LjJYbAK`Y8+*hLox^gE>frB}|NP_>^a2u?OiH3r-V=VttfLK|YdM z5yLebs6lRQdz za8HC&>FJRTd=-%%O>~!-Wx`@9Q|0W^>393HZ{S~eZgzF>%QQyY#e5!PtpY#pXe$0G7aFesNT zwUME7h|!P?cAc1WWUyHy_9nDP|V2>vvcT=kc4&gNKDawS|vt}hm5i^0<}O6e7l1E zmbasZYr_W_e-!!QP0%K@Ma(N{{%%HKvhaX=5u2oyoRoaYOsdZ$;k<+yx6*=;vXXhi zoocx7dCTBR3*nf_j39^WC$o*rQ}6$@ngJZG9NDpH4&uS^$UhOZj+|ktErpTV5mfHF zsPX_BO_DpSHr^)LZ6nxNXgZ!8hpZA9$YTf>yRlf$DE=o5-EMROMFrW=ox%Rh_SF9t z+Y6iH`j*kv(ElBY1QLNnAS9CI$zd|(e7V*wktk3i-2Y}UMvgGwIDKq*E|ihG>D7p5W}>^K|VTo&}pHC(OzM{IS3=5{r2{ic3-N5@qE1B zh((2>zcqT1)oL{6Ta5&(!!|^$)C18dDZ#{RX)x{HeQ@{2ty&rFm2sz*QC=04XN6+q zRLMmXAsNpKnM@)*FeOZzb5c^=xJgo+#eJ4C$%$hXUlQMQZ#sFKVI zV4B@+4W>2ShGTkPcR!}PyElgM{A~A;FoECSW1{~{2FyIX*pkGSDYyXbWnUPRfRTde z^h1f{Wur9@xt~})-DJXrnU>WrQSrU!G(3IvfCRY9O&Dlq5 zYPUw-TNYFAT#T8E#rV8HAT?J|d|tUu)#3Bn%_?OM!3Hv=3Z^VhY#Dx*h8)G9`swT; zkz}~PpytgAN@BMv=(1=KNf_i71rSGRWY%GZF43$E%}QrPH0xhc2`H3H)HRr(vQ)|D z1w@lqZj?+-4m^p$^QzoP=|$ScW^5!3&@e<9j#T)9>|rpGU(RkPhkI?j(Gvst)GjIh zNrV85mALqX+Qsn{it|WXJ_uNgEuSgE7bNYlh?8Ds#8JTGI`(Wy2D>$iA{BS?M`$8C zfG`8aOsVfgZV_{+D(AAm`YtN)(jKw)#kkEq&yV#uj_BIjGY!)pdp-b@xz_7m=L@Qg zT+YJ-;grCO&$sab6!; zAI7sC=k$a1;a)zquNVHtm-M|C0{leZndlkM_G=P6m&FQYm)$orSr1!EWlapA_@`5fcnA! zf;x~474w`vvqkT8SisNt;-l*9U8;5MJuvz`X2g|J80|%GR&6R^=vc)A{R+?^kM>W%)ZafH-Vbc- zUjXnLe{5g^ra1#g;3gQP&B8q_Sn>0kfw7@fNAOd%{-CE}HJ>`D8NAMy4;l(@@ZSe5 zXKTv%dk+SN8i!*k2VYYTRe{)D0D>=z`3`s?`7kBflp&IpkX{ZP4^JPQ5Z1bMLwxN%L3F2mtoJ2i2hY^T74l4_L z0cFFW5TxbfMPo`Ja(E^nbnbh!1A^w&M?b(c^|2wC&UkDsrsq z&ILL?nG0;jX}>iD7okk zLjG5L!?S9ld51zBiZ<1Tcd^@s^DEDeV$*;R9g`4D7f0>TS3GCTF4HLYKSz zV`EogOkoIqBc>?h8Uq{+q>hgTv~r;5gx4tN`zNMT=9ttxPAsoFw8dg|4<4ISrCPoN zeI`9B1{VF*7vhU<~07@$qi}NO_Nn%(}5p)A&I>T{Q3)@!XgqX)=B0Qq-g!+woY1*gi6I3&4+j$r~AL$v%GI zjeNFsAD=uc(5TjRL$#6+;pWV6^V9oy*V)6^>-(`=A@4gUxNg^Z!F9U^_VWpInvzuS z%tKW9H8fSKb_fAb+f{Q8GSXtn{19MA_VaBE9wOz(@pc$~`tbysWdhG2 z!>r+ZpvAPWO@-8*#UiE1yFygEp`0CG^W_T#6J-l;GJ&T|Vj`NDC=IfhP~NH_Cis~} zH(KOYShLmXVUbek(ZXRYP+q~6ccM+ywtVL_;dLqLdj8T(KK0!l;V-D6gj-C@>ygg; zEN*H=o?E;B{wG}{AxvD6LbXaf`-dpA0$cJc1kM&N_rMRlc=-UB&7CXy!yG<*g~7&L zK6SaDV4UWH@vjFSTxf#O$gBuIX}@?pec>s3jl^+Bs9<3_?CYZ1dwzyAy@ z<9$9D3HdzmK`W$=ZSCgQK4=b?dE#nXvg_(cU@c$1x-FZ2fFE3)#|jVd$36_Ss)bCj zRxXajnhBY24w!liPL*=(rn0%VCYrr=kPlrmhzwn{CJPOHehpbF(4b%puq_Aqv_fIH zHHA%M4Tla+kt^+Z#jQJ0;9Q|$wbJ!B*lUOQ)*{OoM~cW8+cuCfu52LfB4@PaY2_hk zh>rpufLYwTaV;F>nO=AV-ELp~MK=X^Q>9?1QMc){&F=x-i; z@w;UflS@a6jK*aK3coP=JG4Bf593}5UdSD^f0;2L?K4t^do!t``({!FN`b1tlvEKI zuq6)aFTeu^ZPE^HkvJI4TJcIL0O!-TW(jz|$+vE8z!svh%Hr6@vw?qY{TM>u!a*2L zY>Lh|ZX0BR$h&f^SKAXX!d%?`L^FB@6&sGVp9VNv;+FWvb7 zT;nx6XTt~l#a%n`7}Mp;3iy!6l(oUFzgJlkH^sy^Y28W*w|LB}C7-g8zgpIVMA}<6 z6&mDxYibxw9IX77Ke9UsE%eInOP+nA)q1!HRxGXAv|W-fTT*86`FmQzZ+!C}500qX z6A$kMZtTHF>9X(Y1bP4W-&tV$vmlQSi@L~``U@Q99sW0c>8?^GD}Vs8X>0dLJw^A` zqC4v3Pxk|)W9;Wm@|N+R$~z(+jGf`}eQ(#@8TgTzZ3CJ6TQTy-rz$Q%=&@QRc>TSR ze9}Q3n|QrL(NTX`Ew(zM@VgFK+ptZ$Cd9$Vvs-%P9Eyq0KSH7*_r$AB>|M4xd)KBz zbP8BhNx1#8k~p+sFJtc-^G!Oi&yNIFd>hX&27Z2cuXSAd@Q51^?WIS$;-Nk0=!*%n zNkMqcGIpD4TJmNUg~&e|6TkGRV1#L<7%Q2Uhwq*Y-)s#Bn#&I!9g7i_nGuzVh|0`} z`pB`tY|GKW$Hz*@Tb?`KGRg>rbe-#<%DshAs*m#Z$6M4ZI3_6%;?=ke^A-I2<4@p- z`%c7?DxW^lyuOJ}dQo@a8B|#qSaD)KSaSJJHAK0lpCY-+PW=TPWBBP-JpFWvC=F#b z$M`3w8-dObo<0V@@=a$RMjS?bpMucu`28ZJ9Qb~AS^<7=5I5%>@`5)}PMPFXYUy}; zGs)M`H46BG>TD?F2dcCEg*4P`E2p+Pbp-z{_3c<+Tc^GBwt9?DSx3#OQj7=PkS^`0 zD|7M=)?8`C-C}XA&0zekvp=)yDn9pI4_F_lJm+Op>-0ksS8HOZm{rr+^$|{rmms=G z;|aYciRgtjs~gseS43iO7h==1b^#xAF&YZ^s~5YVlwV%#($A36=uxW}bA-@{kjiMk zQY)2FzkFN$0@gS1VVB~R^>s$2q3rziOG&VvufLS%9`9v%*^s)6TXO}D81D^IM(U+* zh~c9yR~kb0V4IHzX8yonK&>R5Q4s8oDJi#>Olma9mKsU9X|+^b-7uWdDVcO|^v7;6 zRo@x}(F&d|@ec4a13NtvU6%Oqny!5QPid&5M}K!G*DlioC&V{>OmCXrsFUe8EV$Ab z#f!VrI|#*7UI9Ml%KhwpnIF0SiZPS#znJL`xr_yFm&>49a*?#jwak%t`lAe@_}Bl+ z3d_Y;T2<&-^(nM9)z45{K9KV*c{aTi{cE&$YIAwH|-%F0$9$b4k4X+B7K8JIN5$v{qE& z3)O=u@nt|lRFNIQW)?5G9;e^1K@dOxOD<|^?5}Qr?eHNu@QUb+GU3G_-=SB7Z0XhPWhs=~eyfk1xPKeUlmBSQXZ|*GL@h`EH3m&cT63CJOAEvv z+Qp<=%824KueXDFsMHvK;QCAhHzRA;{`UyHL%v&aW8&80JguvP$3e+D2H-lwh$55h~g zP~R34EXJpr{~kI?!~!Dx9(cEQdHo*@Ec8X!D(qJD(U=zcTQVqNi*I)eO$ed?CjaMI zQ236p#9Da<@=e~RziWraVN36rcVRdL>8>EKl=B`AjMt9_!Fnj8+w}m;HWPse#Wip$Cc{fB7y&V@C*(()$d&EjVp!UBMr;v2DW6+}?Ma|W%yf%tRL zer@Qw(B!uey!7H2W^C2CtENi$aj|=rl#J6dd`T8txS)p%dJ573pT;pXVJ^Z6=H!hR z!GrzKkoZaoPLHJa!CVmw;ld(Ege-~)QI#Uco`O|`fygmltkt*0 zp$GVN77uBrQhLY3KC{?90lLE#{Usy&A_2zXqnwQ%l!+KBEGd(Z|m$TDyLAcyr4>XIo z9V5R0_x#}2h98~7y7{OdY447-lFx~MCit@09X3D z_5Mi^tzS!oY?Dxogiv57&g#RPz?1l@sPAY31!=}_TIkb<@(R9PBW^U*;WQSj*_=<- zeyXBx8f1f9roYz|pVX199Zg|4;{vA9v zvOgZPuxJ;nSarQ38TvV2#V^3P72+b3^|TbwZznuhoZKAx0?g2-w1Bv}n5CeG%#jFI z@fHAK6XFmpb1M#oNoiLUz}amTX!{giP=ST@1l}qvh|bkk;j%Q$BpIhb>|`7dg_GRr z40+^^pv@C!Yb-3nf|zj+~FaS+ufw1mGJhrJ}dUMjQ@m_a-dX0ox4=zcC0 z?hd&@{GpfJ&^M;SGCvuU7NS6MQz=#vl`@T#fc-^|O2O;4P>O&cIj1O)QhuSCNhzX0 zN|{Z4NhyadrHIa?6j8V=J!F8olsnJlJpMkp!uy6CAt>jr&8JYF!`B|&%F)weeJ-Fy{fS@%@dgp!{BXw zLOQfn-@#uUc2<$$H0#r#OrHiRNQ>`+`u22a<~#$j^fE7L8*B8d>5znzHEIv|fvQHo zr#)n=*pBZwl;TX1N5-xmz=J04Ln@kZou1zw+6SAx^uz68B=MnTfX~bu>N_&vJ9tlD zo(Y{yT9ke*6W*%r@L>nYH7k*N_Fb?fintQ8(zi?u;|9Z5?tjo)yB4*qzSO(A_X(XX{Nn!BY5G|DqFgfKvUBPH5G? z>vwgA4A`!Z>kR$ibN%zq&>lY1uXV<@U3xnWI`}^U{PhVsi?Z1&?63-3t-?R7!bYo5 zWEHkpg*8^;3#+irDr~X}J4NBcJ!#{ z)UgxP38N;Cn(+K6b>!G5M(NY~LlR2~)#a|BJ0$IuU6ON}Lw~0$Ow^;g!7^ETv2F3D zZcxLpF}GZD<~sBhJz-LDLNAyrBW*^dfSlG z+WO8a>qUzl1s-_W`!PfVKh9kIdlu)l$^$^2!Y1>j<0egIryQ3S!5TGdp5CG^gon4R3yj6q&2Z2krYs{P zgE=xX@W0<5_8~zW$m&u8tZpWaXZWA~cq40AxGT=#0Ee6@Q;!2=?2Y5ytT8s6Ti4-g z+LVv5#spQLlk6TzEBuYmwx%~<#DW@vq=7x`FdTuO;V4wXF-Uux&1KDXHjll--evRI z0`@+uqgPud^YhlK)T-gCq$$t4!0lpLtZKL>X;KQS9PW};$*qODwNay8QWiGwI{^{c z&m#jn)IE&DRMDzQ?J&pY=K_=2x#8Zo1DA!zT^Me+NJ@}B5x|ouY3eYSoFu8BqD+-g z?|=s9fvrBu^W)Rl&quuz0mArq$kJuFmZHy~D(RF=Xl%5c7` zS#t0yY(O?Ni(9z~9GZU}y%oShJf;tG0r99caI2EC2l(FR`RoVauJjzX7x;kmEfEDs z3sPm9VipuC@VLqtZ|@ch01ElVmirw0?pYG{^Xd#0#_*WT-kvE;wQ5Doog~>WM>0Et z5LYe?80HE|lJW{yPye=b!A!$;z^`T%1i$U6jRUS7b zU4td98V7_pz^;WmLp8~%uw445hNQ5|R#%wzvO9!<#O#{t47IC#eXCfu^m~4&Rm!7P zRy9PGQe`PyuB4)zEgz(uWy=+mL$qu6~kP6cNq?ieQ)%e*Liqvz81h78mQHC!u%uA|j@YF(|6bsas%8Liq= zn9W&NwQC4d8`Uh%NXjH4YH)p}i-EHO z>_cNP<|&>gCbLMPDZ*44OcV;iNg4{#<}o*^FPJ6-D@~2YyLe&BM9Ge5_6RsTgAGiQoQ$0fp$P>j(|{goV2?3s zm|CZ!s%S8F%osI#j0PQp0ZAZ{C}d6hV6$n+*QlU$cE+G!&?u*)V8|#*KxC8!2)0mF zsqd_##^h~ywT&k)_747qPR^?ee3>R`Mc}5bBjIS?BIf~C0(@jn1e*_hYEEK&@m5Lc zPa4C`l1ZQxQCrxHO*mw8PLc;DW@1l6IjMEUAguz6b&TQ;GH%~gc?mNVUTo$?Ws6Eop!=ab5Zw>uVOh+xg$hAgY!l_SEVco2nz+WyS7NRq z(w0z;o++=8{s?ARM<0bLA?YBx#>Wk{6)_iiPb6o}Vsp?5D@_CrQrHgg>!|?h@rbT8 zX+HR1MzTUUn3K|^qS~H$z8<9gm)as1s}iHgk}?!+EYu`F&P4Dim(_N{R$6KsL$s3? zgdi!wXDQnJyZVd&)PGKPr%4_0a5g-%c3Ji_8Lg6fYdfoaodnww87)9KRjU$65Gf4c z_I5GZe-|Q_($+Vm_Cu$07meC zsJ^31_Ter5Lj?&-}mDIL=Tow~s&-mvq7_R*@0 z>lb0DI>tM`bAN#6`R*>sR+{nxztLq4f3MpVxXzv3CqXqIcsh!I**z9ZtA_P}YrJ8P z6zuEXV=27OulLA=H+b`&4Orz#p5JpOHXZ6oO-iqLYD(`F#s87-g7JKQ{)4cbAIqPC zn;6zDIdCD1MM)`qL7!OoCtuTNnR5gN)LJFF5eubSYl;UX(|Mo^QAY4rAIXFdyk9)B z3;-`vk0oF_<*}!m7pMW+8))!>7&Hfv`Qq_lMU6TGv5IjDmL<`JWgNpJ`#uXL+|#!a zOy!IF_Je7hJ^lgvd@En{xYuKZjtJ%etw=yg13br3pZifbG8sldnzVuWg_&aP2H8#C z*xFNc-1xU+s|8(L;HD^`_d@dPwuz86Qg93kB9s<#h`!s~`1yYMFpfXazd!q8JAbEt zIth1s|3pQE3OJC0S4n&KEWh3V47+ufpLwD;yS|;b8=ximM;rkZaSSKgDJ^TgYf+7^A=dtvHZi6EiRyP^~?mZjB0k z?dipswj9(K)7J;B!1TUn`e8cfnL1}7kmR)dGabKV7m>j7NhtA;eMQ-Bb3pteyX zXNF`z7>^(7fq(FYL$AP(eD1UTp@3g{wmbad&HMXgV2gg?o84vqe>lQyp7dOz_2Le^ z{dgaKt_?)pz#`pq42iGwsg@Uyj9|;ldD%#n1p4F11j??D91Jh=fuj-vhvF3NL^+=} zYKtkcKBLLjvc{0ahK?ykD`N<8!^)z@HUK#6%^VjCNXFY^{4~n>kI!dP7WzVC%I<$5 zhB_a8;Yl&5tiTwQJ3$P3enR6oflk$?%@@dVI#gSlRKu}%?F1FzC4Th9BzT!yCw>KI z`8N}L!C7zkODQs2dB)qe@VJF?*OaNiR-NTbr^drFzIAG2Hg6~YV`^9M@VseBY~4=& z?6hQ<#Ai)Y*`=M{mD3IYTfB?UnDHuGzl(>y=51DECRVM`FcW5&$-!NwnOOZe;}>45 z&o1raxj#givU=i&Qoi%`wkWB#d(-6+&LBQ|Z!13jjiD$tD~2eA0%WJ(SPyWKFP@df zKC0kHXBD&Z3O;GJw|>p?b+k1>bQ);p@)s+3_o4wXmlw`n;o1-&tio?_QK0|e^}G4R zHye`VOWsVSY}cDY0cYOa&qz!g=L7-!bvG}6`*9R&%G?Zil|M08;NYdXS){7Z=9WW~ zS$oVa*GZGAZ1*ggDsKr=t^Gvno;`fUJh9%sd9>bJ?}+8wY2`k zeyTh#P_-Gf4vT?m9c_9j2G80WJEg^Tx9Rt@7o0-b#=TEi?Dqbx(9zh7NW`=vn9R4| z!iGLWzk6sl|KXqC|Hbfd;9q|j3{$+@KExXfVyC#&2^aXr(%$GrBNz3Gn+9}z${Kn? zOs8YaC*zIT;Ac9YvS?-$89aJtd=rrA)TUFn5YYNl7q3R(9bMcX5qJNRB$E8VB`*LJ z=r1RL%CCJi5a#jzO9#O_eErg9=oZc|&4qKk`7$T`#(OS%0?(C?m$hU?d-?HY)7hH6 zeE4!NY16qPD&o{$Q=Wl%Z?l@Z;rT0~7~jW-tat?Vx^_hl>a}_WDaNaQJObGEeSG#O zLQiEzcJz}->b&}ip{Vjt?=Zff@AUf6e8x-Cu2@adhR;aafm#KfNLob@Njvy+C%_cm zyle#=;YZ3^*FCx4XC+EK!PHdVXw3|^@&NyIjUT#gT7?LxeCXbDfERut7+dm1YK=3% z01(d}!x_}<(R==jeFDVaeT^Xg;{dN%OCWBtjy5}Fo!IQGbx%XL*$0Ugyhj8~bTagT z=uv8jm`vJI=^=+1z}sz%w`tQ6ayD&EAf3k&#+&As4#et2^NoaZ+;KhuaF)x=pNr?5BBCoBIV z`Upj=h<1E>!w7*-+wvfyYsZ#R2<$dnHB3EQS71xpwgzyCciGk@nkeB-4(}bRHHE!O zZ*EJNTlt+Y-_{K-^Bdb{LNTAQeKUqMJ$LMd4|v1!7NGFH<;jsX`#OjrP5`c3g(3Vb z&dWQ|o{p4HfO>EIXbK!mRIL1wyLTm_!e;OKm-C3|H3SD3$oPb4(Y8odEcjGJP{-4NY*G4w?y6oS7@`2IMW&4`QQc*g2QZIqiIfr~5_l%_| zt5R0zF~g#*D3>~m;j6`Pl+qd-@BBkZjq|625*r-o?>thfR(wejM?RKlu(z3qAr0`;gVUM$GytI)1sBiwv*9(9oh) zp;|3k1zs=B;Q1#JJRc%H4;&&sE!b}yO)nnK1h(_Ach!-2h90ou=q@uhDXWac($30< zFjNjd_F@8_w7Dw7S4JU_%d}!`6}{BQ$D-qx9-%#8ut3g}3FpcDoaf17Bhh&>(|I!C zJelb{-Nzqe+mCp^J-&tvXyVDH5yo*xPt&bcxvR125x(|hlhCrCCFM~UhO(H?hghDT z7{MK<@^N0jQ?cYRpFh<&%)}|-+=c<4GRwQ_)F&V|myYRMr)gscPv4K$F}$i7&#Oum zrGD|(kg5ofL0qcHq&kadq{S+rm7c6Rih~Z zQ#S_BPP9i_7zJh$Z|$R%PEKgPxcp`@UUKd#yL^-{_^lHx@&59gi%~8AauRQLA)1Pv zF626vAyFXlJCx!2OW_c2>uxCFDF5=pk`^_)#5Vw0+a$KR60bKk?fCZOYhOa`oL_8@ z+dpuzeNSWi#vD9b&Fva>Au6$5qkg$&yGH%OJ@s=~U&@DFic^;O%}Ph1@OhV#UL~7udvV4HSAg;~@BjM+W8@BSI>!M5ffx>4bFfAr;eF zts8aH`cfWpwE^y+$YU(-*|I7Gr@EDuyz#Z6cwfR_ieMFAb1j*@eo}{2NDA6z{z{~B z^^eZ{k?ZmGJx+{}C^}N|@QABVPx<)xfSW}sv#+m*)hMWzP^j+?f_Pqhqduu(?TtlH zqW1^@KVcJJC=N`CFGIfl(hhVZ`hrfG!#=pf>g0Z2! zg0sH@)5tqW)lkN}cQW{qn=$;CJ2MRR52|7Ke+SZ^Hc+d6Axfj_TbVF&Hx6h+Z{LRd z`J~%1dM|+KCdY5jnvNW~|JMliQ#`KhKZ_2o8b5TgH?xGF<{9v4(2sCQ{GsckAI7MDCsOE8bnqk2C(Qu$7w zZHGKqtnaZymcOt6UOy5H^|v{#YxjJjO#GZon2F<7p4aJhtbUJaeU##8!|nT#wsmbGM&ycH4igh@Tt=& z$+;|7D?El{X<$Pf2vo%}D%8g^-sBjaW4Dg+1rH~(jmP=MhexwT$9Sv!2OA7#41aJ| zjfGIC^t?-NkSN}t^hx!gXUr;7>LSPH15`(8AJos*gU0e9h}C}%hS(tNmq|OT^!vl1 z9URhM42Op3E6Y9%hrvKjvrZ&D3&-`bk)Xp#-5v$MvdUBXl_=;9r}Qq-Fx|Wh=x3v$ zv+Xn>BeGJZKM(_l99Y9w>Drb#Wo5DO7QnA%T^hg@XhupQzL9~Y#Gr#B24f$kCc~u& z50z;lcuwli#Y2>bVhB9$)1**5Cv&7!K|q){P89AAb8;qUz=`8fCiE?U-6B;j!{6DzDN&AK$9)d^AjN55W=rMSrn?!5iaOk zj7)LDD13gyUrz8EqZH_`IpKw*ix4Xwx#`S}F_sofp)7n$FVd4;(9G`l-m3r21+hp< zyjtShs@^;o^7KtEco-Y4L_YuZruT5p>K z-Qk)(D+!FJ=2(4q612w+OihMndi!KFzzbzqb^?3lto~6$7=rJEdRQY^mTr88fuEhY zm0LnH_>G8Lxec+J#XiLNW<(k0VyFYuf^rA+bB!R8xCl#uzaid6r9cO&u1SH8l;asa zO)wXw7pJ15t=7Lvg-#8wfzv{I#UKoeR(z;d>7xo!*Ap5;Px~Z>jVaiujMC$pz@z%^ z#-RDPmSO86@VtT#;dH#C*ssH%81zG=uZTPI74HHJE|0LoJL%o96Uw6~NdRVtuRw=4 zVZ03mYZwCJtV%41(N(?z#f+lG2rhAq5p@a^2qhEnq(Mxw7(!32BWm~?xiOFgBFK_J#EK*kL4yRcN`F2bQV3$l(xJ8fQVR$) zEA1$EwSc4!J^lg@xP+fcE)|^6&CnsZKvzQd4-!~7UM1-gGE=|M0(#&N26|pgTyWDl zeRxaA!4s@$1~jJTA6h~p%+jy6#N(~DwQmNt&ekVpz$k3}Ed!zg@aIsAQmWUUH6$}8bJF}prItRZ|%TZTUI9Gz*e&iEUZnWb@AHzhyz5iv9SiLn%^{ zoHBiI0&*^jK~_=Ji}mi=&??XzrN5pHgNXkN+2A(w7=3(eIE2wv&-sw4LiXoRzIU`d?)yg>F%yD4hKxG zknEFfWj}X?*|PpY4~W-!cX$*#rXG~+uiDCf?G8n1D{92`CpJLPh=nbRG-t7ww wplnDVbn|8UBY5iIoY_^9z1*Vz@+cIS&3X*uF8%YzVW^(a4?0^4vn1(%0Bo|9ivR!s diff --git a/contracts/tests/sysio.dispatch_tests.cpp b/contracts/tests/sysio.dispatch_tests.cpp index 0bf611d0fe..c8e0d9e30d 100644 --- a/contracts/tests/sysio.dispatch_tests.cpp +++ b/contracts/tests/sysio.dispatch_tests.cpp @@ -610,6 +610,26 @@ class sysio_dispatch_tester : public tester { abi_serializer::create_yield_function(abi_serializer_max_time)); } + /// The first queued outbound attestation of `attest_type`, or a null variant. + /// `queueout` mints sequential ids, so a bounded forward scan finds any a test + /// queued. + fc::variant find_queued_attestation(const char* type_name, uint32_t type_value, + uint64_t scan_until = 32) { + for (uint64_t id = 0; id <= scan_until; ++id) { + auto data = get_row_by_id(MSGCH_ACCOUNT, MSGCH_ACCOUNT, "attestations"_n, id); + if (data.empty()) continue; + auto row = msgch_abi.binary_to_variant( + "attestation_entry", data, + abi_serializer::create_yield_function(abi_serializer_max_time)); + // `type` is an AttestationType; accept either rendering so the helper + // survives a change in how the enum reflects. + const auto& t = row["type"]; + if ((t.is_string() && t.as_string() == type_name) || + (t.is_integer() && t.as_uint64() == type_value)) return row; + } + return fc::variant(); + } + /// Find an operator's balance entry for a (chain_code, token_code) pair. fc::variant find_balance(const fc::variant& op, std::string_view chain_code, @@ -1439,7 +1459,11 @@ BOOST_FIXTURE_TEST_CASE(dispatch_routes_deposit_to_opreg, sysio_dispatch_tester) // unconditional `row.get_object()` then drops the ENTIRE scan cycle rather than // one cell. So the dispatcher drops the attestation -- a check() here would halt // evalcons and stall consensus (feedback_opp_handlers_never_throw). -BOOST_FIXTURE_TEST_CASE(dispatch_drops_uncanonical_token_code, sysio_dispatch_tester) { try { +// A DEPOSIT_REQUEST arrives only after the outpost has taken custody, so an +// unspellable token code must be REFUNDED rather than dropped: a drop leaves the +// depositor with no credit on the depot and no DEPOSIT_REVERT to release the escrow. +// The balance map is keyed by the code, so the credit itself must still not persist. +BOOST_FIXTURE_TEST_CASE(dispatch_refunds_uncanonical_deposit_token_code, sysio_dispatch_tester) { try { bootstrap_for_dispatch(); const auto eth_code = fc::slug_name{"ETH"}.value; @@ -1464,13 +1488,20 @@ BOOST_FIXTURE_TEST_CASE(dispatch_drops_uncanonical_token_code, sysio_dispatch_te sysio::opp::types::ATTESTATION_TYPE_OPERATOR_ACTION, payload); - // The delivery SUCCEEDS -- the attestation is dropped inside dispatch, not reverted. + // The delivery SUCCEEDS -- the rejection happens inside dispatch, never as a revert + // of the envelope (a throw here would halt evalcons and stall consensus). BOOST_REQUIRE_EQUAL(success(), deliver(/*chain_code=*/eth_code, envelope)); - // ...and nothing was persisted, so no stored row can later fail to render. + // Nothing was persisted, so no stored row can later fail to render... const auto after = get_operator(UWRIT_OP); const size_t balances_after = after.is_null() ? 0 : after["balances"].get_array().size(); BOOST_CHECK_EQUAL(balances_before, balances_after); + + // ...and the escrow is released: observe the OUTBOUND revert, not merely the + // absent credit. Without it the depositor's funds sit in outpost custody forever. + BOOST_REQUIRE(!find_queued_attestation( + "ATTESTATION_TYPE_DEPOSIT_REVERT", + sysio::opp::types::ATTESTATION_TYPE_DEPOSIT_REVERT).is_null()); } FC_LOG_AND_RETHROW() } BOOST_FIXTURE_TEST_CASE(dispatch_routes_withdraw_request_to_opreg, sysio_dispatch_tester) { try { diff --git a/contracts/tests/sysio.reserv_tests.cpp b/contracts/tests/sysio.reserv_tests.cpp index ce56f5f5d6..8cb943b568 100644 --- a/contracts/tests/sysio.reserv_tests.cpp +++ b/contracts/tests/sysio.reserv_tests.cpp @@ -353,9 +353,36 @@ class sysio_reserve_tester : public tester { set_abi(MSGCH_ACCOUNT, contracts::msgch_abi().data()); set_privileged(MSGCH_ACCOUNT); produce_blocks(); + const auto* msgch_acct = control->find_account_metadata(MSGCH_ACCOUNT); + BOOST_REQUIRE(msgch_acct != nullptr); + abi_def msgch_def; + BOOST_REQUIRE_EQUAL(abi_serializer::to_abi(msgch_acct->abi, msgch_def), true); + msgch_abi_ser.set_abi(std::move(msgch_def), + abi_serializer::create_yield_function(abi_serializer_max_time)); + } + + /// The first queued outbound attestation of `attest_type`, or a null variant. + /// `queueout` mints sequential ids, so a bounded forward scan finds any a test + /// queued. Requires deploy_msgch(). + fc::variant find_queued_attestation(const char* type_name, uint32_t type_value, + uint64_t scan_until = 32) { + for (uint64_t id = 0; id <= scan_until; ++id) { + auto data = get_row_by_id(MSGCH_ACCOUNT, MSGCH_ACCOUNT, "attestations"_n, id); + if (data.empty()) continue; + auto row = msgch_abi_ser.binary_to_variant( + "attestation_entry", data, + abi_serializer::create_yield_function(abi_serializer_max_time)); + // `type` is an AttestationType; accept either rendering so the helper + // survives a change in how the enum reflects. + const auto& t = row["type"]; + if ((t.is_string() && t.as_string() == type_name) || + (t.is_integer() && t.as_uint64() == type_value)) return row; + } + return fc::variant(); } abi_serializer abi_ser; + abi_serializer msgch_abi_ser; abi_serializer token_abi_ser; abi_serializer authex_abi_ser; abi_serializer chains_abi_ser; @@ -563,6 +590,43 @@ BOOST_FIXTURE_TEST_CASE(oncrtreserve_unregistered_chain_soft_skips_before_queueo BOOST_REQUIRE(find_reserve("NOCHAIN", "ETH", "USERRES").is_null()); } FC_LOG_AND_RETHROW() } +// A reserve_code is chosen by the CALLER on the outpost -- ETH `create_reserve` and SOL +// `create_reserve_handler` validate only that it is non-zero -- and the creator's escrow is +// taken BEFORE the RESERVE_CREATE goes out. A code with no canonical spelling must therefore +// be REFUNDED, never dropped: a drop strands the funds in the outpost's PENDING record with +// no cancel path, since the outpost cannot release escrow on its own authority. +BOOST_FIXTURE_TEST_CASE(oncrtreserve_uncanonical_code_is_cancelled, sysio_reserve_tester) { try { + deploy_msgch(); + + // Below the leading symbol's floor: decodes to "" and packs back to 0, so it is + // not a code and has no spelling. + constexpr uint64_t uncanonical = 7; + BOOST_REQUIRE(!fc::slug_name{uncanonical}.is_canonical()); + + BOOST_REQUIRE_EQUAL(success(), push_action(MSGCH_ACCOUNT, "oncrtreserve"_n, mvo() + ("chain_code", "ETH") + ("token_code", "ETH") + ("reserve_code", mvo()("value", uncanonical)) + ("name", "user reserve") + ("description", "") + ("external_token_amount", 1000) + ("requested_wire_amount", 1000) + ("source_token_precision", 9u) + ("connector_weight_bps", 5000) + ("creator_chain_kind", ChainKind::CHAIN_KIND_EVM) + ("creator_chain_addr", std::vector(20, '\x01')) + ("is_private", false) + ("creator_pub_key", std::vector(33, '\x02')) + )); + + // The refund is what matters: observe the OUTBOUND cancellation, not merely the + // absence of a depot row (the row's key IS the unspellable triple, so it could + // not be looked up by spelling anyway). + BOOST_REQUIRE(!find_queued_attestation( + "ATTESTATION_TYPE_RESERVE_CREATE_CANCELLED", + sysio::opp::types::ATTESTATION_TYPE_RESERVE_CREATE_CANCELLED).is_null()); +} FC_LOG_AND_RETHROW() } + BOOST_FIXTURE_TEST_CASE(oncrtreserve_unlinked_creator_is_cancelled, sysio_reserve_tester) { try { // Create gating: no authex link exists for the creator's pubkey (the // authex account carries no links table here), so the request must be From af01d74979a9ec58a06d84982744a4766058556f Mon Sep 17 00:00:00 2001 From: kevin Heifner Date: Tue, 22 Sep 2026 16:32:57 -0500 Subject: [PATCH 15/29] =?UTF-8?q?fix(chain):=20convert=20a=20slug=5Fname?= =?UTF-8?q?=20totally=20=E2=80=94=20never=20throw,=20never=20fall=20back?= =?UTF-8?q?=20to=20hex?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Nothing validates a raw uint64 for name either: basic_name(uint64_t) is bare in both repos and serialization is member-reflected, so unpack assigns the 8 bytes without running a constructor. A slug holding a value that spells nothing is therefore self-inflicted, exactly as it is for name, and now gets name's treatment — convert and take what you get. to_variant drops its canonicality assert. That also removes a real hazard: underwriter_plugin::scan_cycle wraps its whole pass in one try, so a throwing renderer cost the entire cycle, not one cell. The four JSON key-output hex fallbacks go with it (per-row key, primary cursor, secondary cursor, read-only variant). json=true now has ONE key shape: an unrepresentable key type rejects as a JSON bound already did, instead of emitting a cursor the bound parser would refuse. json=false is unchanged — hex is its requested form, not a fallback. Change-Id: I34d751a8ebfcf5fa311dfdd3c753c89d98509ff6 --- .../include/sysio/chain/database_utils.hpp | 7 +- libraries/libfc/include/fc/slug_name.hpp | 20 +++-- libraries/libfc/test/test_slug_name.cpp | 39 +++++++-- plugins/chain_plugin/src/chain_plugin.cpp | 81 +++++++++---------- unittests/abi_tests.cpp | 14 ++-- unittests/be_key_codec_tests.cpp | 27 ++++--- 6 files changed, 110 insertions(+), 78 deletions(-) diff --git a/libraries/chain/include/sysio/chain/database_utils.hpp b/libraries/chain/include/sysio/chain/database_utils.hpp index 9b417fc8ea..19a0eb4281 100644 --- a/libraries/chain/include/sysio/chain/database_utils.hpp +++ b/libraries/chain/include/sysio/chain/database_utils.hpp @@ -366,9 +366,10 @@ inline fc::variant decode_field(reader& r, key_leaf_kind kind) { case key_leaf_kind::name: return fc::variant(name(r.read_be64()).to_string()); case key_leaf_kind::slug_name: { // Delegates to fc::slug_name's to_variant, so next_key carries the same - // canonical string the row's key field does, and feeding it back as a - // bound re-encodes the identical bytes. A stored key with no spelling - // throws; get_table_rows catches per row and falls back to hex. + // string the row's key field does, and feeding a canonical one back as a + // bound re-encodes the identical bytes. The renderer is TOTAL (parity with + // the `name` arm above), so a stored key with no canonical spelling renders + // rather than throwing — lossily, exactly as `name` does. const fc::slug_name s{ r.read_be64() }; fc::variant v; fc::to_variant(s, v); diff --git a/libraries/libfc/include/fc/slug_name.hpp b/libraries/libfc/include/fc/slug_name.hpp index 5cc1b102a9..9b2199093d 100644 --- a/libraries/libfc/include/fc/slug_name.hpp +++ b/libraries/libfc/include/fc/slug_name.hpp @@ -123,14 +123,20 @@ using slug_name_literals::operator""_s; /// `uint64` becomes a slug (`sysio.msgch`'s dispatch path and the opreg/uwrit /// /reserv writers). Until that lands, a stored one is a defect that surfaces /// here rather than being silently rendered as something it is not. +/// Render a slug as its spelling. TOTAL, exactly like sysio::chain::name — a +/// renderer is a READ path, and a throwing one turns one bad row into a failure +/// of everything that scans it (an unspellable code would stall every +/// underwriter_plugin commit, not just that cell). Validation lives on the WRITE +/// path: from_variant's string arm goes through the validating constructor, so a +/// non-canonical spelling is refused at construction. +/// +/// A value with no canonical spelling renders as whatever it decodes to, and is +/// lossy in the same two ways name is: one with a non-empty char[0] renders a +/// string from_variant then rejects (loud), while one below the 1<<42 floor has +/// an empty char[0] and renders "" — indistinguishable from zero (silent). Both +/// are name's behaviour; neither can be prevented here, because a prepacked +/// binary action sets the reflected `value` directly for either type. inline void to_variant(const slug_name& s, fc::variant& v) { - // is_canonical() is the shared predicate: the spelling must be a valid - // literal AND pack back to this exact value. A round trip alone is not - // enough — a value packed from an illegal spelling (say pack("0")) round- - // trips yet is not a code, so emitting it would produce a string - // from_variant then refuses. - FC_ASSERT(s.is_canonical(), - "slug_name {} is not a code and has no string spelling", s.value); v = s.to_string(); } diff --git a/libraries/libfc/test/test_slug_name.cpp b/libraries/libfc/test/test_slug_name.cpp index 2e2b6c401c..2aa5989a39 100644 --- a/libraries/libfc/test/test_slug_name.cpp +++ b/libraries/libfc/test/test_slug_name.cpp @@ -449,21 +449,46 @@ BOOST_AUTO_TEST_CASE(variant_zero_is_the_empty_string_both_ways) { BOOST_CHECK_EQUAL(back.value, 0u); } -BOOST_AUTO_TEST_CASE(variant_non_canonical_has_no_spelling_and_throws) { - // Every value below 1<<42 has a zero in the char[0] slot, so to_string() - // truncates it to "" and no string recovers it. Rather than grow a second - // carrier for those, the conversion refuses them — which is what keeps "" - // meaning exactly zero. The throw is contained: get_table_rows catches per - // row and renders that cell as hex. +BOOST_AUTO_TEST_CASE(variant_render_is_total_like_name) { + // The renderer never throws — parity with sysio::chain::name, whose key leaf + // is `name(raw).to_string()`. A read path that throws converts one bad row + // into a failure of every scan over it; validation belongs on the write path. + // A prepacked binary action can set the reflected `value` directly for either + // type, so neither can guarantee it only ever holds a canonical value. for (uint64_t raw : {uint64_t{1}, uint64_t{7}, uint64_t{42}, uint64_t{(uint64_t{1} << 42) - 1}, (uint64_t{1} << 48) - 1, // symbols past the alphabet ~uint64_t{0}}) { fc::variant v; - BOOST_CHECK_THROW(fc::to_variant(slug_name{raw}, v), fc::exception); + BOOST_REQUIRE_NO_THROW(fc::to_variant(slug_name{raw}, v)); + BOOST_CHECK(v.is_string()); } } +BOOST_AUTO_TEST_CASE(variant_non_canonical_render_is_lossy_the_same_two_ways_name_is) { + // Losing information is the price of a total renderer, and it takes exactly + // two shapes. Pinned so a future change to to_string() cannot move them + // silently. + fc::variant v; + + // 1. char[0] non-empty -> a spelling exists but is not a legal code, so + // feeding it back fails LOUDLY at construction. + // '7' is symbol 34 in the alphabet, and char[0] occupies bits [42..47]. + constexpr uint64_t packed_seven = uint64_t{34} << 42; + BOOST_REQUIRE(!fc::slug_name{packed_seven}.is_canonical()); + fc::to_variant(slug_name{packed_seven}, v); + BOOST_CHECK_EQUAL(v.as_string(), "7"); + slug_name back; + BOOST_CHECK_THROW(fc::from_variant(v, back), fc::exception); + + // 2. Below the 1<<42 floor -> char[0] is empty, so it renders "" and is + // indistinguishable from zero. This one is SILENT; name has the same hole. + fc::to_variant(slug_name{uint64_t{7}}, v); + BOOST_CHECK_EQUAL(v.as_string(), ""); + fc::from_variant(v, back); + BOOST_CHECK_EQUAL(back.value, 0u); +} + BOOST_AUTO_TEST_CASE(variant_every_canonical_value_round_trips_exactly) { // Injectivity across the boundary for the whole canonical range, including // its floor (1<<42 is "A") and the zero sentinel. diff --git a/plugins/chain_plugin/src/chain_plugin.cpp b/plugins/chain_plugin/src/chain_plugin.cpp index 70977ebbcf..06f83dc7ed 100644 --- a/plugins/chain_plugin/src/chain_plugin.cpp +++ b/plugins/chain_plugin/src/chain_plugin.cpp @@ -2456,10 +2456,11 @@ read_only::get_table_rows( const read_only::get_table_rows_params& p, const fc:: auto key_types = tbl.key_types; // Resolve the ABI-aware encode/decode plan once per request. nullopt when a - // key type is genuinely unrepresentable. The two JSON paths then diverge: a - // JSON bound is REJECTED (clear contract_table_query_exception below), while - // JSON key OUTPUT — the decoded `key` field and the `next_key` cursor — falls - // back to hex. Hex bounds and hex key output are unaffected either way. + // key type is genuinely unrepresentable. json=true then has exactly ONE key + // shape: both the bound and the key OUTPUT (the `key` field and the `next_key` + // cursor) reject with contract_table_query_exception. Falling output back to + // hex while bounds reject it would hand the caller a cursor json=true cannot + // accept — a page-two dead end. json=false is unaffected: hex IS its form. // No in-tree table declares such a key type today (uint256/int256 have no CDT // producer), so the nullopt branches below are defensive; the codec rejection // that drives them is unit-tested in be_key_codec_tests `rejections`. @@ -2470,8 +2471,8 @@ read_only::get_table_rows( const read_only::get_table_rows_params& p, const fc:: // Unrepresentable key type (e.g. uint256/int256): leave key_shapes unset so a // JSON bound is rejected with a clear error while JSON key output falls back to // hex (hex bounds/output keep working). Log so the fallback is diagnosable. - dlog("be_key_codec: table {} has no representable JSON key shape; JSON bounds rejected, " - "JSON key output falls back to hex ({})", + dlog("be_key_codec: table {} has no representable JSON key shape; json=true is rejected " + "for both bounds and key output, json=false still serves hex ({})", p.table, e.top_message()); } @@ -2755,21 +2756,20 @@ read_only::get_table_rows( const read_only::get_table_rows_params& p, const fc:: // a JSON object matching the bound syntax (e.g. `{"byowner":"u4"}`) so // `upper_bound = next_key` round-trips through the bound parser, which // expects JSON when `p.json` is set. Otherwise emit hex of the raw sk - // bytes. Falls back to hex on any decode failure. + // bytes. json=true has ONE shape here too: an unrepresentable key type + // rejects rather than emitting hex the bound parser would then refuse. auto emit_secondary_next_key = [&](std::string_view sk) { if (p.json) { - try { - std::string_view sv = sk; - if (!scope_prefix_bytes.empty() && sv.size() >= scope_prefix_bytes.size()) { - sv.remove_prefix(scope_prefix_bytes.size()); - } - FC_ASSERT(bound_key_shapes, "be_key_codec: key shape unresolved (unrepresentable key type); falling back to hex"); - auto key_var = chain::be_key_codec::decode_key(sv.data(), sv.size(), *bound_key_shapes); - hp.next_key = fc::json::to_string(key_var, fc::time_point::maximum()); - return; - } catch (...) { - // fall through to hex + std::string_view sv = sk; + if (!scope_prefix_bytes.empty() && sv.size() >= scope_prefix_bytes.size()) { + sv.remove_prefix(scope_prefix_bytes.size()); } + SYS_ASSERT(bound_key_shapes, chain::contract_table_query_exception, + "Table {} key type is not representable as JSON; use json=false for hex keys", + p.table); + auto key_var = chain::be_key_codec::decode_key(sv.data(), sv.size(), *bound_key_shapes); + hp.next_key = fc::json::to_string(key_var, fc::time_point::maximum()); + return; } hp.next_key = fc::to_hex(sk.data(), sk.size()); }; @@ -2869,14 +2869,12 @@ read_only::get_table_rows( const read_only::get_table_rows_params& p, const fc:: fc::mutable_variant_object obj; // For secondary queries, decode the primary key as the key field if (p.json) { - try { - FC_ASSERT(key_shapes, "be_key_codec: key shape unresolved (unrepresentable key type); falling back to hex"); - auto full_key = chain::be_key_codec::decode_key( - row.key.data(), row.key.size(), *key_shapes); - obj["key"] = strip_scope_fields(std::move(full_key), scope_key_count); - } catch (...) { - obj["key"] = fc::to_hex(row.key.data(), row.key.size()); - } + SYS_ASSERT(key_shapes, chain::contract_table_query_exception, + "Table {} key type is not representable as JSON; use json=false for hex keys", + table_name); + auto full_key = chain::be_key_codec::decode_key( + row.key.data(), row.key.size(), *key_shapes); + obj["key"] = strip_scope_fields(std::move(full_key), scope_key_count); } else { obj["key"] = fc::to_hex(row.key.data(), row.key.size()); } @@ -2920,14 +2918,12 @@ read_only::get_table_rows( const read_only::get_table_rows_params& p, const fc:: auto collect_next_key = [&](const chain::kv_object& obj) { auto kv = obj.key_view(); if (p.json) { - try { - FC_ASSERT(key_shapes, "be_key_codec: key shape unresolved (unrepresentable key type); falling back to hex"); - auto full_key = chain::be_key_codec::decode_key(kv.data(), kv.size(), *key_shapes); - auto stripped = strip_scope_fields(std::move(full_key), scope_key_count); - hp.next_key = fc::json::to_string(stripped, fc::time_point::maximum()); - } catch (...) { - hp.next_key = fc::to_hex(kv.data(), static_cast(kv.size())); - } + SYS_ASSERT(key_shapes, chain::contract_table_query_exception, + "Table {} key type is not representable as JSON; use json=false for hex keys", + p.table); + auto full_key = chain::be_key_codec::decode_key(kv.data(), kv.size(), *key_shapes); + auto stripped = strip_scope_fields(std::move(full_key), scope_key_count); + hp.next_key = fc::json::to_string(stripped, fc::time_point::maximum()); } else { hp.next_key = fc::to_hex(kv.data(), static_cast(kv.size())); } @@ -3043,16 +3039,15 @@ read_only::get_table_rows( const read_only::get_table_rows_params& p, const fc:: for (auto& row : hp.rows) { fc::mutable_variant_object obj; - // Decode key -- fall back to hex if BE decode fails + // Decode key. json=true has ONE key shape, same as the bound path: an + // unrepresentable key type rejects rather than emitting a second form. if (hp.json) { - try { - FC_ASSERT(key_shapes, "be_key_codec: key shape unresolved (unrepresentable key type); falling back to hex"); - auto full_key = chain::be_key_codec::decode_key( - row.key.data(), row.key.size(), *key_shapes); - obj("key", strip_scope_fields(std::move(full_key), scope_key_count)); - } catch (...) { - obj("key", fc::to_hex(row.key.data(), static_cast(row.key.size()))); - } + SYS_ASSERT(key_shapes, chain::contract_table_query_exception, + "Table {} key type is not representable as JSON; use json=false for hex keys", + tbl_name); + auto full_key = chain::be_key_codec::decode_key( + row.key.data(), row.key.size(), *key_shapes); + obj("key", strip_scope_fields(std::move(full_key), scope_key_count)); } else { obj("key", fc::to_hex(row.key.data(), static_cast(row.key.size()))); } diff --git a/unittests/abi_tests.cpp b/unittests/abi_tests.cpp index 3e4d675e34..1df3b3fa48 100644 --- a/unittests/abi_tests.cpp +++ b/unittests/abi_tests.cpp @@ -753,13 +753,15 @@ BOOST_AUTO_TEST_CASE(slug_name_builtin_type) abis.variant_to_binary("regrow", fc::json::from_string(R"({"code":7})"), yield_fn()), fc::exception); - // And a value with no spelling does not render. Every value below 2^42 has a - // zero in the leading symbol slot, so `to_string` truncates it to "" and no - // string recovers it. get_table_rows wraps each row's render in its own - // try/catch and falls back to hex, so a planted row costs that one cell - // rather than the query (plugins/chain_plugin/src/chain_plugin.cpp). + // A value with no spelling renders anyway — you get what you get. Every value + // below 2^42 has a zero in the leading symbol slot, so `to_string` truncates + // it to "", the same text zero renders. The conversion is TOTAL, exactly like + // `name` (`database_utils.hpp`: `name(raw).to_string()`): a read path that + // throws costs the whole scan, and a raw uint64 that spells nothing is + // self-inflicted — nothing validates the raw ctor for either type. const std::vector planted{ 7, 0, 0, 0, 0, 0, 0, 0 }; // packed LE uint64 7 - BOOST_CHECK_THROW(abis.binary_to_variant("regrow", planted, yield_fn()), fc::exception); + auto rendered = abis.binary_to_variant("regrow", planted, yield_fn()); + BOOST_CHECK_EQUAL(rendered.get_object()["code"].as_string(), ""); } FC_LOG_AND_RETHROW() } diff --git a/unittests/be_key_codec_tests.cpp b/unittests/be_key_codec_tests.cpp index 994244ffd3..9f0d6aff0c 100644 --- a/unittests/be_key_codec_tests.cpp +++ b/unittests/be_key_codec_tests.cpp @@ -107,28 +107,30 @@ BOOST_AUTO_TEST_CASE(slug_name_leaf_roundtrips_the_zero_sentinel_as_empty) { BOOST_CHECK(bytes == encode_single(abi, "composite_key", slug(0))); } -BOOST_AUTO_TEST_CASE(slug_name_leaf_refuses_a_non_canonical_value_both_ways) { +BOOST_AUTO_TEST_CASE(slug_name_leaf_decodes_a_non_canonical_value_lossily) { // A value below 2^42 has no string spelling (to_string truncates at the first - // zero symbol slot), so it is not writable as a bound and not renderable as a - // key. Both directions throw rather than silently collapsing to "" — which - // would re-encode to 0 and restart pagination at the top of the table. - // get_table_rows catches per row and falls back to hex, so a stored key like - // this costs that one cell, not the query. + // zero symbol slot). The leaf converts it anyway — you get what you get, + // identical to the `name` leaf (`name(raw).to_string()`). It neither throws + // nor falls back to hex: a read path that throws costs the whole scan, and a + // raw uint64 that spells nothing is self-inflicted, since nothing validates + // the raw ctor for either type. auto abi = make_test_abi(); auto shapes = codec::build_key_shapes(abi, {"code"}, {"slug_name"}); - // No bound can name it: the integer is refused, and there is no spelling. + // A bound is still named by its SPELLING, so a bare integer is not one. BOOST_CHECK_THROW( codec::encode_key(fc::variant(fc::mutable_variant_object("code", 7u)), shapes), fc::exception); - // And a key already holding one does not decode. Reach past the carrier to - // build those bytes — the transitional object arm is the only writer left - // that can express a raw value. + // A key already holding one decodes to "" — the same text zero renders, so + // feeding it back re-encodes to 0. That is the price of a total conversion. + // Reach past the carrier to build those bytes: the transitional object arm + // is the only writer left that can express a raw value. auto bytes = codec::encode_key( fc::variant(fc::mutable_variant_object("code", slug(7))), shapes); BOOST_REQUIRE_EQUAL(bytes.size(), 8u); - BOOST_CHECK_THROW(codec::decode_key(bytes.data(), bytes.size(), shapes), fc::exception); + auto decoded = codec::decode_key(bytes.data(), bytes.size(), shapes); + BOOST_CHECK_EQUAL(decoded.get_object()["code"].as_string(), ""); } BOOST_AUTO_TEST_CASE(slug_name_leaf_wins_over_a_shadowing_struct_def) { @@ -329,7 +331,8 @@ BOOST_AUTO_TEST_CASE(rejections) { // 256-bit integers have no codec leaf and no CDT producer. build_key_shapes // must reject them — this is exactly what drives chain_plugin to leave - // key_shapes unset and fall back to hex bounds (the defensive nullopt path). + // key_shapes unset, so json=true rejects and hex bounds are the only form + // (the defensive nullopt path). BOOST_CHECK_THROW(codec::build_key_shapes(abi, {"k"}, {"uint256"}), fc::exception); BOOST_CHECK_THROW(codec::build_key_shapes(abi, {"k"}, {"int256"}), fc::exception); From b6cfee0ca8f85826cfce1109293379e1356f9cd5 Mon Sep 17 00:00:00 2001 From: kevin Heifner Date: Tue, 22 Sep 2026 17:28:07 -0500 Subject: [PATCH 16/29] fix(chain): name a slug key only when it can be named; hex otherwise MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit slug_name's to_string is not injective over raw uint64s — 38 of the 64 symbol values are used, symbol 0 terminates, and bits 48-63 are never read — so naming a non-canonical key emits a string that re-encodes to a DIFFERENT key, and a cursor built from it resumes in the wrong place. `name` is not comparable: its alphabet is exactly 2^5 with no gaps and it consumes all 64 bits, so its trim-and-repack IS lossless. Treating the two as equivalent was the error in the previous commit. The key leaf now refuses to name a value it cannot, and key output falls back to bare hex — the same escape every other undecodable ABI value uses, exact by construction. The json=true bound parser accepts that hex straight back, which is the half that was missing: encode(decode(key)) == key now holds for every key, so next_key is always feedable. fc::slug_name::to_variant stays TOTAL — a display cell may be lossy, a resume token may not. get_table_tests covers the page boundary forward and reverse across a key stored through the transitional object carrier; be_key_codec_tests pins the round-trip property and both non-canonical shapes. plugin_test 297/297, be_key_codec_tests 20, abi_tests 65, codename 40. Change-Id: I422eaf4f823f0de29fe077b6e8c8e6c98acb681f --- .../include/sysio/chain/database_utils.hpp | 22 ++- plugins/chain_plugin/src/chain_plugin.cpp | 132 ++++++++++-------- tests/get_table_tests.cpp | 52 +++++++ unittests/be_key_codec_tests.cpp | 56 +++++--- 4 files changed, 180 insertions(+), 82 deletions(-) diff --git a/libraries/chain/include/sysio/chain/database_utils.hpp b/libraries/chain/include/sysio/chain/database_utils.hpp index 19a0eb4281..5acc97a085 100644 --- a/libraries/chain/include/sysio/chain/database_utils.hpp +++ b/libraries/chain/include/sysio/chain/database_utils.hpp @@ -365,12 +365,24 @@ inline fc::variant decode_field(reader& r, key_leaf_kind kind) { } case key_leaf_kind::name: return fc::variant(name(r.read_be64()).to_string()); case key_leaf_kind::slug_name: { - // Delegates to fc::slug_name's to_variant, so next_key carries the same - // string the row's key field does, and feeding a canonical one back as a - // bound re-encodes the identical bytes. The renderer is TOTAL (parity with - // the `name` arm above), so a stored key with no canonical spelling renders - // rather than throwing — lossily, exactly as `name` does. + // A canonical value renders as its spelling, and feeding that back as a + // bound re-encodes the identical bytes. + // + // One with NO canonical spelling cannot be named. Unlike the `name` arm + // above, to_string is not injective over raw uint64s here: 38 of the 64 + // symbol values are used, symbol 0 terminates, and bits 48-63 are never + // read — so a rendered string would re-encode to a DIFFERENT key, and a + // cursor built from it would resume in the wrong place. `name`'s alphabet + // is exactly 2^5 with no gaps and consumes all 64 bits, so its trim-and- + // repack IS lossless; the two are not symmetric. + // + // Fail instead, and the caller emits hex of the raw key bytes — the same + // thing every other undecodable ABI value does, and exact by construction. + // fc::slug_name::to_variant stays TOTAL: a display cell may be lossy, a + // resume token may not. const fc::slug_name s{ r.read_be64() }; + FC_ASSERT(s.is_canonical(), + "slug_name {} has no canonical spelling; key renders as hex", s.value); fc::variant v; fc::to_variant(s, v); return v; diff --git a/plugins/chain_plugin/src/chain_plugin.cpp b/plugins/chain_plugin/src/chain_plugin.cpp index 06f83dc7ed..ce6f97aa27 100644 --- a/plugins/chain_plugin/src/chain_plugin.cpp +++ b/plugins/chain_plugin/src/chain_plugin.cpp @@ -2456,11 +2456,11 @@ read_only::get_table_rows( const read_only::get_table_rows_params& p, const fc:: auto key_types = tbl.key_types; // Resolve the ABI-aware encode/decode plan once per request. nullopt when a - // key type is genuinely unrepresentable. json=true then has exactly ONE key - // shape: both the bound and the key OUTPUT (the `key` field and the `next_key` - // cursor) reject with contract_table_query_exception. Falling output back to - // hex while bounds reject it would hand the caller a cursor json=true cannot - // accept — a page-two dead end. json=false is unaffected: hex IS its form. + // key type is genuinely unrepresentable. Key OUTPUT (the `key` field and the + // `next_key` cursor) then falls back to bare hex, and the json=true BOUND + // parser accepts hex straight back — so a cursor is always feedable, which is + // what page two needs. The same escape covers a leaf the codec can decode but + // cannot NAME (a slug_name with no canonical spelling). // No in-tree table declares such a key type today (uint256/int256 have no CDT // producer), so the nullopt branches below are defensive; the codec rejection // that drives them is unit-tested in be_key_codec_tests `rejections`. @@ -2468,11 +2468,11 @@ read_only::get_table_rows( const read_only::get_table_rows_params& p, const fc:: try { key_shapes = chain::be_key_codec::build_key_shapes(abi, key_names, key_types); } catch (const fc::exception& e) { - // Unrepresentable key type (e.g. uint256/int256): leave key_shapes unset so a - // JSON bound is rejected with a clear error while JSON key output falls back to - // hex (hex bounds/output keep working). Log so the fallback is diagnosable. - dlog("be_key_codec: table {} has no representable JSON key shape; json=true is rejected " - "for both bounds and key output, json=false still serves hex ({})", + // Unrepresentable key type (e.g. uint256/int256): leave key_shapes unset so + // key output falls back to hex, which the bound parser takes back verbatim. + // Log so the fallback is diagnosable. + dlog("be_key_codec: table {} has no representable JSON key shape; keys render as hex " + "and hex bounds are accepted on the way back in ({})", p.table, e.top_message()); } @@ -2611,33 +2611,33 @@ read_only::get_table_rows( const read_only::get_table_rows_params& p, const fc:: bound_key_shapes->begin() + static_cast(scope_key_count)); } - // Parse bounds: when json=true, bounds are JSON key objects; when json=false, hex strings. - std::vector lb_bytes; - if (!effective_lower.empty()) { - if (p.json) { + // Parse bounds. When json=true a bound is a JSON key OBJECT — or the bare hex a + // cursor emits when the codec could not NAME the key (an unrepresentable key + // type, or a slug_name with no canonical spelling). Hex IS the raw key bytes, + // so feeding next_key straight back always re-encodes the identical key, which + // is the property pagination needs. json=false is hex only. + auto parse_bound = [&](const std::string& bound) { + std::vector bytes; + if (p.json && !bound.empty() && bound.front() == '{') { SYS_ASSERT(bound_key_shapes, chain::contract_table_query_exception, "Table {} key type is not representable as a JSON bound; use hex bounds", p.table); - auto lb_var = fc::json::from_string(effective_lower); - lb_bytes = chain::be_key_codec::encode_key(lb_var, *bound_key_shapes); + bytes = chain::be_key_codec::encode_key(fc::json::from_string(bound), *bound_key_shapes); } else { - auto v = fc::from_hex(effective_lower); - lb_bytes.assign(reinterpret_cast(v.data()), - reinterpret_cast(v.data()) + v.size()); + auto v = fc::from_hex(bound); + bytes.assign(reinterpret_cast(v.data()), + reinterpret_cast(v.data()) + v.size()); } + return bytes; + }; + + std::vector lb_bytes; + if (!effective_lower.empty()) { + lb_bytes = parse_bound(effective_lower); } std::vector ub_bytes; bool has_upper = !effective_upper.empty(); if (has_upper) { - if (p.json) { - SYS_ASSERT(bound_key_shapes, chain::contract_table_query_exception, - "Table {} key type is not representable as a JSON bound; use hex bounds", p.table); - auto ub_var = fc::json::from_string(effective_upper); - ub_bytes = chain::be_key_codec::encode_key(ub_var, *bound_key_shapes); - } else { - auto v = fc::from_hex(effective_upper); - ub_bytes.assign(reinterpret_cast(v.data()), - reinterpret_cast(v.data()) + v.size()); - } + ub_bytes = parse_bound(effective_upper); } // For find: upper bound must be exclusive, so increment the encoded bytes @@ -2756,20 +2756,22 @@ read_only::get_table_rows( const read_only::get_table_rows_params& p, const fc:: // a JSON object matching the bound syntax (e.g. `{"byowner":"u4"}`) so // `upper_bound = next_key` round-trips through the bound parser, which // expects JSON when `p.json` is set. Otherwise emit hex of the raw sk - // bytes. json=true has ONE shape here too: an unrepresentable key type - // rejects rather than emitting hex the bound parser would then refuse. + // bytes. A key the codec cannot name falls back to hex, which the json=true + // bound parser accepts back verbatim — so next_key is always feedable. auto emit_secondary_next_key = [&](std::string_view sk) { if (p.json) { - std::string_view sv = sk; - if (!scope_prefix_bytes.empty() && sv.size() >= scope_prefix_bytes.size()) { - sv.remove_prefix(scope_prefix_bytes.size()); + try { + std::string_view sv = sk; + if (!scope_prefix_bytes.empty() && sv.size() >= scope_prefix_bytes.size()) { + sv.remove_prefix(scope_prefix_bytes.size()); + } + FC_ASSERT(bound_key_shapes, "be_key_codec: key shape unresolved"); + auto key_var = chain::be_key_codec::decode_key(sv.data(), sv.size(), *bound_key_shapes); + hp.next_key = fc::json::to_string(key_var, fc::time_point::maximum()); + return; + } catch (...) { + // fall through to hex } - SYS_ASSERT(bound_key_shapes, chain::contract_table_query_exception, - "Table {} key type is not representable as JSON; use json=false for hex keys", - p.table); - auto key_var = chain::be_key_codec::decode_key(sv.data(), sv.size(), *bound_key_shapes); - hp.next_key = fc::json::to_string(key_var, fc::time_point::maximum()); - return; } hp.next_key = fc::to_hex(sk.data(), sk.size()); }; @@ -2869,12 +2871,18 @@ read_only::get_table_rows( const read_only::get_table_rows_params& p, const fc:: fc::mutable_variant_object obj; // For secondary queries, decode the primary key as the key field if (p.json) { - SYS_ASSERT(key_shapes, chain::contract_table_query_exception, - "Table {} key type is not representable as JSON; use json=false for hex keys", - table_name); - auto full_key = chain::be_key_codec::decode_key( - row.key.data(), row.key.size(), *key_shapes); - obj["key"] = strip_scope_fields(std::move(full_key), scope_key_count); + // A key the codec cannot name comes back as bare hex — the same escape + // every other undecodable ABI value uses. It is exact (hex IS the raw + // bytes) and the json=true bound parser accepts it back, so next_key + // stays feedable. + try { + FC_ASSERT(key_shapes, "be_key_codec: key shape unresolved"); + auto full_key = chain::be_key_codec::decode_key( + row.key.data(), row.key.size(), *key_shapes); + obj["key"] = strip_scope_fields(std::move(full_key), scope_key_count); + } catch (...) { + obj["key"] = fc::to_hex(row.key.data(), row.key.size()); + } } else { obj["key"] = fc::to_hex(row.key.data(), row.key.size()); } @@ -2918,12 +2926,14 @@ read_only::get_table_rows( const read_only::get_table_rows_params& p, const fc:: auto collect_next_key = [&](const chain::kv_object& obj) { auto kv = obj.key_view(); if (p.json) { - SYS_ASSERT(key_shapes, chain::contract_table_query_exception, - "Table {} key type is not representable as JSON; use json=false for hex keys", - p.table); - auto full_key = chain::be_key_codec::decode_key(kv.data(), kv.size(), *key_shapes); - auto stripped = strip_scope_fields(std::move(full_key), scope_key_count); - hp.next_key = fc::json::to_string(stripped, fc::time_point::maximum()); + try { + FC_ASSERT(key_shapes, "be_key_codec: key shape unresolved"); + auto full_key = chain::be_key_codec::decode_key(kv.data(), kv.size(), *key_shapes); + auto stripped = strip_scope_fields(std::move(full_key), scope_key_count); + hp.next_key = fc::json::to_string(stripped, fc::time_point::maximum()); + } catch (...) { + hp.next_key = fc::to_hex(kv.data(), static_cast(kv.size())); + } } else { hp.next_key = fc::to_hex(kv.data(), static_cast(kv.size())); } @@ -3039,15 +3049,17 @@ read_only::get_table_rows( const read_only::get_table_rows_params& p, const fc:: for (auto& row : hp.rows) { fc::mutable_variant_object obj; - // Decode key. json=true has ONE key shape, same as the bound path: an - // unrepresentable key type rejects rather than emitting a second form. + // Decode key -- a key the codec cannot name falls back to hex, which the + // json=true bound parser accepts back verbatim. if (hp.json) { - SYS_ASSERT(key_shapes, chain::contract_table_query_exception, - "Table {} key type is not representable as JSON; use json=false for hex keys", - tbl_name); - auto full_key = chain::be_key_codec::decode_key( - row.key.data(), row.key.size(), *key_shapes); - obj("key", strip_scope_fields(std::move(full_key), scope_key_count)); + try { + FC_ASSERT(key_shapes, "be_key_codec: key shape unresolved"); + auto full_key = chain::be_key_codec::decode_key( + row.key.data(), row.key.size(), *key_shapes); + obj("key", strip_scope_fields(std::move(full_key), scope_key_count)); + } catch (...) { + obj("key", fc::to_hex(row.key.data(), static_cast(row.key.size()))); + } } else { obj("key", fc::to_hex(row.key.data(), static_cast(row.key.size()))); } diff --git a/tests/get_table_tests.cpp b/tests/get_table_tests.cpp index 72c8aeee04..f295839925 100644 --- a/tests/get_table_tests.cpp +++ b/tests/get_table_tests.cpp @@ -860,6 +860,58 @@ BOOST_FIXTURE_TEST_CASE( get_table_next_key_test, validating_tester ) try { BOOST_REQUIRE_EQUAL(page2.more, false); BOOST_CHECK_EQUAL(page2.rows[0].get_object()["value"].get_object()["code"].as_string(), "WIRE"); BOOST_CHECK_EQUAL(page2.rows[0].get_object()["value"].get_object()["payload"].as_uint64(), 300u); + + // (d) A key with NO canonical spelling renders as bare hex, and that hex is + // accepted straight back as a json=true bound — so a cursor landing on + // one still resumes exactly. This is the property pagination needs + // (encode(decode(key)) == key), and naming such a key could not provide + // it: slug_name's to_string is not injective over raw uint64s. + // + // Stored through the transitional object carrier, the only writer left + // that can express a raw value. 34<<42 is the packed former spelling + // "7", which the leading-letter rule now rejects; char[0] = 34 sorts it + // after 'W', so it lands last. + push_action("test"_n, "addslug"_n, "test"_n, mutable_variant_object() + ("code", mutable_variant_object()("value", uint64_t{34} << 42))("payload", 400)); + + p.lower_bound.clear(); + p.limit = 3; + auto pageA = get_table_rows_full(plugin, p, fc::time_point::maximum()); + BOOST_REQUIRE_EQUAL(pageA.rows.size(), 3u); + BOOST_REQUIRE_EQUAL(pageA.more, true); + BOOST_REQUIRE(!pageA.next_key.empty()); + // Bare hex, not a JSON key object -- the leaf refused to name it. + BOOST_CHECK(pageA.next_key.front() != '{'); + + p.lower_bound = pageA.next_key; + p.limit = 50; + auto pageB = get_table_rows_full(plugin, p, fc::time_point::maximum()); + BOOST_REQUIRE_EQUAL(pageB.rows.size(), 1u); + BOOST_CHECK_EQUAL(pageB.rows[0].get_object()["value"].get_object()["payload"].as_uint64(), 400u); + // Its own `key` is hex for the same reason... + BOOST_CHECK(pageB.rows[0].get_object()["key"].is_string()); + // ...while the row VALUE still renders, lossily, through the TOTAL + // fc::slug_name::to_variant. A display cell may be lossy; a resume + // token may not, and that asymmetry is deliberate. + BOOST_CHECK_EQUAL(pageB.rows[0].get_object()["value"].get_object()["code"].as_string(), "7"); + + // Reverse pagination over the same boundary resumes exactly too. + p.lower_bound.clear(); + p.reverse = true; + p.limit = 1; + auto revA = get_table_rows_full(plugin, p, fc::time_point::maximum()); + BOOST_REQUIRE_EQUAL(revA.rows.size(), 1u); + BOOST_REQUIRE_EQUAL(revA.more, true); + BOOST_CHECK(revA.next_key.front() != '{'); + BOOST_CHECK_EQUAL(revA.rows[0].get_object()["value"].get_object()["payload"].as_uint64(), 400u); + + p.upper_bound = revA.next_key; + p.limit = 50; + auto revB = get_table_rows_full(plugin, p, fc::time_point::maximum()); + BOOST_REQUIRE_EQUAL(revB.rows.size(), 3u); // WIRE, SOL, ETH + BOOST_CHECK_EQUAL(revB.rows[0].get_object()["value"].get_object()["code"].as_string(), "WIRE"); + p.reverse = false; + p.upper_bound.clear(); } // (sec-5) Invalid index name on multi_index — should throw, not silently diff --git a/unittests/be_key_codec_tests.cpp b/unittests/be_key_codec_tests.cpp index 9f0d6aff0c..dd73bd6cad 100644 --- a/unittests/be_key_codec_tests.cpp +++ b/unittests/be_key_codec_tests.cpp @@ -107,30 +107,52 @@ BOOST_AUTO_TEST_CASE(slug_name_leaf_roundtrips_the_zero_sentinel_as_empty) { BOOST_CHECK(bytes == encode_single(abi, "composite_key", slug(0))); } -BOOST_AUTO_TEST_CASE(slug_name_leaf_decodes_a_non_canonical_value_lossily) { - // A value below 2^42 has no string spelling (to_string truncates at the first - // zero symbol slot). The leaf converts it anyway — you get what you get, - // identical to the `name` leaf (`name(raw).to_string()`). It neither throws - // nor falls back to hex: a read path that throws costs the whole scan, and a - // raw uint64 that spells nothing is self-inflicted, since nothing validates - // the raw ctor for either type. +BOOST_AUTO_TEST_CASE(slug_name_leaf_round_trips_every_canonical_key) { + // THE property pagination depends on: encode(decode(key)) == key. A cursor is + // a resume token, so a key that renders must re-encode to the same bytes. auto abi = make_test_abi(); auto shapes = codec::build_key_shapes(abi, {"code"}, {"slug_name"}); - // A bound is still named by its SPELLING, so a bare integer is not one. + for (const char* spelling : {"A", "ETH", "WIRE", "LIQSOL", "Z1234567", "Z_______"}) { + auto bytes = codec::encode_key( + fc::variant(fc::mutable_variant_object("code", spelling)), shapes); + auto decoded = codec::decode_key(bytes.data(), bytes.size(), shapes); + BOOST_CHECK_EQUAL(decoded.get_object()["code"].as_string(), spelling); + BOOST_CHECK(codec::encode_key(decoded, shapes) == bytes); + } +} + +BOOST_AUTO_TEST_CASE(slug_name_leaf_refuses_to_name_a_non_canonical_key) { + // to_string is NOT injective over raw uint64s here — 38 of 64 symbol values + // are used, symbol 0 terminates, and bits 48-63 are never read — so naming + // such a key would produce a string that re-encodes to a DIFFERENT one, and a + // cursor built from it would resume in the wrong place. (`name` is not + // comparable: its alphabet is exactly 2^5 with no gaps and it consumes all 64 + // bits, so its trim-and-repack IS lossless.) + // + // The leaf therefore refuses, and get_table_rows renders the key as bare hex — + // the same escape every other undecodable ABI value uses, and exact by + // construction. fc::slug_name::to_variant stays TOTAL: a display cell may be + // lossy, a resume token may not. + auto abi = make_test_abi(); + auto shapes = codec::build_key_shapes(abi, {"code"}, {"slug_name"}); + + // A bound is named by its SPELLING, so a bare integer is not one. BOOST_CHECK_THROW( codec::encode_key(fc::variant(fc::mutable_variant_object("code", 7u)), shapes), fc::exception); - // A key already holding one decodes to "" — the same text zero renders, so - // feeding it back re-encodes to 0. That is the price of a total conversion. - // Reach past the carrier to build those bytes: the transitional object arm - // is the only writer left that can express a raw value. - auto bytes = codec::encode_key( - fc::variant(fc::mutable_variant_object("code", slug(7))), shapes); - BOOST_REQUIRE_EQUAL(bytes.size(), 8u); - auto decoded = codec::decode_key(bytes.data(), bytes.size(), shapes); - BOOST_CHECK_EQUAL(decoded.get_object()["code"].as_string(), ""); + // Both shapes of non-canonical value are refused. Reach past the carrier to + // build the bytes: the transitional object arm is the only writer left that + // can express a raw value. + // 7 -> below the 1<<42 floor; char[0] empty, would render "" + // 34 << 42 -> the packed former spelling "7"; leads with a digit + for (uint64_t raw : {uint64_t{7}, uint64_t{34} << 42}) { + auto bytes = codec::encode_key( + fc::variant(fc::mutable_variant_object("code", slug(raw))), shapes); + BOOST_REQUIRE_EQUAL(bytes.size(), 8u); + BOOST_CHECK_THROW(codec::decode_key(bytes.data(), bytes.size(), shapes), fc::exception); + } } BOOST_AUTO_TEST_CASE(slug_name_leaf_wins_over_a_shadowing_struct_def) { From d74ef99e7edc6a2a083f9056552c6f42d524506b Mon Sep 17 00:00:00 2001 From: kevin Heifner Date: Wed, 23 Sep 2026 07:43:11 -0500 Subject: [PATCH 17/29] fix(chain_plugin): keep json=true fallback cursors scope-relative MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit get_table_rows prepends the scope prefix to whatever bound comes back, so a json=true cursor has to be scope-RELATIVE. That held for a decoded JSON key object but not for the hex fallback added in b6cfee0ca8, which hexed the full stored [scope][key]: the seek key became [scope][scope][key] and the query skipped the rest of the range, or returned an empty page. All four json=true key-output sites now emit scope-relative hex, matching strip_scope_fields on the JSON path — the primary cursor, the secondary cursor, the per-row key field, and the read-only variant. json=false is untouched; its bounds are taken verbatim. get_table_test gains a SCOPED slug-keyed kv::scoped_table, because the case is otherwise unreachable — an unscoped table has no prefix to double. get_table_tests sec-11e pages forward and reverse across an un-nameable key under a scope; reverting the primary cursor to absolute hex makes it fail with 0 rows instead of 1, which is how the fix is known to be load-bearing. plugin_test 297/297, be_key_codec_tests, abi_tests. Change-Id: Ib92d86d831d782277c3353993bdb27221565ab64 --- plugins/chain_plugin/src/chain_plugin.cpp | 33 ++++++-- tests/get_table_tests.cpp | 59 ++++++++++++++ .../get_table_test/get_table_test.abi | 76 +++++++++++++----- .../get_table_test/get_table_test.cpp | 5 ++ .../get_table_test/get_table_test.hpp | 35 ++++++++ .../get_table_test/get_table_test.wasm | Bin 22662 -> 23520 bytes 6 files changed, 180 insertions(+), 28 deletions(-) diff --git a/plugins/chain_plugin/src/chain_plugin.cpp b/plugins/chain_plugin/src/chain_plugin.cpp index ce6f97aa27..d9ae2143e7 100644 --- a/plugins/chain_plugin/src/chain_plugin.cpp +++ b/plugins/chain_plugin/src/chain_plugin.cpp @@ -2760,19 +2760,24 @@ read_only::get_table_rows( const read_only::get_table_rows_params& p, const fc:: // bound parser accepts back verbatim — so next_key is always feedable. auto emit_secondary_next_key = [&](std::string_view sk) { if (p.json) { + // Scope-RELATIVE for BOTH shapes: the bound parser prepends + // scope_prefix_bytes whichever one comes back, so hexing the + // absolute key would scope it twice. + std::string_view sv = sk; + if (!scope_prefix_bytes.empty() && sv.size() >= scope_prefix_bytes.size()) { + sv.remove_prefix(scope_prefix_bytes.size()); + } try { - std::string_view sv = sk; - if (!scope_prefix_bytes.empty() && sv.size() >= scope_prefix_bytes.size()) { - sv.remove_prefix(scope_prefix_bytes.size()); - } FC_ASSERT(bound_key_shapes, "be_key_codec: key shape unresolved"); auto key_var = chain::be_key_codec::decode_key(sv.data(), sv.size(), *bound_key_shapes); hp.next_key = fc::json::to_string(key_var, fc::time_point::maximum()); return; } catch (...) { - // fall through to hex + hp.next_key = fc::to_hex(sv.data(), sv.size()); + return; } } + // json=false takes the stored key verbatim on the way back in. hp.next_key = fc::to_hex(sk.data(), sk.size()); }; @@ -2853,6 +2858,7 @@ read_only::get_table_rows( const read_only::get_table_rows_params& p, const fc:: return [p = std::move(hp), abi = std::move(abi), table_name = p.table, key_shapes = std::move(key_shapes), scope_key_count, + scope_prefix_size = scope_prefix_bytes.size(), abi_serializer_max_time = abi_serializer_max_time, shorten_abi_errors = shorten_abi_errors, all_rows = p.all_rows, @@ -2881,7 +2887,10 @@ read_only::get_table_rows( const read_only::get_table_rows_params& p, const fc:: row.key.data(), row.key.size(), *key_shapes); obj["key"] = strip_scope_fields(std::move(full_key), scope_key_count); } catch (...) { - obj["key"] = fc::to_hex(row.key.data(), row.key.size()); + // strip_scope_fields drops the scope on the JSON path; keep the + // hex form scope-relative so the two describe the same key. + obj["key"] = fc::to_hex(row.key.data() + scope_prefix_size, + row.key.size() - scope_prefix_size); } } else { obj["key"] = fc::to_hex(row.key.data(), row.key.size()); @@ -2932,9 +2941,14 @@ read_only::get_table_rows( const read_only::get_table_rows_params& p, const fc:: auto stripped = strip_scope_fields(std::move(full_key), scope_key_count); hp.next_key = fc::json::to_string(stripped, fc::time_point::maximum()); } catch (...) { - hp.next_key = fc::to_hex(kv.data(), static_cast(kv.size())); + // Scope-RELATIVE, exactly like the JSON key object it replaces: the + // bound parser prepends scope_prefix_bytes for either shape, so an + // absolute cursor would be scoped twice and skip the range. + hp.next_key = fc::to_hex(kv.data() + scope_prefix_bytes.size(), + static_cast(kv.size() - scope_prefix_bytes.size())); } } else { + // json=false takes the stored key verbatim on the way back in. hp.next_key = fc::to_hex(kv.data(), static_cast(kv.size())); } }; @@ -3034,6 +3048,7 @@ read_only::get_table_rows( const read_only::get_table_rows_params& p, const fc:: return [hp = std::move(hp), abi = std::move(abi), tbl_name = p.table, key_shapes = std::move(key_shapes), scope_key_count, + scope_prefix_size = scope_prefix_bytes.size(), abi_serializer_max_time = abi_serializer_max_time, shorten_abi_errors = shorten_abi_errors, all_rows = p.all_rows, @@ -3058,7 +3073,9 @@ read_only::get_table_rows( const read_only::get_table_rows_params& p, const fc:: row.key.data(), row.key.size(), *key_shapes); obj("key", strip_scope_fields(std::move(full_key), scope_key_count)); } catch (...) { - obj("key", fc::to_hex(row.key.data(), static_cast(row.key.size()))); + // Scope-relative, matching strip_scope_fields on the JSON path. + obj("key", fc::to_hex(row.key.data() + scope_prefix_size, + static_cast(row.key.size() - scope_prefix_size))); } } else { obj("key", fc::to_hex(row.key.data(), static_cast(row.key.size()))); diff --git a/tests/get_table_tests.cpp b/tests/get_table_tests.cpp index f295839925..2122590cb3 100644 --- a/tests/get_table_tests.cpp +++ b/tests/get_table_tests.cpp @@ -914,6 +914,65 @@ BOOST_FIXTURE_TEST_CASE( get_table_next_key_test, validating_tester ) try { p.upper_bound.clear(); } + // (sec-11e) SCOPED slug-keyed table. A json=true cursor must be scope-RELATIVE: + // get_table_rows prepends the scope prefix to whatever bound comes + // back, so an absolute hex cursor is scoped TWICE and the query skips + // the rest of the range. Unscoped (sec-11d) cannot catch that — there + // is no prefix to double. + { + const uint64_t sc = chain::name("sc1").to_uint64_t(); + push_action("test"_n, "addsslug"_n, "test"_n, mutable_variant_object() + ("scope", sc)("code", "ETH")("payload", 10)); + push_action("test"_n, "addsslug"_n, "test"_n, mutable_variant_object() + ("scope", sc)("code", "SOL")("payload", 20)); + push_action("test"_n, "addsslug"_n, "test"_n, mutable_variant_object() + ("scope", sc)("code", "WIRE")("payload", 30)); + // The un-nameable one, stored through the transitional object carrier. + // 34<<42 is the packed former spelling "7"; char[0] = 34 sorts it last. + push_action("test"_n, "addsslug"_n, "test"_n, mutable_variant_object() + ("scope", sc)("code", mutable_variant_object()("value", uint64_t{34} << 42)) + ("payload", 40)); + + chain_apis::read_only::get_table_rows_params p; + p.json = true; + p.code = "test"_n; + p.scope = "sc1"; + p.table = "sslugobjs"; + + // Forward: page across the boundary onto the un-nameable key. + p.limit = 3; + auto pageA = get_table_rows_full(plugin, p, fc::time_point::maximum()); + BOOST_REQUIRE_EQUAL(pageA.rows.size(), 3u); + BOOST_REQUIRE_EQUAL(pageA.more, true); + BOOST_REQUIRE(!pageA.next_key.empty()); + BOOST_CHECK(pageA.next_key.front() != '{'); // hex fallback, not a JSON key + + p.lower_bound = pageA.next_key; + p.limit = 50; + auto pageB = get_table_rows_full(plugin, p, fc::time_point::maximum()); + // With a scope-ABSOLUTE cursor this returns 0 rows: the bound parser + // prepends the scope a second time and the seek lands past the range. + BOOST_REQUIRE_EQUAL(pageB.rows.size(), 1u); + BOOST_CHECK_EQUAL(pageB.rows[0].get_object()["value"].get_object()["payload"].as_uint64(), 40u); + + // Reverse over the same boundary. + p.lower_bound.clear(); + p.reverse = true; + p.limit = 1; + auto revA = get_table_rows_full(plugin, p, fc::time_point::maximum()); + BOOST_REQUIRE_EQUAL(revA.rows.size(), 1u); + BOOST_REQUIRE_EQUAL(revA.more, true); + BOOST_CHECK(revA.next_key.front() != '{'); + BOOST_CHECK_EQUAL(revA.rows[0].get_object()["value"].get_object()["payload"].as_uint64(), 40u); + + p.upper_bound = revA.next_key; + p.limit = 50; + auto revB = get_table_rows_full(plugin, p, fc::time_point::maximum()); + BOOST_REQUIRE_EQUAL(revB.rows.size(), 3u); // WIRE, SOL, ETH + BOOST_CHECK_EQUAL(revB.rows[0].get_object()["value"].get_object()["payload"].as_uint64(), 30u); + p.reverse = false; + } + // (sec-5) Invalid index name on multi_index — should throw, not silently // return primary rows. { diff --git a/unittests/test-contracts/get_table_test/get_table_test.abi b/unittests/test-contracts/get_table_test/get_table_test.abi index fa7a912fe3..d454d4b2b4 100644 --- a/unittests/test-contracts/get_table_test/get_table_test.abi +++ b/unittests/test-contracts/get_table_test/get_table_test.abi @@ -37,6 +37,24 @@ } ] }, + { + "name": "addsslug", + "base": "", + "fields": [ + { + "name": "scope", + "type": "uint64" + }, + { + "name": "code", + "type": "slug_name" + }, + { + "name": "payload", + "type": "uint64" + } + ] + }, { "name": "addstruct", "base": "", @@ -130,17 +148,31 @@ ] }, { - "name": "slug_name", + "name": "slugobj", "base": "", "fields": [ { - "name": "value", + "name": "code", + "type": "slug_name" + }, + { + "name": "payload", "type": "uint64" } ] }, { - "name": "slugobj", + "name": "slugobj_key", + "base": "", + "fields": [ + { + "name": "code", + "type": "slug_name" + } + ] + }, + { + "name": "sslugobj", "base": "", "fields": [ { @@ -154,7 +186,7 @@ ] }, { - "name": "slugobj_key", + "name": "sslugobj_key", "base": "", "fields": [ { @@ -204,6 +236,11 @@ "type": "addslug", "ricardian_contract": "" }, + { + "name": "addsslug", + "type": "addsslug", + "ricardian_contract": "" + }, { "name": "addstruct", "type": "addstruct", @@ -221,14 +258,6 @@ } ], "tables": [ - { - "name": "hashobj", - "type": "hashobj", - "index_type": "i64", - "key_names": [], - "key_types": [], - "table_id": 37781 - }, { "name": "hashobjs", "type": "hashobj", @@ -249,14 +278,6 @@ } ] }, - { - "name": "numobj", - "type": "numobj", - "index_type": "i64", - "key_names": [], - "key_types": [], - "table_id": 30247 - }, { "name": "numobjs", "type": "numobj", @@ -295,6 +316,21 @@ "key_types": ["slug_name"], "table_id": 41308 }, + { + "name": "sslugobjs", + "type": "sslugobj", + "index_type": "i64", + "key_names": ["scope","code"], + "key_types": ["name","slug_name"], + "table_id": 32371, + "secondary_indexes": [ + { + "name": "bypayload", + "key_type": "uint64", + "table_id": 64287 + } + ] + }, { "name": "structobjs", "type": "structobj", diff --git a/unittests/test-contracts/get_table_test/get_table_test.cpp b/unittests/test-contracts/get_table_test/get_table_test.cpp index 2db7d1c7cc..41b6704f8f 100644 --- a/unittests/test-contracts/get_table_test/get_table_test.cpp +++ b/unittests/test-contracts/get_table_test/get_table_test.cpp @@ -49,3 +49,8 @@ void get_table_test::addslug(slug_name code, uint64_t payload) { slugobjs slugobjs_table( get_self() ); slugobjs_table.emplace( get_self(), { code }, { code, payload } ); } + +void get_table_test::addsslug(uint64_t scope, slug_name code, uint64_t payload) { + sslugobjs sslugobjs_table( get_self(), scope ); + sslugobjs_table.emplace( get_self(), { code }, { code, payload } ); +} diff --git a/unittests/test-contracts/get_table_test/get_table_test.hpp b/unittests/test-contracts/get_table_test/get_table_test.hpp index bbeaf90aec..5efd3c74bf 100644 --- a/unittests/test-contracts/get_table_test/get_table_test.hpp +++ b/unittests/test-contracts/get_table_test/get_table_test.hpp @@ -6,6 +6,7 @@ #include #include +#include #include @@ -156,6 +157,33 @@ class [[sysio::contract]] get_table_test : public sysio::contract { typedef sysio::kv::table< "slugobjs"_n, slugobj_key, slugobj > slugobjs; + // The same registry shape, but SCOPED — stored as [scope:8B BE][key], with a + // secondary index so both cursor paths are reachable. + // + // A json=true cursor must be scope-RELATIVE, because get_table_rows prepends + // the scope prefix to whatever bound comes back. That holds trivially for a + // decoded JSON key object; it is the HEX fallback (a key the codec cannot + // name) that can get it wrong, and an absolute cursor is then scoped twice + // and skips the rest of the range. Unscoped `slugobjs` above cannot catch + // that — there is no prefix to double. + struct sslugobj_key { + slug_name code; + uint64_t primary_key() const { return code.value; } + SYSLIB_SERIALIZE(sslugobj_key, (code)) + }; + + struct [[sysio::table("sslugobjs")]] sslugobj { + slug_name code; + uint64_t payload = 0; + uint64_t by_payload() const { return payload; } + SYSLIB_SERIALIZE(sslugobj, (code)(payload)) + }; + + typedef sysio::kv::scoped_table< + "sslugobjs"_n, sslugobj_key, sslugobj, + sysio::kv::index<"bypayload"_n, + sysio::const_mem_fun>> sslugobjs; + [[sysio::action]] void addnumobj(uint64_t input); @@ -182,5 +210,12 @@ class [[sysio::contract]] get_table_test : public sysio::contract { [[sysio::action]] void addslug(slug_name code, uint64_t payload); + /// Insert a row into the SCOPED slug-keyed table `sslugobjs`. + /// @param scope the table scope + /// @param code the slug forming the primary key + /// @param payload arbitrary row payload; also the `bypayload` secondary + [[sysio::action]] + void addsslug(uint64_t scope, slug_name code, uint64_t payload); + }; diff --git a/unittests/test-contracts/get_table_test/get_table_test.wasm b/unittests/test-contracts/get_table_test/get_table_test.wasm index 4621c65a094d2f34e3217becb0b744d3937b1cec..cf7892e7cc514b085992602a682884f443a1e1ae 100755 GIT binary patch delta 5277 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 BPi+7I delta 4579 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 IgZCNx59}JR2mk;8 From 4d27537715bb539148d7a232aefdcf9d3c9463b6 Mon Sep 17 00:00:00 2001 From: kevin Heifner Date: Wed, 23 Sep 2026 08:18:10 -0500 Subject: [PATCH 18/29] refactor(libfc): derive slug_name; move is_canonical onto it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit fc::slug_name was an alias while the CDT's was a derived struct and sysio::chain::name has always been one — so it was the odd type out, and nothing caught the divergence. Deriving it closes that and gives is_canonical a home where it means something. is_canonical is meaningless on `name`: that alphabet is exactly 2^5 with no gaps and its 13 symbols consume all 64 bits, so every uint64 IS a canonical name and the predicate can never be false. slug_name's encoding is the one with unreachable values — 26 unused symbol values, symbol 0 terminates, bits 48-63 never read. Reflection follows chain::name: FC_REFLECT_DERIVED_EMPTY, since a plain FC_REFLECT cannot take an INHERITED member (the pointer is to the base). Layout unchanged. Two static_asserts pin both properties, matching the pair in wire-cdt b1298b3f, so either side drifting is a compile error. opreg/reserv/uwrit rebuilt: they call is_canonical and its definition moved to the derived type — paired with that CDT commit, no contract source change. -4/-8/-4 bytes, no ABI change; msgch inlines identically. sysio.bios stays excluded. codename_tests 40, be_key_codec_tests, abi_tests, name_tests, plugin_test 297/297, contracts_unit_test 810, CDT ctest 35/35. Change-Id: I7dccb5a216362f4fc07ebda6fa5c083833506d86 --- contracts/sysio.opreg/sysio.opreg.wasm | Bin 96098 -> 96094 bytes contracts/sysio.reserv/sysio.reserv.wasm | Bin 88684 -> 88676 bytes contracts/sysio.uwrit/sysio.uwrit.wasm | Bin 161011 -> 161007 bytes libraries/libfc/include/fc/basic_name.hpp | 12 ----- libraries/libfc/include/fc/slug_name.hpp | 55 +++++++++++++++++++++- 5 files changed, 53 insertions(+), 14 deletions(-) diff --git a/contracts/sysio.opreg/sysio.opreg.wasm b/contracts/sysio.opreg/sysio.opreg.wasm index c8ef62528e407fd0930e5b5b246a27e1138065da..a3ba0bffe820ec8cea6e74f146243ee6ddea1de5 100755 GIT binary patch delta 430 zcmZWjJ4gdj5Zv8MVj#wxqM+D3Pmq8HEJXaE<|!&_A=t#i&hCO>{1qywMZn4nToD^V zEVQu*Jmi9;5Q`AfC|)io1_^>qgIdF$RANkIiN5juC6hd@k@*3s7t?@@J!a9%Psb6;xHmH zZN7OrFe3sr<$AmlAI*ymgYhD$=#xiWA-Grk@W+lNlOO-Ys`Q|Y|I@@l=gK16wL$L! zP=}c^y1A{%GUo@M&v%Nb?C>+&WJZhk&W1MRm4eBQrh!TUqI_qw#$<3EPm-@2xcviAorbOe delta 430 zcmZWjO(;ZR6utMpnK3oSJQ^aK&r_QGl!asn^AW;A*^Gsq-J6u(S)lwxS$R`$F&k5| zu%3MMHnXtMkPUgyL?%g;1(Iy6_=cs;J?GwY?!D*k&HeD^PM^E+!n|;=c4wjtxEj|` z*Qk{;Q4SnJbP&}P209Pud}>;QRym*uBd*Rd81_qxl~oJM4x)RmbqYU$z1JT#^$>xfVyUrhV9=}Gm_u`PyD3t{@c6xp2N zqFq}=ZzqvnZIR6{dTGTGy-%@V0n=ZbL0q@OC3rT7FP3Wz+g#`*JI8SVkF1SxqyXMn cn=y=e@zMI6#THCP;<#(o&tnB9^Ko4J0pE&;vj6}9 diff --git a/contracts/sysio.reserv/sysio.reserv.wasm b/contracts/sysio.reserv/sysio.reserv.wasm index 25fd69285fd720720682b07188442f77691b8b5d..0a6727a897991ce322535aa253864d4be3dc04fa 100755 GIT binary patch delta 1135 zcmY*XdrXaC7=NGlJD+n*DO8rw*I}n(xkSe;^a=T+;}%LW%*J+FVpdshEfceeeDsye z*yNJw!i#UmT+XPqp_x04In>En4y!-VY?}9ZlkwN@d!FC#d7j_%e6RnS+;&Z_FOg~Y z7q(ws2r<4Kg{Z;#R&ZLWo%(WAqLLWu$+1cbajHR1b@LW0u~U!M(=QBsTBtc?&|5B2 zFvoE=N1v8swt=4=HD3;aPNoiDJ&gYHvorRCGJWmU#wTWTU?&q@{SnjW-;k-hRSQZT zPCvD3ajD_yN7_gfsJFblYYZiEgs8)UCt9U>fkcroovrSk&%cq3B2p4fQr)dQM4^of z0Yk1)AcB8`Q>BXniF&(DJC!<$7JIdOsat4!D@$;Mnb418$p?f9OYc&5yVkWd3Lzvd zns$1%nQ3=vQM=ZY=AhkP=2#Zck8h%7WsX7ECxEfcFu(>6vnJBxeyt~~1Idn6<+xq( z>f}Ir{u66*uBHDC8_Gs)+775ZlRE|BO>QIy_A_^xjJP;&pJ3nQPewMVK#ZJG5RT-E zf;C7oV_UB%sZfaW3&)K3U5l-1gbxF7hr!*YEDXjCrT;aifc&zZSzHBzKRb{aVkIRIIj`%t(tkQV#(Y8!0e@5LAt4dVy!2cyr4hZtzk zv*UT5htJ|+4`Rk7c+Am=oeV?p_35(`dWri9P>bZl@#JqgI((0eOBV21KM~% zIQX?NbNVzd7b%$IxRRq^D=^#OH9KwoJrqWnI%``btr%u*#}6|7vtL_~lFxxPq@eqYJE(8p@Mu8}P2A4`RgNWMrLTk*3p?xp3D;fLf|2~%J3^$=4=lW*|3pZS* zZm%}7AsQhxE0*5w*Oq5proH{zc$Sks>SxZ4fdTv`T3+s4gen1y<%R<`xSqF&jty$# zc|Ih4TWj%lN4KpGqLt5Cmum<8Y}iZ|^IaLB(p{K@@VIae2R2r?QAS)>R3+HQ#fy;* zE)gS>OCpfmT(TWWW*nFhrIrd&ap~OI6~iP%QY>nNvLHUWIV{ZFxGT(z3SeK|%fu`zde}tKO;nFy(|aZt zZ&F7kF;-GcpDw<-=SwpdwmusZg}-^PVOae9u$^{#jkh!5{1>!uP;a)uZvI}3X|b?i zDt|EgDm%o%4L#q^^E|v~hYG};ui+L)BYrW=z}Kg*NSq-4O@vmA_d8%Abm&bENC&qu zcA%`w6#S^`m%*)aXrrGQUXPTFQJLevujWNNo;wC3=M0YrSp{7szG+qb|TJuDY8-vdm9$rk_s diff --git a/contracts/sysio.uwrit/sysio.uwrit.wasm b/contracts/sysio.uwrit/sysio.uwrit.wasm index 0799a3bc444c851db8f483ce458a5312dfc0a09a..bd1979853c4a432eb2b44c94d105d7a6f8d4fcd9 100755 GIT binary patch delta 1712 zcmZ`)YfKbZ6u#%|;ttFrgMw1bH4AJ^O(K4 z+xpPI>7oCzc$?tP2GPa1TTQebz@Wy&l$kSr8~LNBP3gn7YFT}1dyoFx)wt6vEdVUvDH=m>=^dhxiE$#|?@U#v_9 z%&FD4r4?9Q&G(ryg)u2fw#?4j;GoAlflL&|?*S|>uGO$ez**FjhOs)33)EG{q(IgyT0&i)lXd$c)^m@Csj4N!%4*blsxx%3jsaEXAZBL7l9QRm0U_ z_0~>j*n>yCJORSlTLsgMb+&dE9E_AQo6(3 z$sEr6SS@kKs44D`q%%!Jg|C}Qm}W0&?ys`(iQ3~X#G)s9;jS3Kn@{vh-)tBm{n}!_ zB~m$a!_BSg<>IMoPVwu~k=CJtw^hGfybkOqpAMDFgfMi92CAKaOSr2x6dmqsgmYmaG=6nY4X^cy&=oT^vd`O<&*o;IeKXDL7y`Mx%F_5HE0l=o3~ zqAL1gL6mLWs_@6pdKV3l{iQkKWN^3Xr|*|qv9n$MqHdfCyX(df=%?z+5`up4nm?c= z#=<0lS{mCTq0vU?%~&4;i~Mj~mvQ4|cx@P#c4<4}U=@11w5~WUi}gl*HB2|{15HD;ylY_VXN=q?$Q{gPJcnC0yw{Cvoyu6u-If5aqG|AXiG>0qp4aSBs#^MP@!nNt2ulZyd`*hjb%+%FOGgH%7Gge5N zA8C6D{Rs~f1`?ho3?{tVhkp}>QU07ToN&Yp#zqrPA{<4?34;hz2qnUGOIK=X>3EnX oyVB0Y{Wfj16H~N8C+;>9Gq3<~WTNrs8XDH0{#it;G>ftS0K_C&`Tzg` delta 1720 zcmZ`)dr%c+6#ve-_j)hvMHT}kd@UCyxnZTHz?g=ghUwsp+B8#E);Q&ZC3&bhBQbMH z88K10xHD|{XmSRWLQULqvk@u8$C#0s&vb?YvkRu6Fv1m(&c{ps>7V_b^ZT7Wk8i&@ z+uRt?+8A&+-tPE32P6;UUuuc=uX%0`D2&VNT%fuz4$0+=vl#61xGV}&d9cbeGF>c= z2%M=Pm)9yRIfz2Y29;jWo^{L(>F^NI8^l>6VXn5(F#$Fk7!5^yHfq-LxxjBX2sz$mNg~}M=v3~RL$(kjnT1ktDy8%WK0|>Z+*BUoQD8JVpmJP5 z1KoVA8R=2Dw@15{o{9U~jpU51K+*J>ta_`!a!;2t{ECmscE!ps*j3)3sPeL2>l$(8z19Vb%D?YZZ6XzuzX2^)8>>ZV zA=;Jg8R4gz`f7?wPZI8F)ZQx?8NT7p|A<=)7LFg;EwA(?dE5P=Lfw7iT1m`Q|Qhg$w9u<1uvZy@srk=TPNez-8r>z3%#_0mF0Jm@4j4pd#_yHZ84T>&)wNAY&4i?j_`o*bm42^!i z_0b^zW;`4b@lrxxd{#nlym>hRssr&vmtH&@qOrNlteOn~c+qcGzX}d3ZtT|2&xJ9B zX470)=?X3C>y4kMMXv4c0DN!=iuRXb6XhBv*B-kEjc5~?2N!*F{|l}!rZ3^ zyF5A_i$w~Pj&5-noH5Jaz|awT!wQ@SOn0xu2WC_Xjt2+hCwxrW^ju~CE);hOZ1SAVdKeYkAR+LW}VYg5wG7|SNjM%o6# z0K%<=L4-RALkJJ_;|aoW@~;v`5I!IrMHu)pV; +/// +/// DERIVED, not an alias, and byte-identical with the contract-side +/// sysio::slug_name, which is derived for the same reason. Two things need it: +/// abigen only matches a builtin on a real type (CDT side), and is_canonical() +/// below belongs to this encoding rather than to every basic_name. +struct slug_name : basic_name { + using base = basic_name; + using base::base; + constexpr slug_name() = default; + + /// Does this value have a canonical spelling? A slug_name built from a RAW + /// uint64 bypasses the validating constructor — and nothing validates on + /// deserialization either, since the reflected member is written directly — + /// so it can hold a value no spelling produces: anything whose leading symbol + /// slot is empty, or that uses one of the 26 unused symbol values, or that + /// sets any of bits 48-63. Such a value cannot round-trip. + /// + /// This lives on slug_name and NOT on basic_name because it is meaningless + /// for `name`: that alphabet is exactly 2^5 with no gaps and its 13 symbols + /// consume all 64 bits, so every uint64 IS a canonical name and the predicate + /// could never be false. + bool is_canonical() const { + const std::string text = to_string(); + return is_valid_literal(text) && pack(text) == value; + } +}; namespace slug_name_literals { @@ -213,3 +237,30 @@ inline void from_variant(const fc::variant& v, slug_name& s) { } } // namespace fc + +/// slug_name is DERIVED, so it needs its own reflection — the base's +/// FC_REFLECT_TEMPLATE does not cover it, and a plain FC_REFLECT cannot take an +/// INHERITED member (the pointer is to the base). Same form sysio::chain::name +/// uses, for the same reason; the layout is unchanged. +FC_REFLECT_DERIVED_EMPTY( fc::slug_name, (fc::basic_name) ) + +namespace fc { + +// --- shape pins ----------------------------------------------------------- +// These two properties drifted apart between this repo and wire-cdt once before, +// silently: CDT derived slug_name for abigen while this side stayed an alias, +// and is_canonical sat on the shared base where `name` inherited a predicate +// that can never be false. Both repos assert the same two things. +static_assert(!std::is_same_v>, + "slug_name must be a DERIVED type, not an alias — abigen matches " + "builtins on a real type, and is_canonical belongs to this encoding"); +template +concept has_is_canonical = requires(const T t) { t.is_canonical(); }; + +static_assert(!has_is_canonical>, + "is_canonical must live on slug_name, not the shared basic_name: " + "`name` shares that base, and every uint64 IS a canonical name, so " + "the predicate could never be false there"); +static_assert(has_is_canonical, "slug_name must carry is_canonical"); + +} // namespace fc From f3dc7e061dcb5dc52ac6a95269a0f148e9a6a8c8 Mon Sep 17 00:00:00 2001 From: kevin Heifner Date: Wed, 23 Sep 2026 08:32:13 -0500 Subject: [PATCH 19/29] refactor(libfc): drop the slug_name shape pins MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit They asserted the two properties that had already drifted, which is not where the next drift will be, and the has_is_canonical concept existed only to feed them — public surface with no runtime value. Both properties are enforced by use anyway: deriving slug_name is what makes abigen match the builtin on the CDT side, and is_canonical only compiles against the derived type. No codegen change — every contract artifact reproduces, sysio.bios excluded as before. codename_tests, be_key_codec_tests, get_table_tests. Paired with wire-cdt 1eb3352c. Change-Id: Iedd375b59e5905b02bf0f955c57bbd1490cc572a --- libraries/libfc/include/fc/slug_name.hpp | 22 ---------------------- 1 file changed, 22 deletions(-) diff --git a/libraries/libfc/include/fc/slug_name.hpp b/libraries/libfc/include/fc/slug_name.hpp index 362b640969..38722f1c54 100644 --- a/libraries/libfc/include/fc/slug_name.hpp +++ b/libraries/libfc/include/fc/slug_name.hpp @@ -23,7 +23,6 @@ #include #include #include -#include namespace fc { @@ -243,24 +242,3 @@ inline void from_variant(const fc::variant& v, slug_name& s) { /// INHERITED member (the pointer is to the base). Same form sysio::chain::name /// uses, for the same reason; the layout is unchanged. FC_REFLECT_DERIVED_EMPTY( fc::slug_name, (fc::basic_name) ) - -namespace fc { - -// --- shape pins ----------------------------------------------------------- -// These two properties drifted apart between this repo and wire-cdt once before, -// silently: CDT derived slug_name for abigen while this side stayed an alias, -// and is_canonical sat on the shared base where `name` inherited a predicate -// that can never be false. Both repos assert the same two things. -static_assert(!std::is_same_v>, - "slug_name must be a DERIVED type, not an alias — abigen matches " - "builtins on a real type, and is_canonical belongs to this encoding"); -template -concept has_is_canonical = requires(const T t) { t.is_canonical(); }; - -static_assert(!has_is_canonical>, - "is_canonical must live on slug_name, not the shared basic_name: " - "`name` shares that base, and every uint64 IS a canonical name, so " - "the predicate could never be false there"); -static_assert(has_is_canonical, "slug_name must carry is_canonical"); - -} // namespace fc From a21b9667d4e90032052e2ab00b20d6e923fdd637 Mon Sep 17 00:00:00 2001 From: kevin Heifner Date: Wed, 23 Sep 2026 10:57:29 -0500 Subject: [PATCH 20/29] fix(libfc): give the derived slug_name its own std::hash slug_name became a derived struct, and std::hash> is a partial specialization matched by deducing the exact type -- it never sees a derived-to-base conversion. Without its own specialization the lookup falls to the disabled primary template and any unordered container over a slug fails to compile. sysio::chain::name carries one for the same reason. Change-Id: Ic25c9ef60e79132ad755e14c37473a6536d18943 --- libraries/libfc/include/fc/slug_name.hpp | 16 ++++++++++++ libraries/libfc/test/test_slug_name.cpp | 32 ++++++++++++++++++++++++ 2 files changed, 48 insertions(+) diff --git a/libraries/libfc/include/fc/slug_name.hpp b/libraries/libfc/include/fc/slug_name.hpp index 38722f1c54..a2ed7eebaf 100644 --- a/libraries/libfc/include/fc/slug_name.hpp +++ b/libraries/libfc/include/fc/slug_name.hpp @@ -237,6 +237,22 @@ inline void from_variant(const fc::variant& v, slug_name& s) { } // namespace fc +namespace std { + /// slug_name is DERIVED, so `std::hash>` no longer + /// covers it: a partial specialization is matched by deducing the exact + /// type, which never considers a derived-to-base conversion. Without this, + /// `std::hash` resolves to the disabled primary template and + /// every unordered container over a slug fails to compile. sysio::chain::name + /// carries its own for the same reason, and hashes the same way. + template <> + struct hash { + size_t operator()(const fc::slug_name& s) const noexcept { + static_assert(sizeof(size_t) == sizeof(uint64_t)); + return __builtin_bswap64(s.value); + } + }; +} // namespace std + /// slug_name is DERIVED, so it needs its own reflection — the base's /// FC_REFLECT_TEMPLATE does not cover it, and a plain FC_REFLECT cannot take an /// INHERITED member (the pointer is to the base). Same form sysio::chain::name diff --git a/libraries/libfc/test/test_slug_name.cpp b/libraries/libfc/test/test_slug_name.cpp index 2aa5989a39..70f29e8503 100644 --- a/libraries/libfc/test/test_slug_name.cpp +++ b/libraries/libfc/test/test_slug_name.cpp @@ -10,6 +10,7 @@ #include #include +#include #include using fc::slug_name; @@ -663,4 +664,35 @@ BOOST_AUTO_TEST_CASE(variant_object_arm_rejects_every_coercible_value_shape) { BOOST_CHECK(back == slug_name{"LIQSOL"}); } +/// slug_name is DERIVED, so it needs its own std::hash — and this is primarily a +/// COMPILE-time guard. A partial specialization over fc::basic_name is +/// matched by deducing the exact type and never considers a derived-to-base +/// conversion, so without fc::slug_name's own specialization the lookup falls to +/// the disabled primary template and none of the lines below compile. +/// +/// `lsb_hash_distinguishes_distinct_values` above cannot catch that: its type IS a +/// basic_name, so the partial specialization covers it either way. Equality is not +/// at risk for the same reason it is not for chain::name — basic_name's `==` is a +/// hidden friend, and ADL finds it through the base. +BOOST_AUTO_TEST_CASE(derived_slug_name_is_usable_in_unordered_containers) { + const slug_name eth{"ETH"}; + const slug_name sol{"SOL"}; + + std::unordered_set codes{eth, sol}; + BOOST_CHECK_EQUAL(codes.size(), 2u); + BOOST_CHECK(codes.contains(eth)); + BOOST_CHECK(codes.contains(sol)); + BOOST_CHECK(!codes.contains(slug_name{"WIRE"})); + BOOST_CHECK(!codes.insert(eth).second); // equal value, same bucket + + std::unordered_map payload{{eth, 10}, {sol, 20}}; + BOOST_CHECK_EQUAL(payload.at(eth), 10u); + BOOST_CHECK_EQUAL(payload.at(sol), 20u); + + // Same hash the base would have produced: the specialization restores + // reachability, it does not change the value. + BOOST_CHECK_EQUAL(std::hash{}(eth), + std::hash>{}(eth)); +} + BOOST_AUTO_TEST_SUITE_END() From 4242ff5188c167aa73d9682eda0310dc0c1beafc Mon Sep 17 00:00:00 2001 From: kevin Heifner Date: Wed, 23 Sep 2026 10:57:30 -0500 Subject: [PATCH 21/29] fix(chain_plugin): tag raw cursors absolute; dispatch bounds past whitespace A json=true cursor for a key the codec cannot name now carries the whole stored key behind the `0x` tag fc::from_hex already trims, and the bound parser feeds it back verbatim. The scope prefix is no longer added or removed by agreement between two sites, so a key that is nothing but the prefix still yields a non-empty, resumable cursor -- an empty next_key reads as "no bound" and restarts the page. Bound dispatch now reads the first non-whitespace character, since fc::json::from_string accepts leading whitespace and ` {"code":"ETH"}` was reaching the hex reader. An all-whitespace bound is refused rather than trimmed to nothing. Scope prefixing keys off whether a bound was supplied rather than its decoded byte length. json=false is unchanged. Change-Id: I3a29d73fe3a7deb2a98a83a822f6860a0aaa1ced --- plugins/chain_plugin/src/chain_plugin.cpp | 125 ++++++++++++------ tests/get_table_tests.cpp | 75 ++++++++--- .../get_table_test/get_table_test.hpp | 13 +- 3 files changed, 150 insertions(+), 63 deletions(-) diff --git a/plugins/chain_plugin/src/chain_plugin.cpp b/plugins/chain_plugin/src/chain_plugin.cpp index d9ae2143e7..04e9a16a8c 100644 --- a/plugins/chain_plugin/src/chain_plugin.cpp +++ b/plugins/chain_plugin/src/chain_plugin.cpp @@ -2446,6 +2446,18 @@ fc::variant strip_scope_fields(fc::variant&& full_key, size_t count) { return fc::variant(std::move(stripped)); } +/// Render a json=true `next_key` for a key the codec could not NAME: the `0x` tag +/// `fc::from_hex` already understands, followed by the hex of the COMPLETE stored key. +/// +/// Absolute, never scope-relative. A relative cursor has to be re-prefixed on the way +/// back in, and a key that is nothing but the scope prefix leaves an EMPTY remainder — +/// which reads as "no bound" and restarts the page instead of resuming it. The tag +/// lets the bound parser hand these bytes to the index untouched, so the scope prefix +/// is never added or removed by agreement between two sites. +std::string to_raw_cursor(std::string_view key) { + return fc::to_hex(key.data(), static_cast(key.size()), /*add_prefix=*/true); +} + read_only::get_table_rows_return_t read_only::get_table_rows( const read_only::get_table_rows_params& p, const fc::time_point& deadline ) const { abi_def abi = sysio::chain_apis::get_abi( db, p.code ); @@ -2611,33 +2623,63 @@ read_only::get_table_rows( const read_only::get_table_rows_params& p, const fc:: bound_key_shapes->begin() + static_cast(scope_key_count)); } - // Parse bounds. When json=true a bound is a JSON key OBJECT — or the bare hex a - // cursor emits when the codec could not NAME the key (an unrepresentable key - // type, or a slug_name with no canonical spelling). Hex IS the raw key bytes, - // so feeding next_key straight back always re-encodes the identical key, which - // is the property pagination needs. json=false is hex only. - auto parse_bound = [&](const std::string& bound) { + // Parse bounds. When json=true the first NON-WHITESPACE character says which of + // three forms a bound takes: + // '{' a JSON key OBJECT — names the fields WITHIN the scope, so the scope + // prefix is prepended below + // "0x" a RAW cursor — already the COMPLETE stored key, used verbatim + // else bare hex — within-scope, like the object form + // A cursor takes the raw form when the codec could not NAME the key (an + // unrepresentable key type, or a slug_name with no canonical spelling); see + // to_raw_cursor for why it carries the whole key rather than the remainder. + // json=false is hex only and unchanged — the complete stored key, untagged. + struct parsed_bound { std::vector bytes; - if (p.json && !bound.empty() && bound.front() == '{') { + bool absolute = false; ///< raw cursor: never scope-prefixed again + }; + auto parse_bound = [&](const std::string& bound) { + parsed_bound out; + // Dispatch on the first non-whitespace character: fc::json::from_string accepts + // leading whitespace, so ` {"code":"ETH"}` is a valid JSON bound and must not + // reach the hex reader, which throws on the brace. + // + // An all-whitespace bound is refused rather than trimmed to nothing: the caller + // only reaches here with a NON-empty bound, and silently turning one into zero + // bytes would read downstream as "no bound" and restart the page. + const size_t start = bound.find_first_not_of(" \t\n\r"); + SYS_ASSERT(start != std::string::npos, chain::contract_table_query_exception, + "Table {} bound is entirely whitespace", p.table); + const std::string body = bound.substr(start); + if (p.json && body.starts_with('{')) { SYS_ASSERT(bound_key_shapes, chain::contract_table_query_exception, "Table {} key type is not representable as a JSON bound; use hex bounds", p.table); - bytes = chain::be_key_codec::encode_key(fc::json::from_string(bound), *bound_key_shapes); - } else { - auto v = fc::from_hex(bound); - bytes.assign(reinterpret_cast(v.data()), - reinterpret_cast(v.data()) + v.size()); + out.bytes = chain::be_key_codec::encode_key(fc::json::from_string(body), *bound_key_shapes); + return out; } - return bytes; + // fc::from_hex trims the tag itself; this only has to NOTICE it — and only under + // json=true, because a json=false bound has always been the complete key whether + // or not it carries the prefix, and that meaning must not change here. + const std::string hex = fc::trim_hex_prefix(body); + out.absolute = p.json && hex.size() != body.size(); + const auto v = fc::from_hex(hex); + out.bytes.assign(v.begin(), v.end()); + return out; }; std::vector lb_bytes; + bool lb_absolute = false; if (!effective_lower.empty()) { - lb_bytes = parse_bound(effective_lower); + auto parsed = parse_bound(effective_lower); + lb_bytes = std::move(parsed.bytes); + lb_absolute = parsed.absolute; } std::vector ub_bytes; + bool ub_absolute = false; bool has_upper = !effective_upper.empty(); if (has_upper) { - ub_bytes = parse_bound(effective_upper); + auto parsed = parse_bound(effective_upper); + ub_bytes = std::move(parsed.bytes); + ub_absolute = parsed.absolute; } // For find: upper bound must be exclusive, so increment the encoded bytes @@ -2662,8 +2704,12 @@ read_only::get_table_rows( const read_only::get_table_rows_params& p, const fc:: scoped.insert(scoped.end(), bound.begin(), bound.end()); return scoped; }; - if (!lb_bytes.empty()) lb_bytes = prepend_scope_sec(lb_bytes); - if (has_upper) ub_bytes = prepend_scope_sec(ub_bytes); + // Keyed on whether a bound was SUPPLIED, not on its decoded byte length: a + // supplied bound that decodes to zero bytes still needs the prefix, or the + // scan starts at the front of the table instead of this scope. A raw cursor + // is exempt — it already carries the scope. + if (!effective_lower.empty() && !lb_absolute) lb_bytes = prepend_scope_sec(lb_bytes); + if (has_upper && !ub_absolute) ub_bytes = prepend_scope_sec(ub_bytes); } } else if (!resolved_index_name.empty()) { // Secondary index on an unscoped kv::table: nothing to prepend. @@ -2676,9 +2722,12 @@ read_only::get_table_rows( const read_only::get_table_rows_params& p, const fc:: memcpy(scoped.data() + scope_prefix_bytes.size(), bound.data(), bound.size()); return scoped; }; - lb_bytes = lb_bytes.empty() ? scope_prefix_bytes : prepend_scope(lb_bytes); + // A raw cursor is exempt from both: it is already the complete stored key. + if (!lb_absolute) + lb_bytes = effective_lower.empty() ? scope_prefix_bytes : prepend_scope(lb_bytes); if (has_upper) { - ub_bytes = prepend_scope(ub_bytes); + if (!ub_absolute) + ub_bytes = prepend_scope(ub_bytes); } else { // No upper bound: iterate the full scope prefix range. // Create an exclusive upper bound by incrementing the scope prefix. @@ -2752,28 +2801,26 @@ read_only::get_table_rows( const read_only::get_table_rows_params& p, const fc:: return r; }; - // Format a secondary-index `next_key` value. When `p.json` is set, emit - // a JSON object matching the bound syntax (e.g. `{"byowner":"u4"}`) so - // `upper_bound = next_key` round-trips through the bound parser, which - // expects JSON when `p.json` is set. Otherwise emit hex of the raw sk - // bytes. A key the codec cannot name falls back to hex, which the json=true - // bound parser accepts back verbatim — so next_key is always feedable. + // Format a secondary-index `next_key` value. When `p.json` is set, emit a JSON + // object matching the bound syntax (e.g. `{"byowner":"u4"}`) so + // `upper_bound = next_key` round-trips through the bound parser, which expects + // JSON when `p.json` is set. A key the codec cannot NAME falls back to a raw + // cursor, which that parser feeds back verbatim — so next_key is always feedable. auto emit_secondary_next_key = [&](std::string_view sk) { if (p.json) { - // Scope-RELATIVE for BOTH shapes: the bound parser prepends - // scope_prefix_bytes whichever one comes back, so hexing the - // absolute key would scope it twice. - std::string_view sv = sk; - if (!scope_prefix_bytes.empty() && sv.size() >= scope_prefix_bytes.size()) { - sv.remove_prefix(scope_prefix_bytes.size()); - } try { + // The shape describes the WITHIN-SCOPE fields, so the decode sees the + // remainder; the raw fallback below still carries the whole key. + std::string_view sv = sk; + if (!scope_prefix_bytes.empty() && sv.size() >= scope_prefix_bytes.size()) { + sv.remove_prefix(scope_prefix_bytes.size()); + } FC_ASSERT(bound_key_shapes, "be_key_codec: key shape unresolved"); auto key_var = chain::be_key_codec::decode_key(sv.data(), sv.size(), *bound_key_shapes); hp.next_key = fc::json::to_string(key_var, fc::time_point::maximum()); return; } catch (...) { - hp.next_key = fc::to_hex(sv.data(), sv.size()); + hp.next_key = to_raw_cursor(sk); return; } } @@ -2889,6 +2936,12 @@ read_only::get_table_rows( const read_only::get_table_rows_params& p, const fc:: } catch (...) { // strip_scope_fields drops the scope on the JSON path; keep the // hex form scope-relative so the two describe the same key. + // + // Deliberately NOT a raw cursor: this is a row's key as DISPLAYED, + // and it has to line up with the stripped object form above. + // next_key is the resume token and is absolute for its own reasons + // (see to_raw_cursor) — both remain feedable as bounds, because + // untagged hex still means "within the scope". obj["key"] = fc::to_hex(row.key.data() + scope_prefix_size, row.key.size() - scope_prefix_size); } @@ -2941,11 +2994,7 @@ read_only::get_table_rows( const read_only::get_table_rows_params& p, const fc:: auto stripped = strip_scope_fields(std::move(full_key), scope_key_count); hp.next_key = fc::json::to_string(stripped, fc::time_point::maximum()); } catch (...) { - // Scope-RELATIVE, exactly like the JSON key object it replaces: the - // bound parser prepends scope_prefix_bytes for either shape, so an - // absolute cursor would be scoped twice and skip the range. - hp.next_key = fc::to_hex(kv.data() + scope_prefix_bytes.size(), - static_cast(kv.size() - scope_prefix_bytes.size())); + hp.next_key = to_raw_cursor(kv); } } else { // json=false takes the stored key verbatim on the way back in. diff --git a/tests/get_table_tests.cpp b/tests/get_table_tests.cpp index 2122590cb3..a592f88f31 100644 --- a/tests/get_table_tests.cpp +++ b/tests/get_table_tests.cpp @@ -861,11 +861,12 @@ BOOST_FIXTURE_TEST_CASE( get_table_next_key_test, validating_tester ) try { BOOST_CHECK_EQUAL(page2.rows[0].get_object()["value"].get_object()["code"].as_string(), "WIRE"); BOOST_CHECK_EQUAL(page2.rows[0].get_object()["value"].get_object()["payload"].as_uint64(), 300u); - // (d) A key with NO canonical spelling renders as bare hex, and that hex is - // accepted straight back as a json=true bound — so a cursor landing on - // one still resumes exactly. This is the property pagination needs - // (encode(decode(key)) == key), and naming such a key could not provide - // it: slug_name's to_string is not injective over raw uint64s. + // (d) A key with NO canonical spelling renders as a RAW CURSOR — the `0x` tag + // plus the hex of the whole stored key — and that is accepted straight + // back as a json=true bound, so a cursor landing on one still resumes + // exactly. This is the property pagination needs (encode(decode(key)) == + // key), and naming such a key could not provide it: slug_name's to_string + // is not injective over raw uint64s. // // Stored through the transitional object carrier, the only writer left // that can express a raw value. 34<<42 is the packed former spelling @@ -879,9 +880,9 @@ BOOST_FIXTURE_TEST_CASE( get_table_next_key_test, validating_tester ) try { auto pageA = get_table_rows_full(plugin, p, fc::time_point::maximum()); BOOST_REQUIRE_EQUAL(pageA.rows.size(), 3u); BOOST_REQUIRE_EQUAL(pageA.more, true); - BOOST_REQUIRE(!pageA.next_key.empty()); - // Bare hex, not a JSON key object -- the leaf refused to name it. - BOOST_CHECK(pageA.next_key.front() != '{'); + // A tagged raw cursor, not a JSON key object -- the leaf refused to name it. + // Unscoped, so the whole key IS the 8-byte slug: 34<<42 = 0000880000000000. + BOOST_CHECK_EQUAL(pageA.next_key, "0x0000880000000000"); p.lower_bound = pageA.next_key; p.limit = 50; @@ -902,7 +903,7 @@ BOOST_FIXTURE_TEST_CASE( get_table_next_key_test, validating_tester ) try { auto revA = get_table_rows_full(plugin, p, fc::time_point::maximum()); BOOST_REQUIRE_EQUAL(revA.rows.size(), 1u); BOOST_REQUIRE_EQUAL(revA.more, true); - BOOST_CHECK(revA.next_key.front() != '{'); + BOOST_CHECK_EQUAL(revA.next_key, "0x0000880000000000"); BOOST_CHECK_EQUAL(revA.rows[0].get_object()["value"].get_object()["payload"].as_uint64(), 400u); p.upper_bound = revA.next_key; @@ -914,11 +915,11 @@ BOOST_FIXTURE_TEST_CASE( get_table_next_key_test, validating_tester ) try { p.upper_bound.clear(); } - // (sec-11e) SCOPED slug-keyed table. A json=true cursor must be scope-RELATIVE: - // get_table_rows prepends the scope prefix to whatever bound comes - // back, so an absolute hex cursor is scoped TWICE and the query skips - // the rest of the range. Unscoped (sec-11d) cannot catch that — there - // is no prefix to double. + // (sec-11e) SCOPED slug-keyed table. A raw cursor carries the COMPLETE stored + // key and the bound parser feeds it back verbatim, so the scope prefix + // is neither stripped on the way out nor re-added on the way in. + // Unscoped (sec-11d) cannot catch a mistake here — there is no prefix + // to double or drop. { const uint64_t sc = chain::name("sc1").to_uint64_t(); push_action("test"_n, "addsslug"_n, "test"_n, mutable_variant_object() @@ -944,14 +945,15 @@ BOOST_FIXTURE_TEST_CASE( get_table_next_key_test, validating_tester ) try { auto pageA = get_table_rows_full(plugin, p, fc::time_point::maximum()); BOOST_REQUIRE_EQUAL(pageA.rows.size(), 3u); BOOST_REQUIRE_EQUAL(pageA.more, true); - BOOST_REQUIRE(!pageA.next_key.empty()); - BOOST_CHECK(pageA.next_key.front() != '{'); // hex fallback, not a JSON key + // The tagged ABSOLUTE key: "0x" + scope name("sc1") + slug 34<<42. A cursor + // carrying only the within-scope remainder would be "0x0000880000000000", + // and the parser — which no longer re-prefixes a tagged bound — would seek + // into the wrong scope entirely. + BOOST_CHECK_EQUAL(pageA.next_key, "0xc2020000000000000000880000000000"); p.lower_bound = pageA.next_key; p.limit = 50; auto pageB = get_table_rows_full(plugin, p, fc::time_point::maximum()); - // With a scope-ABSOLUTE cursor this returns 0 rows: the bound parser - // prepends the scope a second time and the seek lands past the range. BOOST_REQUIRE_EQUAL(pageB.rows.size(), 1u); BOOST_CHECK_EQUAL(pageB.rows[0].get_object()["value"].get_object()["payload"].as_uint64(), 40u); @@ -962,7 +964,7 @@ BOOST_FIXTURE_TEST_CASE( get_table_next_key_test, validating_tester ) try { auto revA = get_table_rows_full(plugin, p, fc::time_point::maximum()); BOOST_REQUIRE_EQUAL(revA.rows.size(), 1u); BOOST_REQUIRE_EQUAL(revA.more, true); - BOOST_CHECK(revA.next_key.front() != '{'); + BOOST_CHECK_EQUAL(revA.next_key, "0xc2020000000000000000880000000000"); BOOST_CHECK_EQUAL(revA.rows[0].get_object()["value"].get_object()["payload"].as_uint64(), 40u); p.upper_bound = revA.next_key; @@ -971,6 +973,41 @@ BOOST_FIXTURE_TEST_CASE( get_table_next_key_test, validating_tester ) try { BOOST_REQUIRE_EQUAL(revB.rows.size(), 3u); // WIRE, SOL, ETH BOOST_CHECK_EQUAL(revB.rows[0].get_object()["value"].get_object()["payload"].as_uint64(), 30u); p.reverse = false; + p.upper_bound.clear(); + + // (sec-11f) A raw cursor is usable at its SHORTEST: bytes that are nothing + // but the scope prefix. Nothing writes such a key through + // kv::scoped_table, but kv_set only requires the COMPLETE key to be + // nonempty, so a raw write can store one — and if a cursor ever + // names it, the within-scope remainder is empty. An empty next_key + // reads as "no bound" and restarts the page forever; the tag is + // what keeps even this cursor nonempty and exact. + // + // Re-prefixing it would seek to , past every row. + p.lower_bound = "0xc202000000000000"; + p.limit = 50; + auto scopeStart = get_table_rows_full(plugin, p, fc::time_point::maximum()); + BOOST_CHECK_EQUAL(scopeStart.rows.size(), 4u); + p.lower_bound.clear(); + + // (sec-11g) A JSON bound with leading whitespace still parses as JSON. + // `fc::json::from_string` has always accepted it, so dispatching on + // the first character rather than the first NON-WHITESPACE one sent + // a valid bound to the hex reader, which throws on the brace. + p.lower_bound = " {\"code\":\"SOL\"}"; + auto padded = get_table_rows_full(plugin, p, fc::time_point::maximum()); + BOOST_REQUIRE_EQUAL(padded.rows.size(), 3u); // SOL, WIRE, 7 + BOOST_CHECK_EQUAL(padded.rows[0].get_object()["value"].get_object()["payload"].as_uint64(), 20u); + + // A bound that is ONLY whitespace is refused, not trimmed away — + // turning one into zero bytes would read as "no bound" downstream + // and silently restart the page. + p.lower_bound = " "; + BOOST_CHECK_THROW( + get_table_rows_full(plugin, p, fc::time_point::maximum()), + chain::contract_table_query_exception + ); + p.lower_bound.clear(); } // (sec-5) Invalid index name on multi_index — should throw, not silently diff --git a/unittests/test-contracts/get_table_test/get_table_test.hpp b/unittests/test-contracts/get_table_test/get_table_test.hpp index 5efd3c74bf..71b68252f8 100644 --- a/unittests/test-contracts/get_table_test/get_table_test.hpp +++ b/unittests/test-contracts/get_table_test/get_table_test.hpp @@ -160,12 +160,13 @@ class [[sysio::contract]] get_table_test : public sysio::contract { // The same registry shape, but SCOPED — stored as [scope:8B BE][key], with a // secondary index so both cursor paths are reachable. // - // A json=true cursor must be scope-RELATIVE, because get_table_rows prepends - // the scope prefix to whatever bound comes back. That holds trivially for a - // decoded JSON key object; it is the HEX fallback (a key the codec cannot - // name) that can get it wrong, and an absolute cursor is then scoped twice - // and skips the rest of the range. Unscoped `slugobjs` above cannot catch - // that — there is no prefix to double. + // Two json=true cursor shapes meet here and only the scoped table can tell + // them apart. A decoded JSON key object names the fields WITHIN the scope, so + // get_table_rows prepends the scope prefix to it; the RAW cursor a key the + // codec cannot name falls back to carries the COMPLETE key and is fed back + // verbatim. Getting either one's prefix handling wrong seeks into the wrong + // scope — and unscoped `slugobjs` above cannot catch it, since there is no + // prefix to double or drop. struct sslugobj_key { slug_name code; uint64_t primary_key() const { return code.value; } From 2dbca9c06ace3d2e57bd0ab1539050892f2e8861 Mon Sep 17 00:00:00 2001 From: kevin Heifner Date: Wed, 23 Sep 2026 12:15:12 -0500 Subject: [PATCH 22/29] fix(chain_plugin): keep an absolute bound inside the scope it names The `0x` tag identifies the CARRIER; it does not prove the bytes came from this request. An unchecked absolute bound skipped prefixing entirely, so `scope=B` with a bound naming scope A seeked into A while the default upper bound was still the end of B -- the scan returned every scope in between. A bare `0x` decoded to nothing and started at the front of the table, and `find` shares the bound path, so it escaped its own scope the same way. parse_bound now requires an absolute bound to be at least the prefix length and to match it exactly, and rejects rather than clamps: a bound naming another scope is a caller error, and quietly returning a different range is how that stays invisible. One guard covers lower, upper and find, since find feeds both. Also: - slug_name.hpp carried a stale doc block from when the renderer threw, and claimed the lossy cases match `name`. They do not: name's alphabet is 2^5 with no gaps over all 64 bits, so every raw uint64 IS canonical and its render is total AND injective. Only slug_name can lose. - docs/get-table-rows-api.md described every bound as a key object and every next_key as scope-stripped; neither holds for the raw cursor. - sslugobjs gains `byalt`, a SLUG-typed secondary. `bypayload` is a uint64 and cannot fail to decode, so the secondary cursor's raw fallback was unreachable rather than merely uncovered; sec-11h now drives it in both directions, and sec-11i covers the scope guard across lower/upper/find on the primary and secondary paths. Change-Id: I483ec801369e6154a8748af23dd025ac7e6a1cbc --- docs/get-table-rows-api.md | 10 +- libraries/libfc/include/fc/slug_name.hpp | 41 ++++---- plugins/chain_plugin/src/chain_plugin.cpp | 16 +++ tests/get_table_tests.cpp | 99 +++++++++++++++++- .../get_table_test/get_table_test.abi | 13 +++ .../get_table_test/get_table_test.cpp | 4 +- .../get_table_test/get_table_test.hpp | 16 ++- .../get_table_test/get_table_test.wasm | Bin 23520 -> 23653 bytes 8 files changed, 167 insertions(+), 32 deletions(-) diff --git a/docs/get-table-rows-api.md b/docs/get-table-rows-api.md index 19f347aae9..4027427702 100644 --- a/docs/get-table-rows-api.md +++ b/docs/get-table-rows-api.md @@ -18,7 +18,7 @@ POST /v1/chain/get_table_rows | `scope` | string | `""` | Scope for scoped tables. Empty = unscoped (all rows). Parsed using ABI type. | | `find` | string | `""` | Exact key lookup (JSON key object or hex). Cannot be combined with bounds. | | `index_name` | string | `""` | Secondary index name (e.g. `"byowner"`) or position (e.g. `"2"`). Empty = primary key. | -| `lower_bound` | string | `""` | Lower bound (inclusive). JSON key object when `json=true`, hex when `json=false`. | +| `lower_bound` | string | `""` | Lower bound (inclusive). When `json=true`: a JSON key object, or a `0x`-prefixed raw cursor as returned by `next_key`. When `json=false`: hex. | | `upper_bound` | string | `""` | Upper bound (exclusive). Same format as `lower_bound`. | | `limit` | uint32 | `50` | Max rows to return | | `reverse` | bool | `false` | Iterate in reverse order | @@ -39,7 +39,7 @@ POST /v1/chain/get_table_rows - `rows` — array of `{key, value}` objects. When `show_payer=true`, includes `payer` field. - `more` — `true` if there are more rows beyond `limit`. -- `next_key` — use as `lower_bound` for the next page. Scope is stripped (pass same `scope` param). +- `next_key` — use as `lower_bound` for the next page. Usually a JSON key object with the scope stripped (pass the same `scope` param); for a key the ABI cannot name it is a `0x` raw cursor instead — an opaque, complete key. Feed either back verbatim. ## Scoped Queries @@ -96,7 +96,9 @@ Where `1397703940` is `symbol_code("SYS").raw()`. Returns alice's SYS balance. ### Bounded range -Bounds are JSON objects with field names matching the table's `key_names` from the ABI. For scoped tables with `scope` set, bounds represent the within-scope key (scope is prepended automatically). +Bounds are JSON objects with field names matching the table's `key_names` from the ABI. For scoped tables with `scope` set, a key object represents the within-scope key and the scope is prepended automatically. + +A `0x` raw cursor is the exception: it is already the complete stored key, so it is used verbatim and nothing is prepended. It must lie inside the `scope` the request names — a cursor from another scope is rejected rather than silently returning that other scope's rows. ```json { @@ -174,7 +176,7 @@ When `more` is `true`, use `next_key` as `lower_bound` for the next request. Kee } ``` -`next_key` is scope-stripped — the scope prefix is not included. Just pass it back as `lower_bound` with the same `scope`. +A `next_key` key object is scope-stripped — the scope prefix is not included, so pass it back as `lower_bound` with the same `scope`. A `0x` raw cursor already carries the scope; pass it back with the same `scope` too, and it is used as-is. ## RAM Payer diff --git a/libraries/libfc/include/fc/slug_name.hpp b/libraries/libfc/include/fc/slug_name.hpp index a2ed7eebaf..c1c8611aff 100644 --- a/libraries/libfc/include/fc/slug_name.hpp +++ b/libraries/libfc/include/fc/slug_name.hpp @@ -139,26 +139,29 @@ using slug_name_literals::operator""_s; /// type-disjoint carrier — and why the carrier could not have been a numeric /// string before the rule existed. /// -/// A packed value whose leading symbol slot is 0 or a digit is not a code and -/// has no spelling; rendering one THROWS. That is an invariant assertion, not a -/// carrier decision: such a value must never be persisted in the first place, -/// which is the job of the validation at the proto boundary where a raw -/// `uint64` becomes a slug (`sysio.msgch`'s dispatch path and the opreg/uwrit -/// /reserv writers). Until that lands, a stored one is a defect that surfaces -/// here rather than being silently rendered as something it is not. -/// Render a slug as its spelling. TOTAL, exactly like sysio::chain::name — a -/// renderer is a READ path, and a throwing one turns one bad row into a failure -/// of everything that scans it (an unspellable code would stall every -/// underwriter_plugin commit, not just that cell). Validation lives on the WRITE -/// path: from_variant's string arm goes through the validating constructor, so a -/// non-canonical spelling is refused at construction. +/// Render a slug as its spelling. TOTAL, like sysio::chain::name — a renderer is +/// a READ path, and a throwing one turns one bad row into a failure of everything +/// that scans it (an unspellable code would stall every underwriter_plugin commit, +/// not just that cell). Validation lives on the WRITE path: from_variant's string +/// arm goes through the validating constructor, so a non-canonical spelling is +/// refused at construction. /// -/// A value with no canonical spelling renders as whatever it decodes to, and is -/// lossy in the same two ways name is: one with a non-empty char[0] renders a -/// string from_variant then rejects (loud), while one below the 1<<42 floor has -/// an empty char[0] and renders "" — indistinguishable from zero (silent). Both -/// are name's behaviour; neither can be prevented here, because a prepacked -/// binary action sets the reflected `value` directly for either type. +/// TOTAL is where the resemblance to `name` ends, and the difference is the whole +/// reason is_canonical() exists here and would be meaningless there. `name`'s +/// alphabet is exactly 2^5 with no gaps and its 13 symbols consume all 64 bits, so +/// every raw uint64 IS a canonical name and its render is total AND injective — +/// nothing is ever lost. slug_name's decode is injective only over its canonical +/// range, so a value outside that range renders as whatever it decodes to, lossily, +/// in two ways with no counterpart in `name`: +/// +/// - a non-empty leading symbol renders a string from_variant then REJECTS (loud); +/// - a value below 1<<42 has an empty leading slot and renders "" — +/// indistinguishable from the zero sentinel (silent). +/// +/// Neither can be prevented in a renderer: a prepacked binary action sets the +/// reflected `value` directly, so such a value is already stored by the time +/// anything reads it. Keeping it out is the job of validation at the proto +/// boundary, where a raw uint64 becomes a slug. inline void to_variant(const slug_name& s, fc::variant& v) { v = s.to_string(); } diff --git a/plugins/chain_plugin/src/chain_plugin.cpp b/plugins/chain_plugin/src/chain_plugin.cpp index 04e9a16a8c..c1f9543b3b 100644 --- a/plugins/chain_plugin/src/chain_plugin.cpp +++ b/plugins/chain_plugin/src/chain_plugin.cpp @@ -2663,6 +2663,22 @@ read_only::get_table_rows( const read_only::get_table_rows_params& p, const fc:: out.absolute = p.json && hex.size() != body.size(); const auto v = fc::from_hex(hex); out.bytes.assign(v.begin(), v.end()); + // The tag identifies the CARRIER; it does not prove these caller-supplied bytes + // belong to the scope this request named. Skipping the prefix on an unchecked + // absolute bound is a scope bypass: `scope=B` with a bound naming scope A seeks + // into A while the default upper bound is still the end of B, so the scan walks + // every scope in between; a bare `0x` decodes to nothing and starts at the front + // of the table. `find` shares this path, so it escapes its own scope the same way. + // + // Rejected rather than clamped — a bound naming another scope is a caller error, + // and quietly returning a different range is how that stays invisible. + if (out.absolute && !scope_prefix_bytes.empty()) { + SYS_ASSERT(out.bytes.size() >= scope_prefix_bytes.size() + && std::equal(scope_prefix_bytes.begin(), scope_prefix_bytes.end(), + out.bytes.begin()), + chain::contract_table_query_exception, + "Table {} bound '{}' is outside scope {}", p.table, bound, p.scope); + } return out; }; diff --git a/tests/get_table_tests.cpp b/tests/get_table_tests.cpp index a592f88f31..2d47ff74ea 100644 --- a/tests/get_table_tests.cpp +++ b/tests/get_table_tests.cpp @@ -922,17 +922,20 @@ BOOST_FIXTURE_TEST_CASE( get_table_next_key_test, validating_tester ) try { // to double or drop. { const uint64_t sc = chain::name("sc1").to_uint64_t(); + // `alt` is the SLUG-typed secondary (`byalt`). It mirrors `code`'s canonicality + // so the same boundary row is un-nameable on both the primary and the secondary + // cursor; `bypayload` is a uint64 and always decodes, so it cannot reach either. push_action("test"_n, "addsslug"_n, "test"_n, mutable_variant_object() - ("scope", sc)("code", "ETH")("payload", 10)); + ("scope", sc)("code", "ETH")("payload", 10)("alt", "AAA")); push_action("test"_n, "addsslug"_n, "test"_n, mutable_variant_object() - ("scope", sc)("code", "SOL")("payload", 20)); + ("scope", sc)("code", "SOL")("payload", 20)("alt", "BBB")); push_action("test"_n, "addsslug"_n, "test"_n, mutable_variant_object() - ("scope", sc)("code", "WIRE")("payload", 30)); + ("scope", sc)("code", "WIRE")("payload", 30)("alt", "CCC")); // The un-nameable one, stored through the transitional object carrier. // 34<<42 is the packed former spelling "7"; char[0] = 34 sorts it last. push_action("test"_n, "addsslug"_n, "test"_n, mutable_variant_object() ("scope", sc)("code", mutable_variant_object()("value", uint64_t{34} << 42)) - ("payload", 40)); + ("payload", 40)("alt", mutable_variant_object()("value", uint64_t{34} << 42))); chain_apis::read_only::get_table_rows_params p; p.json = true; @@ -1008,6 +1011,94 @@ BOOST_FIXTURE_TEST_CASE( get_table_next_key_test, validating_tester ) try { chain::contract_table_query_exception ); p.lower_bound.clear(); + + // (sec-11h) The SECONDARY cursor's raw fallback. `bypayload` is a uint64 and + // always decodes, so only a SLUG-typed secondary can reach the + // emitter's catch at all — `byalt` exists for that. + p.index_name = "byalt"; + p.limit = 3; + auto secA = get_table_rows_full(plugin, p, fc::time_point::maximum()); + BOOST_REQUIRE_EQUAL(secA.rows.size(), 3u); + BOOST_REQUIRE_EQUAL(secA.more, true); + // Absolute and tagged, exactly like the primary: "0x" + scope + the packed alt. + BOOST_CHECK_EQUAL(secA.next_key, "0xc2020000000000000000880000000000"); + + p.lower_bound = secA.next_key; + p.limit = 50; + auto secB = get_table_rows_full(plugin, p, fc::time_point::maximum()); + BOOST_REQUIRE_EQUAL(secB.rows.size(), 1u); + BOOST_CHECK_EQUAL(secB.rows[0].get_object()["value"].get_object()["payload"].as_uint64(), 40u); + p.lower_bound.clear(); + + // REVERSE on the same index. The reverse branch has its own emitter + // call and names the LAST RETURNED row rather than the first unseen + // one, so a forward-only test leaves exactly the half where the + // earlier relative/absolute cursor defects lived. + p.reverse = true; + p.limit = 1; + auto secRevA = get_table_rows_full(plugin, p, fc::time_point::maximum()); + BOOST_REQUIRE_EQUAL(secRevA.rows.size(), 1u); + BOOST_REQUIRE_EQUAL(secRevA.more, true); + BOOST_CHECK_EQUAL(secRevA.rows[0].get_object()["value"].get_object()["payload"].as_uint64(), 40u); + BOOST_CHECK_EQUAL(secRevA.next_key, "0xc2020000000000000000880000000000"); + + p.upper_bound = secRevA.next_key; + p.limit = 50; + auto secRevB = get_table_rows_full(plugin, p, fc::time_point::maximum()); + BOOST_REQUIRE_EQUAL(secRevB.rows.size(), 3u); // CCC, BBB, AAA + BOOST_CHECK_EQUAL(secRevB.rows[0].get_object()["value"].get_object()["payload"].as_uint64(), 30u); + p.upper_bound.clear(); + p.reverse = false; + p.index_name.clear(); + + // (sec-11i) A raw cursor is a CARRIER tag, not proof of provenance. An absolute + // bound naming a different scope must be refused, not silently + // honoured -- seeking into scope A while the default upper bound is + // still the end of B returns every scope in between. Same for a bound + // too short to carry a scope at all, and for `find`, which feeds both + // bounds from one value. + const std::string other_scope_cursor = "0xc302000000000000" "0000154200000000"; + for (const std::string& bad : { other_scope_cursor, // names another scope + std::string("0x"), // no bytes at all + std::string("0xc2020000") }) { // truncated prefix + p.lower_bound = bad; + BOOST_CHECK_THROW( + get_table_rows_full(plugin, p, fc::time_point::maximum()), + chain::contract_table_query_exception + ); + p.lower_bound.clear(); + + p.upper_bound = bad; + BOOST_CHECK_THROW( + get_table_rows_full(plugin, p, fc::time_point::maximum()), + chain::contract_table_query_exception + ); + p.upper_bound.clear(); + + p.find = bad; + BOOST_CHECK_THROW( + get_table_rows_full(plugin, p, fc::time_point::maximum()), + chain::contract_table_query_exception + ); + p.find.clear(); + + // ...and the same three on the secondary path. + p.index_name = "byalt"; + p.lower_bound = bad; + BOOST_CHECK_THROW( + get_table_rows_full(plugin, p, fc::time_point::maximum()), + chain::contract_table_query_exception + ); + p.lower_bound.clear(); + p.index_name.clear(); + } + + // The in-scope cursor still works, so the guard rejects provenance and not + // every absolute bound. + p.lower_bound = "0xc2020000000000000000880000000000"; + auto stillOk = get_table_rows_full(plugin, p, fc::time_point::maximum()); + BOOST_CHECK_EQUAL(stillOk.rows.size(), 1u); + p.lower_bound.clear(); } // (sec-5) Invalid index name on multi_index — should throw, not silently diff --git a/unittests/test-contracts/get_table_test/get_table_test.abi b/unittests/test-contracts/get_table_test/get_table_test.abi index d454d4b2b4..9b69b9f93c 100644 --- a/unittests/test-contracts/get_table_test/get_table_test.abi +++ b/unittests/test-contracts/get_table_test/get_table_test.abi @@ -52,6 +52,10 @@ { "name": "payload", "type": "uint64" + }, + { + "name": "alt", + "type": "slug_name" } ] }, @@ -182,6 +186,10 @@ { "name": "payload", "type": "uint64" + }, + { + "name": "alt", + "type": "slug_name" } ] }, @@ -328,6 +336,11 @@ "name": "bypayload", "key_type": "uint64", "table_id": 64287 + }, + { + "name": "byalt", + "key_type": "slug_name", + "table_id": 52539 } ] }, diff --git a/unittests/test-contracts/get_table_test/get_table_test.cpp b/unittests/test-contracts/get_table_test/get_table_test.cpp index 41b6704f8f..b49159d190 100644 --- a/unittests/test-contracts/get_table_test/get_table_test.cpp +++ b/unittests/test-contracts/get_table_test/get_table_test.cpp @@ -50,7 +50,7 @@ void get_table_test::addslug(slug_name code, uint64_t payload) { slugobjs_table.emplace( get_self(), { code }, { code, payload } ); } -void get_table_test::addsslug(uint64_t scope, slug_name code, uint64_t payload) { +void get_table_test::addsslug(uint64_t scope, slug_name code, uint64_t payload, slug_name alt) { sslugobjs sslugobjs_table( get_self(), scope ); - sslugobjs_table.emplace( get_self(), { code }, { code, payload } ); + sslugobjs_table.emplace( get_self(), { code }, { code, payload, alt } ); } diff --git a/unittests/test-contracts/get_table_test/get_table_test.hpp b/unittests/test-contracts/get_table_test/get_table_test.hpp index 71b68252f8..97397248c1 100644 --- a/unittests/test-contracts/get_table_test/get_table_test.hpp +++ b/unittests/test-contracts/get_table_test/get_table_test.hpp @@ -176,14 +176,21 @@ class [[sysio::contract]] get_table_test : public sysio::contract { struct [[sysio::table("sslugobjs")]] sslugobj { slug_name code; uint64_t payload = 0; + // A SLUG-typed secondary. `bypayload` is a uint64 and always decodes, so it + // can never reach the secondary cursor's raw fallback; only a secondary whose + // key can fail to be NAMED exercises that path. + slug_name alt; uint64_t by_payload() const { return payload; } - SYSLIB_SERIALIZE(sslugobj, (code)(payload)) + slug_name by_alt() const { return alt; } + SYSLIB_SERIALIZE(sslugobj, (code)(payload)(alt)) }; typedef sysio::kv::scoped_table< "sslugobjs"_n, sslugobj_key, sslugobj, sysio::kv::index<"bypayload"_n, - sysio::const_mem_fun>> sslugobjs; + sysio::const_mem_fun>, + sysio::kv::index<"byalt"_n, + sysio::const_mem_fun>> sslugobjs; [[sysio::action]] void addnumobj(uint64_t input); @@ -215,8 +222,11 @@ class [[sysio::contract]] get_table_test : public sysio::contract { /// @param scope the table scope /// @param code the slug forming the primary key /// @param payload arbitrary row payload; also the `bypayload` secondary + /// @param alt a second slug, indexed by `byalt` -- a SLUG-typed secondary, + /// so a non-canonical value here cannot be named and drives the + /// secondary cursor's raw fallback [[sysio::action]] - void addsslug(uint64_t scope, slug_name code, uint64_t payload); + void addsslug(uint64_t scope, slug_name code, uint64_t payload, slug_name alt); }; diff --git a/unittests/test-contracts/get_table_test/get_table_test.wasm b/unittests/test-contracts/get_table_test/get_table_test.wasm index cf7892e7cc514b085992602a682884f443a1e1ae..18a79c6e986c4933d5f35811f629b0b65016e0bd 100755 GIT binary patch delta 378 zcmYL_y-Nc@5XE<9?k+)7tROK2l#L!F0mWRC6Kq@y2}aN&hKP;z6&7L>3$^zvvYm}e z8nF_$P)H|&miY@bjkSfXg>#8wGrz~<&10C}1AIKf-8JG~)3kBNjM7Hp9ERGr(6qZW z;ioeavij>A6ev_!z(Z87&e)s$yq7wLY-|@WB~R?<%q$LHA<_ZuPYK;K@A>5eg=H)# zis8plPE^KeMcR*eWFonmRkSEVl7F5Yc!NOlKOlYs{f#0ebFQKyiJTWh<`Ue_A`?=B z6&J2C5fHPaIKLfO^ HY;@@dIayEu delta 187 zcmaF5gYm(3#tjRZm?|qKFJ>y5EXEwcK7o;I{z}G*&85t8DwE~aSeZZnVVrzhO?mS^ zbu~7gZbm5uMg=AfCLV#F%^!62nHc|0*3-|Kyk3vj(22pEnL~luQ6>vWaAXPiOF=nc zmL@ZUBC7(kCNqa3n*uY4Au##0zS(4DLkX4(3|T6Z4;r>iUTr11`G(;#rOEe0WG2rF gF_^3!I-jw3^XX6)M#jF)7s8G(GWKtt8gYRc03Ome=Kufz From cd0246e51d395ab0bbc6cff2a28b3580b74d933f Mon Sep 17 00:00:00 2001 From: kevin Heifner Date: Wed, 23 Sep 2026 14:03:27 -0500 Subject: [PATCH 23/29] docs(slug_name): correct the non-canonical rendering contract Two documentation contracts still described behavior the code no longer has. The lossy cases were given as two -- loud rejection or an empty render -- with a non-empty leading symbol implying the loud one. That is false. to_string() reads only bits 0-47 and stops at the first zero symbol, so anything in bits 48-63 or after an interior zero is never looked at: `pack("ETH") | 1<<63` renders "ETH" and `pack("A") | 1` renders "A", both VALID spellings that re-parse cleanly to a different value than they came from. There are three outcomes, and the leading symbol does not say which. The consequence worth knowing is that two distinct raw values can share one spelling, so a successful render proves nothing; is_canonical() is what separates them. The registry guard's rationale still said an uncanonical code makes rendering throw, makes get_table_rows fall back to hex, and drops an underwriter scan. The renderer is total now, so none of that holds -- but the guard matters more for it, not less: the damage went from loud to silent. Restated as the invariant it actually keeps, and swept through the writer and the registry tests. Comments and one test only; no production code and no artifact change. Change-Id: I8551023bbc9eb64cc78b44364a47e8097c5a5576 --- .../sysio.opp.common/registry_codes.hpp | 28 +++++++-------- contracts/sysio.opreg/src/sysio.opreg.cpp | 16 +++++---- contracts/tests/sysio.chains_tests.cpp | 3 +- contracts/tests/sysio.opreg_tests.cpp | 6 ++-- contracts/tests/sysio.reserv_tests.cpp | 8 +++-- contracts/tests/sysio.tokens_tests.cpp | 7 ++-- libraries/libfc/include/fc/slug_name.hpp | 17 +++++---- libraries/libfc/test/test_slug_name.cpp | 36 +++++++++++-------- 8 files changed, 66 insertions(+), 55 deletions(-) diff --git a/contracts/sysio.opp.common/include/sysio.opp.common/registry_codes.hpp b/contracts/sysio.opp.common/include/sysio.opp.common/registry_codes.hpp index e3b4f3bd76..2a4ea6a58e 100644 --- a/contracts/sysio.opp.common/include/sysio.opp.common/registry_codes.hpp +++ b/contracts/sysio.opp.common/include/sysio.opp.common/registry_codes.hpp @@ -4,22 +4,22 @@ * @brief The spelling guard every depot registry writer applies to a `slug_name` code. * * A `slug_name` is carried on the wire and in chain state as a packed `uint64`, but its - * JSON carrier is the canonical STRING spelling: `fc::slug_name`'s `to_variant` renders - * `to_string()` and asserts the value round-trips. Not every 64-bit value has such a - * spelling -- `7` does not -- and the raw `slug_name{uint64}` constructor does not check, - * by design, because the OPP dispatch surfaces must never throw on operator-relayed data. + * JSON carrier is the canonical STRING spelling. Not every 64-bit value has one -- `7` + * does not -- and the raw `slug_name{uint64}` constructor does not check, by design, + * because the OPP dispatch surfaces must never throw on operator-relayed data. * - * The consequence is asymmetric: a code with no spelling can be WRITTEN into a registry - * row, and every later attempt to RENDER that row throws. A depot registry has no erase - * action, so such a row is permanent, and the readers that trip over it are not the writer - * -- `get_table_rows` falls back to hex per cell, while the underwriter plugin's scan - * reaches an unconditional `get_object()` and drops its entire cycle. + * `fc::slug_name::to_variant` is TOTAL -- it renders `to_string()` without asserting + * canonicality, because a throwing read path turns one bad row into a failure of every + * scan over it. So an uncanonical code does not fail loudly, it renders MISLEADINGLY: + * `to_string()` reads only bits 0-47 and stops at the first zero symbol, so + * `pack("ETH") | 1<<63` renders "ETH" and re-parses to a different value. * - * Registry writers are privileged, top-level actions rather than dispatch handlers, so - * unlike the OPP surfaces they CAN refuse: `sysio::check` here reverts one admin - * transaction and writes nothing. That is the whole point of this guard -- it keeps the - * "a registered code is a renderable code" invariant true by construction, which is what - * lets every downstream registry LOOKUP stand in for a spelling check. + * Hence this guard. A registered code stays canonical, and distinct raw values never + * alias onto one spelling. A depot registry has no erase action, so a bad row is + * permanent -- and now silent. Registry writers are privileged top-level actions, so + * unlike the OPP dispatch surfaces they CAN refuse: `sysio::check` reverts one admin + * transaction and writes nothing, which is what lets a registry LOOKUP stand in for a + * canonicality check. * * Mirrors the shape of `registry_metadata.hpp`: a throwing helper for the admin writers, * while a never-throw dispatch handler asks `slug_name::is_canonical()` directly and diff --git a/contracts/sysio.opreg/src/sysio.opreg.cpp b/contracts/sysio.opreg/src/sysio.opreg.cpp index 32caefd184..6d331c8392 100644 --- a/contracts/sysio.opreg/src/sysio.opreg.cpp +++ b/contracts/sysio.opreg/src/sysio.opreg.cpp @@ -151,9 +151,10 @@ void require_positive_min_bond(const std::vector& v, /// Reject a collateral-requirement entry whose codes have no canonical string /// spelling. These entries persist on the config row and are rendered by every -/// reader of it, so an unspellable code makes the whole row unreadable — see -/// `registry_codes.hpp`. `setconfig` is a privileged top-level action, so it -/// refuses rather than absorbing the value the way a dispatch handler must. +/// reader of it — and an uncanonical code does not announce itself: it can render +/// a valid spelling that re-parses to a different value, aliasing onto another +/// code. See `registry_codes.hpp`. `setconfig` is a privileged top-level action, +/// so it refuses rather than absorbing the value the way a dispatch handler must. void require_canonical_codes(const std::vector& v, const char* role_label) { for (const auto& entry : v) { @@ -1253,10 +1254,11 @@ void opreg::depositinle(name account, auto deposit_action = build_deposit_action(actor, chain_code, token_code, amount); - // The balance map is keyed by (chain_code, token_code), so an unspellable token - // code cannot be persisted — every later render of that row would throw, and the - // underwriter's values_only scan drops the whole cycle rather than one cell. The - // outpost has already taken custody, so refund rather than drop. + // The balance map is keyed by (chain_code, token_code), so an uncanonical token + // code must not be persisted: rendering is total and will not complain, but the + // code can render a valid spelling that re-parses as a DIFFERENT code, aliasing + // one operator's balance onto another's key. The outpost has already taken + // custody, so refund rather than drop. if (!token_code.is_canonical()) { const std::string err = "token code has no canonical slug_name spelling"; emit_deposit_revert(get_self(), chain_code, actor, token_code, amount, diff --git a/contracts/tests/sysio.chains_tests.cpp b/contracts/tests/sysio.chains_tests.cpp index 53b32e52d6..6153a2b531 100644 --- a/contracts/tests/sysio.chains_tests.cpp +++ b/contracts/tests/sysio.chains_tests.cpp @@ -150,7 +150,8 @@ BOOST_FIXTURE_TEST_CASE(regchain_evm_bad_hex_rejected, sysio_chains_tester) { tr // `slug_name`'s canonical carrier is the STRING, but the transitional object form // `{"value": N}` packs a raw uint64 with no spelling check — that is how an unspellable // code reaches action JSON at all. The registry has no erase action, so such a row would -// be permanently unrenderable (and `to_variant` throws on every later read of it). +// be permanent — and rendering is TOTAL, so nothing downstream reports it: the value can +// decode to "" or to a valid spelling that re-parses as a DIFFERENT chain code. // `regchain` is a privileged top-level action, so unlike an OPP dispatch handler it can // simply refuse. BOOST_FIXTURE_TEST_CASE(regchain_uncanonical_code_rejected, sysio_chains_tester) { try { diff --git a/contracts/tests/sysio.opreg_tests.cpp b/contracts/tests/sysio.opreg_tests.cpp index 01f3dcac8c..401749a03f 100644 --- a/contracts/tests/sysio.opreg_tests.cpp +++ b/contracts/tests/sysio.opreg_tests.cpp @@ -625,9 +625,9 @@ BOOST_FIXTURE_TEST_CASE(setconfig_rejects_zero_min_bond, sysio_opreg_tester) { t BOOST_FIXTURE_TEST_CASE(setconfig_rejects_uncanonical_collateral_code, sysio_opreg_tester) { try { // A `slug_name` reaches action JSON either as its canonical STRING or through the // transitional object form `{"value": N}`, and only the string arm validates. These - // entries persist on the config row, so an unspellable code makes the whole row - // unrenderable — `to_variant` throws on every later read of it. `setconfig` is a - // privileged top-level action and refuses. + // entries persist on the config row, and rendering is TOTAL so an uncanonical code + // never announces itself — it can decode to "" or to a valid spelling that re-parses + // as a DIFFERENT code. `setconfig` is a privileged top-level action and refuses. constexpr uint64_t uncanonical = 7; // decodes to "", packs back to 0 — not a code BOOST_REQUIRE(!fc::slug_name{uncanonical}.is_canonical()); diff --git a/contracts/tests/sysio.reserv_tests.cpp b/contracts/tests/sysio.reserv_tests.cpp index 8cb943b568..89766a1d08 100644 --- a/contracts/tests/sysio.reserv_tests.cpp +++ b/contracts/tests/sysio.reserv_tests.cpp @@ -417,9 +417,11 @@ BOOST_FIXTURE_TEST_CASE(regreserve_creates_reserve_row, sysio_reserve_tester) { // A `slug_name` reaches action JSON either as its canonical STRING or through the // transitional object form `{"value": N}`, and only the string arm validates. A reserve -// row is keyed on all three codes and there is no erase action, so an unspellable code -// would make the row permanently unrenderable — `to_variant` throws on every later read. -// `regreserve` is a privileged bootstrap-window action and refuses. +// row is keyed on all three codes and there is no erase action, so an uncanonical code +// would be permanent. Rendering is TOTAL and will not complain: the value can decode to +// "" or to a valid spelling that re-parses as a DIFFERENT code, aliasing this reserve +// onto another's identity. `regreserve` is a privileged bootstrap-window action and +// refuses. BOOST_FIXTURE_TEST_CASE(regreserve_uncanonical_code_rejected, sysio_reserve_tester) { try { // Below the leading symbol's floor: decodes to "" and packs back to 0, so it is not a // code and has no spelling. diff --git a/contracts/tests/sysio.tokens_tests.cpp b/contracts/tests/sysio.tokens_tests.cpp index 4f85d2d863..2ef974af72 100644 --- a/contracts/tests/sysio.tokens_tests.cpp +++ b/contracts/tests/sysio.tokens_tests.cpp @@ -142,9 +142,10 @@ BOOST_FIXTURE_TEST_CASE(regctok_records_binding, sysio_tokens_tester) { try { // A `slug_name` reaches action JSON either as its canonical STRING or through the // transitional object form `{"value": N}`, and only the string arm validates. Neither -// registry has an erase action, so a code with no spelling would become a permanently -// unrenderable row — `to_variant` throws on every later read. Both writers are -// privileged top-level actions and refuse. +// registry has an erase action, so an uncanonical code would become a permanent row. +// Rendering is TOTAL and will not complain — the value can decode to "" or to a valid +// spelling that re-parses as a DIFFERENT code. Both writers are privileged top-level +// actions and refuse. BOOST_FIXTURE_TEST_CASE(regtoken_regctok_uncanonical_code_rejected, sysio_tokens_tester) { try { // Below the leading symbol's floor: decodes to "" and packs back to 0, so it is not a // code and has no spelling. diff --git a/libraries/libfc/include/fc/slug_name.hpp b/libraries/libfc/include/fc/slug_name.hpp index c1c8611aff..914042dad9 100644 --- a/libraries/libfc/include/fc/slug_name.hpp +++ b/libraries/libfc/include/fc/slug_name.hpp @@ -151,17 +151,16 @@ using slug_name_literals::operator""_s; /// alphabet is exactly 2^5 with no gaps and its 13 symbols consume all 64 bits, so /// every raw uint64 IS a canonical name and its render is total AND injective — /// nothing is ever lost. slug_name's decode is injective only over its canonical -/// range, so a value outside that range renders as whatever it decodes to, lossily, -/// in two ways with no counterpart in `name`: +/// range. Outside it the render either REJECTS on the way back or silently +/// NORMALIZES, and the leading symbol does not tell you which: /// -/// - a non-empty leading symbol renders a string from_variant then REJECTS (loud); -/// - a value below 1<<42 has an empty leading slot and renders "" — -/// indistinguishable from the zero sentinel (silent). +/// - a digit-leading symbol spells something from_variant refuses (loud); +/// - a value below 1<<42 renders "" — indistinguishable from the zero sentinel; +/// - to_string() reads only bits 0-47 and stops at the first zero symbol, so +/// `pack("ETH") | 1<<63` renders "ETH" and re-parses to a DIFFERENT value. /// -/// Neither can be prevented in a renderer: a prepacked binary action sets the -/// reflected `value` directly, so such a value is already stored by the time -/// anything reads it. Keeping it out is the job of validation at the proto -/// boundary, where a raw uint64 becomes a slug. +/// The last one means two distinct raw values can share a spelling. is_canonical() +/// is what separates them; a successful render is not. inline void to_variant(const slug_name& s, fc::variant& v) { v = s.to_string(); } diff --git a/libraries/libfc/test/test_slug_name.cpp b/libraries/libfc/test/test_slug_name.cpp index 70f29e8503..aaff55b153 100644 --- a/libraries/libfc/test/test_slug_name.cpp +++ b/libraries/libfc/test/test_slug_name.cpp @@ -454,8 +454,9 @@ BOOST_AUTO_TEST_CASE(variant_render_is_total_like_name) { // The renderer never throws — parity with sysio::chain::name, whose key leaf // is `name(raw).to_string()`. A read path that throws converts one bad row // into a failure of every scan over it; validation belongs on the write path. - // A prepacked binary action can set the reflected `value` directly for either - // type, so neither can guarantee it only ever holds a canonical value. + // A prepacked binary action sets the reflected `value` directly, so slug_name + // cannot guarantee it only ever holds a canonical value. (`name` needs no such + // guarantee: every uint64 is a canonical name — see the case below.) for (uint64_t raw : {uint64_t{1}, uint64_t{7}, uint64_t{42}, uint64_t{(uint64_t{1} << 42) - 1}, (uint64_t{1} << 48) - 1, // symbols past the alphabet @@ -466,28 +467,33 @@ BOOST_AUTO_TEST_CASE(variant_render_is_total_like_name) { } } -BOOST_AUTO_TEST_CASE(variant_non_canonical_render_is_lossy_the_same_two_ways_name_is) { - // Losing information is the price of a total renderer, and it takes exactly - // two shapes. Pinned so a future change to to_string() cannot move them - // silently. +BOOST_AUTO_TEST_CASE(variant_non_canonical_render_rejects_or_silently_normalizes) { + // A total renderer loses information in THREE ways, and the leading symbol does + // not tell you which: to_string() reads only bits 0-47 and stops at the first + // zero symbol. fc::variant v; + slug_name back; - // 1. char[0] non-empty -> a spelling exists but is not a legal code, so - // feeding it back fails LOUDLY at construction. - // '7' is symbol 34 in the alphabet, and char[0] occupies bits [42..47]. - constexpr uint64_t packed_seven = uint64_t{34} << 42; - BOOST_REQUIRE(!fc::slug_name{packed_seven}.is_canonical()); - fc::to_variant(slug_name{packed_seven}, v); + // LOUD — a digit-leading symbol spells something from_variant refuses. + fc::to_variant(slug_name{uint64_t{34} << 42}, v); // 34 is '7' BOOST_CHECK_EQUAL(v.as_string(), "7"); - slug_name back; BOOST_CHECK_THROW(fc::from_variant(v, back), fc::exception); - // 2. Below the 1<<42 floor -> char[0] is empty, so it renders "" and is - // indistinguishable from zero. This one is SILENT; name has the same hole. + // SILENT — below the 1<<42 floor renders "", aliasing the zero sentinel. fc::to_variant(slug_name{uint64_t{7}}, v); BOOST_CHECK_EQUAL(v.as_string(), ""); fc::from_variant(v, back); BOOST_CHECK_EQUAL(back.value, 0u); + + // SILENT — unread bits render a VALID spelling that re-parses to a different + // value, so two distinct raws can share one spelling. + for (uint64_t raw : { slug_name{"ETH"}.value | (uint64_t{1} << 63), // bits 48-63 + slug_name{"A"}.value | uint64_t{1} }) { // past the terminator + BOOST_REQUIRE(!slug_name{raw}.is_canonical()); + fc::to_variant(slug_name{raw}, v); + BOOST_REQUIRE_NO_THROW(fc::from_variant(v, back)); + BOOST_CHECK_NE(back.value, raw); + } } BOOST_AUTO_TEST_CASE(variant_every_canonical_value_round_trips_exactly) { From 07ebfc9466f2d69b70d26bb7150f5da44ce3f6b3 Mon Sep 17 00:00:00 2001 From: kevin Heifner Date: Wed, 23 Sep 2026 14:51:38 -0500 Subject: [PATCH 24/29] docs(slug_name): finish the total-renderer sweep The previous pass grepped for the exact phrasings it had written and missed every variant, so five more sites kept asserting that an uncanonical code cannot be rendered: sysio.chains.cpp, sysio.tokens.cpp, sysio.msgch.cpp, sysio.uwrit.cpp and the two dispatch tests. Three more turned up that the review had not named -- sysio.reserv.cpp twice and a second site in sysio.tokens.cpp. Each says the same thing now, inverted: the row is not unreadable, it is WRONG. Rendering is total, so an uncanonical code renders a spelling that packs back to a different value -- possibly another real code's -- and nothing downstream reports it. "a value with no spelling" was the phrasing underneath the original mistake, and it is false: to_string() is total, so every value HAS a spelling. What an uncanonical one lacks is a CANONICAL spelling. Normalized across the six files that used it, including abi_tests.cpp. test_slug_name.cpp's carrier preamble also still said the render throws and that the string is the only carrier, while the transitional {"value": N} object is accepted later in the same suite. Both corrected. Comments only; no production code, no artifact change. Change-Id: Ibb4b37a3d36fe6cac183bd2732bfdad39451c801 --- contracts/sysio.chains/src/sysio.chains.cpp | 6 ++++-- contracts/sysio.msgch/src/sysio.msgch.cpp | 8 ++++---- .../include/sysio.opp.common/registry_codes.hpp | 6 +++--- contracts/sysio.reserv/src/sysio.reserv.cpp | 4 ++-- contracts/sysio.tokens/src/sysio.tokens.cpp | 8 +++++--- contracts/sysio.uwrit/src/sysio.uwrit.cpp | 10 +++++----- contracts/tests/sysio.dispatch_tests.cpp | 14 +++++++------- libraries/libfc/include/fc/slug_name.hpp | 2 +- libraries/libfc/test/test_slug_name.cpp | 13 +++++++------ unittests/abi_tests.cpp | 2 +- 10 files changed, 39 insertions(+), 34 deletions(-) diff --git a/contracts/sysio.chains/src/sysio.chains.cpp b/contracts/sysio.chains/src/sysio.chains.cpp index 74882474dd..3c87dab928 100644 --- a/contracts/sysio.chains/src/sysio.chains.cpp +++ b/contracts/sysio.chains/src/sysio.chains.cpp @@ -143,8 +143,10 @@ void chains::regchain(opp::types::ChainKind kind, sysio::check(kind != opp::types::CHAIN_KIND_UNKNOWN, "sysio.chains: kind must not be UNKNOWN"); - // The code is this row's PRIMARY KEY and is rendered as a string by every reader -- - // refuse one with no spelling before it becomes a permanent, unrenderable row. + // The code is this row's PRIMARY KEY and is rendered as a string by every reader. + // Rendering is total, so an uncanonical one does not fail -- it renders something + // that does not pack back, possibly the spelling of a DIFFERENT chain. Refuse it + // before it becomes a permanent row. opp::registry::check_codes({code}, "sysio.chains"); // Both strings persist into a `sysio`-billed row -- bound them before emplace. opp::registry::check_metadata(name, description, "sysio.chains"); diff --git a/contracts/sysio.msgch/src/sysio.msgch.cpp b/contracts/sysio.msgch/src/sysio.msgch.cpp index 26c58416e0..215b52c2bc 100644 --- a/contracts/sysio.msgch/src/sysio.msgch.cpp +++ b/contracts/sysio.msgch/src/sysio.msgch.cpp @@ -338,10 +338,10 @@ name resolve_account_from_op_address(const opp::types::ChainAddress& op_address) /// `chain_code` is proven — `source_chain_binding_ok` binds it to the delivering /// outpost. `token_code` / `reserve_code` are NOT: they arrive as raw protobuf /// uint64s and reach a slug_name through the non-validating raw constructor, so a -/// forged payload can carry a value no spelling produces. Such a value can never -/// have been registered, and persisting it makes every later render of that row -/// throw — in a `values_only` scan the underwriter's unconditional -/// `row.get_object()` then drops the WHOLE cycle, not one cell. +/// forged payload can carry a value that does not round-trip through its spelling. +/// Such a value can never have been registered, and rendering is total so it will +/// not announce itself: it renders a string that packs back to something else, which +/// can be the spelling of a DIFFERENT, real code. /// /// Drop the attestation instead; never check(), per /// feedback_opp_handlers_never_throw — a check() here halts evalcons and stalls diff --git a/contracts/sysio.opp.common/include/sysio.opp.common/registry_codes.hpp b/contracts/sysio.opp.common/include/sysio.opp.common/registry_codes.hpp index 2a4ea6a58e..a4c8fede25 100644 --- a/contracts/sysio.opp.common/include/sysio.opp.common/registry_codes.hpp +++ b/contracts/sysio.opp.common/include/sysio.opp.common/registry_codes.hpp @@ -41,9 +41,9 @@ namespace sysio::opp::registry { * Call before the row is emplaced/modified, alongside `check_metadata`. * * The zero code passes deliberately: it spells as `""`, which is a valid literal that - * packs back to zero, so it renders and round-trips like any other code. Whether an - * EMPTY code belongs in a given registry row is that registry's own question -- this - * guard is only about values that cannot be rendered at all. + * packs back to zero, so it round-trips like any other code. Whether an EMPTY code + * belongs in a given registry row is that registry's own question -- this guard is only + * about values that do not survive the round trip. * * @param codes The row's `slug_name` columns. * @param context Contract-scoped message prefix, e.g. `"sysio.tokens"`. diff --git a/contracts/sysio.reserv/src/sysio.reserv.cpp b/contracts/sysio.reserv/src/sysio.reserv.cpp index 290e9cafc1..2c1021a3f0 100644 --- a/contracts/sysio.reserv/src/sysio.reserv.cpp +++ b/contracts/sysio.reserv/src/sysio.reserv.cpp @@ -386,7 +386,7 @@ void reserve::regreserve(sysio::slug_name chain_code, sysio::check(!is_private || owner != sysio::name{}, "a private bootstrap reserve must name an owner"); // The three codes form this row's COMPOSITE PRIMARY KEY and are rendered as strings by - // every reader -- refuse any with no spelling before the row becomes permanent. + // every reader -- refuse any that does not round-trip before the row becomes permanent. opp::registry::check_codes({chain_code, token_code, reserve_code}, "sysio.reserv"); // Both strings persist into a `sysio`-billed row -- bound them before emplace. opp::registry::check_metadata(name, description, "sysio.reserv"); @@ -452,7 +452,7 @@ void reserve::oncrtreserve(sysio::slug_name chain_code, // The reserve row is keyed by (chain, token, reserve) codes, so an unspellable one // cannot be persisted. The creator's escrow is already in outpost custody, so refund // rather than drop. No CANCELLED tombstone (unlike the rejections below): the row's - // key IS the unspellable triple, and a code with no spelling can be neither squatted + // key IS the unspellable triple, and a code with no canonical spelling can be neither squatted // nor reclaimed, so the tombstone has nothing to protect. if (!token_code.is_canonical() || !reserve_code.is_canonical()) { sysio::print("oncrtreserve: rejecting with RESERVE_CREATE_CANCELLED " diff --git a/contracts/sysio.tokens/src/sysio.tokens.cpp b/contracts/sysio.tokens/src/sysio.tokens.cpp index 3b161a0649..bfb7d50e0c 100644 --- a/contracts/sysio.tokens/src/sysio.tokens.cpp +++ b/contracts/sysio.tokens/src/sysio.tokens.cpp @@ -52,8 +52,10 @@ void tokens::regtoken(opp::types::TokenKind kind, "sysio.tokens: token kind must not be UNKNOWN"); sysio::check(precision <= MAX_TOKEN_PRECISION, "sysio.tokens: precision exceeds the depot frame maximum (9)"); - // The code is this row's PRIMARY KEY and is rendered as a string by every reader -- - // refuse one with no spelling before it becomes a permanent, unrenderable row. + // The code is this row's PRIMARY KEY and is rendered as a string by every reader. + // Rendering is total, so an uncanonical one does not fail -- it renders something + // that does not pack back, possibly the spelling of a DIFFERENT token. Refuse it + // before it becomes a permanent row. opp::registry::check_codes({code}, "sysio.tokens"); // Both strings persist into a `sysio`-billed row -- bound them before emplace. opp::registry::check_metadata(symbol_name, description, "sysio.tokens"); @@ -101,7 +103,7 @@ void tokens::regctok(sysio::slug_name chain_code, require_priv_caller(); // Both codes form this row's COMPOSITE PRIMARY KEY and are rendered as strings by - // every reader -- refuse either with no spelling before the row becomes permanent. + // every reader -- refuse either that does not round-trip before the row becomes permanent. opp::registry::check_codes({chain_code, token_code}, "sysio.tokens"); chaintokens_t tbl(get_self()); diff --git a/contracts/sysio.uwrit/src/sysio.uwrit.cpp b/contracts/sysio.uwrit/src/sysio.uwrit.cpp index 0d72d3f81e..9d3f282e9e 100644 --- a/contracts/sysio.uwrit/src/sysio.uwrit.cpp +++ b/contracts/sysio.uwrit/src/sysio.uwrit.cpp @@ -1063,17 +1063,17 @@ void uwrit::createuwreq(uint64_t attestation_id, // uwreq row IS created from them below on a path that no registry lookup gates: the // zero-quote guard fails closed only when `required_reserves_active` holds, so a // MISSING reserve (the unprovisioned-LP case) falls through to `reqs.emplace` with - // these codes stored verbatim. A stored code with no spelling makes the row - // unrenderable — `get_table_rows` degrades it to hex, and the underwriter plugin's - // scan reaches an unconditional `get_object()` and drops its whole cycle, stalling - // every commit. Refund rather than drop: the user's deposit is escrowed on the source + // these codes stored verbatim. Rendering is total, so an uncanonical code does not + // make the row unreadable — it makes it WRONG: the row renders a spelling that packs + // back to a different value, so a uwreq can name a reserve that is not the one it was + // created from. Refund rather than drop: the user's deposit is escrowed on the source // outpost, so a silent skip would strand it. Never `check()` — we are inside the // evalcons dispatch chain (`feedback_opp_handlers_never_throw`). // // The two CHAIN codes are deliberately absent: `src_chain_code` was just proven equal // to the delivering outpost's `chain_code`, and `dst_chain_code` must pass // `chain_registered_active` below. Both therefore name a `sysio.chains` row, and - // `sysio.chains::regchain` refuses a code with no spelling — so the registry itself + // `sysio.chains::regchain` refuses a code with no canonical spelling — so the registry itself // carries that guarantee. if (!src_token_code.is_canonical() || !src_reserve_code.is_canonical() || !dst_token_code.is_canonical() || !dst_reserve_code.is_canonical()) { diff --git a/contracts/tests/sysio.dispatch_tests.cpp b/contracts/tests/sysio.dispatch_tests.cpp index c8e0d9e30d..c98c40e7cf 100644 --- a/contracts/tests/sysio.dispatch_tests.cpp +++ b/contracts/tests/sysio.dispatch_tests.cpp @@ -1454,10 +1454,10 @@ BOOST_FIXTURE_TEST_CASE(dispatch_routes_deposit_to_opreg, sysio_dispatch_tester) // // `chain_code` is proven -- source_chain_binding_ok binds it to the delivering // outpost -- but `token_code` rides the FORGEABLE payload and reaches slug_name -// through the non-validating raw constructor. Persisting one would make every -// later render of that balance row throw; under `values_only` the underwriter's -// unconditional `row.get_object()` then drops the ENTIRE scan cycle rather than -// one cell. So the dispatcher drops the attestation -- a check() here would halt +// through the non-validating raw constructor. Rendering is total, so persisting one +// would not fail loudly -- the balance row would render a spelling that packs back to +// a DIFFERENT code, silently aliasing one operator's balance onto another's key. +// So the dispatcher drops the attestation -- a check() here would halt // evalcons and stall consensus (feedback_opp_handlers_never_throw). // A DEPOSIT_REQUEST arrives only after the outpost has taken custody, so an // unspellable token code must be REFUNDED rather than dropped: a drop leaves the @@ -1649,9 +1649,9 @@ BOOST_FIXTURE_TEST_CASE(swap_request_mismatched_source_chain_is_refunded, // through the non-validating raw constructor. Nothing downstream gates them: the // zero-quote guard fails closed only when `required_reserves_active` holds, so a code // naming NO reserve leaves the quote at zero, skips that guard, and reaches -// `reqs.emplace` — persisting a uwreq row no reader can render. `get_table_rows` -// degrades such a row to hex; the underwriter plugin's scan hits an unconditional -// `get_object()` and drops its whole cycle, stalling every commit. The request must be +// `reqs.emplace`. Rendering is total, so the row is not unreadable — it is WRONG: it +// renders a spelling that packs back to a different value, so the uwreq can name a +// reserve other than the one it was created from. The request must be // REFUNDED rather than dropped: the user's deposit is escrowed on the source outpost. BOOST_FIXTURE_TEST_CASE(swap_request_uncanonical_code_is_refunded, sysio_dispatch_tester) { try { diff --git a/libraries/libfc/include/fc/slug_name.hpp b/libraries/libfc/include/fc/slug_name.hpp index 914042dad9..5b01c0bba7 100644 --- a/libraries/libfc/include/fc/slug_name.hpp +++ b/libraries/libfc/include/fc/slug_name.hpp @@ -177,7 +177,7 @@ namespace detail { /// from the far end of the table. Every shape that is not an exact non-negative /// integer is rejected here instead. /// -/// Canonicality is deliberately NOT required: a packed value with no spelling is +/// Canonicality is deliberately NOT required: a packed value with no canonical spelling is /// the one thing the string carrier cannot express, so this arm is the only way /// to name such a key as a bound. inline uint64_t checked_packed_value(const fc::variant& v) { diff --git a/libraries/libfc/test/test_slug_name.cpp b/libraries/libfc/test/test_slug_name.cpp index aaff55b153..064649b9d6 100644 --- a/libraries/libfc/test/test_slug_name.cpp +++ b/libraries/libfc/test/test_slug_name.cpp @@ -423,10 +423,11 @@ BOOST_AUTO_TEST_CASE(non_zero_terminator_trait_accepts_alphabet_zero) { } // ── variant carrier ──────────────────────────────────────────────────────── -// ONE carrier: the canonical string spelling. A slug renders as its text, zero -// as "", and a value with no spelling throws. The cases below pin that single -// shape from both directions — every writable spelling lands on a string, and -// every non-string is refused rather than coerced. +// ONE emitted carrier: the canonical string spelling. A slug renders as its text +// and zero as "", and the render is TOTAL — an uncanonical value still produces a +// string (see the reject-or-normalize case below). Input additionally accepts the +// transitional `{"value": N}` object, which is the only non-string form taken and +// the only way to name a value the string carrier cannot express. BOOST_AUTO_TEST_CASE(variant_canonical_slug_is_a_string) { fc::variant v; @@ -610,7 +611,7 @@ BOOST_AUTO_TEST_CASE(variant_rejects_every_non_string_carrier) { // target, it WRAPS; a bound of -1 would page from the far // end of the table // null/false -> 0, the absent sentinel, silently - // true -> 1, a value with no spelling at all + // true -> 1, a value with no canonical spelling at all slug_name back; BOOST_CHECK_THROW(fc::from_variant(fc::variant(uint64_t{7}), back), fc::exception); BOOST_CHECK_THROW(fc::from_variant(fc::variant(uint64_t{1} << 42), back), fc::exception); @@ -660,7 +661,7 @@ BOOST_AUTO_TEST_CASE(variant_object_arm_rejects_every_coercible_value_shape) { fc::exception); // And the shapes a real pre-builtin writer emits still work. Canonicality is - // NOT required here: a value with no spelling is exactly what the string + // NOT required here: a value with no canonical spelling is exactly what the string // carrier cannot express, so this arm is the only way to name such a bound. fc::from_variant(obj(fc::variant(uint64_t{7})), back); BOOST_CHECK_EQUAL(back.value, 7u); diff --git a/unittests/abi_tests.cpp b/unittests/abi_tests.cpp index 1df3b3fa48..3f88c93eb3 100644 --- a/unittests/abi_tests.cpp +++ b/unittests/abi_tests.cpp @@ -753,7 +753,7 @@ BOOST_AUTO_TEST_CASE(slug_name_builtin_type) abis.variant_to_binary("regrow", fc::json::from_string(R"({"code":7})"), yield_fn()), fc::exception); - // A value with no spelling renders anyway — you get what you get. Every value + // A value with no canonical spelling renders anyway — you get what you get. Every value // below 2^42 has a zero in the leading symbol slot, so `to_string` truncates // it to "", the same text zero renders. The conversion is TOTAL, exactly like // `name` (`database_utils.hpp`: `name(raw).to_string()`): a read path that From 88c97b086a8873dd04084768c7ecd8de06965334 Mon Sep 17 00:00:00 2001 From: kevin Heifner Date: Wed, 23 Sep 2026 16:38:41 -0500 Subject: [PATCH 25/29] docs(slug_name): correct the number-carrier rationale and keep all three outcomes Two contradictions, both introduced here. The reason given for refusing a JSON number was that `"123"` / `"7"` are canonical slugs whose packed values differ from the literal -- but the leading-letter rule this PR adds makes both invalid, so the argument used an example the same PR outlaws. The actual contract: a slug field carries a SPELLING, and `leading_alphabet` already makes that string unambiguous, so a numeric carrier would only add a second way to say the same thing. It is refused in both directions; the transitional object is the one exception. The previous sweep then replaced "cannot be rendered" with "renders a spelling that packs back to a different value" -- which describes only the SILENT outcome and drops the loud one documented two commits earlier. A digit-leading raw such as 34<<42 renders "7", which validation rejects. Every site now carries the whole invariant: the rendered text either FAILS validation on the way back, or silently re-parses as a DIFFERENT code. Swept msgch, chains, tokens, uwrit, the two dispatch-test comments, and the two further dispatch sites still saying "fail to render" / "renderable". The header's carrier block and abi_tests' number case now also state that to_variant ALWAYS emits a string and that from_variant takes the string plus the transitional object, rather than calling the string the only carrier in both directions. Comments only; no production code, no artifact change. Change-Id: Iaf1c474d5a4e327c4b47eef1cda567cffaa4f56d --- contracts/sysio.chains/src/sysio.chains.cpp | 6 ++--- contracts/sysio.msgch/src/sysio.msgch.cpp | 6 ++--- contracts/sysio.tokens/src/sysio.tokens.cpp | 6 ++--- contracts/sysio.uwrit/src/sysio.uwrit.cpp | 4 ++-- contracts/tests/sysio.dispatch_tests.cpp | 17 +++++++------- libraries/libfc/include/fc/slug_name.hpp | 25 ++++++++++++--------- unittests/abi_tests.cpp | 6 ++--- 7 files changed, 37 insertions(+), 33 deletions(-) diff --git a/contracts/sysio.chains/src/sysio.chains.cpp b/contracts/sysio.chains/src/sysio.chains.cpp index 3c87dab928..4e3ba03d84 100644 --- a/contracts/sysio.chains/src/sysio.chains.cpp +++ b/contracts/sysio.chains/src/sysio.chains.cpp @@ -144,9 +144,9 @@ void chains::regchain(opp::types::ChainKind kind, sysio::check(kind != opp::types::CHAIN_KIND_UNKNOWN, "sysio.chains: kind must not be UNKNOWN"); // The code is this row's PRIMARY KEY and is rendered as a string by every reader. - // Rendering is total, so an uncanonical one does not fail -- it renders something - // that does not pack back, possibly the spelling of a DIFFERENT chain. Refuse it - // before it becomes a permanent row. + // Rendering is total, so an uncanonical one still produces text -- text that either + // fails validation on the way back, or silently re-parses as a DIFFERENT chain. + // Refuse it before it becomes a permanent row. opp::registry::check_codes({code}, "sysio.chains"); // Both strings persist into a `sysio`-billed row -- bound them before emplace. opp::registry::check_metadata(name, description, "sysio.chains"); diff --git a/contracts/sysio.msgch/src/sysio.msgch.cpp b/contracts/sysio.msgch/src/sysio.msgch.cpp index 215b52c2bc..3ade62e746 100644 --- a/contracts/sysio.msgch/src/sysio.msgch.cpp +++ b/contracts/sysio.msgch/src/sysio.msgch.cpp @@ -339,9 +339,9 @@ name resolve_account_from_op_address(const opp::types::ChainAddress& op_address) /// outpost. `token_code` / `reserve_code` are NOT: they arrive as raw protobuf /// uint64s and reach a slug_name through the non-validating raw constructor, so a /// forged payload can carry a value that does not round-trip through its spelling. -/// Such a value can never have been registered, and rendering is total so it will -/// not announce itself: it renders a string that packs back to something else, which -/// can be the spelling of a DIFFERENT, real code. +/// Such a value can never have been registered, and rendering is total, so it still +/// produces text: text that either FAILS validation on the way back, or silently +/// re-parses as a DIFFERENT, real code. /// /// Drop the attestation instead; never check(), per /// feedback_opp_handlers_never_throw — a check() here halts evalcons and stalls diff --git a/contracts/sysio.tokens/src/sysio.tokens.cpp b/contracts/sysio.tokens/src/sysio.tokens.cpp index bfb7d50e0c..e609d3c243 100644 --- a/contracts/sysio.tokens/src/sysio.tokens.cpp +++ b/contracts/sysio.tokens/src/sysio.tokens.cpp @@ -53,9 +53,9 @@ void tokens::regtoken(opp::types::TokenKind kind, sysio::check(precision <= MAX_TOKEN_PRECISION, "sysio.tokens: precision exceeds the depot frame maximum (9)"); // The code is this row's PRIMARY KEY and is rendered as a string by every reader. - // Rendering is total, so an uncanonical one does not fail -- it renders something - // that does not pack back, possibly the spelling of a DIFFERENT token. Refuse it - // before it becomes a permanent row. + // Rendering is total, so an uncanonical one still produces text -- text that either + // fails validation on the way back, or silently re-parses as a DIFFERENT token. + // Refuse it before it becomes a permanent row. opp::registry::check_codes({code}, "sysio.tokens"); // Both strings persist into a `sysio`-billed row -- bound them before emplace. opp::registry::check_metadata(symbol_name, description, "sysio.tokens"); diff --git a/contracts/sysio.uwrit/src/sysio.uwrit.cpp b/contracts/sysio.uwrit/src/sysio.uwrit.cpp index 9d3f282e9e..ef84b1b387 100644 --- a/contracts/sysio.uwrit/src/sysio.uwrit.cpp +++ b/contracts/sysio.uwrit/src/sysio.uwrit.cpp @@ -1064,8 +1064,8 @@ void uwrit::createuwreq(uint64_t attestation_id, // zero-quote guard fails closed only when `required_reserves_active` holds, so a // MISSING reserve (the unprovisioned-LP case) falls through to `reqs.emplace` with // these codes stored verbatim. Rendering is total, so an uncanonical code does not - // make the row unreadable — it makes it WRONG: the row renders a spelling that packs - // back to a different value, so a uwreq can name a reserve that is not the one it was + // make the row unreadable — it makes it WRONG: the rendered text either fails + // validation on the way back, or re-parses as a DIFFERENT reserve than the uwreq was // created from. Refund rather than drop: the user's deposit is escrowed on the source // outpost, so a silent skip would strand it. Never `check()` — we are inside the // evalcons dispatch chain (`feedback_opp_handlers_never_throw`). diff --git a/contracts/tests/sysio.dispatch_tests.cpp b/contracts/tests/sysio.dispatch_tests.cpp index c98c40e7cf..21b61ab482 100644 --- a/contracts/tests/sysio.dispatch_tests.cpp +++ b/contracts/tests/sysio.dispatch_tests.cpp @@ -1455,8 +1455,9 @@ BOOST_FIXTURE_TEST_CASE(dispatch_routes_deposit_to_opreg, sysio_dispatch_tester) // `chain_code` is proven -- source_chain_binding_ok binds it to the delivering // outpost -- but `token_code` rides the FORGEABLE payload and reaches slug_name // through the non-validating raw constructor. Rendering is total, so persisting one -// would not fail loudly -- the balance row would render a spelling that packs back to -// a DIFFERENT code, silently aliasing one operator's balance onto another's key. +// leaves a balance row whose rendered code either fails validation on the way back, or +// silently re-parses as a DIFFERENT code -- aliasing one operator's balance onto +// another's key. // So the dispatcher drops the attestation -- a check() here would halt // evalcons and stall consensus (feedback_opp_handlers_never_throw). // A DEPOSIT_REQUEST arrives only after the outpost has taken custody, so an @@ -1492,7 +1493,7 @@ BOOST_FIXTURE_TEST_CASE(dispatch_refunds_uncanonical_deposit_token_code, sysio_d // of the envelope (a throw here would halt evalcons and stall consensus). BOOST_REQUIRE_EQUAL(success(), deliver(/*chain_code=*/eth_code, envelope)); - // Nothing was persisted, so no stored row can later fail to render... + // Nothing was persisted, so no stored row can later render as the wrong code... const auto after = get_operator(UWRIT_OP); const size_t balances_after = after.is_null() ? 0 : after["balances"].get_array().size(); BOOST_CHECK_EQUAL(balances_before, balances_after); @@ -1649,9 +1650,9 @@ BOOST_FIXTURE_TEST_CASE(swap_request_mismatched_source_chain_is_refunded, // through the non-validating raw constructor. Nothing downstream gates them: the // zero-quote guard fails closed only when `required_reserves_active` holds, so a code // naming NO reserve leaves the quote at zero, skips that guard, and reaches -// `reqs.emplace`. Rendering is total, so the row is not unreadable — it is WRONG: it -// renders a spelling that packs back to a different value, so the uwreq can name a -// reserve other than the one it was created from. The request must be +// `reqs.emplace`. Rendering is total, so the row is not unreadable — it is WRONG: its +// rendered code either fails validation on the way back, or re-parses as a DIFFERENT +// reserve than the one it was created from. The request must be // REFUNDED rather than dropped: the user's deposit is escrowed on the source outpost. BOOST_FIXTURE_TEST_CASE(swap_request_uncanonical_code_is_refunded, sysio_dispatch_tester) { try { @@ -1670,8 +1671,8 @@ BOOST_FIXTURE_TEST_CASE(swap_request_uncanonical_code_is_refunded, const auto primary = fc::slug_name{"PRIMARY"}.value; // Below the leading symbol's floor: decodes to "" and packs back to 0, so it is not a - // code and has no spelling. The TARGET CHAIN stays valid — the point is that a - // registered chain does not imply a renderable token/reserve code. + // code and does not round-trip. The TARGET CHAIN stays valid — the point is that a + // registered chain does not imply a canonical token/reserve code. constexpr uint64_t uncanonical = 7; BOOST_REQUIRE(!fc::slug_name{uncanonical}.is_canonical()); diff --git a/libraries/libfc/include/fc/slug_name.hpp b/libraries/libfc/include/fc/slug_name.hpp index 5b01c0bba7..ecc2d3aadb 100644 --- a/libraries/libfc/include/fc/slug_name.hpp +++ b/libraries/libfc/include/fc/slug_name.hpp @@ -130,14 +130,17 @@ inline constexpr slug_name operator""_s() { using slug_name_literals::operator""_s; -/// JSON carrier for a slug_name: the canonical string spelling, and nothing -/// else. A slug renders as its text (`"LIQSOL"`), the zero sentinel as `""`. +/// JSON carrier for a slug_name. `to_variant` ALWAYS emits a string: the canonical +/// spelling for a canonical value (`"LIQSOL"`), `""` for zero, and whatever +/// `to_string()` yields for anything else. `from_variant` takes that string, +/// validating, plus the transitional `{"value": N}` object below. /// -/// The string is unambiguous because `leading_alphabet` forbids a code from -/// starting with a digit: no legal spelling can be read as a number, so a bare -/// JSON string is always a code. That is what removes the need for a second, -/// type-disjoint carrier — and why the carrier could not have been a numeric -/// string before the rule existed. +/// A JSON NUMBER is never a slug carrier in either direction. `leading_alphabet` +/// forbids a code from starting with a digit, so no legal spelling can be read as a +/// decimal and the string form is unambiguous on its own — a numeric carrier would +/// only add a second way to say the same thing. (Before that rule the string could +/// not have carried it alone, which is why this is stated as a consequence of the +/// rule rather than a free choice.) /// /// Render a slug as its spelling. TOTAL, like sysio::chain::name — a renderer is /// a READ path, and a throwing one turns one bad row into a failure of everything @@ -226,10 +229,10 @@ inline void from_variant(const fc::variant& v, slug_name& s) { s = slug_name{ detail::checked_packed_value(v.get_object()["value"]) }; return; } - // A number is REJECTED, never coerced. The slug alphabet contains digits, so - // `"123"` is itself a canonical slug whose packed value is nothing like 123 - // — reading the JSON number 123 as either one would be a silent mis-decode. - // Same for null/bool, which `as_uint64` would quietly turn into 0/1. + // A number is REJECTED, never coerced: a slug field carries a SPELLING, so a bare + // number would be a second carrier for the same value with nothing to say which was + // meant. Same for null/bool, which `as_uint64` would quietly turn into 0/1. The + // object arm above is the one exception, and it is transitional. FC_ASSERT(v.is_string(), "slug_name must be a string, got {}", fc::reflector::to_string(v.get_type())); // Validating: the ctor round-trip-checks and rejects a non-canonical or diff --git a/unittests/abi_tests.cpp b/unittests/abi_tests.cpp index 3f88c93eb3..7d460d8d0f 100644 --- a/unittests/abi_tests.cpp +++ b/unittests/abi_tests.cpp @@ -746,9 +746,9 @@ BOOST_AUTO_TEST_CASE(slug_name_builtin_type) BOOST_REQUIRE(back.get_object()["code"].is_string()); BOOST_CHECK_EQUAL(back.get_object()["code"].as_string(), "ETH"); - // The string is the ONLY carrier, in both directions. A JSON number is not a - // second spelling of a slug — `"7"` is itself a canonical slug whose packed - // value is nothing like 7 — so it is refused rather than read as either one. + // A JSON number is never a slug carrier: the field carries a SPELLING, so a number + // is refused rather than read as a packed value. (`from_variant` also takes the + // transitional `{"value": N}` object; a bare number is not that.) BOOST_CHECK_THROW( abis.variant_to_binary("regrow", fc::json::from_string(R"({"code":7})"), yield_fn()), fc::exception); From fc6ed3d0d67a9d5a96137fbdead936f08c1cc5a8 Mon Sep 17 00:00:00 2001 From: kevin Heifner Date: Wed, 23 Sep 2026 20:52:59 -0500 Subject: [PATCH 26/29] fix(chain_plugin): make a scope-only displayed key replayable; correct public docs The displayed row key kept its scope-relative hex fallback even when the within-scope remainder was EMPTY -- a row whose whole key is the scope prefix rendered as "", which every bound path treats as absent, so the key could not be replayed. The comment above it claimed both forms stayed feedable; that was only true for the non-empty case. Both sites now emit the tagged complete key there. Public documentation corrections, all of which described behavior the code does not have: - get-table-rows-api.md told every caller to feed next_key to lower_bound. Reverse scans need upper_bound -- next_key is the last key RETURNED, so the exclusive bound must advance below it, which this PR's own reverse tests already do. Added the reverse example and enumerated the three accepted json=true bound forms (key object, untagged within-scope hex, 0x complete cursor); aligned the two chain_plugin.hpp field docs. - platform-bootstrap-config.md and bootstrap.proto still advertised [A-Z0-9_] with only a length limit, so a config with "1ETH" passed documentation review and failed at runtime. Both now state [A-Z][A-Z0-9_]{0,7}, pinned by a new bootstrap-validator case covering digit-leading, underscore-leading, lowercase, out-of-alphabet and over-length. The proto edit is comments only -- no field, number or type changes, so the wire format is untouched. - sysio.chains.hpp said deserialization enforces the slug format. It does not: reflected/raw action data writes the packed member directly. The writer guard is what enforces canonicality. Three factual edges in the carrier documentation: - database_utils.hpp said a noncanonical rendering re-encodes to a different key; a digit-leading spelling fails validation instead. Third site of that same claim. - slug_name.hpp counted raw zero among values without a canonical spelling -- "" IS zero's spelling and packs straight back. - basic_name.hpp described is_valid_literal(str) as a predicate over a packed value; it takes the spelling. is_canonical() is the packed-value question. And the five registry ABIs on this branch no longer emit a slug_name struct_def at all, so abi_tests, be_key_codec_tests and the TS generator now describe that fixture as legacy/deployed-ABI compatibility rather than as what ships today. Change-Id: Ic56dad94db488c800b4eb2c4cbd7d4b71b16ff8f --- .../include/sysio.chains/sysio.chains.hpp | 6 ++-- .../tools/generate-sysio-contract-types.py | 5 +-- docs/get-table-rows-api.md | 21 ++++++++++--- docs/platform-bootstrap-config.md | 5 +-- .../include/sysio/chain/database_utils.hpp | 6 ++-- libraries/libfc/include/fc/basic_name.hpp | 9 +++--- libraries/libfc/include/fc/slug_name.hpp | 7 +++-- .../proto/sysio/opp/bootstrap/bootstrap.proto | 7 +++-- .../test/test_bootstrap_platform_config.cpp | 23 ++++++++++++-- .../sysio/chain_plugin/chain_plugin.hpp | 4 +-- plugins/chain_plugin/src/chain_plugin.cpp | 31 ++++++++++++------- unittests/abi_tests.cpp | 6 ++-- unittests/be_key_codec_tests.cpp | 5 +-- 13 files changed, 92 insertions(+), 43 deletions(-) diff --git a/contracts/sysio.chains/include/sysio.chains/sysio.chains.hpp b/contracts/sysio.chains/include/sysio.chains/sysio.chains.hpp index 1f25837573..413de12c9a 100644 --- a/contracts/sysio.chains/include/sysio.chains/sysio.chains.hpp +++ b/contracts/sysio.chains/include/sysio.chains/sysio.chains.hpp @@ -76,8 +76,10 @@ namespace sysio { /// inline; else `active=false`. /// /// Validation: - /// * `code` slug_name format already enforced by the type itself at - /// deserialization (alphabet `[A-Z0-9_]+`, ≤8 chars). + /// * `code` canonicality is enforced by the WRITER guard, not by the type: + /// reflected/raw action deserialization writes the packed member directly + /// and validates nothing. A spelling supplied as a string goes through the + /// validating constructor (`[A-Z][A-Z0-9_]{0,7}`); a raw uint64 does not. /// * `code` must be unique. /// * `kind=WIRE` may appear at most once (the depot self-row). /// * `kind=EVM` rows must carry a unique `external_chain_id` — the pair diff --git a/contracts/tools/generate-sysio-contract-types.py b/contracts/tools/generate-sysio-contract-types.py index 4563c87492..8a6dc88dec 100755 --- a/contracts/tools/generate-sysio-contract-types.py +++ b/contracts/tools/generate-sysio-contract-types.py @@ -109,9 +109,10 @@ def emit_contract_interface(abi: dict, contract_name: str, contract_prefix: str) 'name': {'type': 'string', 'pattern': '^[a-z1-5.]{1,13}$'}, # slug_name is an abi_serializer builtin whose JSON carrier is its canonical # spelling — up to 8 symbols over [A-Z0-9_], with "" the zero sentinel. The - # entry must precede the structs lookup: every registry ABI still ships a + # entry must precede the structs lookup: a legacy or deployed ABI can still ship a # `slug_name` struct_def, and without this the field would resolve to that - # `{value: uint64}` shape (or, once abigen stops emitting it, to `unknown`). + # `{value: uint64}` shape. Current ABIs emit no such struct, so it would be + # `unknown` instead -- wrong either way. # A code must START with a letter, so the pattern is NOT [A-Z0-9_]{0,8}: that # would accept "7" and "_LEAD", which fc::slug_name now rejects, leaving the # generated schema disagreeing with the ABI serializer. The outer group is diff --git a/docs/get-table-rows-api.md b/docs/get-table-rows-api.md index 4027427702..8bad179c04 100644 --- a/docs/get-table-rows-api.md +++ b/docs/get-table-rows-api.md @@ -18,7 +18,7 @@ POST /v1/chain/get_table_rows | `scope` | string | `""` | Scope for scoped tables. Empty = unscoped (all rows). Parsed using ABI type. | | `find` | string | `""` | Exact key lookup (JSON key object or hex). Cannot be combined with bounds. | | `index_name` | string | `""` | Secondary index name (e.g. `"byowner"`) or position (e.g. `"2"`). Empty = primary key. | -| `lower_bound` | string | `""` | Lower bound (inclusive). When `json=true`: a JSON key object, or a `0x`-prefixed raw cursor as returned by `next_key`. When `json=false`: hex. | +| `lower_bound` | string | `""` | Lower bound (inclusive). Forward pagination feeds `next_key` here. When `json=true` one of three forms: a JSON key object, untagged hex (both within-scope), or a `0x` raw cursor (a complete key, used verbatim). When `json=false`: hex of the complete key. | | `upper_bound` | string | `""` | Upper bound (exclusive). Same format as `lower_bound`. | | `limit` | uint32 | `50` | Max rows to return | | `reverse` | bool | `false` | Iterate in reverse order | @@ -39,7 +39,7 @@ POST /v1/chain/get_table_rows - `rows` — array of `{key, value}` objects. When `show_payer=true`, includes `payer` field. - `more` — `true` if there are more rows beyond `limit`. -- `next_key` — use as `lower_bound` for the next page. Usually a JSON key object with the scope stripped (pass the same `scope` param); for a key the ABI cannot name it is a `0x` raw cursor instead — an opaque, complete key. Feed either back verbatim. +- `next_key` — the cursor for the next page. **Forward scans feed it to `lower_bound`; reverse scans feed it to `upper_bound`** (reverse `next_key` is the last key RETURNED, so the exclusive upper bound must advance below it). Usually a JSON key object with the scope stripped (pass the same `scope` param); for a key the ABI cannot name it is a `0x` raw cursor instead — an opaque, complete key. Feed either back verbatim. ## Scoped Queries @@ -164,7 +164,7 @@ Position 1 = primary, 2 = first secondary, 3 = second secondary, etc. ## Pagination -When `more` is `true`, use `next_key` as `lower_bound` for the next request. Keep the same `scope` parameter: +When `more` is `true`, feed `next_key` back for the next request — to **`lower_bound` going forward, `upper_bound` going in reverse**. Keep the same `scope` parameter: ```json { @@ -176,7 +176,20 @@ When `more` is `true`, use `next_key` as `lower_bound` for the next request. Kee } ``` -A `next_key` key object is scope-stripped — the scope prefix is not included, so pass it back as `lower_bound` with the same `scope`. A `0x` raw cursor already carries the scope; pass it back with the same `scope` too, and it is used as-is. +The direction matters: a forward `next_key` is the first key NOT returned, so it belongs in `lower_bound`. A reverse `next_key` is the last key that WAS returned, so it belongs in `upper_bound` — the exclusive bound then advances below it. Putting a reverse cursor in `lower_bound` re-reads the same top row. + +```json +{ + "code": "mycontract", + "table": "mytable", + "scope": "myscope", + "reverse": true, + "upper_bound": "...(next_key from previous response)...", + "limit": 50 +} +``` + +A `next_key` key object is scope-stripped — the scope prefix is not included, so pass it back with the same `scope`. A `0x` raw cursor already carries the scope; pass it back with the same `scope` too, and it is used as-is. A `0x` cursor must lie inside the `scope` the request names — one from another scope is rejected rather than silently returning that scope's rows. ## RAM Payer diff --git a/docs/platform-bootstrap-config.md b/docs/platform-bootstrap-config.md index 2ad65d3aa8..ae1fbca36f 100644 --- a/docs/platform-bootstrap-config.md +++ b/docs/platform-bootstrap-config.md @@ -49,7 +49,8 @@ which are wire messages with packed-uint64 codes, raw `bytes` addresses, and lifecycle fields that are outputs. A hand-authored config wants the opposite: - **Codes are strings** (`"ETHEREUM"`, `"USDC"`, `"PRIMARY"`); the tool packs - them via `slug_name` (`[A-Z0-9_]`, ≤ 8 chars). + them via `slug_name` (`[A-Z][A-Z0-9_]{0,7}` -- a code must START with a + letter, ≤ 8 chars). - **Addresses are strings** in chain-native display form (`0x`-hex for EVM, base58 for SVM) so each is verifiable against a block explorer. `bytes` would render as base64 in JSON. @@ -164,7 +165,7 @@ because the file is hand-authored and drives irreversible actions. A validator | # | Invariant | |---|---| | V1 | `schema_version == 1`; `network` non-empty | -| V2 | every code is a valid slug (`[A-Z0-9_]`, ≤ 8 chars) | +| V2 | every code is a valid slug: `[A-Z][A-Z0-9_]{0,7}` -- leading character must be a letter, ≤ 8 chars | | V3 | chain codes unique; exactly one `CHAIN_KIND_WIRE` chain, code `WIRE` | | V4 | token codes unique; `chain_code` declared; `precision` ∈ 1..9 (the depot frame — `sysio.tokens::regtoken` rejects anything higher, so a wider bound here would pass validation and then fail mid-bootstrap); native ⇔ kind `NATIVE` + empty address; non-native address well-formed for the chain kind (EVM `0x`+40 hex; SVM base58 → 32 bytes) | | V5 | exactly one native token per non-depot chain | diff --git a/libraries/chain/include/sysio/chain/database_utils.hpp b/libraries/chain/include/sysio/chain/database_utils.hpp index 5acc97a085..7e9e3797da 100644 --- a/libraries/chain/include/sysio/chain/database_utils.hpp +++ b/libraries/chain/include/sysio/chain/database_utils.hpp @@ -368,11 +368,11 @@ inline fc::variant decode_field(reader& r, key_leaf_kind kind) { // A canonical value renders as its spelling, and feeding that back as a // bound re-encodes the identical bytes. // - // One with NO canonical spelling cannot be named. Unlike the `name` arm + // One with NO canonical spelling cannot be NAMED. Unlike the `name` arm // above, to_string is not injective over raw uint64s here: 38 of the 64 // symbol values are used, symbol 0 terminates, and bits 48-63 are never - // read — so a rendered string would re-encode to a DIFFERENT key, and a - // cursor built from it would resume in the wrong place. `name`'s alphabet + // read — so a rendered string either fails validation on the way back, or + // re-encodes to a DIFFERENT key and resumes in the wrong place. `name`'s alphabet // is exactly 2^5 with no gaps and consumes all 64 bits, so its trim-and- // repack IS lossless; the two are not symmetric. // diff --git a/libraries/libfc/include/fc/basic_name.hpp b/libraries/libfc/include/fc/basic_name.hpp index 7f25781bd9..6e9e9e6326 100644 --- a/libraries/libfc/include/fc/basic_name.hpp +++ b/libraries/libfc/include/fc/basic_name.hpp @@ -110,10 +110,11 @@ struct basic_name { return v; } - /// Is `str` a valid, canonical spelling? The literal path's gate — and the - /// predicate a caller uses to ask whether a raw packed value has a spelling - /// at all. Delegates to validity_error so the literal path and the throwing - /// constructor can never disagree. + /// Is `str` a valid, canonical spelling? The literal path's gate. Takes the + /// SPELLING, not a packed value — asking whether a raw uint64 round-trips is + /// `is_canonical()`, which calls this on its own `to_string()`. Delegates to + /// validity_error so the literal path and the throwing constructor can never + /// disagree. static constexpr bool is_valid_literal(std::string_view str) { return validity_error(str) == nullptr; } diff --git a/libraries/libfc/include/fc/slug_name.hpp b/libraries/libfc/include/fc/slug_name.hpp index ecc2d3aadb..d3c9760af1 100644 --- a/libraries/libfc/include/fc/slug_name.hpp +++ b/libraries/libfc/include/fc/slug_name.hpp @@ -93,9 +93,10 @@ struct slug_name : basic_name { /// Does this value have a canonical spelling? A slug_name built from a RAW /// uint64 bypasses the validating constructor — and nothing validates on /// deserialization either, since the reflected member is written directly — - /// so it can hold a value no spelling produces: anything whose leading symbol - /// slot is empty, or that uses one of the 26 unused symbol values, or that - /// sets any of bits 48-63. Such a value cannot round-trip. + /// so it can hold a value that does not round-trip: a NON-ZERO value whose leading + /// symbol slot is empty, one that uses any of the 26 unused symbol values, or one + /// that sets any of bits 48-63. Raw zero is not among them — `""` is its canonical + /// spelling and packs straight back. /// /// This lives on slug_name and NOT on basic_name because it is meaningless /// for `name`: that alphabet is exactly 2^5 with no gaps and its 13 symbols diff --git a/libraries/opp/proto/sysio/opp/bootstrap/bootstrap.proto b/libraries/opp/proto/sysio/opp/bootstrap/bootstrap.proto index 51690a3ae6..ac88777c07 100644 --- a/libraries/opp/proto/sysio/opp/bootstrap/bootstrap.proto +++ b/libraries/opp/proto/sysio/opp/bootstrap/bootstrap.proto @@ -21,7 +21,8 @@ option cc_enable_arenas = true; // addresses, and lifecycle fields that are outputs, not inputs: // // * codes are STRINGS ("ETHEREUM", "USDC", "PRIMARY"); the tool packs them -// via slug_name (alphabet [A-Z0-9_], <= 8 chars). Packed uint64s are +// via slug_name ([A-Z][A-Z0-9_]{0,7} -- must start with a letter, <= 8 +// chars). Packed uint64s are // write-only for humans and render as opaque digit strings in JSON. // * addresses are STRINGS in chain-native display form (0x-hex for EVM, // base58 for SVM) so each one is eyeball-verifiable against a block @@ -48,7 +49,7 @@ option cc_enable_arenas = true; message ChainSpec { // VM family. CHAIN_KIND_WIRE marks the singleton depot chain. sysio.opp.types.ChainKind kind = 1; - // Chain codename slug, e.g. "ETHEREUM" (<= 8 chars, [A-Z0-9_]). + // Chain codename slug, e.g. "ETHEREUM" ([A-Z][A-Z0-9_]{0,7}: must start with a letter). string code = 2; // External chain id: 1 = Ethereum mainnet; 0 = WIRE depot / Solana. uint32 external_chain_id = 3; @@ -68,7 +69,7 @@ message ChainSpec { message TokenSpec { // Token standard. NATIVE marks the chain's own gas asset. sysio.opp.types.TokenKind kind = 1; - // Token codename slug, e.g. "USDCSOL" (<= 8 chars, [A-Z0-9_]). + // Token codename slug, e.g. "USDCSOL" ([A-Z][A-Z0-9_]{0,7}: must start with a letter). string code = 2; // Display symbol / name, e.g. "USD Coin". string symbol_name = 3; diff --git a/libraries/opp/test/test_bootstrap_platform_config.cpp b/libraries/opp/test/test_bootstrap_platform_config.cpp index f22bea6d11..ad8ed32e5f 100644 --- a/libraries/opp/test/test_bootstrap_platform_config.cpp +++ b/libraries/opp/test/test_bootstrap_platform_config.cpp @@ -59,8 +59,9 @@ bool parse_strict(const std::string& json, BootstrapPlatformConfig& out, std::st return true; } -/// True iff `s` is a valid slug_name (alphabet [A-Z0-9_], 1..8 chars). Uses the -/// real `fc::slug_name`, which throws on an out-of-alphabet or over-length code. +/// True iff `s` is a valid slug_name: `[A-Z][A-Z0-9_]{0,7}` -- a code must START with +/// a letter, and is at most 8 characters. Uses the real `fc::slug_name`, which throws +/// on a digit- or underscore-leading, out-of-alphabet, or over-length code. bool slug_ok(const std::string& s) { if (s.empty()) return false; try { @@ -233,6 +234,24 @@ BOOST_AUTO_TEST_CASE(dev_config_parses_and_validates) { } /// A typo'd / unknown JSON key must fail the strict parse, not be dropped. +// V2's grammar is `[A-Z][A-Z0-9_]{0,7}`, and the LEADING rule is what makes a bare JSON +// string unambiguous -- no legal code can be read as a decimal. The public authoring docs +// advertise that grammar, so it is pinned here rather than left to the constructor. +BOOST_AUTO_TEST_CASE(v2_slug_grammar_requires_a_leading_letter) { + for (const char* ok : {"ETHEREUM", "WIRE", "USDC", "V1", "TRAIL_", "Z_______", "A"}) + BOOST_CHECK_MESSAGE(slug_ok(ok), std::string("should accept ") + ok); + + for (const char* bad : {"7", // digit-leading + "1ETH", // digit-leading + "0X10", // digit-leading, JS-numeric shaped + "_LEAD", // underscore-leading + "________", // underscore-leading + "eth", // lowercase + "ETH-MAIN", // out of alphabet + "TOOLONG12"}) // over length + BOOST_CHECK_MESSAGE(!slug_ok(bad), std::string("should reject ") + bad); +} + BOOST_AUTO_TEST_CASE(strict_parse_rejects_unknown_field) { BootstrapPlatformConfig cfg; std::string err; diff --git a/plugins/chain_plugin/include/sysio/chain_plugin/chain_plugin.hpp b/plugins/chain_plugin/include/sysio/chain_plugin/chain_plugin.hpp index afa1ec069d..c670f84fe5 100644 --- a/plugins/chain_plugin/include/sysio/chain_plugin/chain_plugin.hpp +++ b/plugins/chain_plugin/include/sysio/chain_plugin/chain_plugin.hpp @@ -530,7 +530,7 @@ class read_only : public api_base { string scope; ///< empty = unscoped query, non-empty = scope prefix (parsed via ABI key type) string find; ///< exact key lookup (JSON obj or hex); errors if combined with lower/upper string index_name; ///< secondary index name (e.g. "byowner") or numeric position (e.g. "2") - string lower_bound; ///< inclusive lower key (JSON obj when json=true, hex when json=false) + string lower_bound; ///< inclusive lower key; forward pagination feeds `next_key` here. json=true: a JSON key object, untagged hex (both within-scope), or a `0x` raw cursor (complete key, verbatim). json=false: hex of the complete key string upper_bound; ///< exclusive upper key uint32_t limit = 50; ///< max rows to return in a single page; the caller paginates by re-issuing with `lower_bound`/`upper_bound = next_key`. Capped per page by the deadline. std::optional reverse; ///< iterate in reverse; pairs with `limit` to return the most recent N rows @@ -553,7 +553,7 @@ class read_only : public api_base { struct get_table_rows_result { fc::variants rows; ///< array of {key: {...}, value: {...}, payer?: "..."} objects (or bare values when `values_only` is set) bool more = false; - string next_key; ///< scope-stripped key for pagination + string next_key; ///< pagination cursor -- feed to `lower_bound` forward, `upper_bound` in reverse. A scope-stripped key object, or a `0x` raw cursor (complete key) when the ABI cannot name the key }; using get_table_rows_return_t = std::function()>; diff --git a/plugins/chain_plugin/src/chain_plugin.cpp b/plugins/chain_plugin/src/chain_plugin.cpp index c1f9543b3b..48ba81c83a 100644 --- a/plugins/chain_plugin/src/chain_plugin.cpp +++ b/plugins/chain_plugin/src/chain_plugin.cpp @@ -2950,16 +2950,19 @@ read_only::get_table_rows( const read_only::get_table_rows_params& p, const fc:: row.key.data(), row.key.size(), *key_shapes); obj["key"] = strip_scope_fields(std::move(full_key), scope_key_count); } catch (...) { - // strip_scope_fields drops the scope on the JSON path; keep the - // hex form scope-relative so the two describe the same key. + // strip_scope_fields drops the scope on the JSON path, so the hex + // form is scope-relative too and the two describe the same key. + // Untagged hex still means "within the scope", so it remains + // feedable as a bound. // - // Deliberately NOT a raw cursor: this is a row's key as DISPLAYED, - // and it has to line up with the stripped object form above. - // next_key is the resume token and is absolute for its own reasons - // (see to_raw_cursor) — both remain feedable as bounds, because - // untagged hex still means "within the scope". - obj["key"] = fc::to_hex(row.key.data() + scope_prefix_size, - row.key.size() - scope_prefix_size); + // Except when the remainder is EMPTY — a row whose whole key is the + // scope prefix. "" reads as no bound at all, so the displayed key + // would not be replayable; emit the tagged complete key instead, + // which is (see to_raw_cursor). + obj["key"] = row.key.size() == scope_prefix_size + ? to_raw_cursor(std::string_view(row.key.data(), row.key.size())) + : fc::to_hex(row.key.data() + scope_prefix_size, + row.key.size() - scope_prefix_size); } } else { obj["key"] = fc::to_hex(row.key.data(), row.key.size()); @@ -3138,9 +3141,13 @@ read_only::get_table_rows( const read_only::get_table_rows_params& p, const fc:: row.key.data(), row.key.size(), *key_shapes); obj("key", strip_scope_fields(std::move(full_key), scope_key_count)); } catch (...) { - // Scope-relative, matching strip_scope_fields on the JSON path. - obj("key", fc::to_hex(row.key.data() + scope_prefix_size, - static_cast(row.key.size() - scope_prefix_size))); + // Scope-relative, matching strip_scope_fields on the JSON path -- except + // when the remainder is EMPTY, where "" would read as no bound and the + // displayed key would not be replayable. See the secondary path above. + obj("key", row.key.size() == scope_prefix_size + ? to_raw_cursor(std::string_view(row.key.data(), row.key.size())) + : fc::to_hex(row.key.data() + scope_prefix_size, + static_cast(row.key.size() - scope_prefix_size))); } } else { obj("key", fc::to_hex(row.key.data(), static_cast(row.key.size()))); diff --git a/unittests/abi_tests.cpp b/unittests/abi_tests.cpp index 7d460d8d0f..c485e3e9eb 100644 --- a/unittests/abi_tests.cpp +++ b/unittests/abi_tests.cpp @@ -691,8 +691,10 @@ BOOST_AUTO_TEST_CASE(slug_name_builtin_type) // contracts/tests cannot catch either case, because fc::slug_name's // from_variant accepts the string, the integer AND the object form, so those // reads pass identically whether or not this registration exists. - // The `slug_name` struct_def below is NOT filler: every one of the five - // registry ABIs shipped today carries exactly this shadowed definition, and + // The `slug_name` struct_def below is NOT filler. The five registry ABIs on this + // branch no longer emit it -- abigen stopped once slug_name became a real builtin -- + // so this fixture now stands in for a DEPLOYED or legacy ABI that still carries the + // shadowed definition, which set_abi must keep resolving the same way. And // `slug_name` is the ONLY builtin name so shadowed (`symbol`, `name`, // `asset` appear in no `structs[]`). `set_abi` has no collision check, so // the ABI is genuinely ambiguous and is resolved only by LOOKUP ORDER — diff --git a/unittests/be_key_codec_tests.cpp b/unittests/be_key_codec_tests.cpp index dd73bd6cad..d5be7c361d 100644 --- a/unittests/be_key_codec_tests.cpp +++ b/unittests/be_key_codec_tests.cpp @@ -161,8 +161,9 @@ BOOST_AUTO_TEST_CASE(slug_name_leaf_wins_over_a_shadowing_struct_def) { // structs at :778); the key codec is its own lookup (leaf_kind_of at // database_utils.hpp:558 before the struct table), and it needs its own pin. // - // Every shipped registry ABI carries a `slug_name` struct_def alongside the - // field, and `slug_name` is the ONLY builtin name so shadowed. `set_abi` has + // A legacy or deployed registry ABI carries a `slug_name` struct_def alongside the + // field (this branch's own ABIs no longer emit one), and `slug_name` is the ONLY + // builtin name so shadowed. `set_abi` has // no collision check, so the ABI is genuinely ambiguous and resolved only by // lookup order. If the struct won here, `encode_key` would demand the nested // `{"code":{"value":N}}` form and `decode_key` would emit it — so `is_leaf` From 59263b057bb98f8afa07373303ba03d2e30a7198 Mon Sep 17 00:00:00 2001 From: kevin Heifner Date: Thu, 24 Sep 2026 08:15:15 -0500 Subject: [PATCH 27/29] test(sysio.liq): read registry slug codes as their canonical string slug_name is an ABI builtin on this branch, so a stat row's chain_code and token_code render as strings, not {"value": N} objects. Change-Id: I2734fbc0fd711a561621b15933561aa55002290b --- contracts/tests/sysio.liq_tests.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/tests/sysio.liq_tests.cpp b/contracts/tests/sysio.liq_tests.cpp index e898aeb33d..0fa00acb23 100644 --- a/contracts/tests/sysio.liq_tests.cpp +++ b/contracts/tests/sysio.liq_tests.cpp @@ -413,8 +413,8 @@ BOOST_AUTO_TEST_SUITE(sysio_liq_tests) BOOST_FIXTURE_TEST_CASE(create_binds_an_active_liq_token, sysio_liq_tester) try { const auto st = stat_row(); BOOST_REQUIRE_EQUAL(0, st["supply"].as().get_amount()); - BOOST_REQUIRE_EQUAL(slug_value(SOLANA), st["chain_code"]["value"].as_uint64()); - BOOST_REQUIRE_EQUAL(slug_value(LIQSOL), st["token_code"]["value"].as_uint64()); + BOOST_REQUIRE_EQUAL(SOLANA, st["chain_code"].as_string()); + BOOST_REQUIRE_EQUAL(LIQSOL, st["token_code"].as_string()); BOOST_REQUIRE_EQUAL(wasm_assert_msg("token_code already has a shadow symbol"), create(symbol::from_string("9,LIQSOLB"), SOLANA, LIQSOL)); From 4d619292e39328e33a41beb5c4b081c34f82dba6 Mon Sep 17 00:00:00 2001 From: kevin Heifner Date: Thu, 24 Sep 2026 08:55:47 -0500 Subject: [PATCH 28/29] test(opp): validate reserve codes in the bootstrap config preflight regreserve rejects a non-canonical reserve code, so validate() must check reserves[].code against the V2 slug grammar like chain and token codes. Change-Id: I234bd451fee5c3c76b61498c2bc208d18acb2fea --- libraries/opp/test/test_bootstrap_platform_config.cpp | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/libraries/opp/test/test_bootstrap_platform_config.cpp b/libraries/opp/test/test_bootstrap_platform_config.cpp index bb452cacc9..9a3348a6d5 100644 --- a/libraries/opp/test/test_bootstrap_platform_config.cpp +++ b/libraries/opp/test/test_bootstrap_platform_config.cpp @@ -22,6 +22,7 @@ #include #include +#include #include #include #include @@ -186,6 +187,7 @@ std::vector validate(const BootstrapPlatformConfig& c) { std::set> triples; unsigned __int128 sum_wire = 0; for (const auto& r : c.reserves()) { + if (!slug_ok(r.code())) e.push_back("V2 reserve code: " + r.code()); const auto key = std::make_tuple(r.chain_code(), r.token_code(), r.code()); if (!triples.insert(key).second) e.push_back("V6 duplicate reserve: " + r.chain_code() + "/" + r.token_code() + "/" + r.code()); @@ -345,6 +347,15 @@ BOOST_AUTO_TEST_CASE(validator_rejects_mutations) { { auto c = base; c.mutable_chains(1)->set_code("TOOLONG99"); // 9 chars > 8 BOOST_CHECK(!validate(c).empty()); } // V2 over-length slug + // Reserve codes get the same V2 grammar as chain and token codes; regreserve + // refuses the digit- and underscore-leading ones outright. + for (const char* bad : {"1BAD", "_BAD", ""}) { + auto c = base; c.mutable_reserves(0)->set_code(bad); + const auto errs = validate(c); + BOOST_CHECK_MESSAGE(std::any_of(errs.begin(), errs.end(), + [](const std::string& s) { return s.starts_with("V2 reserve code"); }), + std::string("reserve code should be rejected: '") + bad + "'"); + } { auto c = base; c.mutable_chains(2)->set_kind(ChainKind::CHAIN_KIND_WIRE); BOOST_CHECK(!validate(c).empty()); } // V3 two depots { auto c = base; // V5 second native on ETHEREUM From 1e6694285eb565124b66c3eb5789e849f9b81fa4 Mon Sep 17 00:00:00 2001 From: kevin Heifner Date: Thu, 24 Sep 2026 16:35:32 -0500 Subject: [PATCH 29/29] fix(query_engine): treat slug_name as the builtin primitive it now is slug_name has no struct definition in a contract ABI any more, so a table with a slug field failed to plan (Unsupported ABI type), COUNT(*) included. Compile it like every other primitive and drop the struct-form branches: the host rejects an ABI that redefines a builtin, so that form is unreachable. Change-Id: Id57363cc43fa4087e62e3d961b2945c25fcab76e --- .../query_engine_plugin/src/query_values.cpp | 19 +--------- .../test/test_query_integration.cpp | 38 +++++++++++++++++++ 2 files changed, 39 insertions(+), 18 deletions(-) diff --git a/plugins/query_engine_plugin/src/query_values.cpp b/plugins/query_engine_plugin/src/query_values.cpp index 2e4c0c87fb..f47fdaa8a7 100644 --- a/plugins/query_engine_plugin/src/query_values.cpp +++ b/plugins/query_engine_plugin/src/query_values.cpp @@ -289,7 +289,7 @@ class descriptor_compiler { name = std::string(magic_enum::enum_name(primitive_type::checksum256)); const auto primitive = name == abi_boolean ? std::optional{primitive_type::boolean} : magic_enum::enum_cast(name); - if (primitive && *primitive != primitive_type::slug_name) { + if (primitive) { result->primitive = *primitive; result->logical = primitive_logical(*primitive); return result; @@ -297,13 +297,6 @@ class descriptor_compiler { for (const auto& structure : abi.structs) { if (structure.name != name) continue; - if (primitive == primitive_type::slug_name && structure.base.empty() && structure.fields.size() == 1 && - structure.fields.front().name == constants::value_namespace && - be_key_codec::resolve_key_type(abi, structure.fields.front().type) == - magic_enum::enum_name(primitive_type::uint64)) { - result->primitive = primitive_type::slug_name; - return result; - } result->kind = type_kind::structure; result->logical = logical_type::json; if (!structure.base.empty()) { @@ -834,16 +827,6 @@ fc::variant decode_key_node(be_key_codec::reader& input, const be_key_codec::key budget.check(); budget.charge_memory(scalar_allocation_bytes); if (!shape.is_leaf) { - // slug_name is an ABI struct backed by uint64, projected as its canonical name. - if (type.primitive == primitive_type::slug_name && type.kind == type_kind::primitive) { - const auto raw = be_key_codec::decode_shape(input, shape); - value result; - result.null = false; - result.primitive = primitive_type::slug_name; - const auto name = fc::slug_name(raw[constants::value_namespace].as_uint64()); - result.text = name.to_string(); - return to_cell(result, budget); - } fc::mutable_variant_object result; if (shape.children.size() != type.fields.size()) throw query_error(error_kind::ROW_DECODE_ERROR, "Key descriptor mismatch"); diff --git a/plugins/query_engine_plugin/test/test_query_integration.cpp b/plugins/query_engine_plugin/test/test_query_integration.cpp index e710c3edbd..2299b6f5f6 100644 --- a/plugins/query_engine_plugin/test/test_query_integration.cpp +++ b/plugins/query_engine_plugin/test/test_query_integration.cpp @@ -1,6 +1,7 @@ #include "query_fixture.hpp" #include +#include #include #include @@ -379,6 +380,43 @@ BOOST_AUTO_TEST_CASE(checksum_alias_compiles_for_several_fields) { BOOST_CHECK_EQUAL(row["second"].as_string(), std::string(2 * digest_bytes, '2')); } +/// `slug_name` is an ABI builtin with no struct definition. A table carrying one compiles for any +/// query, including COUNT(*), and the field decodes, filters and renders as its canonical string. +BOOST_AUTO_TEST_CASE(builtin_slug_name_fields_compile_filter_and_render) { + local_table_source source(*validating_node); + const auto plan_with_slug = [&](const char* sql, query_budget& budget) { + const auto ast = parse_query(sql, budget); + auto schemas = source.describe(ast, budget); + for (auto& structure : schemas.front().abi.structs) + if (structure.name == schemas.front().table.type) + structure.fields = { + {"code", "slug_name"}, + {"units", "uint64" } + }; + return create_plan(ast, std::move(schemas), budget); + }; + const auto slug_row = [](std::string_view code, uint64_t units) { + auto bytes = fc::raw::pack(fc::slug_name(code).value); + const auto packed_units = fc::raw::pack(units); + bytes.insert(bytes.end(), packed_units.begin(), packed_units.end()); + return bytes; + }; + + query_budget counting(relaxed_config()); + BOOST_CHECK_NO_THROW(plan_with_slug("SELECT COUNT(*) AS records FROM sample.positions", counting)); + + query_budget budget(relaxed_config()); + const auto plan = plan_with_slug("SELECT code, units FROM sample.positions WHERE code = 'SOL'", budget); + auto input = source.capture(plan, budget); + input.rows.resize(2); + input.rows[0].row.value = slug_row("ETH", 1); + input.rows[1].row.value = slug_row("SOL", 2); + const auto rows = fc::variant(evaluate(plan, std::move(input), budget))["rows"]; + BOOST_REQUIRE_EQUAL(rows.get_array().size(), 1u); + BOOST_CHECK_EQUAL(rows[size_t{0}]["code"].as_string(), "SOL"); + BOOST_CHECK_EQUAL(rows[size_t{0}]["units"].as_string(), "2"); +} + /// The table name `fasp` hashes to the highest table id, 0xFFFF. A forward query reads it like any /// other table, and a reverse page without an upper bound starts at the partition's end instead of /// wrapping the successor id to zero and returning nothing.