From a482fbdfa9280090187a883cfe846d44634cefd2 Mon Sep 17 00:00:00 2001 From: Kevin Heifner Date: Sat, 7 Nov 2026 08:37:00 -0500 Subject: [PATCH] Require sysio.payer on msig propose propose already bills the proposal's RAM to the proposer. It now also requires {proposer, sysio.payer} as the first authorization, so the proposer pays its CPU and NET too, including the NET of the proposed transaction. clio multisig propose, propose_trx, exec and cancel now keep the signer's own permission when -P is given without -p. Change-Id: I4adac9eab1645a2665cc960e920214b609f2ead0 --- contracts/README.md | 2 +- contracts/sysio.msig/sysio.msig.cpp | 8 ++ contracts/sysio.msig/sysio.msig.hpp | 3 + contracts/sysio.msig/sysio.msig.wasm | Bin 37718 -> 37732 bytes contracts/tests/sysio.msig_tests.cpp | 120 +++++++++++++++++++++--- contracts/tests/sysio.system_tester.hpp | 13 +++ contracts/tests/sysio.system_tests.cpp | 33 ------- docs/contract-upgrade-order.md | 4 +- docs/roa-overview.md | 3 + programs/clio/main.cpp | 65 +++++-------- tests/multisig_review_test.py | 11 +-- 11 files changed, 163 insertions(+), 99 deletions(-) diff --git a/contracts/README.md b/contracts/README.md index 2198c60558..1d3b57f786 100644 --- a/contracts/README.md +++ b/contracts/README.md @@ -17,7 +17,7 @@ The collection of system contracts consists of the following individual contract + Allocation of CPU/NET resources based on core tokens in which the core tokens are either staked for an indefinite allocation of some fraction of available CPU/NET resources, or they are paid as a fee in exchange for a time-limited allocation of CPU/NET resources via ROA. + An automated market maker enabling a market for RAM resources which allows users to buy or sell available RAM allocations. + An auction for bidding for premium account names. -* [multisig contract](contracts/sysio.msig/include/sysio.msig/sysio.msig.hpp): A contract that enables proposing transactions on the blockchain, collecting authorization approvals for many accounts, and then executing the actions within the transaction after authorization requirements of the transaction have been reached. (Note: this contract must be deployed to a privileged account.) +* [multisig contract](contracts/sysio.msig/include/sysio.msig/sysio.msig.hpp): A contract that enables proposing transactions on the blockchain, collecting authorization approvals for many accounts, and then executing the actions within the transaction after authorization requirements of the transaction have been reached. `propose` must carry `{proposer, sysio.payer}` as its first authorization, so the proposer pays its CPU and NET. (Note: this contract must be deployed to a privileged account.) * [wrap contract](contracts/sysio.wrap/include/sysio.wrap/sysio.wrap.hpp): A contract that wraps around any transaction and allows for executing its actions without needing to satisfy the authorization requirements of the transaction. If used, the permissions of the account hosting this contract should be configured to only allow highly trusted parties (e.g. the operators of the blockchain) to have the ability to execute its actions. (Note: this contract must be deployed to a privileged account.) ## Depot flow tests diff --git a/contracts/sysio.msig/sysio.msig.cpp b/contracts/sysio.msig/sysio.msig.cpp index 3ad5253f72..2bf6c4bca9 100644 --- a/contracts/sysio.msig/sysio.msig.cpp +++ b/contracts/sysio.msig/sysio.msig.cpp @@ -6,6 +6,12 @@ namespace sysio { +namespace { +/// Marks its actor as the payer of an action's CPU and NET when it is the action's first +/// authorization (see `action::payer()` on the host). +constexpr name payer_permission = "sysio.payer"_n; +} + transaction_header get_trx_header(const char* ptr, size_t sz); bool trx_is_authorized(const std::vector& approvals, const std::vector& packed_trx); @@ -120,6 +126,8 @@ void multisig::propose( name proposer, ignore trx ) { require_auth( proposer ); + // The proposer, not msig, pays the CPU and NET of propose, whose data is the proposed transaction. + require_auth( permission_level{ proposer, payer_permission } ); auto& ds = get_datastream(); const char* trx_pos = ds.pos(); diff --git a/contracts/sysio.msig/sysio.msig.hpp b/contracts/sysio.msig/sysio.msig.hpp index 30c6798232..c132cf5b93 100644 --- a/contracts/sysio.msig/sysio.msig.hpp +++ b/contracts/sysio.msig/sysio.msig.hpp @@ -32,6 +32,9 @@ class [[sysio::contract("sysio.msig")]] multisig : public contract { * saved in the proposals table and the `requested` permission levels to the * approvals table (for the `proposer` context). Storage changes are billed to `proposer`. * + * The action must carry `{proposer, sysio.payer}` as its first authorization, so `proposer` + * pays its CPU and NET instead of this contract. + * * @param proposer - The account proposing a transaction * @param proposal_name - The name of the proposal (should be unique for proposer) * @param requested - Permission levels expected to approve the proposal diff --git a/contracts/sysio.msig/sysio.msig.wasm b/contracts/sysio.msig/sysio.msig.wasm index 32b0b1099c4a4a32ed3ee315db0d120f5d1f70fa..2e58b9524873983b864f07c13a61d8c8c6a20634 100755 GIT binary patch delta 871 zcmYjNYeh{pdhihV-K(l0<(LU4+msFcAFP6?)Gs=p1<7_q^}(KJW8=Z!-9E2Hz-m z;zUB#sX~cPwW27hPNF0rDhg624rV1|SRRP=gymo;7VY+`1nNP%8nkM;FK!G)B62ty zjC6;*l~pRjYuf)GQdcY-miwZ?u-qm0#O1ImH>d552hYh-dK+CrI_kzAx0VnQn?zB> zW;2$|2FOG#>P2iJv;*7E5<~9hW1GoN>_jBQZp3=YV#AUEOe_$AA1sPQa`0h(aK0J} zSi%Nk88B5JW% zL0dU7%{@;AQY_QLJwtkxHJ!>0Oa)GBxX1Q3fw<~t%jmkZl1-dBsR6OCAuH^@^Rr72 zGr#&aftS75gbXw}Zl#U=i=b^BIK~BA!^?o4$N=DEvHH3IN~5IAUZg@CWEN^G*N& delta 844 zcmYjNT}YEr7~VPC&OXmK-8bA%ZVeV>6A7|0caza(BrA!u(q>_6*q48$?%Pjbq)a8W zw6a6iO$bHeozMv-`hg&dLcA%23>2~qp}z~kx)6HLBIq1=-shb6d7t+^=hFn9pTO5k zM|ao_xTi-qx_6a@kUMJBs~wsAcrvMKsaUcl?k^AOh`1yFAC#uFrl}pNn5H(Vtr=C* z2qD7f^PY)tQx51M zyeT+{J#>p+4zFCnUcDEG&23&>f`@TfC$=Kt9uy|TH1E!!SI<(Pe0TrZ^> z*xRD>$jqx<2H{xa=@<7k9vA=atrv8$ILlvdb&9Tw#UDj@?iP zMj^%8g6-_i&fk!|<*&n#{T6URRw|E+!nR=6__rzFSAh|9jqKhIR&1lv#jouVgYu2a zIT2>7ZVSKJsw`iL #include #include +#include #include #include @@ -74,26 +75,86 @@ class sysio_msig_tester : public tester { return asset( result, symbol(CORE_SYM) ); } + /// `{account, sysio.payer}, {account, active}`: `account` authorizes the action and pays its CPU and NET. + static vector payer_auths( account_name account ) { + return { { account, config::sysio_payer_name }, { account, config::active_name } }; + } + + /// `propose` must be paid by its signer; the other msig actions bill the contract. + static bool requires_payer( const action_name& name ) { + return name == "propose"_n; + } + transaction_trace_ptr push_action( const account_name& signer, const action_name& name, const variant_object& data, bool auth = true ) { - vector accounts; + vector auths; if( auth ) - accounts.push_back( signer ); - auto trace = base_tester::push_action( "sysio.msig"_n, name, accounts, data ); + auths = requires_payer( name ) ? payer_auths( signer ) + : vector{ { signer, config::active_name } }; + auto trace = base_tester::push_action( "sysio.msig"_n, name, auths, data ); produce_block(); BOOST_REQUIRE_EQUAL( true, chain_has_transaction(trace->id) ); return trace; + } - /* - string action_type_name = abi_ser.get_action_type(name); + /// CPU and NET usage accumulators of `account`. + struct resource_usage { + resource_limits::usage_accumulator cpu; + resource_limits::usage_accumulator net; + }; + + resource_usage get_resource_usage( account_name account ) const { + const auto& usage = control->db().get( account ); + return { usage.cpu_usage, usage.net_usage }; + } + + /// Pushes `trx` with its CPU measured rather than the tester's fixed bill, and checks that the whole + /// transaction's CPU and NET went to `payer` while every account in `unbilled` was left untouched. + /// The pending block is not produced, so the expected usage is the payer's accumulator plus this + /// transaction at the pending slot. + transaction_trace_ptr push_and_check_billed_to( signed_transaction& trx, account_name payer, + const vector& unbilled ) { + const auto payer_before = get_resource_usage( payer ); + vector unbilled_before; + for( const auto& account : unbilled ) + unbilled_before.push_back( get_resource_usage( account ) ); + + auto trace = base_tester::push_transaction( trx, fc::time_point::maximum(), measured_cpu ); + + const auto& cfg = control->db().get(); + const auto slot = block_timestamp_type( control->pending_block_time() ).slot; + auto expected = payer_before; + expected.cpu.add( trace->total_cpu_usage_us, slot, cfg.account_cpu_usage_average_window ); + expected.net.add( trace->net_usage, slot, cfg.account_net_usage_average_window ); + check_usage_equal( expected, get_resource_usage( payer ) ); + + for( size_t i = 0; i < unbilled.size(); ++i ) + check_usage_equal( unbilled_before[i], get_resource_usage( unbilled[i] ) ); + return trace; + } - action act; - act.account = "sysio.msig"_n; - act.name = name; - act.data = abi_ser.variant_to_binary( action_type_name, data, fixture_abi_yield_function() ); - //std::cout << "test:\n" << fc::to_hex(act.data.data(), act.data.size()) << " size = " << act.data.size() << std::endl; + static void check_usage_equal( const resource_usage& expected, const resource_usage& actual ) { + BOOST_CHECK_EQUAL( expected.cpu.last_ordinal, actual.cpu.last_ordinal ); + BOOST_CHECK_EQUAL( expected.cpu.value_ex, actual.cpu.value_ex ); + BOOST_CHECK_EQUAL( expected.cpu.consumed, actual.cpu.consumed ); + BOOST_CHECK_EQUAL( expected.net.last_ordinal, actual.net.last_ordinal ); + BOOST_CHECK_EQUAL( expected.net.value_ex, actual.net.value_ex ); + BOOST_CHECK_EQUAL( expected.net.consumed, actual.net.consumed ); + } - return base_tester::push_action( std::move(act), auth ? uint64_t(signer) : 0 ); - */ + /// `billed_cpu_time_us` of 0 makes the chain measure the transaction's CPU. + static constexpr uint32_t measured_cpu = 0; + + /// A one-action transaction on `code`, signed with the active key of every non-payer authorizer. + signed_transaction create_signed_trx( account_name code, action_name act, const vector& auths, + const variant_object& data ) { + signed_transaction trx; + trx.actions.emplace_back( get_action( code, act, auths, data ) ); + set_transaction_headers( trx ); + for( const auto& auth : auths ) { + if( auth.permission != config::sysio_payer_name ) + trx.sign( get_private_key( auth.actor, auth.permission.to_string() ), control->get_chain_id() ); + } + return trx; } transaction reqauth( account_name from, const vector& auths, uint32_t delay_sec = 0 ); @@ -185,6 +246,39 @@ BOOST_FIXTURE_TEST_CASE( propose_approve_execute, sysio_msig_tester ) try { } ); } FC_LOG_AND_RETHROW() +// `propose` bills the proposer: it fails without `{proposer, sysio.payer}`, and when another account is the payer. +BOOST_FIXTURE_TEST_CASE( propose_requires_payer_auth, sysio_msig_tester ) try { + auto trx = reqauth( "alice"_n, {permission_level{"alice"_n, config::active_name}} ); + const auto propose_data = mvo() + ("proposer", "alice") + ("proposal_name", "first") + ("trx", trx) + ("requested", vector{{ "alice"_n, config::active_name }}); + + BOOST_REQUIRE_EXCEPTION( base_tester::push_action( "sysio.msig"_n, "propose"_n, "alice"_n, propose_data ), + missing_auth_exception, + fc_exception_message_is("missing authority of alice/sysio.payer") ); + + vector bob_pays = payer_auths( "bob"_n ); + bob_pays.push_back( { "alice"_n, config::active_name } ); + BOOST_REQUIRE_EXCEPTION( base_tester::push_action( "sysio.msig"_n, "propose"_n, bob_pays, propose_data ), + missing_auth_exception, + fc_exception_message_is("missing authority of alice/sysio.payer") ); + + base_tester::push_action( "sysio.msig"_n, "propose"_n, payer_auths( "alice"_n ), propose_data ); +} FC_LOG_AND_RETHROW() + +// The proposer pays the CPU and NET of `propose`; msig and the proposal's requested approver are not billed. +BOOST_FIXTURE_TEST_CASE( propose_billed_to_proposer, sysio_msig_tester ) try { + auto proposed = reqauth( "carol"_n, {permission_level{"carol"_n, config::active_name}} ); + auto propose_trx = create_signed_trx( "sysio.msig"_n, "propose"_n, payer_auths( "alice"_n ), mvo() + ("proposer", "alice") + ("proposal_name", "first") + ("trx", proposed) + ("requested", vector{{ "carol"_n, config::active_name }}) ); + push_and_check_billed_to( propose_trx, "alice"_n, { "sysio.msig"_n, "carol"_n } ); +} FC_LOG_AND_RETHROW() + // A proposal carrying a non-zero `delay_sec` executes once its delay has elapsed, and not // before. Nothing else in this suite proposes with a delay, so without this case the whole @@ -1007,7 +1101,7 @@ BOOST_FIXTURE_TEST_CASE( sendinline, sysio_msig_tester ) try { transaction trx = reqauth( "alice"_n, {permission_level{"alice"_n, "perm"_n}} ); - base_tester::push_action( "sysio.msig"_n, "propose"_n, "bob"_n, mvo() + base_tester::push_action( "sysio.msig"_n, "propose"_n, payer_auths( "bob"_n ), mvo() ("proposer", "bob") ("proposal_name", "first") ("trx", trx) diff --git a/contracts/tests/sysio.system_tester.hpp b/contracts/tests/sysio.system_tester.hpp index 53ca1b15f4..6e5be2fadc 100644 --- a/contracts/tests/sysio.system_tester.hpp +++ b/contracts/tests/sysio.system_tester.hpp @@ -357,6 +357,19 @@ class sysio_system_tester : public TESTER { msig_abi_ser.set_abi(msig_abi, fixture_abi_yield_function()); } + /// Pushes a `sysio.msig` action signed by `signer`. `propose` carries `{signer, sysio.payer}` first, as + /// msig requires, so `signer` pays; the other msig actions bill the contract. + action_result push_action_msig( const account_name& signer, const action_name& name, const variant_object& data ) { + action act; + act.account = "sysio.msig"_n; + act.name = name; + act.data = msig_abi_ser.variant_to_binary( msig_abi_ser.get_action_type(name), data, fixture_abi_yield_function() ); + + if( name == "propose"_n ) + return base_tester::push_action( std::move(act), signer.to_uint64_t() ); + return base_tester::push_contract_paid_action( std::move(act), signer.to_uint64_t() ); + } + /// Compatibility spelling for activate_producers(). Producer registration itself now requires /// the ACTIVE OPERATOR_TYPE_PRODUCER rows that activate_producers installs first. /// diff --git a/contracts/tests/sysio.system_tests.cpp b/contracts/tests/sysio.system_tests.cpp index 06affd782a..0f2d86fcb8 100644 --- a/contracts/tests/sysio.system_tests.cpp +++ b/contracts/tests/sysio.system_tests.cpp @@ -248,17 +248,6 @@ BOOST_FIXTURE_TEST_CASE(producers_upgrade_system_contract, sysio_system_tester) produce_blocks(); - //helper function - auto push_action_msig = [&]( const account_name& signer, const action_name &name, const variant_object &data, bool auth = true ) -> action_result { - string action_type_name = msig_abi_ser.get_action_type(name); - - action act; - act.account = "sysio.msig"_n; - act.name = name; - act.data = msig_abi_ser.variant_to_binary( action_type_name, data, fixture_abi_yield_function() ); - - return base_tester::push_contract_paid_action( std::move(act), (auth ? signer : signer == "bob111111111"_n ? "alice1111111"_n : "bob111111111"_n).to_uint64_t() ); - }; // test begins vector prod_perms; for ( auto& x : producer_names ) { @@ -380,17 +369,6 @@ BOOST_FIXTURE_TEST_CASE( setparams, sysio_system_tester ) try { add_roa_policy(NODE_DADDY, "alice1111111"_n, "32.0000 SYS", "32.0000 SYS", "500.0000 SYS", 0, 0); - //helper function - auto push_action_msig = [&]( const account_name& signer, const action_name &name, const variant_object &data, bool auth = true ) -> action_result { - string action_type_name = msig_abi_ser.get_action_type(name); - - action act; - act.account = "sysio.msig"_n; - act.name = name; - act.data = msig_abi_ser.variant_to_binary( action_type_name, data, fixture_abi_yield_function() ); - - return base_tester::push_contract_paid_action( std::move(act), (auth ? signer : signer == "bob111111111"_n ? "alice1111111"_n : "bob111111111"_n).to_uint64_t() ); - }; // test begins vector prod_perms; @@ -476,17 +454,6 @@ BOOST_FIXTURE_TEST_CASE( wasmcfg, sysio_system_tester ) try { add_roa_policy(NODE_DADDY, "alice1111111"_n, "32.0000 SYS", "32.0000 SYS", "500.0000 SYS", 0, 0); - //helper function - auto push_action_msig = [&]( const account_name& signer, const action_name &name, const variant_object &data, bool auth = true ) -> action_result { - string action_type_name = msig_abi_ser.get_action_type(name); - - action act; - act.account = "sysio.msig"_n; - act.name = name; - act.data = msig_abi_ser.variant_to_binary( action_type_name, data, fixture_abi_yield_function() ); - - return base_tester::push_contract_paid_action( std::move(act), (auth ? signer : signer == "bob111111111"_n ? "alice1111111"_n : "bob111111111"_n).to_uint64_t() ); - }; // test begins vector prod_perms; diff --git a/docs/contract-upgrade-order.md b/docs/contract-upgrade-order.md index 613decbfcd..64d183f9ad 100644 --- a/docs/contract-upgrade-order.md +++ b/docs/contract-upgrade-order.md @@ -49,7 +49,9 @@ other account in the set**. **2. A large set does not fit in one transaction.** The default `max_transaction_net_usage` is **524,288** bytes (`config::default_max_block_net_usage / 2`), and `sysio.msig::propose` receives the complete inner transaction before it -chunks storage, so proposing through msig does not dodge that input NET. The +chunks storage, so proposing through msig does not dodge that input NET. `propose` +requires `{proposer, sysio.payer}`, so that NET is billed to the proposer, whose ROA +policy must cover it. The contracts on `advance`'s path are not small (sizes of the builds in this tree): | | code | abi | total | diff --git a/docs/roa-overview.md b/docs/roa-overview.md index f295ded19e..b809218220 100644 --- a/docs/roa-overview.md +++ b/docs/roa-overview.md @@ -236,6 +236,9 @@ later quota sync by passing `-1, -1` for a `sysio.`-prefixed owner. The prefix a nothing — `activateroa` deliberately creates `sysio.acct` at **zero** CPU and NET, a pure RAM bucket for account creation. +`sysio.msig::propose` is the exception: its data is a caller-supplied transaction, so it requires +`{proposer, sysio.payer}` first and the proposer pays its CPU and NET. + > **What `-1` means here.** It is a **sentinel** meaning "no limit," not a quantity. The chain > tests for it explicitly (`is_unlimited_cpu` returns `cpu_weight == -1`) and substitutes a large > finite number where arithmetic is needed. Seeing `-1` in a `get_account` response is not an diff --git a/programs/clio/main.cpp b/programs/clio/main.cpp index cf297bb04a..41be4975c4 100644 --- a/programs/clio/main.cpp +++ b/programs/clio/main.cpp @@ -326,6 +326,23 @@ vector get_account_permissions(const vector& pe } } +/// Authorizations for a multisig action signed by `signer`: the `-p` permissions, or `signer@active` when there +/// are none, behind the `-P` payer. An empty `signer` is set to the first actor that is not the payer marker. +vector get_signer_permissions(string& signer, std::string_view signer_param) { + auto permissions = signer.empty() + ? get_account_permissions(tx_permission) + : get_account_permissions(tx_permission, {name(signer), sysio::chain::config::active_name}); + const auto signer_permission = std::ranges::find_if(permissions, [](const chain::permission_level& p) { + return p.permission != sysio::chain::config::sysio_payer_name; + }); + SYS_ASSERT(signer_permission != permissions.end(), missing_auth_exception, + "Authority is not provided (either by multisig parameter <{}> or -p)", signer_param); + if (signer.empty()) { + signer = signer_permission->actor.to_string(); + } + return permissions; +} + template fc::variant call( const std::string& url, const std::string& path, @@ -3554,17 +3571,7 @@ int main( int argc, char** argv ) { trxperm = transaction_perm_var.as>(); } SYS_RETHROW_EXCEPTIONS(transaction_type_exception, "Wrong transaction permissions format: '{}'", fc::json::to_log_string(transaction_perm_var)); - auto accountPermissions = get_account_permissions(tx_permission); - if (accountPermissions.empty()) { - if (!proposer.empty()) { - accountPermissions = vector{{name(proposer), sysio::chain::config::active_name}}; - } else { - SYS_THROW(missing_auth_exception, "Authority is not provided (either by multisig parameter or -p)"); - } - } - if (proposer.empty()) { - proposer = name(accountPermissions.at(0).actor).to_string(); - } + auto accountPermissions = get_signer_permissions(proposer, "proposer"); transaction trx; @@ -3623,17 +3630,7 @@ int main( int argc, char** argv ) { trx_var = trx; // implicit conversion serializes vector action data as hex } - auto account_permissions = get_account_permissions(tx_permission); - if (account_permissions.empty()) { - if (!proposer.empty()) { - account_permissions = vector{{name(proposer), sysio::chain::config::active_name}}; - } else { - SYS_THROW(missing_auth_exception, "Authority is not provided (either by multisig parameter or -p)"); - } - } - if (proposer.empty()) { - proposer = name(account_permissions.at(0).actor).to_string(); - } + auto account_permissions = get_signer_permissions(proposer, "proposer"); auto args = fc::mutable_variant_object() ("proposer", proposer ) @@ -3954,17 +3951,7 @@ int main( int argc, char** argv ) { cancel->add_option("proposal_name", proposal_name, localized("The proposal name (string)"))->required(); cancel->add_option("canceler", canceler, localized("The canceler name (string)")); cancel->callback([&]() { - auto accountPermissions = get_account_permissions(tx_permission); - if (accountPermissions.empty()) { - if (!canceler.empty()) { - accountPermissions = vector{{name(canceler), sysio::chain::config::active_name}}; - } else { - SYS_THROW(missing_auth_exception, "Authority is not provided (either by multisig parameter or -p)"); - } - } - if (canceler.empty()) { - canceler = name(accountPermissions.at(0).actor).to_string(); - } + auto accountPermissions = get_signer_permissions(canceler, "canceler"); auto args = fc::mutable_variant_object() ("proposer", proposer) ("proposal_name", proposal_name) @@ -3982,17 +3969,7 @@ int main( int argc, char** argv ) { exec->add_option("proposal_name", proposal_name, localized("The proposal name (string)"))->required(); exec->add_option("executer", executer, localized("The account paying for execution (string)")); exec->callback([&] { - auto accountPermissions = get_account_permissions(tx_permission); - if (accountPermissions.empty()) { - if (!executer.empty()) { - accountPermissions = vector{{name(executer), sysio::chain::config::active_name}}; - } else { - SYS_THROW(missing_auth_exception, "Authority is not provided (either by multisig parameter or -p)"); - } - } - if (executer.empty()) { - executer = name(accountPermissions.at(0).actor).to_string(); - } + auto accountPermissions = get_signer_permissions(executer, "executer"); auto args = fc::mutable_variant_object() ("proposer", proposer ) diff --git a/tests/multisig_review_test.py b/tests/multisig_review_test.py index ca6dc1ff93..55608a3e8d 100755 --- a/tests/multisig_review_test.py +++ b/tests/multisig_review_test.py @@ -195,18 +195,15 @@ Print(f"Inner trx JSON written to {trx_path} (size={trx_path.stat().st_size} bytes)") # ----- Propose the chunked transaction via clio. - # `-p alice@sysio.payer -p alice@active` — Wire's ROA layer requires the - # virtual `sysio.payer` permission whenever a contract bills RAM to the - # signer, and propose stores the proposal row + chunk rows on the - # proposer's quota. The chain also enforces that the explicit payer - # permission must be the FIRST declared authorization on the action, so - # `sysio.payer` has to precede `active` in the -p list. + # sysio.msig::propose requires `alice@sysio.payer` as the first + # authorization, so alice pays its CPU and NET. `-P alice` with the + # positional proposer makes clio send `alice@sysio.payer, alice@active`. Print("Submit `multisig propose_trx` against the API node") propose_cmd = ( f"multisig propose_trx -j chunkprop " f"{shlex.quote(str(perm_path))} " f"{shlex.quote(str(trx_path))} " - f"alice -p alice@sysio.payer -p alice@active" + f"alice -P alice" ) # Submit through the API node so that once we wait for the proposal # transaction to land in a block, the proposal row is in the API node's