From e7aa863ff30ef0e6108b02e49efea0a4cb779496 Mon Sep 17 00:00:00 2001 From: joshglogau Date: Wed, 11 Nov 2026 12:16:15 -0500 Subject: [PATCH 1/9] fix(roa): restrict NFT registration to new accounts (WIRE-439) Change-Id: I622f4c346ca4eed323f5aaca77d6751dc5717cc7 --- CLAUDE.md | 2 + README.md | 2 + contracts/sysio.msgch/src/sysio.msgch.cpp | 17 +-- contracts/sysio.roa/sysio.roa.cpp | 91 ++++--------- contracts/sysio.roa/sysio.roa.hpp | 69 ++++------ contracts/tests/contract_test_support.hpp | 1 + contracts/tests/sysio.dispatch_tests.cpp | 61 ++++++--- contracts/tests/sysio.roa_tests.cpp | 153 ++++++++++------------ docs/contract-upgrade-order.md | 13 ++ docs/roa-overview.md | 20 ++- tests/nodeownreg_test.py | 27 ++-- 11 files changed, 211 insertions(+), 245 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 8b86547bf5..716eeec67c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -399,6 +399,8 @@ cd $BUILD_DIR && python3 tests/.py ``` Do NOT run from the source root — that would use stale or missing binaries. +Review [contract upgrade order](docs/contract-upgrade-order.md) when changing inline calls between system contracts; unchanged ABIs do not guarantee mixed-version compatibility. + ## Smart Contract Compilation (System Contracts specifically) The system contracts are compiled via the `contracts_project` CMake target. diff --git a/README.md b/README.md index 3221f6c1fd..d3f64ae03e 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,8 @@ Wire Sysio is a fork of Spring, a C++ implementation of the [AntelopeIO](https://github.com/AntelopeIO) protocol. It contains blockchain node software and supporting tools for developers and node operators. +Resource allocation and node-owner registration are described in [the ROA overview](docs/roa-overview.md). + ## Branches The `master` branch is the latest stable branch. diff --git a/contracts/sysio.msgch/src/sysio.msgch.cpp b/contracts/sysio.msgch/src/sysio.msgch.cpp index 5a08492e37..3d706a9456 100644 --- a/contracts/sysio.msgch/src/sysio.msgch.cpp +++ b/contracts/sysio.msgch/src/sysio.msgch.cpp @@ -585,16 +585,13 @@ std::optional em_identity_from_uncompressed_key(const std::vector& data, uint64_t chain_code) auto eth_identity = em_identity_from_uncompressed_key(reg.actor_pub_key); if (!eth_identity) return; // unusable depositor ETH key; drop - // 1) Create the account (idempotent; soft-skips a name that breaks the tier rule). - action(permission_level{ROA_ACCOUNT, "active"_n}, ROA_ACCOUNT, "newnameduser"_n, - std::make_tuple(*owner, *wire_pk, tier)).send(); - - // 2) Register the owner + record the ETH link, with claim-payload soft-fail + audit recording. + // ROA owns the nonexistence check and account creation within the same claim. action(permission_level{ROA_ACCOUNT, "active"_n}, ROA_ACCOUNT, "nodeownreg"_n, std::make_tuple(*owner, tier, eth_identity->key, *wire_pk, eth_identity->address)).send(); } diff --git a/contracts/sysio.roa/sysio.roa.cpp b/contracts/sysio.roa/sysio.roa.cpp index 3212711c1b..fb411bc9fc 100644 --- a/contracts/sysio.roa/sysio.roa.cpp +++ b/contracts/sysio.roa/sysio.roa.cpp @@ -6,7 +6,6 @@ #include #include #include // add_sat_u64 / add_sat_i64 -- never-throw saturating accumulators -#include // get_permission -- read an account's active authority in nodeownreg #include #include @@ -19,6 +18,8 @@ namespace sysio { // literals -- a contract rename is one change here, not scattered across call sites). constexpr name AUTHEX_ACCOUNT = "sysio.authex"_n; constexpr name AUTHEX_RECORDLINK = "recordlink"_n; + /// Existing privileged allocator, queued after the claim creates its account. + constexpr name ROA_FORCEREG = "forcereg"_n; /// Names under this prefix belong to system accounts. The chain refuses them only to non-privileged /// creators, and sysio.roa is privileged, so node-owner claims must refuse them here. @@ -764,13 +765,8 @@ namespace sysio { void roa::nodeownreg(const name& owner, const uint8_t& tier, const public_key& eth_pub_key, const public_key& wire_pub_key, const bytes& eth_address) { - // Dispatched by the OPP depot (sysio.msgch) when it processes an inbound - // ATTESTATION_TYPE_NODE_OWNER_REG attestation. msgch inline-sends newnameduser (account - // create) and then this action, both declaring permission_level{sysio.roa, active}. - // Privileged sysio.msgch may declare that target permission without a cross-contract active - // grant, so deployment must preserve msgch's privileged status. Inline actions run - // depth-first, so any newaccount has executed before this runs. Invalid names and full - // tiers skip creation; this action records their rejection instead. + // One authenticated depot claim owns creation and registration. Existing accounts are + // never eligible, even when the payload copies their public active key (WIRE-439). require_auth(get_self()); // ---- Envelope / system invariants (depot misuse) ---- @@ -798,48 +794,28 @@ namespace sysio { // ---- Claim-payload checks (trust-OPP soft-fail) ---- // The OPP envelope can be well-formed while the claim inside it is bad (the name the user - // chose is invalid, names an account they do not control, or is a replay). Aborting the + // chose is invalid, names an existing account, or is a replay). Aborting the // dispatching transaction would be bad UX, leave no audit row, and give the batch operator // undefined retry semantics. Instead record a REJECTED row with a reason and return, so the // transaction commits and the failure is queryable on Wire (no outbound error attestation). - // (1) the chosen name must be valid for the tier (newnameduser skipped creation otherwise). if (!valid_name_for_tier(owner, tier)) { record_nodereg(owner, tier, REJECTED, NAME_INVALID, gen); return; } - // (2) newnameduser skips creation at capacity to avoid spending sysio's RAM on a rejected - // claim. Otherwise a missing account means the creation step did not occur. - if (!is_account(owner)) { - const auto reason = nodeowner_count(get_self(), gen, tier) >= nodeowner_cap(tier) - ? TIER_CAP_REACHED : OWNER_NOT_ACCOUNT; - record_nodereg(owner, tier, REJECTED, reason, gen); + // Preserve the duplicate outcome for an already registered owner. + nodeowners_t nodeowners(get_self(), gen); + if (nodeowners.contains(nodeowner_key{owner.value})) { + record_nodereg(owner, tier, REJECTED, DUPLICATE, gen); return; } - - // (3) the account must be controlled by exactly the claimed wire key. For a just-created - // account this is the key newnameduser set; for a pre-existing account it proves the claimant - // controls it -- an NFT cannot be claimed into an account someone else holds. - if (!active_key_matches(owner, wire_pub_key)) { - record_nodereg(owner, tier, REJECTED, ACCOUNT_KEY_MISMATCH, gen); + if (is_account(owner)) { + record_nodereg(owner, tier, REJECTED, ACCOUNT_EXISTS, gen); return; } - - // (4) must not already be a registered node owner -- a replay / double-claim. Record - // DUPLICATE and return so the claim is idempotent and auditable. - { - nodeowners_t nodeowners(get_self(), gen); - if (nodeowners.contains(nodeowner_key{owner.value})) { - record_nodereg(owner, tier, REJECTED, DUPLICATE, gen); - return; - } - } - - // (5) the account must not already carry a *different* EVM link than this claim. recordlink is - // idempotent per (account, chain) and would silently keep the existing key, so a claim with a - // new depositor key would otherwise record CONFIRMED against a stale link. Detect that here and - // soft-fail rather than overwrite or mis-record. + // A trusted import may have recorded a link before account creation. Never confirm + // against a different depositor identity; recordlink intentionally keeps an existing link. { authex::links_t links(AUTHEX_ACCOUNT); auto by_namechain = links.get_index<"bynamechain"_n>(); @@ -849,22 +825,17 @@ namespace sysio { return; } } - - // (6) capacity exhaustion is a terminal claim outcome, not a malformed envelope. Soft-fail - // so another claim in the same consensus envelope cannot roll back every registration and - // leave batch operators retrying the deterministically failing envelope forever. if (nodeowner_count(get_self(), gen, tier) >= nodeowner_cap(tier)) { record_nodereg(owner, tier, REJECTED, TIER_CAP_REACHED, gen); return; } - // (7) a pre-existing reslimit row no longer blocks registration. An attacker can plant one on - // any account via addpolicy (no target consent), which previously forced this claim into a - // permanent OWNER_HAS_RESLIMIT soft-fail -- a valid registration could be griefed indefinitely. - // regnodeowner now reconciles instead: it stacks the node-owner allocation onto the existing - // row via increase_reslimit rather than creating from scratch. The planted policy stays backed - // by its issuer's reserve and remains independently reclaimable via reducepolicy; no value is - // stolen and the claim reaches CONFIRMED. (SEC-087) + // Queue native newaccount before the privileged allocator. Call the helper directly to + // preserve inline-depth headroom for recordlink's DClaim sweep. All effects, including the + // terminal audit below, commit together or roll back if an inline action fails. + newnameduser(owner, wire_pub_key, tier); + action(permission_level{get_self(), "active"_n}, get_self(), ROA_FORCEREG, + std::make_tuple(owner, tier)).send(); // Record the depositor's ETH key as a sysio.authex link via the trusted depot-only path. // recordlink requires sysio.authex.active; privileged sysio.roa may declare that permission @@ -875,7 +846,6 @@ namespace sysio { std::make_tuple(owner, opp::types::ChainKind::CHAIN_KIND_EVM, eth_pub_key, eth_address)).send(); - regnodeowner(owner, tier); record_nodereg(owner, tier, CONFIRMED, NONE, gen); }; @@ -911,21 +881,6 @@ namespace sysio { return len >= 1 && len <= 12; } - bool roa::active_key_matches(const name& account, const public_key& key) { - // The claim is valid iff `key` can satisfy the account's `active` authority by itself: it - // appears among the keys with weight >= threshold. We deliberately do NOT require a single-key - // authority or an empty `accounts` list -- a standard account also carries an - // @sysio.code entry (benign: it grants the account's own contract inline-action - // rights, not external control), and newnameduser-created accounts have neither. A claim into - // an account a different key controls still fails: that key won't be among `auth.keys`. - auto perm = get_permission(account, "active"_n); - if (!perm.has_value() || perm->auth.threshold == 0) return false; - for (const auto& kw : perm->auth.keys) { - if (kw.key == key && kw.weight >= perm->auth.threshold) return true; - } - return false; - } - uint32_t roa::nodeowner_cap(uint8_t tier) { switch (tier) { case 1: @@ -1267,17 +1222,15 @@ namespace sysio { } void roa::newnameduser(const name& account, const public_key& pubkey, uint8_t tier) { - // Dispatched by the OPP depot (sysio.msgch) in the NFT node-owner claim flow, the same way - // as nodeownreg: privileged sysio.msgch sends this inline declaring {sysio.roa, active}, - // without requiring a cross-contract active grant. + // Called directly by nodeownreg, or as a standalone privileged account-creation action. require_auth(get_self()); roastate_t roastate(get_self()); auto state = roastate.get(); check(state.is_active, "ROA is not active yet"); - // Idempotent: if the account already exists, the create step is already done. A key or tier - // mismatch on a pre-existing account is caught downstream by nodeownreg's soft-fail. + // Standalone privileged creation remains idempotent. nodeownreg checks nonexistence + // before calling this helper; a separately created account is not eligible for a claim. if (is_account(account)) return; // Tier is a depot/system invariant -- a bad value means the dispatch is malformed, so abort diff --git a/contracts/sysio.roa/sysio.roa.hpp b/contracts/sysio.roa/sysio.roa.hpp index b6748696cf..83e8b708fd 100644 --- a/contracts/sysio.roa/sysio.roa.hpp +++ b/contracts/sysio.roa/sysio.roa.hpp @@ -116,7 +116,7 @@ namespace sysio { /** * @brief Directly register `owner` as a node owner at `tier`, bypassing the OPP claim - * flow. Privileged bootstrap/test path -- NOT the production registration route. + * flow. Also used as nodeownreg's internal allocation step after native account creation. * * Requires `sysio.roa` active authority, so it is governance/system-only and never * user-callable. It exists to seed node owners where the full OPP machinery is absent or @@ -137,27 +137,19 @@ namespace sysio { void forcereg(const name& owner, const uint8_t& tier); /** - * @brief Registers a node owner when the depot (sysio.msgch) processes an inbound OPP - * NodeOwnerRegistration attestation. The register step of the NFT claim flow; the - * depot inline-sends newnameduser immediately before this. It creates a missing - * account only when the name is valid and the tier has registration capacity. - * - * Trust-OPP: the OPP envelope is the deposit proof, so this RECORDS the depositor's ETH - * key as a sysio.authex link (inline recordlink) rather than verifying a pre-existing - * createlink. Claim-payload problems (bad name, account controlled by a different key, - * already registered, tier full) are soft-failed -- recorded in `nodeownerreg` with a reject_reason - * and returned -- never thrown, so the dispatching transaction commits. Depot/system - * invariants (tier range, ETH key type and curve membership, ROA active) stay hard checks: - * sysio.msgch drops a depositor key that is not a secp256k1 point before dispatch, and - * one that still arrived would leave recordlink skipping the link this action confirms. - * - * @param owner The Wire account to register as node owner (created in-flow if absent). - * @param tier Node owner tier: 1, 2, or 3. - * @param eth_pub_key The depositor's ETH public key (Wire PUB_EM format); recorded as the link. - * @param wire_pub_key The Wire account owner/active key the claim specified; an existing - * account must be controlled by exactly this key or the claim is rejected. - * @param eth_address Depositor 20-byte ETH address used to sweep DClaim rewards - * deposited before the link existed. + * @brief Create and register a new node-owner account from an authenticated OPP claim. + * + * Requires sysio.roa authority. Existing accounts are rejected even if wire_pub_key + * matches a public active key. Valid claims queue native account creation, tier allocation + * through forcereg, and the depositor's AuthEx link in one atomic transaction. + * Claim-payload errors record a terminal rejection without aborting depot dispatch. + * Tier range, depositor key/address validity and ROA activation remain hard invariants. + * + * @param owner New Wire account name, valid for the claimed tier. + * @param tier Node-owner tier 1, 2 or 3. + * @param eth_pub_key Depositor EM public key, recorded as the account's Ethereum link. + * @param wire_pub_key Owner and active key for the newly created account. + * @param eth_address Depositor's 20-byte address, used to sweep pre-link DClaim rewards. */ [[sysio::action]] void nodeownreg(const name& owner, const uint8_t& tier, const public_key& eth_pub_key, @@ -236,11 +228,10 @@ namespace sysio { /** * @brief Create a node-owner account with a user-chosen (vanity) name and the holder's * K1 key as owner/active, funded with the fixed newaccount_ram from sysio's pool. The - * create step of the OPP NFT claim flow (newnameduser -> nodeownreg, whose inline - * recordlink records the external-chain link and sweeps pre-link rewards). + * creation helper called internally by nodeownreg before its queued allocation and AuthEx link. * - * Dispatched by privileged sysio.msgch as {sysio.roa, active}, without a cross-contract - * active grant, like nodeownreg. Idempotent: a no-op if the account already exists. + * Standalone creation is privileged and idempotent. An account created separately is + * not eligible for a later NFT claim. * Tier-based name rules: tier-1 = 2-6 char prefix; tier 2/3 = up to 12 chars. * Invalid names and full tiers skip creation without spending RAM; nodeownreg records * the rejection. Capacity comes from the live generation's authoritative owner rows. @@ -439,20 +430,15 @@ namespace sysio { // Rejection reason for `nodeownerreg::reason` (only meaningful when status == REJECTED). // Under trust-OPP, nodeownreg soft-fails claim-payload errors by recording one of these // instead of aborting the dispatching transaction, so the failure is queryable on Wire. - // (Values renumbered for the create-in-flow model: the ETH link is now *recorded* via - // sysio.authex::recordlink rather than verified against a pre-existing createlink, so the - // former NO_AUTHEX_LINK / KEY_MISMATCH reasons no longer apply. Pre-launch: no stored rows - // to migrate.) enum reject_reason : uint8_t { NONE = 0, // not rejected NAME_INVALID = 1, // chosen name violates the tier's length rule or is a reserved sysio. name - OWNER_NOT_ACCOUNT = 2, // account does not exist (creation did not occur) - ACCOUNT_KEY_MISMATCH = 3, // existing account's active authority != the single claimed wire key + OWNER_NOT_ACCOUNT = 2, // historical: creation did not occur + ACCOUNT_KEY_MISMATCH = 3, // historical: submitted key did not match active authority DUPLICATE = 4, // owner is already a registered node owner LINK_KEY_MISMATCH = 5, // account already carries a different external-chain link key - TIER_CAP_REACHED = 6 // the claimed tier has no remaining registration capacity - // (OWNER_HAS_RESLIMIT removed: a pre-existing reslimit row no longer rejects registration -- - // regnodeowner reconciles it via increase_reslimit. Pre-launch: no stored rows to migrate. SEC-087.) + TIER_CAP_REACHED = 6, // the claimed tier has no remaining registration capacity + ACCOUNT_EXISTS = 7 // the target existed before this claim; registration requires a new account }; // Under trust-OPP the OPP envelope is the deposit proof, so this audit row records only @@ -606,18 +592,7 @@ namespace sysio { */ static bool valid_name_for_tier(const name& account, uint8_t tier); - /** - * @brief Whether `key` can satisfy `account`'s `active` authority by itself -- i.e. it - * appears among the active keys with weight >= threshold. Passes for a - * newnameduser-created account ({key}) and for a standard account that also carries - * the benign @sysio.code entry; rejects an account a different key controls - * (that key won't be present) and a multi-sig where `key` alone is insufficient. - * - * @param account The account whose active permission to inspect. - * @param key The claimed owner/active public key. - * @return true if `key` alone controls `account`'s active permission. - */ - static bool active_key_matches(const name& account, const public_key& key); + /** diff --git a/contracts/tests/contract_test_support.hpp b/contracts/tests/contract_test_support.hpp index ff3051d40e..07dd36774d 100644 --- a/contracts/tests/contract_test_support.hpp +++ b/contracts/tests/contract_test_support.hpp @@ -213,6 +213,7 @@ inline constexpr uint64_t reason_account_key_mismatch = 3; inline constexpr uint64_t reason_duplicate = 4; inline constexpr uint64_t reason_link_key_mismatch = 5; inline constexpr uint64_t reason_tier_cap_reached = 6; +inline constexpr uint8_t reason_account_exists = 7; } // namespace nodeownerreg namespace nodeowners { diff --git a/contracts/tests/sysio.dispatch_tests.cpp b/contracts/tests/sysio.dispatch_tests.cpp index 51e282586f..befe9f8f93 100644 --- a/contracts/tests/sysio.dispatch_tests.cpp +++ b/contracts/tests/sysio.dispatch_tests.cpp @@ -324,9 +324,7 @@ class sysio_dispatch_tester : public tester { static constexpr auto ROA_ACCOUNT = "sysio.roa"_n; static constexpr auto BATCHOP = "batchop.a"_n; static constexpr auto UWRIT_OP = "uwrit.alice"_n; - // Pre-created claim account for the NodeOwnerRegistration test: a fresh single-key account, so - // nodeownreg's active_key_matches succeeds when the claim carries that same key (existing-account - // path -- exercises the dispatch decode + routing without the account-creation machinery). + /// Node-owner claim target, deliberately absent until depot dispatch. static constexpr auto CLAIM_ACCOUNT = "claimacct"_n; static constexpr uint64_t ROA_NETWORK_GEN = 0; /// Termination prune delay for the suite's opreg config: ten minutes, far beyond any test's wall clock. @@ -340,15 +338,6 @@ class sysio_dispatch_tester : public tester { CHALG_ACCOUNT, TOKEN_ACCOUNT, CHAINS_ACCOUNT, DCLAIM_ACCOUNT, BATCHOP, UWRIT_OP }); - // CLAIM_ACCOUNT with NO roa policy (include_roa_policy=false) so regnodeowner exercises the - // fresh create-branch of increase_reslimit. (A pre-existing reslimit row would now be reconciled, - // not rejected -- SEC-087 -- but this dispatch test keeps the clean create path.) include_code=true - // leaves the standard @sysio.code on active, which exercises active_key_matches against a - // real (non-single-entry) authority. - create_account(CLAIM_ACCOUNT, config::system_account_name, - /*multisig=*/false, /*include_code=*/true, /*include_roa_policy=*/false); - produce_blocks(2); - deploy(MSGCH_ACCOUNT, contracts::msgch_wasm(), contracts::msgch_abi(), msgch_abi); deploy(OPREG_ACCOUNT, contracts::opreg_wasm(), contracts::opreg_abi(), opreg_abi); deploy(EPOCH_ACCOUNT, contracts::epoch_wasm(), contracts::epoch_abi(), epoch_abi); @@ -935,11 +924,7 @@ BOOST_FIXTURE_TEST_CASE(deliver_envelope_at_size_cap_succeeds, sysio_dispatch_te BOOST_REQUIRE_EQUAL(current_epoch(), row["epoch_index"].as()); } FC_LOG_AND_RETHROW() } -// NodeOwnerRegistration: msgch decodes the attestation and inline-sends sysio.roa::newnameduser then -// nodeownreg. CLAIM_ACCOUNT pre-exists with a single-key active, so newnameduser no-ops and the -// claim's matching wire key drives nodeownreg's existing-account path to CONFIRMED (registers the -// owner and inline-records the depositor's ETH link in sysio.authex). Exercises the full dispatch: -// proto decode (account name + WireKey + ETH key) -> routing -> both roa actions -> recordlink. +/// Full depot dispatch creates the account, registers it and sweeps pre-link DClaim rewards. BOOST_FIXTURE_TEST_CASE(dispatch_routes_node_owner_reg_to_roa, sysio_dispatch_tester) { try { // Node-owner NFT deposits originate on the Ethereum outpost (code "ETHEREUM", matching the launch // bootstrap config and msgch's NODE_OWNER_SRC_CHAIN). Bootstrap it as the scheduled source outpost @@ -947,7 +932,7 @@ BOOST_FIXTURE_TEST_CASE(dispatch_routes_node_owner_reg_to_roa, sysio_dispatch_te bootstrap_for_dispatch("ETHEREUM"); const auto eth_code = fc::slug_name{"ETHEREUM"}.value; - // The claim must carry CLAIM_ACCOUNT's own active key so nodeownreg's active_key_matches passes. + // The supplied key becomes the new account's owner and active authority. auto wire_key = k1_pubkey_bytes(get_public_key(CLAIM_ACCOUNT, "active")); // BAR supplies the depositor's ETH key as an uncompressed 65-byte point. auto eth_pub = fc::crypto::private_key::generate(fc::crypto::private_key::key_type::em).get_public_key(); @@ -985,6 +970,46 @@ BOOST_FIXTURE_TEST_CASE(dispatch_routes_node_owner_reg_to_roa, sysio_dispatch_te BOOST_REQUIRE_EQUAL(pending["balance"].as().get_amount(), 4321); } FC_LOG_AND_RETHROW() } +/// A copied public key cannot authorize an existing name, and rejection must not block a fresh claim. +BOOST_FIXTURE_TEST_CASE(dispatch_existing_node_owner_name_rejects_without_blocking_fresh_claim, sysio_dispatch_tester) { try { + namespace audit = sysio_system::test_support::nodeownerreg; + bootstrap_for_dispatch("ETHEREUM"); + create_accounts({CLAIM_ACCOUNT}); + constexpr auto fresh = "freshclaim"_n; + const auto victim_key = get_public_key(CLAIM_ACCOUNT, "active"); + const auto eth_pub = fc::crypto::private_key::generate(fc::crypto::private_key::key_type::em).get_public_key(); + const auto eth_address = fc::crypto::ethereum::address_to_bytes(eth_pub); + const std::vector native_address(eth_address.begin(), eth_address.end()); + BOOST_REQUIRE_EQUAL(success(), push(DCLAIM_ACCOUNT, dclaim_abi, DCLAIM_ACCOUNT, "importseed"_n, mvo() + ("chain", ChainKind::CHAIN_KIND_EVM)("credits", fc::variants{ + mvo()("native_address", native_address)("wire_atomic", int64_t{4321})}))); + std::vector claims; + for (const auto owner : {CLAIM_ACCOUNT, fresh}) { + claims.push_back(encode_node_owner_registration( + owner.to_string(), 2, sysio::opp::types::WIRE_KEY_TYPE_K1, + k1_pubkey_bytes(get_public_key(owner, "active")), em_uncompressed_pubkey_bytes(eth_pub), eth_address)); + } + BOOST_REQUIRE_EQUAL(success(), deliver(fc::slug_name{"ETHEREUM"}.value, + encode_envelope_with_attestations(current_epoch(), sysio::opp::types::ATTESTATION_TYPE_NODE_OWNER_REG, claims))); + const auto rejected = get_nodeownerreg(CLAIM_ACCOUNT); + BOOST_REQUIRE(!rejected.is_null()); + BOOST_CHECK_EQUAL(rejected["status"].as(), audit::status_rejected); + BOOST_CHECK_EQUAL(rejected["reason"].as(), audit::reason_account_exists); + BOOST_CHECK(get_nodeowner(CLAIM_ACCOUNT).is_null()); + BOOST_CHECK(get_dclaim_row("pclaims"_n, "pending_claim", CLAIM_ACCOUNT.to_uint64_t()).is_null()); + // Only the fresh account's link may consume the depositor's imported credit. + const auto linked = get_row_by_id(AUTHEX_ACCOUNT, AUTHEX_ACCOUNT, "links"_n, 0); + BOOST_REQUIRE(!linked.empty()); + BOOST_CHECK_EQUAL(authex_abi.binary_to_variant("links_s", linked, fixture_abi_yield_function())["username"].as(), fresh); + BOOST_CHECK(get_row_by_id(AUTHEX_ACCOUNT, AUTHEX_ACCOUNT, "links"_n, 1).empty()); + BOOST_REQUIRE(!get_nodeowner(fresh).is_null()); + BOOST_CHECK_EQUAL(get_nodeownerreg(fresh)["status"].as(), audit::status_confirmed); + BOOST_CHECK_EQUAL(get_dclaim_row("pclaims"_n, "pending_claim", fresh.to_uint64_t())["balance"].as().get_amount(), 4321); + const auto& active = control->get_authorization_manager().get_permission({CLAIM_ACCOUNT, config::active_name}); + BOOST_REQUIRE_EQUAL(active.auth.keys.size(), 1u); + BOOST_CHECK_EQUAL(active.auth.keys.front().key, victim_key); +} FC_LOG_AND_RETHROW() } + // Only ATTESTATION_TYPE_NODE_OWNER_REG reaches the node-owner handler. The outbound-only types // (OPERATORS, BATCH_OPERATOR_GROUPS) are dropped on receipt even when // the payload is a well-formed NodeOwnerRegistration from the node-owner source outpost: a payload diff --git a/contracts/tests/sysio.roa_tests.cpp b/contracts/tests/sysio.roa_tests.cpp index b15746bc1c..ad64f7e21a 100644 --- a/contracts/tests/sysio.roa_tests.cpp +++ b/contracts/tests/sysio.roa_tests.cpp @@ -2684,11 +2684,9 @@ BOOST_FIXTURE_TEST_CASE( newnameduser_tier_name_rules, sysio_roa_tester ) try { // --------------------------------------------------------------------------- // nodeownreg tests (OPP Node Owner NFT claim: create-in-flow + register + record ETH link) // -// Under the create-in-flow model the depot (sysio.msgch) inline-sends newnameduser (creates the -// account with the claimed wire key) then nodeownreg (registers + records the depositor's ETH key -// via an inline sysio.authex::recordlink). These unit tests drive the two sysio.roa actions -// directly, signed by privileged ROA, the same way the depot would. No cross-contract active -// permission is installed: privilege authorizes the declared sysio.authex.active permission. +// The depot dispatches nodeownreg once. It creates a fresh account, allocates the owner and +// records its ETH link atomically. Existing names must soft-reject even with a matching public +// key. Privileged standalone account creation and allocation remain separate administrative paths. // --------------------------------------------------------------------------- /// Shared mirrors of sysio.roa's node-owner registration audit values. @@ -2754,7 +2752,7 @@ class sysio_roa_nodeownreg_tester : public sysio_roa_tester { "links_s", data, fixture_abi_yield_function()); } - // Create the claim account in-flow (depot path) with `wire_pub_key` as owner/active. + // Create a pre-existing account fixture with `wire_pub_key` as owner/active. action_result newnameduser(const name& account, const fc::crypto::public_key& wire_pub_key, uint8_t tier) { return push_action(ROA, "newnameduser"_n, mvo() ("account", account)("pubkey", wire_pub_key)("tier", tier)); @@ -2787,8 +2785,7 @@ class sysio_roa_nodeownreg_tester : public sysio_roa_tester { static constexpr uint64_t CONFIRMED = nodeownerreg_audit::status_confirmed; static constexpr uint64_t REJECTED = nodeownerreg_audit::status_rejected; static constexpr uint64_t R_NAME_INVALID = nodeownerreg_audit::reason_name_invalid; - static constexpr uint64_t R_OWNER_NOT_ACCOUNT = nodeownerreg_audit::reason_owner_not_account; - static constexpr uint64_t R_ACCOUNT_KEY_MISMATCH = nodeownerreg_audit::reason_account_key_mismatch; + static constexpr uint64_t R_ACCOUNT_EXISTS = nodeownerreg_audit::reason_account_exists; static constexpr uint64_t R_DUPLICATE = nodeownerreg_audit::reason_duplicate; static constexpr uint64_t R_LINK_KEY_MISMATCH = nodeownerreg_audit::reason_link_key_mismatch; static constexpr uint64_t R_TIER_CAP_REACHED = nodeownerreg_audit::reason_tier_cap_reached; @@ -2796,31 +2793,53 @@ class sysio_roa_nodeownreg_tester : public sysio_roa_tester { abi_serializer authex_abi_ser; }; -// Happy path: depot creates the account with the claimed wire key, then nodeownreg registers and -// records the depositor's ETH link. +/// Fresh claims create the chosen authority, membership and Ethereum link for all tiers. BOOST_FIXTURE_TEST_CASE( nodeownreg_happy_path, sysio_roa_nodeownreg_tester ) try { - const auto owner = "claimacct"_n; const auto wire_pub = gen_k1_key(); - const auto eth_pub = gen_em_key(); + const auto eth_pub = gen_em_key(); + for (const auto& [owner, tier] : {std::pair{"freshone"_n, uint8_t{2}}, + std::pair{"fresh"_n, uint8_t{1}}, + std::pair{"freshthree"_n, uint8_t{3}}}) { + BOOST_REQUIRE(control->db().find(owner) == nullptr); + BOOST_REQUIRE_EQUAL(success(), nodeownreg(owner, tier, eth_pub, wire_pub)); + produce_blocks(); + const auto reg = get_nodeowner(owner); + BOOST_REQUIRE(!reg.is_null()); + BOOST_CHECK_EQUAL(reg["tier"].as(), tier); + BOOST_CHECK_EQUAL(get_nodeownerreg(owner)["status"].as(), CONFIRMED); + const auto& active = control->get_authorization_manager().get_permission({owner, config::active_name}); + BOOST_REQUIRE_EQUAL(active.auth.keys.size(), 1u); + BOOST_CHECK_EQUAL(active.auth.keys.front().key, wire_pub); + } +} FC_LOG_AND_RETHROW() +/// Public-key equality never authorizes registration into an existing account. +BOOST_FIXTURE_TEST_CASE( nodeownreg_rejects_existing_account_with_matching_key, sysio_roa_nodeownreg_tester ) try { + const auto owner = "claimacct"_n; + const auto wire_pub = gen_k1_key(); BOOST_REQUIRE_EQUAL(success(), newnameduser(owner, wire_pub, 2)); produce_blocks(); - - BOOST_REQUIRE_EQUAL(success(), nodeownreg(owner, 2, eth_pub, wire_pub)); + const auto pool_before = get_reslimit(config::system_account_name); + int64_t ram_before = 0, net_before = 0, cpu_before = 0; + auto& limits = control->get_resource_limits_manager(); + limits.get_account_limits(owner, ram_before, net_before, cpu_before); + BOOST_REQUIRE_EQUAL(success(), nodeownreg(owner, 2, gen_em_key(), wire_pub)); produce_blocks(); - - // A CONFIRMED registration proves the account was created with wire_pub: nodeownreg's - // active_key_matches requires the account to exist and be controlled by exactly that key, so - // reaching CONFIRMED (rather than OWNER_NOT_ACCOUNT / ACCOUNT_KEY_MISMATCH) is the existence proof. - auto reg = get_nodeowner(owner); - BOOST_REQUIRE_EQUAL(reg.is_null(), false); - BOOST_REQUIRE_EQUAL(reg["tier"].as(), 2); - auto audit = get_nodeownerreg(owner); - BOOST_REQUIRE_EQUAL(audit.is_null(), false); - BOOST_REQUIRE_EQUAL(audit["status"].as(), CONFIRMED); - // nodeownreg returning success implies the inline recordlink ({sysio.authex, active}) was - // authorized and ran -- an unauthorized inline send would have aborted the whole transaction. - // recordlink's own table effects are covered by the sysio.authex unit tests. + BOOST_CHECK(get_nodeowner(owner).is_null()); + BOOST_CHECK(get_authex_link(0).is_null()); + const auto audit = get_nodeownerreg(owner); + BOOST_CHECK_EQUAL(audit["status"].as(), REJECTED); + BOOST_CHECK_EQUAL(audit["reason"].as(), R_ACCOUNT_EXISTS); + const auto& active = control->get_authorization_manager().get_permission({owner, config::active_name}); + BOOST_REQUIRE_EQUAL(active.auth.keys.size(), 1u); + BOOST_CHECK_EQUAL(active.auth.keys.front().key, wire_pub); + int64_t ram_after = 0, net_after = 0, cpu_after = 0; + limits.get_account_limits(owner, ram_after, net_after, cpu_after); + BOOST_CHECK_EQUAL(ram_after, ram_before); + BOOST_CHECK_EQUAL(net_after, net_before); + BOOST_CHECK_EQUAL(cpu_after, cpu_before); + BOOST_CHECK_EQUAL(get_reslimit(config::system_account_name)["ram_bytes"].as_int64(), + pool_before["ram_bytes"].as_int64()); } FC_LOG_AND_RETHROW() // The depot envelope carries a raw EVM address. Anything other than exactly 20 bytes is a hard @@ -2831,8 +2850,6 @@ BOOST_FIXTURE_TEST_CASE( nodeownreg_rejects_malformed_eth_address_lengths, const auto wire_pub = gen_k1_key(); const auto eth_pub = gen_em_key(); - BOOST_REQUIRE_EQUAL(success(), newnameduser(owner, wire_pub, 2)); - produce_blocks(); for (const auto size : {size_t{19}, size_t{21}}) { const std::vector malformed_address(size, char{0x01}); @@ -2845,8 +2862,8 @@ BOOST_FIXTURE_TEST_CASE( nodeownreg_rejects_malformed_eth_address_lengths, } } FC_LOG_AND_RETHROW() -// Existing account controlled by a different key than the claim -> REJECTED/ACCOUNT_KEY_MISMATCH. -BOOST_FIXTURE_TEST_CASE( nodeownreg_account_key_mismatch, sysio_roa_nodeownreg_tester ) try { +// A different submitted key cannot bypass the existing-account rejection either. +BOOST_FIXTURE_TEST_CASE( nodeownreg_rejects_existing_account_with_different_key, sysio_roa_nodeownreg_tester ) try { const auto owner = "claimacct"_n; const auto real_k = gen_k1_key(); // key the account is actually created with const auto claimed = gen_k1_key(); // a different key in the claim @@ -2861,22 +2878,17 @@ BOOST_FIXTURE_TEST_CASE( nodeownreg_account_key_mismatch, sysio_roa_nodeownreg_t BOOST_REQUIRE(get_nodeowner(owner).is_null()); auto audit = get_nodeownerreg(owner); BOOST_REQUIRE_EQUAL(audit["status"].as(), REJECTED); - BOOST_REQUIRE_EQUAL(audit["reason"].as(), R_ACCOUNT_KEY_MISMATCH); + BOOST_REQUIRE_EQUAL(audit["reason"].as(), R_ACCOUNT_EXISTS); } FC_LOG_AND_RETHROW() -// SEC-087: an account that already carries a reslimit row must NOT be blocked from registering. Such a -// row can be planted on any account by a node owner via addpolicy (no target consent), which previously -// forced the claim into a permanent OWNER_HAS_RESLIMIT soft-fail -- a denial-of-registration grief. -// nodeownreg now RECONCILES: regnodeowner stacks the node-owner allocation onto the existing row via -// increase_reslimit, the claim reaches CONFIRMED, and the planted policy is preserved (still the -// issuer's, reclaimable via reducepolicy). No value is stolen and the gift is not double-counted. -BOOST_FIXTURE_TEST_CASE( nodeownreg_reconciles_existing_reslimit, sysio_roa_nodeownreg_tester ) try { +// SEC-087 resource reconciliation remains supported through privileged forcereg. +// Public NFT claims reject existing accounts under WIRE-439. +BOOST_FIXTURE_TEST_CASE( forcereg_reconciles_existing_reslimit, sysio_roa_nodeownreg_tester ) try { const auto owner = "claimacct"_n; const auto wire_pub = gen_k1_key(); - const auto eth_pub = gen_em_key(); - // Create the claim account in-flow, then have NODE_DADDY plant a RAM-only policy on it so it carries - // a reslimit row before the claim is resolved (NODE_DADDY is the tier-1 issuer seeded by the fixture). + // Create an account and add a policy before privileged registration (NODE_DADDY is the + // tier-1 issuer seeded by the fixture). BOOST_REQUIRE_EQUAL(success(), newnameduser(owner, wire_pub, 2)); produce_blocks(); add_roa_policy(NODE_DADDY, owner, "0.0000 SYS", "0.0000 SYS", "1.0000 SYS", 0, 0); @@ -2897,17 +2909,15 @@ BOOST_FIXTURE_TEST_CASE( nodeownreg_reconciles_existing_reslimit, sysio_roa_node BOOST_REQUIRE_EQUAL(r["ram_bytes"].as_int64(), planted_ram_bytes + newaccount_ram); const auto daddy_before = get_nodeowner(NODE_DADDY); - // The claim must commit AND register -- not soft-fail, not throw. - BOOST_REQUIRE_EQUAL(success(), nodeownreg(owner, 2, eth_pub, wire_pub)); + // Privileged allocation must preserve the existing policy. + BOOST_REQUIRE_EQUAL(success(), push_action(ROA, "forcereg"_n, mvo()("owner", owner)("tier", 2))); produce_blocks(); // Registration completed (the SEC-087 outcome). auto reg = get_nodeowner(owner); BOOST_REQUIRE_EQUAL(reg.is_null(), false); BOOST_REQUIRE_EQUAL(reg["tier"].as(), 2); - auto audit = get_nodeownerreg(owner); - BOOST_REQUIRE_EQUAL(audit["status"].as(), CONFIRMED); - BOOST_REQUIRE_EQUAL(audit["reason"].as(), 0); // NONE + BOOST_REQUIRE(get_nodeownerreg(owner).is_null()); // reslimit reconciled: the planted row is preserved untouched and the gift is NOT re-added. A // tier-2 registration adds no personal weight, so the row is exactly what addpolicy left. @@ -2931,7 +2941,7 @@ BOOST_FIXTURE_TEST_CASE( nodeownreg_reconciles_existing_reslimit, sysio_roa_node BOOST_REQUIRE_EQUAL(pol["cpu_weight"].as_string(), "0.0000 SYS"); BOOST_REQUIRE_EQUAL(pol["ram_weight"].as_string(), "1.0000 SYS"); - // Conservation: nodeownreg does not touch the planted-policy issuer's allocations (the planted + // Conservation: forcereg does not touch the planted-policy issuer's allocations (the planted // resources stay backed by NODE_DADDY's reserve; the node-owner allocation is independently backed). auto daddy_after = get_nodeowner(NODE_DADDY); BOOST_REQUIRE_EQUAL(daddy_after["allocated_sys"].as_string(), daddy_before["allocated_sys"].as_string()); @@ -2940,11 +2950,10 @@ BOOST_FIXTURE_TEST_CASE( nodeownreg_reconciles_existing_reslimit, sysio_roa_node } FC_LOG_AND_RETHROW() // SEC-087 companion: a planted policy can carry NET/CPU as well as RAM. Verify the reconcile leaves -// them intact (the increase_reslimit MODIFY branch) and registration still reaches CONFIRMED. -BOOST_FIXTURE_TEST_CASE( nodeownreg_reconciles_existing_reslimit_net_cpu, sysio_roa_nodeownreg_tester ) try { +// them intact (the increase_reslimit MODIFY branch) through privileged registration. +BOOST_FIXTURE_TEST_CASE( forcereg_reconciles_existing_reslimit_net_cpu, sysio_roa_nodeownreg_tester ) try { const auto owner = "claimacct"_n; const auto wire_pub = gen_k1_key(); - const auto eth_pub = gen_em_key(); BOOST_REQUIRE_EQUAL(success(), newnameduser(owner, wire_pub, 2)); produce_blocks(); @@ -2954,11 +2963,11 @@ BOOST_FIXTURE_TEST_CASE( nodeownreg_reconciles_existing_reslimit_net_cpu, sysio_ const int64_t bytes_per_unit = 104; const int64_t planted_ram_bytes = 10000 * bytes_per_unit; - BOOST_REQUIRE_EQUAL(success(), nodeownreg(owner, 2, eth_pub, wire_pub)); + BOOST_REQUIRE_EQUAL(success(), push_action(ROA, "forcereg"_n, mvo()("owner", owner)("tier", 2))); produce_blocks(); BOOST_REQUIRE_EQUAL(get_nodeowner(owner).is_null(), false); - BOOST_REQUIRE_EQUAL(get_nodeownerreg(owner)["status"].as(), CONFIRMED); + BOOST_REQUIRE(get_nodeownerreg(owner).is_null()); // A tier-2 registration adds no personal weight, so the planted 1.0000 SYS of net/cpu and the // planted RAM (plus the one-time gift) survive the reconcile unchanged. @@ -2981,10 +2990,9 @@ BOOST_FIXTURE_TEST_CASE( nodeownreg_reconciles_existing_reslimit_net_cpu, sysio_ // node-owner personal weights must stack, and the one-time newaccount_ram gift must be counted once // rather than re-added. Without this the arithmetic that reconcile depends on would go untested on // the registration path. -BOOST_FIXTURE_TEST_CASE( nodeownreg_tier1_reconcile_stacks_personal_weights, sysio_roa_nodeownreg_tester ) try { +BOOST_FIXTURE_TEST_CASE( forcereg_tier1_reconcile_stacks_personal_weights, sysio_roa_nodeownreg_tester ) try { const auto owner = "t1own"_n; // tier-1 name rule: 2-6 characters const auto wire_pub = gen_k1_key(); - const auto eth_pub = gen_em_key(); BOOST_REQUIRE_EQUAL(success(), newnameduser(owner, wire_pub, 1)); produce_blocks(); @@ -3001,11 +3009,11 @@ BOOST_FIXTURE_TEST_CASE( nodeownreg_tier1_reconcile_stacks_personal_weights, sys BOOST_REQUIRE_EQUAL(r["net_weight"].as_string(), "1.0000 SYS"); BOOST_REQUIRE_EQUAL(r["ram_bytes"].as_int64(), planted_ram_bytes + newaccount_ram); - BOOST_REQUIRE_EQUAL(success(), nodeownreg(owner, 1, eth_pub, wire_pub)); + BOOST_REQUIRE_EQUAL(success(), push_action(ROA, "forcereg"_n, mvo()("owner", owner)("tier", 1))); produce_blocks(); BOOST_REQUIRE_EQUAL(get_nodeowner(owner)["tier"].as(), 1); - BOOST_REQUIRE_EQUAL(get_nodeownerreg(owner)["status"].as(), CONFIRMED); + BOOST_REQUIRE(get_nodeownerreg(owner).is_null()); // MODIFY branch with non-zero deltas: planted 1.0000 + personal 0.0500 = 1.0500 SYS of net/cpu, // ram = planted + gift + personal, with the gift NOT re-added. @@ -3087,8 +3095,6 @@ BOOST_FIXTURE_TEST_CASE( nodeownreg_link_key_mismatch, sysio_roa_nodeownreg_test const auto eth_a = gen_em_key(); // key already linked to the account const auto eth_b = gen_em_key(); // depositor key in the new claim - BOOST_REQUIRE_EQUAL(success(), newnameduser(owner, wire_pub, 2)); - produce_blocks(); // Pre-existing EVM link with eth_a; the account is not yet a node owner. BOOST_REQUIRE_EQUAL(success(), recordlink(eth_a, owner)); produce_blocks(); @@ -3142,26 +3148,12 @@ BOOST_FIXTURE_TEST_CASE( nodeownreg_rejects_reserved_system_names, sysio_roa_nod } } FC_LOG_AND_RETHROW() -// Valid-for-tier name that is not an account -> REJECTED/OWNER_NOT_ACCOUNT. -BOOST_FIXTURE_TEST_CASE( nodeownreg_owner_not_account, sysio_roa_nodeownreg_tester ) try { - const auto owner = "ghost"_n; // 5 chars: valid for tier 1, but no account was created - BOOST_REQUIRE_EQUAL(success(), nodeownreg(owner, 1, gen_em_key(), gen_k1_key())); - produce_blocks(); - - BOOST_REQUIRE(get_nodeowner(owner).is_null()); - auto audit = get_nodeownerreg(owner); - BOOST_REQUIRE_EQUAL(audit["status"].as(), REJECTED); - BOOST_REQUIRE_EQUAL(audit["reason"].as(), R_OWNER_NOT_ACCOUNT); -} FC_LOG_AND_RETHROW() - // Replay after a successful claim -> REJECTED/DUPLICATE, no abort. BOOST_FIXTURE_TEST_CASE( nodeownreg_already_registered, sysio_roa_nodeownreg_tester ) try { const auto owner = "claimacct"_n; const auto wire_pub = gen_k1_key(); const auto eth_pub = gen_em_key(); - BOOST_REQUIRE_EQUAL(success(), newnameduser(owner, wire_pub, 2)); - produce_blocks(); BOOST_REQUIRE_EQUAL(success(), nodeownreg(owner, 2, eth_pub, wire_pub)); produce_blocks(); @@ -3194,7 +3186,7 @@ BOOST_FIXTURE_TEST_CASE( nodeownreg_full_tier_has_no_registration_side_effects, const auto audit = get_nodeownerreg(owner); BOOST_REQUIRE(!audit.is_null()); BOOST_CHECK_EQUAL(audit["status"].as(), REJECTED); - BOOST_CHECK_EQUAL(audit["reason"].as(), R_TIER_CAP_REACHED); + BOOST_CHECK_EQUAL(audit["reason"].as(), owner == fresh ? R_TIER_CAP_REACHED : R_ACCOUNT_EXISTS); BOOST_CHECK(get_nodeowner(owner).is_null()); BOOST_CHECK(get_reslimit(owner).is_null()); BOOST_CHECK(get_authex_link(0).is_null()); @@ -3214,8 +3206,7 @@ BOOST_FIXTURE_TEST_CASE( nodeownreg_full_tier_preserves_rejection_precedence, sysio_roa_nodeownreg_tester ) try { namespace owners = sysio_system::test_support::nodeowners; const auto eth_pub = gen_em_key(); - BOOST_REQUIRE_EQUAL(success(), nodeownreg("alice"_n, owners::tier1, eth_pub, - get_public_key("alice"_n, "active"))); + BOOST_REQUIRE_EQUAL(success(), push_action(ROA, "forcereg"_n, mvo()("owner", "alice")("tier", owners::tier1))); produce_block(); const auto linked_key = gen_em_key(); BOOST_REQUIRE_EQUAL(success(), recordlink(linked_key, "bob"_n)); @@ -3224,8 +3215,8 @@ BOOST_FIXTURE_TEST_CASE( nodeownreg_full_tier_preserves_rejection_precedence, for (const auto& [owner, key, reason] : { std::tuple{"alice"_n, get_public_key("alice"_n, "active"), R_DUPLICATE}, - std::tuple{"carol"_n, gen_k1_key(), R_ACCOUNT_KEY_MISMATCH}, - std::tuple{"bob"_n, get_public_key("bob"_n, "active"), R_LINK_KEY_MISMATCH}, + std::tuple{"carol"_n, gen_k1_key(), R_ACCOUNT_EXISTS}, + std::tuple{"bob"_n, get_public_key("bob"_n, "active"), R_ACCOUNT_EXISTS}, std::tuple{"toolong"_n, gen_k1_key(), R_NAME_INVALID}}) { BOOST_REQUIRE_EQUAL(success(), newnameduser(owner, key, owners::tier1)); produce_block(); @@ -3241,8 +3232,8 @@ BOOST_FIXTURE_TEST_CASE( nodeownreg_full_tier_preserves_rejection_precedence, BOOST_CHECK(get_nodeowner("bob"_n).is_null()); BOOST_CHECK(get_nodeowner("carol"_n).is_null()); BOOST_CHECK((control->db().find("toolong"_n) == nullptr)); - BOOST_CHECK_EQUAL(get_authex_link(1)["pub_key"].as(), linked_key); - BOOST_CHECK(get_authex_link(2).is_null()); + BOOST_CHECK_EQUAL(get_authex_link(0)["pub_key"].as(), linked_key); + BOOST_CHECK(get_authex_link(1).is_null()); } FC_LOG_AND_RETHROW() // Invalid tier is a depot/system invariant -> hard abort (not a soft-fail). @@ -3263,8 +3254,6 @@ BOOST_FIXTURE_TEST_CASE( nodeownreg_invalid_tier, sysio_roa_nodeownreg_tester ) BOOST_FIXTURE_TEST_CASE( nodeownreg_rejects_em_key_off_the_curve, sysio_roa_nodeownreg_tester ) try { const auto owner = "claimacct"_n; const auto wire_pub = gen_k1_key(); - BOOST_REQUIRE_EQUAL(success(), newnameduser(owner, wire_pub, 2)); - produce_blocks(); // x = 0 has no point (7 is not a square mod p); the all-zero bytes have no valid prefix either. fc::em::public_key_data zero_x{}; diff --git a/docs/contract-upgrade-order.md b/docs/contract-upgrade-order.md index 64d183f9ad..a7af3c712f 100644 --- a/docs/contract-upgrade-order.md +++ b/docs/contract-upgrade-order.md @@ -383,3 +383,16 @@ Deploy this `sysio.msgch` on fresh state and regenerate the SDK contract types w written before the change has no `byqueue` entry, so `buildenv` never finds it: a queued `DESYNDICATE_LIQ` would never ship, and an inbound row would never be erased. There is no migration path. + +## WIRE-439 node-owner registration + +Deploy the matching `sysio.roa` and `sysio.msgch` WASMs together in one transaction, +or quiesce inbound dispatch until both are installed. The ABI and stored-row +layouts are unchanged, but mixed revisions are not compatible: old MSGCH creates +the account before new ROA rejects it, while new MSGCH leaves creation to ROA. +Do not process node-owner claims between separate deployments. Preserve both +contracts' privileged status and the default inline-depth limit. + +Deploy the new-account-only Hub and Tools companions with this pair. Existing +memberships and historical audit reason values remain readable; this update does +not remediate previously bound links or release escrowed NFTs. diff --git a/docs/roa-overview.md b/docs/roa-overview.md index b568332157..c1608bc75e 100644 --- a/docs/roa-overview.md +++ b/docs/roa-overview.md @@ -875,8 +875,8 @@ and are omitted. | `newuser` | tier-1 node owner | Create a sponsored sub-account | | `raiseacctlim` | tier-1 node owner | Raise its own account limit, at most by the configured multiplier | | `setacctlim` | `sysio.roa` | Set the default account limit and the raise multiplier | -| `newnameduser` | `sysio.roa` | Create a vanity account (NFT-claim flow) | -| `nodeownreg` | `sysio.roa` | Register a node owner from an OPP attestation | +| `newnameduser` | `sysio.roa` | Privileged vanity-account creation; also called internally by `nodeownreg` | +| `nodeownreg` | `sysio.roa` | Create and register a fresh node-owner account from an OPP attestation | | `forcereg` | `sysio.roa` | Register a node owner directly (governance/bootstrap) | | `activateroa` | `sysio.roa` | Activate ROA and set the initial byte price | | `setbyteprice` | `sysio.roa` | Change bytes-per-unit on network expansion | @@ -914,3 +914,19 @@ and are omitted. above — contract-as-payer billing, the failure mode of an unprovisioned contract, RAM-free token transfers, and multi-issuer policy stacking — were each verified against the code and confirmed with executable tests. The transcript blocks reproduce actual test output.* + +## NFT registration names + +Node-owner NFT claims create **new accounts only** (admin-approved WIRE-439 policy). +`sysio.msgch` dispatches `nodeownreg`, which rejects an existing name with +`ACCOUNT_EXISTS` (7), including when the claim copies its public active key. +Already registered owners retain the `DUPLICATE` outcome. Rejection records an +audit row without changing the account authority, AuthEx link or owner allocation. +A fresh accepted claim creates the requested authority, allocates the owner and +records the depositor's Ethereum link in one atomic Wire transaction. + +The Hub checks name availability before BAR commitment; this cannot reserve the +name against a competing claim. A name taken before depot execution is rejected +and the NFT remains subject to BAR's existing escrow/recovery rules. This change +adds no automatic refund or recovery action. Deploy the coupled contracts as +specified in [contract upgrade order](contract-upgrade-order.md#wire-439-node-owner-registration). diff --git a/tests/nodeownreg_test.py b/tests/nodeownreg_test.py index 56a8498833..8faf4a7ec9 100755 --- a/tests/nodeownreg_test.py +++ b/tests/nodeownreg_test.py @@ -4,10 +4,9 @@ nodeownreg_test.py — OPP Node Owner NFT Registration integration test (create-in-flow). Drives sysio.roa the way the depot (sysio.msgch) does for an inbound NodeOwnerRegistration, -pushing the two inline actions directly as sysio.roa: - 1. newnameduser(account, wire_key, tier) -- create the claim account - 2. nodeownreg(account, tier, eth_key, wire_key, eth_address) - -- register + inline-record the ETH link +pushing nodeownreg(account, tier, eth_key, wire_key, eth_address) as sysio.roa. +The claim creates the account, registers it and records its ETH link atomically. +Existing names are rejected even when the submitted key matches their active authority. Verifies the nodeowners + nodeownerreg tables, plus the soft-fail (audit row) and hard-fail paths. The bios bootstrap marks sysio.roa privileged, so its inline recordlink may declare @@ -23,7 +22,7 @@ # nodeownerreg reg_status + reject_reason values (mirror sysio.roa.hpp). CONFIRMED, REJECTED = 0, 1 -R_NAME_INVALID, R_OWNER_NOT_ACCOUNT, R_ACCOUNT_KEY_MISMATCH, R_DUPLICATE = 1, 2, 3, 4 +R_NAME_INVALID, R_DUPLICATE, R_ACCOUNT_EXISTS = 1, 4, 7 # --------------------------------------------------------------------------- @@ -120,10 +119,9 @@ def get_audit(node, owner): Utils.Print(f" eth (EM) key: {eth_key}") Utils.Print(f" wire (K1) key: {wire_key}") - # ---- Test 1: happy path -- create the account then register it ---- - Utils.Print("=== Test 1: newnameduser + nodeownreg happy path ===") + # ---- Test 1: happy path -- one claim creates and registers the account ---- + Utils.Print("=== Test 1: nodeownreg create-and-register happy path ===") owner = "claimacct1a" - assert push_newnameduser(node, owner, wire_key, 2)[0], "newnameduser failed" assert push_nodeownreg(node, owner, 2, eth_address, eth_key, wire_key)[0], "nodeownreg failed" reg = get_nodeowner(node, owner) @@ -133,20 +131,19 @@ def get_audit(node, owner): assert audit is not None and int(audit["status"]) == CONFIRMED, f"expected CONFIRMED, got {audit}" Utils.Print(f" Verified: {owner} registered tier-2, audit CONFIRMED") - # ---- Test 2: account controlled by a different key -> soft-fail ACCOUNT_KEY_MISMATCH ---- - Utils.Print("=== Test 2: nodeownreg with a non-matching wire key (soft-fail) ===") + # ---- Test 2: existing account with a matching public key -> soft-fail ACCOUNT_EXISTS ---- + Utils.Print("=== Test 2: nodeownreg with a copied matching wire key (soft-fail) ===") owner2 = "claimacct2a" real_key = wallet_create_key(walletMgr.host, walletMgr.port, "K1") - other_key = wallet_create_key(walletMgr.host, walletMgr.port, "K1") assert push_newnameduser(node, owner2, real_key, 2)[0], "newnameduser (owner2) failed" - # Claim owner2 with a different wire key than it was created with. - assert push_nodeownreg(node, owner2, 2, eth_address, eth_key, other_key)[0], \ + # Public active-key equality is not authorization. + assert push_nodeownreg(node, owner2, 2, eth_address, eth_key, real_key)[0], \ "nodeownreg should soft-fail, not abort" assert get_nodeowner(node, owner2) is None, "owner2 must not be registered" audit2 = get_audit(node, owner2) assert audit2 is not None and int(audit2["status"]) == REJECTED \ - and int(audit2["reason"]) == R_ACCOUNT_KEY_MISMATCH, f"expected ACCOUNT_KEY_MISMATCH, got {audit2}" - Utils.Print(" Correctly soft-failed ACCOUNT_KEY_MISMATCH") + and int(audit2["reason"]) == R_ACCOUNT_EXISTS, f"expected ACCOUNT_EXISTS, got {audit2}" + Utils.Print(" Correctly soft-failed ACCOUNT_EXISTS") # ---- Test 3: re-register the owner -> soft-fail DUPLICATE ---- # Use a fresh ETH key so the replay is a *distinct* transaction: a byte-identical resubmission of From d8fe120fe0b71062a1ac2734ee2c3dd3d1a21d14 Mon Sep 17 00:00:00 2001 From: joshglogau Date: Wed, 11 Nov 2026 12:27:57 -0500 Subject: [PATCH 2/9] WIRE-439: use supported authority-key access in regressions Change-Id: I93cd68c81137191c881226c09b9f085e94b357db --- contracts/tests/sysio.dispatch_tests.cpp | 2 +- contracts/tests/sysio.roa_tests.cpp | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/contracts/tests/sysio.dispatch_tests.cpp b/contracts/tests/sysio.dispatch_tests.cpp index befe9f8f93..29d0f134b0 100644 --- a/contracts/tests/sysio.dispatch_tests.cpp +++ b/contracts/tests/sysio.dispatch_tests.cpp @@ -1007,7 +1007,7 @@ BOOST_FIXTURE_TEST_CASE(dispatch_existing_node_owner_name_rejects_without_blocki BOOST_CHECK_EQUAL(get_dclaim_row("pclaims"_n, "pending_claim", fresh.to_uint64_t())["balance"].as().get_amount(), 4321); const auto& active = control->get_authorization_manager().get_permission({CLAIM_ACCOUNT, config::active_name}); BOOST_REQUIRE_EQUAL(active.auth.keys.size(), 1u); - BOOST_CHECK_EQUAL(active.auth.keys.front().key, victim_key); + BOOST_CHECK_EQUAL(active.auth.keys[0].key, victim_key); } FC_LOG_AND_RETHROW() } // Only ATTESTATION_TYPE_NODE_OWNER_REG reaches the node-owner handler. The outbound-only types diff --git a/contracts/tests/sysio.roa_tests.cpp b/contracts/tests/sysio.roa_tests.cpp index ad64f7e21a..0525bda0f7 100644 --- a/contracts/tests/sysio.roa_tests.cpp +++ b/contracts/tests/sysio.roa_tests.cpp @@ -2809,7 +2809,7 @@ BOOST_FIXTURE_TEST_CASE( nodeownreg_happy_path, sysio_roa_nodeownreg_tester ) tr BOOST_CHECK_EQUAL(get_nodeownerreg(owner)["status"].as(), CONFIRMED); const auto& active = control->get_authorization_manager().get_permission({owner, config::active_name}); BOOST_REQUIRE_EQUAL(active.auth.keys.size(), 1u); - BOOST_CHECK_EQUAL(active.auth.keys.front().key, wire_pub); + BOOST_CHECK_EQUAL(active.auth.keys[0].key, wire_pub); } } FC_LOG_AND_RETHROW() @@ -2832,7 +2832,7 @@ BOOST_FIXTURE_TEST_CASE( nodeownreg_rejects_existing_account_with_matching_key, BOOST_CHECK_EQUAL(audit["reason"].as(), R_ACCOUNT_EXISTS); const auto& active = control->get_authorization_manager().get_permission({owner, config::active_name}); BOOST_REQUIRE_EQUAL(active.auth.keys.size(), 1u); - BOOST_CHECK_EQUAL(active.auth.keys.front().key, wire_pub); + BOOST_CHECK_EQUAL(active.auth.keys[0].key, wire_pub); int64_t ram_after = 0, net_after = 0, cpu_after = 0; limits.get_account_limits(owner, ram_after, net_after, cpu_after); BOOST_CHECK_EQUAL(ram_after, ram_before); From 8d13791de031e2391f26f525460cb44e01aa39f5 Mon Sep 17 00:00:00 2001 From: joshglogau Date: Wed, 11 Nov 2026 12:38:00 -0500 Subject: [PATCH 3/9] WIRE-439: rebuild ROA and MSGCH contract artifacts Change-Id: I8d51b10b76475c365faf119cd1fc4ba345be0f20 --- contracts/sysio.msgch/sysio.msgch.wasm | Bin 161761 -> 161053 bytes contracts/sysio.roa/sysio.roa.wasm | Bin 62209 -> 58576 bytes 2 files changed, 0 insertions(+), 0 deletions(-) diff --git a/contracts/sysio.msgch/sysio.msgch.wasm b/contracts/sysio.msgch/sysio.msgch.wasm index fbf2d55f798a1324cd53d7ad39d03a6288baf610..85f86d65338c548baf69410a2bdb96534923f9de 100755 GIT binary patch delta 8914 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 delta 9267 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 H+r$4C`#WT? diff --git a/contracts/sysio.roa/sysio.roa.wasm b/contracts/sysio.roa/sysio.roa.wasm index 74a1a742da18378d80c0fa3b8e8baf55013a85d5..f0556a423015dbb27d43975c78f8b475e791aac3 100755 GIT binary patch delta 15806 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 From 9c1a7047869c0f02fb87b97e40e9555f02e4f0e6 Mon Sep 17 00:00:00 2001 From: joshglogau Date: Wed, 11 Nov 2026 12:42:40 -0500 Subject: [PATCH 4/9] WIRE-439: compare shared keys without stream formatting Change-Id: I14fae99267a55370fe846682ffa26ab4f2d9b0dc --- contracts/tests/sysio.dispatch_tests.cpp | 2 +- contracts/tests/sysio.roa_tests.cpp | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/contracts/tests/sysio.dispatch_tests.cpp b/contracts/tests/sysio.dispatch_tests.cpp index 29d0f134b0..262b924617 100644 --- a/contracts/tests/sysio.dispatch_tests.cpp +++ b/contracts/tests/sysio.dispatch_tests.cpp @@ -1007,7 +1007,7 @@ BOOST_FIXTURE_TEST_CASE(dispatch_existing_node_owner_name_rejects_without_blocki BOOST_CHECK_EQUAL(get_dclaim_row("pclaims"_n, "pending_claim", fresh.to_uint64_t())["balance"].as().get_amount(), 4321); const auto& active = control->get_authorization_manager().get_permission({CLAIM_ACCOUNT, config::active_name}); BOOST_REQUIRE_EQUAL(active.auth.keys.size(), 1u); - BOOST_CHECK_EQUAL(active.auth.keys[0].key, victim_key); + BOOST_CHECK(active.auth.keys[0].key == victim_key); } FC_LOG_AND_RETHROW() } // Only ATTESTATION_TYPE_NODE_OWNER_REG reaches the node-owner handler. The outbound-only types diff --git a/contracts/tests/sysio.roa_tests.cpp b/contracts/tests/sysio.roa_tests.cpp index 0525bda0f7..60b039cac3 100644 --- a/contracts/tests/sysio.roa_tests.cpp +++ b/contracts/tests/sysio.roa_tests.cpp @@ -2809,7 +2809,7 @@ BOOST_FIXTURE_TEST_CASE( nodeownreg_happy_path, sysio_roa_nodeownreg_tester ) tr BOOST_CHECK_EQUAL(get_nodeownerreg(owner)["status"].as(), CONFIRMED); const auto& active = control->get_authorization_manager().get_permission({owner, config::active_name}); BOOST_REQUIRE_EQUAL(active.auth.keys.size(), 1u); - BOOST_CHECK_EQUAL(active.auth.keys[0].key, wire_pub); + BOOST_CHECK(active.auth.keys[0].key == wire_pub); } } FC_LOG_AND_RETHROW() @@ -2832,7 +2832,7 @@ BOOST_FIXTURE_TEST_CASE( nodeownreg_rejects_existing_account_with_matching_key, BOOST_CHECK_EQUAL(audit["reason"].as(), R_ACCOUNT_EXISTS); const auto& active = control->get_authorization_manager().get_permission({owner, config::active_name}); BOOST_REQUIRE_EQUAL(active.auth.keys.size(), 1u); - BOOST_CHECK_EQUAL(active.auth.keys[0].key, wire_pub); + BOOST_CHECK(active.auth.keys[0].key == wire_pub); int64_t ram_after = 0, net_after = 0, cpu_after = 0; limits.get_account_limits(owner, ram_after, net_after, cpu_after); BOOST_CHECK_EQUAL(ram_after, ram_before); From 01edf6ee2de3fb38b7e2390d52a4f3ce347bfe38 Mon Sep 17 00:00:00 2001 From: joshglogau Date: Wed, 11 Nov 2026 12:48:03 -0500 Subject: [PATCH 5/9] WIRE-439: protect template comma in Boost assertion Change-Id: I3ce5eb434008a5ada8fead632516b6a92edd0e03 --- contracts/tests/sysio.roa_tests.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/tests/sysio.roa_tests.cpp b/contracts/tests/sysio.roa_tests.cpp index 60b039cac3..c2f3ed9d91 100644 --- a/contracts/tests/sysio.roa_tests.cpp +++ b/contracts/tests/sysio.roa_tests.cpp @@ -2800,7 +2800,7 @@ BOOST_FIXTURE_TEST_CASE( nodeownreg_happy_path, sysio_roa_nodeownreg_tester ) tr for (const auto& [owner, tier] : {std::pair{"freshone"_n, uint8_t{2}}, std::pair{"fresh"_n, uint8_t{1}}, std::pair{"freshthree"_n, uint8_t{3}}}) { - BOOST_REQUIRE(control->db().find(owner) == nullptr); + BOOST_REQUIRE((control->db().find(owner) == nullptr)); BOOST_REQUIRE_EQUAL(success(), nodeownreg(owner, tier, eth_pub, wire_pub)); produce_blocks(); const auto reg = get_nodeowner(owner); From c622b5fd537cddfd4f00898e52119eb318e50926 Mon Sep 17 00:00:00 2001 From: joshglogau Date: Wed, 11 Nov 2026 13:05:09 -0500 Subject: [PATCH 6/9] WIRE-439: retain existing-account fixtures for unrelated dispatch paths Change-Id: I4efd23669f99a9e1c51f6c5606fd8373f6e20e52 --- contracts/tests/sysio.dispatch_tests.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/contracts/tests/sysio.dispatch_tests.cpp b/contracts/tests/sysio.dispatch_tests.cpp index 262b924617..3003ef85a3 100644 --- a/contracts/tests/sysio.dispatch_tests.cpp +++ b/contracts/tests/sysio.dispatch_tests.cpp @@ -1346,6 +1346,7 @@ class node_owner_claim_tester : public sysio_dispatch_tester { // attacker-controlled. It must reach sysio as an ordinary notification and change nothing: not sysio's owner, active, // auth.ext or any other permission, and not a user's. BOOST_FIXTURE_TEST_CASE(claimed_auth_msg_notification_changes_no_permission, node_owner_claim_tester) { try { + create_account(CLAIM_ACCOUNT); bootstrap_for_dispatch(NODE_OWNER_SOURCE_CHAIN); create_account(PAYER_ACCOUNT); deploy_system_contract(); @@ -1361,6 +1362,7 @@ BOOST_FIXTURE_TEST_CASE(claimed_auth_msg_notification_changes_no_permission, nod const auto replacement_key = get_public_key(REPLACEMENT_KEY_NAME, "active"); const auto sysio_before = permissions_of(config::system_account_name); const auto user_before = permissions_of(CLAIM_ACCOUNT); + BOOST_REQUIRE(!user_before.empty()); for (const auto target : {config::system_account_name, CLAIM_ACCOUNT}) { for (const auto permission : {config::owner_name, config::active_name, AUTH_EXT_PERMISSION, OTHER_PERMISSION}) { BOOST_CHECK_MESSAGE(notify_onlinkauth(target, permission, replacement_key), @@ -2397,6 +2399,7 @@ BOOST_AUTO_TEST_CASE(generic_external_simulator_drives_production_dispatch) { // Funded pre-launch imports link and settle the exact token balance. BOOST_FIXTURE_TEST_CASE(funded_import_claims_link_and_settle, sysio_dispatch_tester) { using namespace sysio::testing::external; + create_account(CLAIM_ACCOUNT); bootstrap_for_dispatch(First.chain, First.kind); deploy(TOKEN_ACCOUNT, contracts::token_wasm(), contracts::token_abi(), token_abi); BOOST_REQUIRE_EQUAL(success(), push(TOKEN_ACCOUNT, token_abi, TOKEN_ACCOUNT, "create"_n, From ad43ac7ab13d4d9b8ce68b886715be2cce023f1f Mon Sep 17 00:00:00 2001 From: joshglogau Date: Wed, 11 Nov 2026 13:07:30 -0500 Subject: [PATCH 7/9] WIRE-439: account for bootstrap AuthX linkage in dispatch regression Change-Id: Id3107ab0b98010394e17fc4c10c2142be9c37963 --- contracts/tests/sysio.dispatch_tests.cpp | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/contracts/tests/sysio.dispatch_tests.cpp b/contracts/tests/sysio.dispatch_tests.cpp index 3003ef85a3..9e4c213965 100644 --- a/contracts/tests/sysio.dispatch_tests.cpp +++ b/contracts/tests/sysio.dispatch_tests.cpp @@ -977,6 +977,9 @@ BOOST_FIXTURE_TEST_CASE(dispatch_existing_node_owner_name_rejects_without_blocki create_accounts({CLAIM_ACCOUNT}); constexpr auto fresh = "freshclaim"_n; const auto victim_key = get_public_key(CLAIM_ACCOUNT, "active"); + const auto operator_link = get_row_by_id(AUTHEX_ACCOUNT, AUTHEX_ACCOUNT, "links"_n, 0); + BOOST_REQUIRE(!operator_link.empty()); + BOOST_REQUIRE(get_row_by_id(AUTHEX_ACCOUNT, AUTHEX_ACCOUNT, "links"_n, 1).empty()); const auto eth_pub = fc::crypto::private_key::generate(fc::crypto::private_key::key_type::em).get_public_key(); const auto eth_address = fc::crypto::ethereum::address_to_bytes(eth_pub); const std::vector native_address(eth_address.begin(), eth_address.end()); @@ -998,10 +1001,12 @@ BOOST_FIXTURE_TEST_CASE(dispatch_existing_node_owner_name_rejects_without_blocki BOOST_CHECK(get_nodeowner(CLAIM_ACCOUNT).is_null()); BOOST_CHECK(get_dclaim_row("pclaims"_n, "pending_claim", CLAIM_ACCOUNT.to_uint64_t()).is_null()); // Only the fresh account's link may consume the depositor's imported credit. - const auto linked = get_row_by_id(AUTHEX_ACCOUNT, AUTHEX_ACCOUNT, "links"_n, 0); + // Bootstrap already linked the underwriter; preserve it and add only the fresh owner's link. + BOOST_CHECK(get_row_by_id(AUTHEX_ACCOUNT, AUTHEX_ACCOUNT, "links"_n, 0) == operator_link); + const auto linked = get_row_by_id(AUTHEX_ACCOUNT, AUTHEX_ACCOUNT, "links"_n, 1); BOOST_REQUIRE(!linked.empty()); BOOST_CHECK_EQUAL(authex_abi.binary_to_variant("links_s", linked, fixture_abi_yield_function())["username"].as(), fresh); - BOOST_CHECK(get_row_by_id(AUTHEX_ACCOUNT, AUTHEX_ACCOUNT, "links"_n, 1).empty()); + BOOST_CHECK(get_row_by_id(AUTHEX_ACCOUNT, AUTHEX_ACCOUNT, "links"_n, 2).empty()); BOOST_REQUIRE(!get_nodeowner(fresh).is_null()); BOOST_CHECK_EQUAL(get_nodeownerreg(fresh)["status"].as(), audit::status_confirmed); BOOST_CHECK_EQUAL(get_dclaim_row("pclaims"_n, "pending_claim", fresh.to_uint64_t())["balance"].as().get_amount(), 4321); From c0a27c981bd4c5ab5b3b24fbc40e89139c1f115c Mon Sep 17 00:00:00 2001 From: joshglogau Date: Wed, 11 Nov 2026 14:03:23 -0500 Subject: [PATCH 8/9] WIRE-439: align ROA comments with atomic registration Change-Id: Icf262a04b0c508e3761f8cadfd9d77efbff3db5d --- contracts/sysio.roa/sysio.roa.cpp | 2 +- contracts/sysio.roa/sysio.roa.hpp | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/contracts/sysio.roa/sysio.roa.cpp b/contracts/sysio.roa/sysio.roa.cpp index fb411bc9fc..991043c0f2 100644 --- a/contracts/sysio.roa/sysio.roa.cpp +++ b/contracts/sysio.roa/sysio.roa.cpp @@ -974,7 +974,7 @@ namespace sysio { // aborting, and never double-counting the gift. Then sync the on-chain quota to the resulting // row totals so the reslimit row and the resource limits stay equal on BOTH the fresh and // reconcile paths. - // Conservation: production (newnameduser -> nodeownreg) funds newaccount_ram at creation, so + // Conservation: nodeownreg's newnameduser call funds newaccount_ram before forcereg, so // the create path stays conservation-exact; the reconcile path adds only the node-owner delta. // forcereg over a pre-existing row now reconciles too (previously threw in set_reslimit) -- // acceptable for the governance/bootstrap path. diff --git a/contracts/sysio.roa/sysio.roa.hpp b/contracts/sysio.roa/sysio.roa.hpp index 83e8b708fd..b2068e90db 100644 --- a/contracts/sysio.roa/sysio.roa.hpp +++ b/contracts/sysio.roa/sysio.roa.hpp @@ -123,11 +123,11 @@ namespace sysio { * unnecessary: the C++ unit-test base tester (registers `nodedaddy` as the default policy * issuer for the suite), emissions tests, the TestHarness bios bootstrap, and dev clusters. * - * Production node-owner registration goes through `nodeownreg` instead -- dispatched by + * Production node-owner registration enters through `nodeownreg` -- dispatched by * the OPP depot (sysio.msgch) on an inbound NodeOwnerRegistration attestation, or * governance-signed during bootstrap. Beyond the tier-range check, `forcereg` performs - * none of `nodeownreg`'s claim-payload validation (no tier name-rule check, no active-key - * match, no sysio.authex link) and writes no `nodeownerreg` audit row -- it simply + * none of `nodeownreg`'s claim-payload validation (no tier name-rule or new-account + * check, no sysio.authex link) and writes no `nodeownerreg` audit row -- it simply * allocates the tier's SYS via the shared `regnodeowner` helper. * * @param owner The account to register as a node owner. @@ -649,7 +649,7 @@ namespace sysio { * one-time `newaccount_ram` gift is folded into its ram_bytes; when present, only the passed * deltas are added (so the gift is never double-counted). Accumulation saturates rather than * throwing on overflow (see safe_ops): this helper sits on the OPP never-throw dispatch path - * (nodeownreg -> regnodeowner, newnameduser). + * (nodeownreg -> forcereg -> regnodeowner, and newnameduser). * * @param require_to_exist assert the row already exists (no create); used by callers that * must only ever top up a known account (e.g. sysio.acct funding). From c60ff1571f98e77a1e4f1059092d67788ef2beb5 Mon Sep 17 00:00:00 2001 From: joshglogau Date: Wed, 11 Nov 2026 15:22:59 -0500 Subject: [PATCH 9/9] WIRE-439: isolate claim allocation and address review feedback Change-Id: Ib5852de26333e4006481538423cdfd75cdef60f6 --- contracts/sysio.roa/sysio.roa.abi | 19 +++++++++++ contracts/sysio.roa/sysio.roa.cpp | 32 ++++++++++-------- contracts/sysio.roa/sysio.roa.hpp | 31 ++++++++++------- contracts/sysio.roa/sysio.roa.wasm | Bin 58576 -> 59012 bytes contracts/tests/contract_test_support.hpp | 2 +- contracts/tests/sysio.dispatch_tests.cpp | 11 +++++- contracts/tests/sysio.roa_tests.cpp | 22 +++++++++--- docs/contract-upgrade-order.md | 10 ++++-- docs/roa-overview.md | 6 +++- unittests/intrinsic_probe_tests.cpp | 2 +- .../intrinsic_probe/intrinsic_probe.cpp | 10 +++--- 11 files changed, 103 insertions(+), 42 deletions(-) diff --git a/contracts/sysio.roa/sysio.roa.abi b/contracts/sysio.roa/sysio.roa.abi index 578068bd26..01ddb61d17 100644 --- a/contracts/sysio.roa/sysio.roa.abi +++ b/contracts/sysio.roa/sysio.roa.abi @@ -253,6 +253,20 @@ } ] }, + { + "name": "nodeownfin", + "base": "", + "fields": [ + { + "name": "owner", + "type": "name" + }, + { + "name": "tier", + "type": "uint8" + } + ] + }, { "name": "nodeownreg", "base": "", @@ -575,6 +589,11 @@ "type": "newuser", "ricardian_contract": "" }, + { + "name": "nodeownfin", + "type": "nodeownfin", + "ricardian_contract": "" + }, { "name": "nodeownreg", "type": "nodeownreg", diff --git a/contracts/sysio.roa/sysio.roa.cpp b/contracts/sysio.roa/sysio.roa.cpp index 991043c0f2..ed470b3dff 100644 --- a/contracts/sysio.roa/sysio.roa.cpp +++ b/contracts/sysio.roa/sysio.roa.cpp @@ -18,8 +18,8 @@ namespace sysio { // literals -- a contract rename is one change here, not scattered across call sites). constexpr name AUTHEX_ACCOUNT = "sysio.authex"_n; constexpr name AUTHEX_RECORDLINK = "recordlink"_n; - /// Existing privileged allocator, queued after the claim creates its account. - constexpr name ROA_FORCEREG = "forcereg"_n; + /// Claim allocation continuation, queued after native account creation. + constexpr name ROA_NODEOWNFIN = "nodeownfin"_n; /// Names under this prefix belong to system accounts. The chain refuses them only to non-privileged /// creators, and sysio.roa is privileged, so node-owner claims must refuse them here. @@ -752,8 +752,8 @@ namespace sysio { void roa::forcereg(const name& owner, const uint8_t& tier) { // Privileged bootstrap/test registration -- see the header doc. Governance/system-only - // (require_auth self); bypasses the OPP claim path (nodeownreg). Beyond the tier-range guard - // it does no name-rule / active-key / authex validation and writes no nodeownerreg audit row + // (require_auth self); separate from the OPP claim path. Beyond the tier-range guard + // it does no name-rule / new-account / authex validation and writes no nodeownerreg audit row // -- it just runs the shared tier allocator. require_auth(get_self()); @@ -766,7 +766,7 @@ namespace sysio { const public_key& wire_pub_key, const bytes& eth_address) { // One authenticated depot claim owns creation and registration. Existing accounts are - // never eligible, even when the payload copies their public active key (WIRE-439). + // never eligible, even when the payload copies their public active key. require_auth(get_self()); // ---- Envelope / system invariants (depot misuse) ---- @@ -830,11 +830,10 @@ namespace sysio { return; } - // Queue native newaccount before the privileged allocator. Call the helper directly to - // preserve inline-depth headroom for recordlink's DClaim sweep. All effects, including the + // Allocation must run after the queued native newaccount. All effects, including the // terminal audit below, commit together or roll back if an inline action fails. newnameduser(owner, wire_pub_key, tier); - action(permission_level{get_self(), "active"_n}, get_self(), ROA_FORCEREG, + action(permission_level{get_self(), "active"_n}, get_self(), ROA_NODEOWNFIN, std::make_tuple(owner, tier)).send(); // Record the depositor's ETH key as a sysio.authex link via the trusted depot-only path. @@ -849,6 +848,13 @@ namespace sysio { record_nodereg(owner, tier, CONFIRMED, NONE, gen); }; + void roa::nodeownfin(const name& owner, const uint8_t& tier) { + require_auth(get_self()); + check(get_sender() == get_self(), "nodeownfin must be sent inline by sysio.roa"); + check(is_account(owner), "claim account was not created"); + regnodeowner(owner, tier); + } + // ---- Private Helper Function ---- void roa::record_nodereg(const name& owner, uint8_t tier, uint8_t status, uint8_t reason, @@ -974,7 +980,7 @@ namespace sysio { // aborting, and never double-counting the gift. Then sync the on-chain quota to the resulting // row totals so the reslimit row and the resource limits stay equal on BOTH the fresh and // reconcile paths. - // Conservation: nodeownreg's newnameduser call funds newaccount_ram before forcereg, so + // Conservation: nodeownreg's newnameduser call funds newaccount_ram before nodeownfin, so // the create path stays conservation-exact; the reconcile path adds only the node-owner delta. // forcereg over a pre-existing row now reconciles too (previously threw in set_reslimit) -- // acceptable for the governance/bootstrap path. @@ -1236,13 +1242,11 @@ namespace sysio { // Tier is a depot/system invariant -- a bad value means the dispatch is malformed, so abort // (the name charset [a-z1-5.] is already enforced by the name type). check(tier >= 1 && tier <= 3, "Tier level must be between 1 and 3"); - // The chosen name is claim-payload: if it violates the tier's length rule, skip creation - // (non-throwing) and let nodeownreg soft-fail with NAME_INVALID. Without this guard a bad - // name would either abort the depot dispatch or create an account the claim then rejects. + // Standalone creation silently skips invalid names. nodeownreg validates and records + // claim rejections before calling this helper. if (!valid_name_for_tier(account, tier)) return; - // A full tier is another soft rejection. Leave the account and sysio's RAM pool untouched; - // nodeownreg records TIER_CAP_REACHED even though no account was created. + // Standalone creation also skips full tiers without spending sysio's RAM. if (nodeowner_count(get_self(), state.network_gen, tier) >= nodeowner_cap(tier)) return; // Create the account with the holder's key as both owner and active. diff --git a/contracts/sysio.roa/sysio.roa.hpp b/contracts/sysio.roa/sysio.roa.hpp index b2068e90db..0f5958e36a 100644 --- a/contracts/sysio.roa/sysio.roa.hpp +++ b/contracts/sysio.roa/sysio.roa.hpp @@ -115,18 +115,16 @@ namespace sysio { /** - * @brief Directly register `owner` as a node owner at `tier`, bypassing the OPP claim - * flow. Also used as nodeownreg's internal allocation step after native account creation. + * @brief Directly register an existing account for governance/bootstrap and tests. * * Requires `sysio.roa` active authority, so it is governance/system-only and never * user-callable. It exists to seed node owners where the full OPP machinery is absent or * unnecessary: the C++ unit-test base tester (registers `nodedaddy` as the default policy * issuer for the suite), emissions tests, the TestHarness bios bootstrap, and dev clusters. * - * Production node-owner registration enters through `nodeownreg` -- dispatched by - * the OPP depot (sysio.msgch) on an inbound NodeOwnerRegistration attestation, or - * governance-signed during bootstrap. Beyond the tier-range check, `forcereg` performs - * none of `nodeownreg`'s claim-payload validation (no tier name-rule or new-account + * Production NFT registration enters through `nodeownreg`, dispatched by sysio.msgch, + * and allocates through the self-inline `nodeownfin` action. Beyond the tier-range check, + * `forcereg` performs none of `nodeownreg`'s claim-payload validation (no tier name-rule or new-account * check, no sysio.authex link) and writes no `nodeownerreg` audit row -- it simply * allocates the tier's SYS via the shared `regnodeowner` helper. * @@ -141,7 +139,7 @@ namespace sysio { * * Requires sysio.roa authority. Existing accounts are rejected even if wire_pub_key * matches a public active key. Valid claims queue native account creation, tier allocation - * through forcereg, and the depositor's AuthEx link in one atomic transaction. + * through nodeownfin, and the depositor's AuthEx link in one atomic transaction. * Claim-payload errors record a terminal rejection without aborting depot dispatch. * Tier range, depositor key/address validity and ROA activation remain hard invariants. * @@ -156,6 +154,15 @@ namespace sysio { const public_key& wire_pub_key, const bytes& eth_address); + /** + * @brief Allocate a validated claim after its queued native account creation. + * Requires sysio.roa authority and an inline sender of sysio.roa; top-level calls fail. + * @param owner Newly created claim account. + * @param tier Node-owner tier already validated by nodeownreg. + */ + [[sysio::action]] + void nodeownfin(const name& owner, const uint8_t& tier); + /** * @brief Creates a new user account on the network and records the sponsor mapping. * @@ -233,8 +240,9 @@ namespace sysio { * Standalone creation is privileged and idempotent. An account created separately is * not eligible for a later NFT claim. * Tier-based name rules: tier-1 = 2-6 char prefix; tier 2/3 = up to 12 chars. - * Invalid names and full tiers skip creation without spending RAM; nodeownreg records - * the rejection. Capacity comes from the live generation's authoritative owner rows. + * Standalone invalid names and full tiers skip creation without an audit or RAM spend. + * nodeownreg records those rejections before calling this helper. Capacity comes from + * the live generation's authoritative owner rows. * * @param account The user-chosen account name. * @param pubkey The holder's K1 public key (becomes owner and active). @@ -592,9 +600,6 @@ namespace sysio { */ static bool valid_name_for_tier(const name& account, uint8_t tier); - - - /** * @brief A node owner's SYS budget for tier 1, 2, or 3: the tier allocation, net of sysio's * carve-out for tier 1. Matches the rounding activateroa uses to size the reserve. @@ -649,7 +654,7 @@ namespace sysio { * one-time `newaccount_ram` gift is folded into its ram_bytes; when present, only the passed * deltas are added (so the gift is never double-counted). Accumulation saturates rather than * throwing on overflow (see safe_ops): this helper sits on the OPP never-throw dispatch path - * (nodeownreg -> forcereg -> regnodeowner, and newnameduser). + * (nodeownreg -> nodeownfin -> regnodeowner, and newnameduser). * * @param require_to_exist assert the row already exists (no create); used by callers that * must only ever top up a known account (e.g. sysio.acct funding). diff --git a/contracts/sysio.roa/sysio.roa.wasm b/contracts/sysio.roa/sysio.roa.wasm index f0556a423015dbb27d43975c78f8b475e791aac3..896599dd05717e758cc0cbb05170ae838960a88b 100755 GIT binary patch delta 11752 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 delta 10966 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 S(=G4vh)Hh&5!U<;p diff --git a/contracts/tests/contract_test_support.hpp b/contracts/tests/contract_test_support.hpp index 07dd36774d..80182afe7c 100644 --- a/contracts/tests/contract_test_support.hpp +++ b/contracts/tests/contract_test_support.hpp @@ -213,7 +213,7 @@ inline constexpr uint64_t reason_account_key_mismatch = 3; inline constexpr uint64_t reason_duplicate = 4; inline constexpr uint64_t reason_link_key_mismatch = 5; inline constexpr uint64_t reason_tier_cap_reached = 6; -inline constexpr uint8_t reason_account_exists = 7; +inline constexpr uint64_t reason_account_exists = 7; } // namespace nodeownerreg namespace nodeowners { diff --git a/contracts/tests/sysio.dispatch_tests.cpp b/contracts/tests/sysio.dispatch_tests.cpp index 9e4c213965..c3ed52198a 100644 --- a/contracts/tests/sysio.dispatch_tests.cpp +++ b/contracts/tests/sysio.dispatch_tests.cpp @@ -954,7 +954,16 @@ BOOST_FIXTURE_TEST_CASE(dispatch_routes_node_owner_reg_to_roa, sysio_dispatch_te auto envelope = encode_envelope_with_one_attestation( current_epoch(), sysio::opp::types::ATTESTATION_TYPE_NODE_OWNER_REG, payload); - BOOST_REQUIRE_EQUAL(success(), deliver(/*chain_code=*/eth_code, envelope)); + const auto trace = deliver_trace(eth_code, envelope); + BOOST_REQUIRE(trace != nullptr); + BOOST_REQUIRE(!trace->except); + const auto allocation = std::find_if(trace->action_traces.begin(), trace->action_traces.end(), + [](const auto& at) { return at.receiver == ROA_ACCOUNT && at.act.name == "nodeownfin"_n; }); + BOOST_REQUIRE(allocation != trace->action_traces.end()); + BOOST_CHECK(std::none_of(trace->action_traces.begin(), trace->action_traces.end(), + [](const auto& at) { + return at.receiver == ROA_ACCOUNT && (at.act.name == "forcereg"_n || at.act.name == "newnameduser"_n); + })); // Registered at the claimed tier, audited CONFIRMED. auto reg = get_nodeowner(CLAIM_ACCOUNT); diff --git a/contracts/tests/sysio.roa_tests.cpp b/contracts/tests/sysio.roa_tests.cpp index c2f3ed9d91..6fe5eb5cac 100644 --- a/contracts/tests/sysio.roa_tests.cpp +++ b/contracts/tests/sysio.roa_tests.cpp @@ -2813,6 +2813,17 @@ BOOST_FIXTURE_TEST_CASE( nodeownreg_happy_path, sysio_roa_nodeownreg_tester ) tr } } FC_LOG_AND_RETHROW() +/// ROA authority alone cannot invoke the claim-only allocation continuation. +BOOST_FIXTURE_TEST_CASE( nodeownfin_rejects_top_level_call, sysio_roa_nodeownreg_tester ) try { + constexpr auto owner = "alice"_n; + BOOST_REQUIRE((control->db().find(owner) != nullptr)); + BOOST_REQUIRE_EQUAL( + error("assertion failure with message: nodeownfin must be sent inline by sysio.roa"), + push_action(ROA, "nodeownfin"_n, mvo()("owner", owner)("tier", 2))); + BOOST_CHECK(get_nodeowner(owner).is_null()); + BOOST_CHECK(get_nodeownerreg(owner).is_null()); +} FC_LOG_AND_RETHROW() + /// Public-key equality never authorizes registration into an existing account. BOOST_FIXTURE_TEST_CASE( nodeownreg_rejects_existing_account_with_matching_key, sysio_roa_nodeownreg_tester ) try { const auto owner = "claimacct"_n; @@ -2882,7 +2893,7 @@ BOOST_FIXTURE_TEST_CASE( nodeownreg_rejects_existing_account_with_different_key, } FC_LOG_AND_RETHROW() // SEC-087 resource reconciliation remains supported through privileged forcereg. -// Public NFT claims reject existing accounts under WIRE-439. +// Public NFT claims reject existing accounts. BOOST_FIXTURE_TEST_CASE( forcereg_reconciles_existing_reslimit, sysio_roa_nodeownreg_tester ) try { const auto owner = "claimacct"_n; const auto wire_pub = gen_k1_key(); @@ -3209,14 +3220,16 @@ BOOST_FIXTURE_TEST_CASE( nodeownreg_full_tier_preserves_rejection_precedence, BOOST_REQUIRE_EQUAL(success(), push_action(ROA, "forcereg"_n, mvo()("owner", "alice")("tier", owners::tier1))); produce_block(); const auto linked_key = gen_em_key(); - BOOST_REQUIRE_EQUAL(success(), recordlink(linked_key, "bob"_n)); + constexpr auto linked_name = "linked"_n; + BOOST_REQUIRE((control->db().find(linked_name) == nullptr)); + BOOST_REQUIRE_EQUAL(success(), recordlink(linked_key, linked_name)); produce_block(); owners::fill_tier1(*this, abi_ser); for (const auto& [owner, key, reason] : { std::tuple{"alice"_n, get_public_key("alice"_n, "active"), R_DUPLICATE}, std::tuple{"carol"_n, gen_k1_key(), R_ACCOUNT_EXISTS}, - std::tuple{"bob"_n, get_public_key("bob"_n, "active"), R_ACCOUNT_EXISTS}, + std::tuple{linked_name, gen_k1_key(), R_LINK_KEY_MISMATCH}, std::tuple{"toolong"_n, gen_k1_key(), R_NAME_INVALID}}) { BOOST_REQUIRE_EQUAL(success(), newnameduser(owner, key, owners::tier1)); produce_block(); @@ -3229,7 +3242,8 @@ BOOST_FIXTURE_TEST_CASE( nodeownreg_full_tier_preserves_rejection_precedence, BOOST_CHECK_EQUAL(owners::count(*this, abi_ser, owners::tier1), owners::tier1_cap); } BOOST_CHECK(!get_nodeowner("alice"_n).is_null()); - BOOST_CHECK(get_nodeowner("bob"_n).is_null()); + BOOST_CHECK(get_nodeowner(linked_name).is_null()); + BOOST_CHECK((control->db().find(linked_name) == nullptr)); BOOST_CHECK(get_nodeowner("carol"_n).is_null()); BOOST_CHECK((control->db().find("toolong"_n) == nullptr)); BOOST_CHECK_EQUAL(get_authex_link(0)["pub_key"].as(), linked_key); diff --git a/docs/contract-upgrade-order.md b/docs/contract-upgrade-order.md index a7af3c712f..067ba1a5df 100644 --- a/docs/contract-upgrade-order.md +++ b/docs/contract-upgrade-order.md @@ -387,12 +387,18 @@ path. ## WIRE-439 node-owner registration Deploy the matching `sysio.roa` and `sysio.msgch` WASMs together in one transaction, -or quiesce inbound dispatch until both are installed. The ABI and stored-row -layouts are unchanged, but mixed revisions are not compatible: old MSGCH creates +or quiesce inbound dispatch until both are installed. ROA's ABI adds the self-inline +`nodeownfin(owner, tier)` allocation action; stored-row layouts are unchanged. +Mixed revisions are not compatible: old MSGCH creates the account before new ROA rejects it, while new MSGCH leaves creation to ROA. Do not process node-owner claims between separate deployments. Preserve both contracts' privileged status and the default inline-depth limit. +Claim traces contain `nodeownreg`, native `newaccount`, `nodeownfin` and +`recordlink`; `newnameduser` is called internally and emits no separate action trace. +Index registration outcomes from `nodeowners`/`nodeownerreg`, not the presence +of a `newnameduser` trace. The external `nodeownreg` payload is unchanged. + Deploy the new-account-only Hub and Tools companions with this pair. Existing memberships and historical audit reason values remain readable; this update does not remediate previously bound links or release escrowed NFTs. diff --git a/docs/roa-overview.md b/docs/roa-overview.md index c1608bc75e..b9148b376a 100644 --- a/docs/roa-overview.md +++ b/docs/roa-overview.md @@ -877,6 +877,7 @@ and are omitted. | `setacctlim` | `sysio.roa` | Set the default account limit and the raise multiplier | | `newnameduser` | `sysio.roa` | Privileged vanity-account creation; also called internally by `nodeownreg` | | `nodeownreg` | `sysio.roa` | Create and register a fresh node-owner account from an OPP attestation | +| `nodeownfin` | `sysio.roa`, self-inline only | Allocate a validated claim after native account creation | | `forcereg` | `sysio.roa` | Register a node owner directly (governance/bootstrap) | | `activateroa` | `sysio.roa` | Activate ROA and set the initial byte price | | `setbyteprice` | `sysio.roa` | Change bytes-per-unit on network expansion | @@ -917,13 +918,16 @@ with executable tests. The transcript blocks reproduce actual test output.* ## NFT registration names -Node-owner NFT claims create **new accounts only** (admin-approved WIRE-439 policy). +Node-owner NFT claims create **new accounts only**. `sysio.msgch` dispatches `nodeownreg`, which rejects an existing name with `ACCOUNT_EXISTS` (7), including when the claim copies its public active key. Already registered owners retain the `DUPLICATE` outcome. Rejection records an audit row without changing the account authority, AuthEx link or owner allocation. A fresh accepted claim creates the requested authority, allocates the owner and records the depositor's Ethereum link in one atomic Wire transaction. +Allocation uses the self-inline `nodeownfin` continuation; `forcereg` remains +the separate privileged governance/bootstrap path. AuthEx `recordlink` remains +a sibling action of `nodeownfin`, so its DClaim sweep stays within the inline-depth limit. The Hub checks name availability before BAR commitment; this cannot reserve the name against a competing claim. A name taken before depot execution is rejected diff --git a/unittests/intrinsic_probe_tests.cpp b/unittests/intrinsic_probe_tests.cpp index 0b731cedb5..27157ba0c7 100644 --- a/unittests/intrinsic_probe_tests.cpp +++ b/unittests/intrinsic_probe_tests.cpp @@ -546,7 +546,7 @@ BOOST_FIXTURE_TEST_CASE(set_action_return_value_empty, intrinsic_probe_fixture) // I. get_permission_lower_bound (P2) // // Wire-specific intrinsic (libraries/chain/webassembly/permission.cpp) backing CDT's sysio::get_permission -- -// the only host path a contract has to read another account's authority on-chain (sysio.roa::active_key_matches). +// the host path a contract uses to read another account's authority on-chain. // Each probe queries get_self()'s owner/active permissions (created with a single K1 key at // threshold 1) and self-verifies the record via in-contract check(); the driver asserts the host stays silent, // so any layout / size-query / sentinel / lower-bound-semantics regression surfaces as a diff --git a/unittests/test-contracts/intrinsic_probe/intrinsic_probe.cpp b/unittests/test-contracts/intrinsic_probe/intrinsic_probe.cpp index 0012504cfb..bca00b3dd3 100644 --- a/unittests/test-contracts/intrinsic_probe/intrinsic_probe.cpp +++ b/unittests/test-contracts/intrinsic_probe/intrinsic_probe.cpp @@ -88,8 +88,8 @@ int32_t check_transaction_authorization( const char* trx_data, uint32_t trx_len const char* perms_data, uint32_t perms_len ); // get_permission_lower_bound: aligned_span out-param (buffer). Wire-specific intrinsic -// (libraries/chain/webassembly/permission.cpp) backing CDT's sysio::get_permission wrapper, used on-chain by -// sysio.roa::active_key_matches to read an account's authority. by_owner lower_bound on +// (libraries/chain/webassembly/permission.cpp) backing CDT's sysio::get_permission wrapper +// for reading an account's authority. by_owner lower_bound on // (account, permission); returns -1 when no permission for `account` exists at/after the key, else the full // serialized record size (which may exceed buffer_size -- the size-query-then-fetch contract). The serialized // payload is pack(perm_name) ++ pack(parent_name) ++ pack(last_updated) ++ pack(authority). @@ -324,8 +324,8 @@ constexpr uint64_t U64_MAX = 0xFFFFFFFFFFFFFFFFULL; // ----------------------------------------------------------------------------- // get_permission_lower_bound record layout. A byte-for-byte mirror of CDT's // sysio::permission_record / perm_authority (libraries/sysiolib/contracts/sysio/permission.hpp): the exact -// shape on-chain consumers (sysio.roa::active_key_matches) unpack from this intrinsic's output. -// Defined locally so the probe verifies the raw host serialization against the real consumer contract without +// shape the CDT wrapper unpacks from this intrinsic's output. +// Defined locally so the probe verifies the raw host serialization against the CDT layout without // routing the CALL through the CDT wrapper. Note the host also packs a trailing `waits` vector that the // consumer struct intentionally omits -- unpack stops after `accounts` and tolerates the trailing bytes. // ----------------------------------------------------------------------------- @@ -1355,7 +1355,7 @@ class [[sysio::contract("intrinsic_probe")]] intrinsic_probe : public contract { // P2 -- get_permission_lower_bound // // Wire-specific intrinsic backing CDT's sysio::get_permission -- the only host path a contract has to read - // another account's authority on-chain (sysio.roa::active_key_matches). These probes pin the + // another account's authority on-chain. These probes pin the // pieces the CDT wrapper and its callers depend on: the serialized record layout, the size-query / // small-buffer contract, the not-found -1 sentinel, and the lower-bound (NOT exact-match) lookup semantics. // All queries target get_self(), whose owner/active permissions the Boost driver creates with a single K1