diff --git a/plugins/batch_operator_plugin/README.md b/plugins/batch_operator_plugin/README.md index a95120e1d0..b83a214fb2 100644 --- a/plugins/batch_operator_plugin/README.md +++ b/plugins/batch_operator_plugin/README.md @@ -76,6 +76,8 @@ permission then need not be linked for `sysio.kicker::kick`. | `--batch-underwriter-max-exposure` | none | Most the underwriter may have bonded at once in one shadow token, as an asset (`100.000000000 LIQETH`). Repeat per token; a token without one is not underwritten | | `--batch-kick-crank` | true | Push `sysio.kicker::kick` for every kicker pool that could pay. Off, the relay's permission need not authorize `kick` | | `--batch-kick-interval-ms` | 60000 | First wait before this operator retries a `sysio.kicker::kick` that did not pay (ms); doubles per unpaid attempt, up to an hour. Must be 1 to 3600000; startup fails otherwise | +| `--batch-operator-explicit-payer` | false | The relay's account pays for its pushes instead of each action's contract (see [Who pays](#who-pays)) | +| `--batch-underwriter-explicit-payer` | false | The underwriter's account pays for its pushes instead of each action's contract (see [Who pays](#who-pays)) | There is no separate enable flag: the relay runs when `--batch-operator-account` is configured, the way `producer_plugin` keys off `--producer-name`, and the @@ -103,6 +105,20 @@ still satisfies its authorization; after `updateauth` rotates it, the role stops pushing (the underwriter) or logs (the relay) until the node is restarted with the new key. +### Who pays + +By default each pushed action's contract pays for its CPU and NET. With +`--batch-operator-explicit-payer` (the relay) or `--batch-underwriter-explicit-payer` +(the underwriter), the role's own account pays instead: each action declares +`{account, sysio.payer}` ahead of the role's permission, and the role's one signature +covers both. The options are per role because the roles may run as different accounts. + +An account that pays needs CPU and NET of its own, from an ROA policy; a role whose +account has none stops the node at startup. Size the policy for a day of pushes. The +account pays for each action's whole execution, inline actions included (a `chkcons` +that advances the epoch runs `sysio.epoch::advance`), and other nodes bill its failed +pushes against the same limits. + ### Outpost wiring Nothing about an outpost is declared per node. @@ -171,7 +187,8 @@ those lines is left to log tooling: ### Underwriter setup On the depot, the underwriter account holds the shadow LIQ it bonds, RAM for its -bond rows, and CPU for a few pushes per poll. With a dedicated permission, link it to +bond rows, and, with `--batch-underwriter-explicit-payer`, CPU and NET for a few pushes +per poll. With a dedicated permission, link it to exactly the actions it pushes, so the node's key can move the inventory nowhere else: `sysio.bond::accept`, `approve`, `claim`, `prune` and `sysio.synd::crank`, `pruneenv`. The WIRE its bonds earn is withdrawn with `sysio.bond::claimwire`, which diff --git a/plugins/batch_operator_plugin/src/batch_operator_plugin.cpp b/plugins/batch_operator_plugin/src/batch_operator_plugin.cpp index 8e6d9a61d1..5652afb039 100644 --- a/plugins/batch_operator_plugin/src/batch_operator_plugin.cpp +++ b/plugins/batch_operator_plugin/src/batch_operator_plugin.cpp @@ -71,6 +71,9 @@ namespace { /// `--batch-kick-crank` default: on, like every other depot crank, which /// runs whenever its contract is deployed. constexpr bool KICK_CRANK_ENABLED = true; + /// `--batch-operator-explicit-payer` / `--batch-underwriter-explicit-payer` default: off, so each pushed action's + /// contract pays for it. + constexpr bool EXPLICIT_PAYER_ENABLED = false; /// Minimum private cron-service thread count even when 0 outposts are /// discovered at startup — keeps `epoch_tick` viable so a cold-sync node @@ -256,11 +259,12 @@ namespace { fc::slug_name chain_code{}; ///< the outpost whose token it shadows }; - /// The authorization a role declares on its depot actions and the signature provider whose key alone satisfies - /// it, resolved once the node is synced (`impl::resolve_signer`). + /// The authorization a role declares on its depot actions, the signature provider whose key alone satisfies it, + /// and who pays for the actions, resolved once the node is synced (`impl::resolve_signer`). struct signer { - chain::permission_level auth; - fc::crypto::signature_provider_ptr provider; + chain::permission_level auth; + fc::crypto::signature_provider_ptr provider; + batch_operator_detail::action_payer payer = batch_operator_detail::action_payer::contract; }; /// The descriptor of `chain_code` in `outposts`, or nullptr when it is not there. @@ -280,6 +284,14 @@ namespace { return meta != nullptr && meta->code_hash != chain::digest_type(); } + /// Whether `account` exists and has CPU and NET of its own to pay with. Call from a read window. + bool can_pay_for_itself(const chain::controller& controller, chain::name account) { + if (controller.find_account_metadata(account) == nullptr) return false; + int64_t ram_bytes = 0, net_weight = 0, cpu_weight = 0; + controller.get_resource_limits_manager().get_account_limits(account, ram_bytes, net_weight, cpu_weight); + return batch_operator_detail::has_own_resources(net_weight, cpu_weight); + } + /// "request ( epoch )", or "request " when its statement does not decode. std::string describe(const uw::request& r) { const std::optional s = uw::decode_statement(r.statement); @@ -309,6 +321,9 @@ struct batch_operator_plugin::impl { bool underwriter_enabled = false; /// `batch-underwriter-max-exposure`, by shadow symbol code: the most the underwriter may have bonded at once. std::map underwriter_caps; + /// Who pays for each role's pushes (`batch-operator-explicit-payer`, `batch-underwriter-explicit-payer`). + batch_operator_detail::action_payer relay_payer = batch_operator_detail::action_payer::contract; + batch_operator_detail::action_payer underwriter_payer = batch_operator_detail::action_payer::contract; /// The relay's and the underwriter's signers, resolved by `resolve_signers` once the node is synced. std::optional relay_signer; @@ -1345,11 +1360,20 @@ struct batch_operator_plugin::impl { /// The signer of `role`, declaring `level`: the one operator-configured WIRE signature provider whose key alone /// satisfies `level` on chain. Nullopt, logged, when there is none, when there is more than one (the choice would - /// fall to provider order), when its signatures do not recover to its key, or when `level` may not declare one - /// of `actions`. Call on the main thread, after the sync gate. + /// fall to provider order), when its signatures do not recover to its key, when `level` may not declare one + /// of `actions`, or when `payer` is the account and it has no CPU or NET. Call on the main thread, after the + /// sync gate. std::optional resolve_signer(std::string_view role, const chain::permission_level& level, - std::span actions) { + std::span actions, + batch_operator_detail::action_payer payer) { const chain::controller& chain = chain_plug->chain(); + // An account without CPU or NET of its own would fail every push it pays for. + const bool can_pay = + payer == batch_operator_detail::action_payer::contract || can_pay_for_itself(chain, level.actor); + if (!can_pay) { + elog("batch_operator: {} pays for its pushes, but {} has no CPU or NET of its own: give it an ROA policy " + "or turn its explicit payer off", role, level.actor.to_string()); + } const std::vector unlinked = batch_operator_detail::unlinked_actions( actions, [&](const pushed_action& a) { return permission_satisfies_link(chain, level, chain::name(a.contract), chain::name(a.action)); @@ -1372,8 +1396,8 @@ struct batch_operator_plugin::impl { "{}@{}, found {}", role, level.actor.to_string(), level.permission.to_string(), choice.matches); return std::nullopt; } - if (!signs_for_its_key(role, **choice.chosen) || !linked) return std::nullopt; - return signer{.auth = level, .provider = *choice.chosen}; + if (!signs_for_its_key(role, **choice.chosen) || !linked || !can_pay) return std::nullopt; + return signer{.auth = level, .provider = *choice.chosen, .payer = payer}; } /// Whether `provider`'s signature over a probe digest recovers to its own key, which is how the chain checks a @@ -1399,11 +1423,13 @@ struct batch_operator_plugin::impl { // A disabled crank's action is not required: its link cannot stop the node. const std::vector relay_actions = batch_operator_detail::required_actions( RELAY_ACTIONS, std::array{batch_operator_detail::optional_action{KICK_ACTION, kick_crank_enabled}}); - relay_signer = resolve_signer("the relay", {operator_account, chain::config::active_name}, relay_actions); + relay_signer = resolve_signer("the relay", {operator_account, chain::config::active_name}, relay_actions, + relay_payer); resolved = relay_signer.has_value() && resolved; } if (underwriter_enabled) { - underwriter_signer = resolve_signer("the underwriter", underwriter_auth, UNDERWRITER_ACTIONS); + underwriter_signer = + resolve_signer("the underwriter", underwriter_auth, UNDERWRITER_ACTIONS, underwriter_payer); resolved = underwriter_signer.has_value() && resolved; } return resolved; @@ -1852,10 +1878,11 @@ struct batch_operator_plugin::impl { chain::digest_type digest; }; - /// `contract::action_name` declaring `auth`, unsigned, against the current head. Reads chain state (the ABI, the - /// head): call on the main thread or in a read window. Nullopt, logged, when it cannot be built. + /// `contract::action_name` declaring `authorization`, unsigned, against the current head. Reads chain state (the + /// ABI, the head): call on the main thread or in a read window. Nullopt, logged, when it cannot be built. std::optional build_unsigned(const std::string& contract, const std::string& action_name, - const chain::permission_level& auth, const fc::variant_object& data) { + const std::vector& authorization, + const fc::variant_object& data) { const chain::controller& chain = chain_plug->chain(); try { auto resolver = make_resolver(chain, delivery_timeout, throw_on_yield::no); @@ -1868,8 +1895,8 @@ struct batch_operator_plugin::impl { chain::bytes action_data = abis_opt->variant_to_binary( action_type, fc::variant(data), chain::abi_serializer::create_yield_function(delivery_timeout)); unsigned_trx out; - out.trx.actions.emplace_back(std::vector{auth}, chain::name(contract), - chain::name(action_name), std::move(action_data)); + out.trx.actions.emplace_back(authorization, chain::name(contract), chain::name(action_name), + std::move(action_data)); out.trx.set_reference_block(chain.head().id()); out.trx.expiration = fc::time_point_sec(chain.head().block_time() + PUSH_EXPIRATION); out.digest = out.trx.sig_digest(chain.get_chain_id(), out.trx.context_free_data); @@ -1914,8 +1941,9 @@ struct batch_operator_plugin::impl { } } - /// Build, sign and submit a depot action, declaring `by.auth` and signed by `by.provider`. True when the node - /// accepted the transaction within `delivery_timeout`; a refusal is logged by the push callback. + /// Build, sign and submit a depot action, declaring `by.auth` (after its `sysio.payer` marker when `by.payer` is + /// the account) and signed by `by.provider`. True when the node accepted the transaction within + /// `delivery_timeout`; a refusal is logged by the push callback. /// /// Called from cron threads, which may not read chain state: the build runs in a read window and the submission /// on the main thread, while the signature, possibly a remote signer, stays here. The waits do not cancel @@ -1927,22 +1955,25 @@ struct batch_operator_plugin::impl { if (shutting_down) return false; const fc::time_point deadline = fc::time_point::now() + delivery_timeout; const bool on_main = std::this_thread::get_id() == app().executor().get_main_thread_id(); + std::vector authorization = + batch_operator_detail::declared_authorization(by.auth, by.payer); std::optional trx; if (on_main) { - trx = build_unsigned(contract, action_name, by.auth, data); + trx = build_unsigned(contract, action_name, authorization, data); } else { std::shared_ptr>> built = std::make_shared>>(); std::future> built_future = built->get_future(); // Shutdown is checked again when the task runs, since chain state may be going away by then. app().executor().post(appbase::priority::medium, appbase::exec_queue::read_only, - [this, built, contract, action_name, auth = by.auth, data = fc::variant_object(data)] { + [this, built, contract, action_name, authorization = std::move(authorization), + data = fc::variant_object(data)] { if (shutting_down) { built->set_value(std::nullopt); return; } - built->set_value(build_unsigned(contract, action_name, auth, data)); + built->set_value(build_unsigned(contract, action_name, authorization, data)); }); if (!ready_by(built_future, deadline)) { if (shutting_down) wlog("batch_operator: push {}::{} abandoned on shutdown", contract, action_name); @@ -2131,6 +2162,15 @@ void batch_operator_plugin::set_program_options(options_description& cli, opts("batch-underwriter-max-exposure", bpo::value>()->composing(), "Most the underwriter may have bonded at once in one shadow token, as an asset such as " "100.000000000 LIQETH. Repeat for each token; requests in a token without one are not bonded."); + // One option per role: the two roles may run as different accounts, and each account needs its own CPU and NET + // before it can pay. + opts("batch-operator-explicit-payer", bpo::value()->default_value(EXPLICIT_PAYER_ENABLED), + "The relay's account pays for its pushes from its own CPU and NET, through a sysio.payer authorization, " + "instead of each action's contract. The account needs CPU and NET of its own; the node stops otherwise."); + opts("batch-underwriter-explicit-payer", bpo::value()->default_value(EXPLICIT_PAYER_ENABLED), + "The underwriter's account pays for its pushes from its own CPU and NET, through a sysio.payer " + "authorization, instead of each action's contract. The account needs CPU and NET of its own; the node " + "stops otherwise."); opts("batch-kick-crank", bpo::value()->default_value(KICK_CRANK_ENABLED), "Push sysio.kicker::kick for every kicker pool that could pay"); opts("batch-kick-interval-ms", bpo::value()->default_value(whole_milliseconds(KICK_INTERVAL)), @@ -2164,6 +2204,20 @@ void batch_operator_plugin::plugin_initialize(const variables_map& options) { if (!_impl->underwriter_enabled && !_impl->underwriter_caps.empty()) { wlog("batch_operator_plugin: batch-underwriter-max-exposure has no effect without batch-underwriter-account"); } + const auto payer_of = [](bool explicit_payer) { + return explicit_payer ? batch_operator_detail::action_payer::account + : batch_operator_detail::action_payer::contract; + }; + const bool relay_explicit_payer = options["batch-operator-explicit-payer"].as(); + const bool underwriter_explicit_payer = options["batch-underwriter-explicit-payer"].as(); + _impl->relay_payer = payer_of(relay_explicit_payer); + _impl->underwriter_payer = payer_of(underwriter_explicit_payer); + if (relay_explicit_payer && !_impl->enabled) { + wlog("batch_operator_plugin: batch-operator-explicit-payer has no effect without batch-operator-account"); + } + if (underwriter_explicit_payer && !_impl->underwriter_enabled) { + wlog("batch_operator_plugin: batch-underwriter-explicit-payer has no effect without batch-underwriter-account"); + } _impl->kick_crank_enabled = options["batch-kick-crank"].as(); const uint32_t kick_interval_ms = options["batch-kick-interval-ms"].as(); FC_ASSERT(batch_operator_detail::kick_interval_valid(kick_interval_ms), diff --git a/plugins/batch_operator_plugin/src/role_config.hpp b/plugins/batch_operator_plugin/src/role_config.hpp index 4bf27c66da..93a4a057dc 100644 --- a/plugins/batch_operator_plugin/src/role_config.hpp +++ b/plugins/batch_operator_plugin/src/role_config.hpp @@ -1,8 +1,8 @@ #pragma once /** * @file role_config.hpp - * @brief Parsing of the batch-operator plugin's role options and the choice of the key a role signs with. Pure - * functions, so the plugin's tests drive them without a node. + * @brief Parsing of the batch-operator plugin's role options, the choice of the key a role signs with, and the + * authorization it declares. Pure functions, so the plugin's tests drive them without a node. */ #include @@ -45,6 +45,27 @@ inline std::map parse_exposure_caps(const std: return caps; } +/// Who pays the CPU and NET of the actions a role pushes. +enum class action_payer { + contract, ///< each action's contract, the chain's default + account, ///< the role's own account, declared as the explicit payer +}; + +/// The authorization a role declares on each action it pushes: `level`, preceded by the `sysio.payer` marker of +/// its account when the account pays. The chain requires the marker first and paired with a real permission of the +/// same actor, so `level`'s signature is all the transaction needs. +inline std::vector declared_authorization(const chain::permission_level& level, + action_payer payer) { + if (payer == action_payer::contract) return {level}; + return {{level.actor, chain::config::sysio_payer_name}, level}; +} + +/// Whether an account with these resource weights has CPU and NET to pay with: zero gives it none, negative is +/// unlimited. +inline bool has_own_resources(int64_t net_weight, int64_t cpu_weight) { + return net_weight != 0 && cpu_weight != 0; +} + /// A depot action a role pushes. struct pushed_action { const char* contract; diff --git a/plugins/batch_operator_plugin/test/test_batch_operator_plugin.cpp b/plugins/batch_operator_plugin/test/test_batch_operator_plugin.cpp index 4e46848e3c..2aba5bfb82 100644 --- a/plugins/batch_operator_plugin/test/test_batch_operator_plugin.cpp +++ b/plugins/batch_operator_plugin/test/test_batch_operator_plugin.cpp @@ -29,6 +29,9 @@ constexpr uint32_t expected_error_log_count = 1; /// Default delivery budget in milliseconds. constexpr auto delivery_timeout_option = "batch-delivery-timeout-ms"; constexpr uint32_t expected_delivery_timeout_ms = 60'000; +/// Whether each role's account pays for its own pushes. +constexpr auto relay_explicit_payer_option = "batch-operator-explicit-payer"; +constexpr auto underwriter_explicit_payer_option = "batch-underwriter-explicit-payer"; namespace callback_lifetime_action { constexpr auto contract = "sysio.system"; @@ -62,6 +65,8 @@ BOOST_AUTO_TEST_CASE(plugin_options_are_registered) try { BOOST_CHECK(option_names.count("batch-yield-tick-interval-ms") > 0); BOOST_CHECK(option_names.count("batch-kick-crank") > 0); BOOST_CHECK(option_names.count("batch-kick-interval-ms") > 0); + BOOST_CHECK(option_names.count(relay_explicit_payer_option) > 0); + BOOST_CHECK(option_names.count(underwriter_explicit_payer_option) > 0); // Configuring batch-operator-account is what enables the relay. A separate // enable flag would let an operator set the account and still relay nothing, // which is indistinguishable from a healthy node until the group misses an epoch. @@ -92,6 +97,9 @@ BOOST_AUTO_TEST_CASE(default_options_are_correct) try { // pool no faster than the tickyield crank ticks one. BOOST_CHECK(vm["batch-kick-crank"].as()); BOOST_CHECK_EQUAL(vm["batch-kick-interval-ms"].as(), 60000u); + // Each pushed action's contract pays unless a role's account is set up to pay for itself. + BOOST_CHECK(!vm[relay_explicit_payer_option].as()); + BOOST_CHECK(!vm[underwriter_explicit_payer_option].as()); // No default account: an unconfigured node leaves the relay off. BOOST_CHECK_EQUAL(vm.count("batch-operator-account"), 0u); } FC_LOG_AND_RETHROW(); diff --git a/plugins/batch_operator_plugin/test/test_explicit_payer.cpp b/plugins/batch_operator_plugin/test/test_explicit_payer.cpp new file mode 100644 index 0000000000..35031662ce --- /dev/null +++ b/plugins/batch_operator_plugin/test/test_explicit_payer.cpp @@ -0,0 +1,147 @@ +#include "../src/role_config.hpp" + +#include + +#include +#include +#include + +#include + +using namespace sysio::chain; +using namespace sysio::testing; +using sysio::batch_operator_detail::action_payer; +using sysio::batch_operator_detail::declared_authorization; +using sysio::batch_operator_detail::has_own_resources; + +namespace { + +/// A chain whose operator account starts as `sysio.system::newaccount` leaves one, with no CPU or NET. It pushes +/// `sysio::reqauth`, which, like the depot actions the plugin pushes, is a `sysio.*` contract's action that needs +/// only the role's authorization. +struct explicit_payer_fixture : tester { + static constexpr account_name operator_account = "batchop"_n; + static constexpr permission_name underwrite = "underwrite"_n; + static constexpr action_name setalimits = "setalimits"_n; + static constexpr action_name reqauth = "reqauth"_n; + static constexpr int64_t unlimited = -1; + static constexpr int64_t no_weight = 0; + /// The ROA policy an operator holds before turning the option on. + static constexpr auto policy_weight = "10.0000 SYS"; + static constexpr auto policy_ram = "1.0000 SYS"; + + /// NET and CPU billed to an account, read within one pending block. + struct billed { + uint64_t net = 0; + uint64_t cpu = 0; + }; + + const permission_level active{operator_account, config::active_name}; + + explicit_payer_fixture() + : tester(setup_policy::full) { + produce_block(); + create_account(operator_account, config::system_account_name, false, false, false, false); + push_action(config::system_account_name, setalimits, config::system_account_name, + fc::mutable_variant_object()("account", operator_account)("ram_bytes", unlimited)( + "net_weight", no_weight)("cpu_weight", no_weight)); + produce_block(); + } + + /// Give the operator CPU and NET through an ROA policy. + void add_policy() { + add_roa_policy(NODE_DADDY, operator_account, policy_weight, policy_weight, policy_ram, 0, 0); + produce_block(); + } + + /// Push `sysio::reqauth` for the operator declaring `level` as `payer` says, signed only by `key`. + transaction_trace_ptr push_as(const permission_level& level, action_payer payer, const private_key_type& key) { + return push_reqauth(operator_account, declared_authorization(level, payer), {key}); + } + + /// Whether the operator has CPU and NET of its own, by its limits on chain. + bool can_pay() const { + int64_t ram_bytes = 0, net_weight = 0, cpu_weight = 0; + control->get_resource_limits_manager().get_account_limits(operator_account, ram_bytes, net_weight, cpu_weight); + return has_own_resources(net_weight, cpu_weight); + } + + billed billed_to(account_name account) const { + const auto& usage = + control->db().get(account); + return {.net = usage.net_usage.value_ex, .cpu = usage.cpu_usage.value_ex}; + } +}; + +} // namespace + +BOOST_AUTO_TEST_SUITE(explicit_payer_tests) + +/// Off, the contract pays, so an operator without CPU or NET still pushes and is billed nothing. +BOOST_FIXTURE_TEST_CASE(the_contract_pays_by_default, explicit_payer_fixture) try { + BOOST_REQUIRE(!can_pay()); + const billed operator_before = billed_to(operator_account); + const billed contract_before = billed_to(config::system_account_name); + + const transaction_trace_ptr trace = + push_as(active, action_payer::contract, get_private_key(operator_account, "active")); + BOOST_REQUIRE(!trace->action_traces.empty()); + BOOST_CHECK_EQUAL(config::system_account_name, trace->action_traces.front().act.payer()); + + const billed operator_after = billed_to(operator_account); + const billed contract_after = billed_to(config::system_account_name); + BOOST_CHECK_EQUAL(operator_before.net, operator_after.net); + BOOST_CHECK_EQUAL(operator_before.cpu, operator_after.cpu); + BOOST_CHECK_GT(contract_after.net, contract_before.net); + BOOST_CHECK_GT(contract_after.cpu, contract_before.cpu); +} FC_LOG_AND_RETHROW() + +/// An operator without CPU or NET cannot pay, which is why the plugin refuses to start that way. +BOOST_FIXTURE_TEST_CASE(an_operator_without_resources_cannot_pay, explicit_payer_fixture) try { + BOOST_REQUIRE(!can_pay()); + BOOST_CHECK_THROW(push_as(active, action_payer::account, get_private_key(operator_account, "active")), + resource_exhausted_exception); +} FC_LOG_AND_RETHROW() + +/// With an ROA policy, the explicitly paid push passes authorization with only the active key's signature, and the +/// operator pays instead of the contract. +BOOST_FIXTURE_TEST_CASE(an_operator_with_a_policy_pays_for_its_pushes, explicit_payer_fixture) try { + add_policy(); + BOOST_REQUIRE(can_pay()); + const billed operator_before = billed_to(operator_account); + const billed contract_before = billed_to(config::system_account_name); + + const transaction_trace_ptr trace = + push_as(active, action_payer::account, get_private_key(operator_account, "active")); + BOOST_REQUIRE(!trace->action_traces.empty()); + BOOST_CHECK_EQUAL(operator_account, trace->action_traces.front().act.payer()); + + const billed operator_after = billed_to(operator_account); + const billed contract_after = billed_to(config::system_account_name); + BOOST_CHECK_GT(operator_after.net, operator_before.net); + BOOST_CHECK_GT(operator_after.cpu, operator_before.cpu); + BOOST_CHECK_EQUAL(contract_before.net, contract_after.net); + BOOST_CHECK_EQUAL(contract_before.cpu, contract_after.cpu); +} FC_LOG_AND_RETHROW() + +/// The underwriter declares a dedicated permission linked to its actions: the marker pairs with it, and that +/// permission's key alone signs. +BOOST_FIXTURE_TEST_CASE(a_linked_permission_pays_without_the_active_key, explicit_payer_fixture) try { + add_policy(); + set_authority(operator_account, underwrite, authority(get_public_key(operator_account, underwrite.to_string())), + config::active_name); + link_authority(operator_account, config::system_account_name, underwrite, reqauth); + produce_block(); + const billed operator_before = billed_to(operator_account); + const billed contract_before = billed_to(config::system_account_name); + + push_as({operator_account, underwrite}, action_payer::account, + get_private_key(operator_account, underwrite.to_string())); + + const billed operator_after = billed_to(operator_account); + const billed contract_after = billed_to(config::system_account_name); + BOOST_CHECK_GT(operator_after.cpu, operator_before.cpu); + BOOST_CHECK_EQUAL(contract_before.cpu, contract_after.cpu); +} FC_LOG_AND_RETHROW() + +BOOST_AUTO_TEST_SUITE_END() diff --git a/plugins/batch_operator_plugin/test/test_role_config.cpp b/plugins/batch_operator_plugin/test/test_role_config.cpp index 919ae00b55..58ddb4615b 100644 --- a/plugins/batch_operator_plugin/test/test_role_config.cpp +++ b/plugins/batch_operator_plugin/test/test_role_config.cpp @@ -86,4 +86,30 @@ BOOST_AUTO_TEST_CASE(a_disabled_crank_action_is_not_required_of_the_permission) BOOST_CHECK_EQUAL(std::string("kick"), unlinked.front().action); } +/// Off, a role declares only its permission and the contract pays. On, its account's `sysio.payer` marker comes +/// first, paired with the same permission. +BOOST_AUTO_TEST_CASE(a_role_declares_the_payer_marker_only_when_its_account_pays) { + const auto level = parse_permission_level("uw.acct@underwrite"); + + const auto contract_pays = declared_authorization(level, action_payer::contract); + BOOST_REQUIRE_EQUAL(1u, contract_pays.size()); + BOOST_CHECK(level == contract_pays[0]); + + const auto account_pays = declared_authorization(level, action_payer::account); + BOOST_REQUIRE_EQUAL(2u, account_pays.size()); + BOOST_CHECK(account_pays[0] == (sysio::chain::permission_level{level.actor, sysio::chain::config::sysio_payer_name})); + BOOST_CHECK(level == account_pays[1]); +} + +/// An account pays only with both CPU and NET; a negative weight is unlimited. +BOOST_AUTO_TEST_CASE(an_account_has_its_own_resources_only_with_cpu_and_net) { + constexpr int64_t none = 0, some = 1, unlimited = -1; + BOOST_CHECK(has_own_resources(some, some)); + BOOST_CHECK(has_own_resources(unlimited, unlimited)); + BOOST_CHECK(has_own_resources(some, unlimited)); + BOOST_CHECK(!has_own_resources(none, some)); + BOOST_CHECK(!has_own_resources(some, none)); + BOOST_CHECK(!has_own_resources(none, none)); +} + BOOST_AUTO_TEST_SUITE_END()