diff --git a/@l10n/ru/docs/protocol/operations/agent-access.md b/@l10n/ru/docs/protocol/operations/agent-access.md index 31af31701a..358fe29469 100644 --- a/@l10n/ru/docs/protocol/operations/agent-access.md +++ b/@l10n/ru/docs/protocol/operations/agent-access.md @@ -62,16 +62,21 @@ ## Правила -- **Не делегируются никогда:** `set_agent_permission`, `proposal_create`, `proposal_update`, `proposal_delete`, `account_update`, `recover_account`, `change_recovery_account`, `set_account_price`, `set_subaccount_price`, `target_account_sale`. Виртуальные операции и устаревшие алиасы (писать `validator_update`, а не `witness_update`) отвергаются. +- **Не делегируются никогда:** `set_agent_permission`, `proposal_update`, `account_update`, `recover_account`, `change_recovery_account`, `set_account_price`, `set_subaccount_price`, `target_account_sale`. Виртуальные операции и устаревшие алиасы (писать `validator_update`, а не `witness_update`) отвергаются. - **Один ключ — один агент:** ключ, уже привязанный к другому имени того же принципала, отвергается. - **Не больше 16 агентов** на принципала. Выдача сначала удаляет его истёкших агентов. -- **Когда подпись агента засчитывается:** транзакции не нужны master- и regular-подписи, собственные ключи принципала её не подписывают, агент жив (срок не истёк, список операций не пуст), его список покрывает **каждую** операцию транзакции, которой нужна подпись, и его ключ есть среди подписей. Через вложенные `account_auths` доступа нет. +- **Дополнительные возможности, не исправления:** явная выдача разрешает прямые требования **active и regular**. Область проверяется по операции и принципалу: агент Alice с `transfer` не обязан иметь `custom` Bob, подписанный отдельно; собственная невыданная операция Alice запрещена. Сначала проверяются обычные полномочия, включая master; лишние подписи по-прежнему отвергаются. Смешивать regular с active/master нельзя. +- **Без повышения полномочий:** агент не заменяет master, произвольные `other` или вложенные `account_auths`. Его доказательство не становится одобрением аккаунта для другого требования транзакции. +- **Предложения:** явно выданный `proposal_create` лишь сохраняет предложение, даже с невыданными внутренними операциями: **одобрения пусты**, ничего не исполняется. `proposal_delete` разрешает вето только с проверками requester существующего evaluator. `proposal_update` запрещён: подпись агента не создаёт постоянное одобрение предложения ни аккаунтом, ни ключом. +- **Отзыв при применении:** только агентские доказательства перепроверяются непосредственно перед соответствующей операцией. Предшествующий отзыв, ограничение списка, замена ключа или стирание агентов отменяет такое доказательство. Обычные полномочия проверяются на входе; привычная смена ключей с последующим использованием в той же транзакции сохраняется. - **Удаление:** все агенты принципала стираются при смене master, смене active, восстановлении аккаунта, прямой продаже и закрытии аукциона. Смена только regular их не трогает. «Смена» — это **наличие** поля в `account_update`: если в операции есть `master` или `active`, агенты стираются даже при том же ключе. Клиент, который правит только regular или memo, обязан не передавать `master`/`active` (в viz-php-lib `build_account_update` передайте `null` для роли, чтобы её не слать). ## Чтение агентов `database_api.get_agent_permissions(account)` — см. [database_api](../../plugins/database-api.md#get-agent-permissions-account). +`get_potential_signatures` возвращает ключи-кандидаты, включая обычный fallback к active/master и явно выданные ключи агентов; это не разрешение операции. `get_required_signatures` возвращает доступные подписи-вклады, даже если независимому аккаунту или явной authority ещё не хватает подписей. Уже подписанные ключи исключаются; ключ агента учитывается только при покрытии всех прямых требований своего аккаунта. Частичные обычные multisig-подписи сохраняются. Подбор подписей не означает принятие транзакции: `verify_authority` и применение по-прежнему требуют все полномочия и проверяют границы гранта. + ## В кошельках **Выдача (принципал).** WebVIZWallet → *Агенты*: имя агента, публичный ключ (кнопка *Сгенерировать* создаёт новую пару — приватный ключ сохраните, он показывается один раз), галочки разрешённых операций, срок (*бессрочно* или дата), addons строкой через запятую. На странице — список текущих агентов с кнопкой *Отозвать*. Выдача подписывается **active**-ключом принципала. Vizonator даёт ту же операцию сайтам через `window.vizonator`, всегда через окно подтверждения с ключом, операциями, сроком и addons. diff --git a/docs/protocol/operations/agent-access.md b/docs/protocol/operations/agent-access.md index 6576a5e632..66624f823e 100644 --- a/docs/protocol/operations/agent-access.md +++ b/docs/protocol/operations/agent-access.md @@ -62,16 +62,21 @@ Re-issuing by the same name replaces the key, operations, addons and expiration ## Rules -- **Never delegable:** `set_agent_permission`, `proposal_create`, `proposal_update`, `proposal_delete`, `account_update`, `recover_account`, `change_recovery_account`, `set_account_price`, `set_subaccount_price`, `target_account_sale`. Virtual operations and deprecated aliases (use `validator_update`, not `witness_update`) are rejected. +- **Never delegable:** `set_agent_permission`, `proposal_update`, `account_update`, `recover_account`, `change_recovery_account`, `set_account_price`, `set_subaccount_price`, `target_account_sale`. Virtual operations and deprecated aliases (use `validator_update`, not `witness_update`) are rejected. - **One key, one agent:** a key already bound to another agent name of the same principal is rejected. - **At most 16 agents** per principal. A grant first removes the principal's expired agents. -- **When an agent signature counts:** the transaction needs no master or regular authority, the principal's own keys do not already sign it, the agent is live (not expired, operation list non-empty), its list covers **every** operation of the transaction that needs a signature, and its key is among the signatures. No reach through nested `account_auths`. +- **Optional capabilities, not correctness fixes:** explicit grants may satisfy direct top-level **active or regular** requirements. Coverage is per operation and principal: an Alice `transfer` grant need not cover Bob's separately signed `custom`, but Alice's own ungranted operation is still rejected. Ordinary authority is tried first, including master fallback; unused extra signatures remain invalid. Legacy prohibition on mixing regular with active/master operations remains. +- **No authority escalation:** agents never satisfy master, arbitrary `other` authorities or nested `account_auths`. A successful agent proof is not an account approval and cannot authorize another account in the same transaction. +- **Proposals:** explicit `proposal_create` permits storing a proposal, even with ungranted inner operations, but creates **no approvals** and executes nothing. Explicit `proposal_delete` permits a veto only when the unchanged evaluator accepts the requester. `proposal_update` remains denied: agent proofs never become persistent proposal approval, including account or key approvals. +- **Apply-time revocation:** agent-dependent requirements are rechecked immediately before their operation. A preceding revoke, grant restriction, key replacement or authority wipe cannot leave an entry-time agent proof usable. Ordinary entry-time authority proofs are not rechecked; ordinary rotate-and-use behavior is unchanged. - **Wipes:** all agents of the principal are removed on master change, active change, account recovery, direct sale and auction close. A regular-only change keeps them. "Change" means the field is **present** in `account_update`: sending `master` or `active` wipes the agents even if the key is the same, so a client that only edits regular or memo must leave `master`/`active` out of the operation (in viz-php-lib `build_account_update` pass `null` for a role to leave it out). ## Reading agents `database_api.get_agent_permissions(account)` — see [database_api](../../plugins/database-api.md#get-agent-permissions-account). +`get_potential_signatures` returns candidate keys, including ordinary active/master fallback and directly granted agent keys; it is not an authorization decision. `get_required_signatures` returns available contributions, even when an independent principal or explicit authority is still missing. It excludes keys already signed and only counts an agent when its grants cover every direct requirement of that principal; ordinary partial multisig contributions remain discoverable. Discovery is not acceptance: `verify_authority` and transaction application still require all authorities and enforce the actual grant scope. + ## In wallets **Granting (principal).** WebVIZWallet → *Agents*: agent name, public key (the *Generate* button creates a fresh pair — save the private key, it is shown once), tick the allowed operations, expiration (*perpetual* or a date), addons as a comma-separated list. The page lists current agents with a *Revoke* button. The grant is signed by the principal's **active** key. Vizonator exposes the same operation to sites through `window.vizonator`, always behind a confirmation window that shows the key, operations, expiration and addons. diff --git a/libraries/chain/agent_evaluator.cpp b/libraries/chain/agent_evaluator.cpp index c996723f54..8dad07e3e2 100644 --- a/libraries/chain/agent_evaluator.cpp +++ b/libraries/chain/agent_evaluator.cpp @@ -15,132 +15,58 @@ using namespace graphene::protocol; namespace { -/// Wire names of the operations of `trx` that require SOME authority. An operation that requires -/// nothing grants nothing, so it puts no coverage demand on a delegation. -flat_set authority_requiring_operation_names(const signed_transaction& trx) { - flat_set names; - for (const auto& op : trx.operations) { - flat_set active, master, regular; - std::vector other; - operation_get_required_authorities(op, active, master, regular, other); - if (active.empty() && master.empty() && regular.empty() && other.empty()) continue; - names.insert(fc::resolve_operation_name(operation_wire_name(op))); - } - return names; -} - -/// The plain ACTIVE getter: what the chain has always used. Delegation is layered on top of it. -authority_getter plain_active_authority_getter(const database& db) { - return [&db](const account_name_type& name) { - return authority(db.get(name).active); - }; +bool usable_agent_row(const database& db, const agent_permission_object& row, const string& wire) { + if (row.expiration != time_point_sec() && row.expiration <= db.head_block_time()) return false; + const auto grants = unpack_operation_names(row.operations); + if (!grants.count(wire)) return false; + for (const auto& name : grants) + if (never_delegable_operation_names().count(name)) return false; + return true; } } // anonymous namespace -fc::flat_map -delegated_active_authorities(const database& db, const signed_transaction& trx, - const chain_id_type& chain_id, - const flat_set* candidate_keys) { - fc::flat_map delegated; - - if (!db.has_hardfork(CHAIN_HARDFORK_15)) - return delegated; - - flat_set required_active, required_master, required_regular; - std::vector other; - trx.get_required_authorities(required_active, required_master, required_regular, other); - - if (required_active.empty()) - return delegated; - - // Master and regular are out of scope for an agent, and rather than argue about the nested - // paths that reach them (sign_state resolves nested account authorities through ACTIVE), we - // simply do not delegate in such a transaction. - if (!required_master.empty() || !required_regular.empty()) - return delegated; - - flat_set sigs; - bool sigs_ready = false; - // Signature recovery is the expensive part of validation, and verify_authority performs it - // again right after us. So it is deferred until an agent row actually exists for some - // principal: with no rows the hook must add no work at all to the ordinary path. - auto ensure_signatures = [&]() -> bool { - if (!sigs_ready) { - sigs_ready = true; - try { - sigs = candidate_keys ? *candidate_keys : trx.get_signature_keys(chain_id); - } catch (...) { - // Unsigned or malformed: leave the verdict to verify_authority, which reports it. - sigs.clear(); - return false; - } - } - return true; - }; - - const flat_set tx_ops = authority_requiring_operation_names(trx); - const authority_getter get_active = plain_active_authority_getter(db); - const flat_set no_extra_keys; // a validating node can produce no extra keys - - const auto& pidx = db.get_index().indices().get(); - const time_point_sec now = db.head_block_time(); - const flat_set& denied = never_delegable_operation_names(); - - for (const account_name_type& principal : required_active) { - // This principal's agents only — at most CHAIN_AGENT_MAX_PER_ACCOUNT rows. No row, no work. - auto it = pidx.lower_bound(boost::make_tuple(principal)); - if (it == pidx.end() || it->account != principal) - continue; - - if (!ensure_signatures()) - return delegated; - - // The principal's own authority always wins: substituting unconditionally would break valid - // transactions the moment the account issues its first agent. - { - sign_state principal_signs(sigs, get_active, no_extra_keys); - if (principal_signs.check_authority(principal)) - continue; - } +bool agent_operation_allowed(const database& db, const operation& op, + const account_name_type& principal, const public_key_type& key) { + if (!db.has_hardfork(CHAIN_HARDFORK_15)) return false; + const string wire = operation_wire_name(op); + if (never_delegable_operation_names().count(wire)) return false; + const auto& idx = db.get_index().indices().get(); + for (auto it = idx.lower_bound(boost::make_tuple(principal)); + it != idx.end() && it->account == principal; ++it) + if (it->agent_key == key && usable_agent_row(db, *it, wire)) return true; + return false; +} - for (; it != pidx.end() && it->account == principal; ++it) { - const agent_permission_object& row = *it; - - // The agent's key must actually have signed. - if (!sigs.count(row.agent_key)) - continue; - - // Expiration: epoch means perpetual; a past date means the row is already dead. - if (row.expiration != time_point_sec() && row.expiration <= now) - continue; - - const flat_set granted = unpack_operation_names(row.operations); - if (granted.empty()) - continue; - - // A row holding a non-delegable name is an invariant breach (the evaluator refuses - // those), and the hook trusts these rows — so fail closed instead of trusting it. - bool usable = true; - for (const string& name : granted) { - if (denied.count(name)) { usable = false; break; } - } - if (!usable) - continue; - - // Full coverage of the transaction, per the header's contract. - for (const string& name : tx_ops) { - if (!granted.count(name)) { usable = false; break; } - } - if (!usable) - continue; - - delegated[principal] = row.agent_key; - break; +void verify_agent_transaction(const database& db, const signed_transaction& trx, + const flat_set& keys, bool allow_unused, + flat_set* used, agent_proofs* proofs) { + const auto get_active = [&](const account_name_type& n) { + return authority(db.get(n).active); + }; + const auto get_master = [&](const account_name_type& n) { + return authority(db.get(n).master); + }; + const auto get_regular = [&](const account_name_type& n) { + return authority(db.get(n).regular); + }; + if (proofs) proofs->operations.assign(trx.operations.size(), {}); + const auto direct = [&](const operation& op, const account_name_type& principal, + bool /* regular */, sign_state& state) { + if (!db.has_hardfork(CHAIN_HARDFORK_15)) return false; + const string wire = operation_wire_name(op); + if (never_delegable_operation_names().count(wire)) return false; + const auto& idx = db.get_index().indices().get(); + for (auto it = idx.lower_bound(boost::make_tuple(principal)); + it != idx.end() && it->account == principal; ++it) { + if (!usable_agent_row(db, *it, wire) || !state.signed_by(it->agent_key)) continue; + if (proofs) proofs->operations[&op - trx.operations.data()][principal] = it->agent_key; + return true; } - } - - return delegated; + return false; + }; + protocol::verify_authority(trx.operations, keys, get_active, get_master, get_regular, + CHAIN_MAX_SIG_CHECK_DEPTH, false, {}, {}, {}, {}, allow_unused, used, direct); } // ─── set_agent_permission ──────────────────────────────────────────────────── diff --git a/libraries/chain/database.cpp b/libraries/chain/database.cpp index 753e29d6a7..e8a666ba08 100644 --- a/libraries/chain/database.cpp +++ b/libraries/chain/database.cpp @@ -5673,12 +5673,13 @@ namespace graphene { namespace chain { skip_tapos_check; // in case of multi-thread application, it's allow to validate transaction in read-thread + agent_proofs proofs; if ((skip & validate_transaction_steps) != validate_transaction_steps) { // this method can be used only for push_transaction(), // because such transactions only added to pending list, // and they will be rechecked on block generation auto validate_action = [&]() { - _validate_transaction(trx, skip); + _validate_transaction(trx, skip, &proofs); }; if (!(skip & skip_database_locking)) { @@ -5693,7 +5694,7 @@ namespace graphene { namespace chain { if (!(skip & skip_apply_transaction)) { auto apply_action = [&]() { auto session = start_undo_session(); - _apply_transaction(trx, skip); + _apply_transaction(trx, skip, &proofs); session.undo(); }; @@ -5704,6 +5705,16 @@ namespace graphene { namespace chain { } } + // The chain plugin validates with skip_apply_transaction, then pushes + // using these returned flags. Agent-dependent transactions must collect + // fresh entry proofs under the write lock, not inherit the signature skip. + // Keep the legacy fast path for every ordinary-only transaction. + for (const auto& operation_proofs : proofs.operations) { + if (!operation_proofs.empty()) { + skip &= ~(skip_transaction_signatures | skip_authority_check); + break; + } + } return skip; } @@ -5712,7 +5723,7 @@ namespace graphene { namespace chain { return get_validator(name).signing_key; } - void database::_validate_transaction(const signed_transaction &trx, uint32_t skip) { + void database::_validate_transaction(const signed_transaction &trx, uint32_t skip, agent_proofs* proofs) { if (!(skip & skip_validate_operations)) { /* issue #505 explains why this skip_flag is disabled */ trx.validate(); } @@ -5720,35 +5731,14 @@ namespace graphene { namespace chain { if (!(skip & (skip_transaction_signatures | skip_authority_check))) { const chain_id_type &chain_id = CHAIN_ID; - // HF15 agent access is a direct top-level active fallback. Nested - // account authorities continue to use the unmodified active getter; - // the permission map is empty before HF15 and for master/regular txs. - const auto delegated = delegated_active_authorities(*this, trx, chain_id); - - auto get_active = [&](const account_name_type& name) { - return authority(get(name).active); - }; - - auto get_master = [&](const account_name_type& name) { - return authority(get(name).master); - }; - - auto get_regular = [&](const account_name_type& name) { - return authority(get(name).regular); - }; - try { - protocol::verify_authority(trx.operations, trx.get_signature_keys(chain_id), - get_active, get_master, get_regular, CHAIN_MAX_SIG_CHECK_DEPTH, - false, {}, {}, {}, [&](const account_name_type& id, protocol::sign_state& state) { - const auto it = delegated.find(id); - return it != delegated.end() && state.signed_by(it->second); - }); + verify_agent_transaction(*this, trx, trx.get_signature_keys(chain_id), false, nullptr, proofs); } catch (protocol::tx_missing_active_auth &e) { if (get_shared_db_merkle().find(head_block_num() + 1) == get_shared_db_merkle().end()) { throw e; } + if (proofs) proofs->operations.clear(); } } @@ -6047,7 +6037,7 @@ namespace graphene { namespace chain { notify_on_applied_transaction(trx); } - void database::_apply_transaction(const signed_transaction &trx, uint32_t skip) { + void database::_apply_transaction(const signed_transaction &trx, uint32_t skip, const agent_proofs* proofs) { try { _current_trx_id = trx.id(); _current_virtual_op = 0; @@ -6059,7 +6049,12 @@ namespace graphene { namespace chain { trx_idx.indices().get().find(trx_id) == trx_idx.indices().get().end(), "Duplicate transaction check failed", ("trx_ix", trx_id)); - _validate_transaction(trx, skip); + // Retain entry-time proofs across the read-validation/dry-apply split. + // Full block application collects them in its existing validation pass; + // ordinary proofs are never rechecked after authority rotation. + agent_proofs entry_proofs; + _validate_transaction(trx, skip, &entry_proofs); + if (!proofs) proofs = &entry_proofs; flat_set required; vector other; @@ -6094,6 +6089,11 @@ namespace graphene { namespace chain { _current_op_in_trx = 0; for (const auto &op : trx.operations) { try { + if (!proofs->operations.empty()) { + for (const auto& proof : proofs->operations[_current_op_in_trx]) + FC_ASSERT(agent_operation_allowed(*this, op, proof.first, proof.second), + "Agent permission revoked before operation", ("account", proof.first)); + } apply_operation(op); ++_current_op_in_trx; } FC_CAPTURE_AND_RETHROW((op)); diff --git a/libraries/chain/include/graphene/chain/agent_evaluator.hpp b/libraries/chain/include/graphene/chain/agent_evaluator.hpp index b19b0ea312..bcae0d04d4 100644 --- a/libraries/chain/include/graphene/chain/agent_evaluator.hpp +++ b/libraries/chain/include/graphene/chain/agent_evaluator.hpp @@ -1,6 +1,7 @@ #pragma once #include +#include namespace graphene { namespace chain { @@ -16,15 +17,22 @@ namespace graphene { namespace chain { void do_apply(const operation_type& o); }; - /// HF15: eligible direct ACTIVE fallback keys for principals required by `trx`. - /// The ordinary active getter is never replaced, so nested account_auths cannot - /// inherit an agent's rights. Every authority-requiring operation must be covered. - /// `candidate_keys` supports unsigned RPC signature discovery; when null the - /// transaction's actual signatures are recovered and checked. - fc::flat_map - delegated_active_authorities(const database& db, - const graphene::protocol::signed_transaction& trx, - const graphene::protocol::chain_id_type& chain_id, - const fc::flat_set* candidate_keys = nullptr); + // Only direct agent-dependent requirements, indexed by operation. These are + // ephemeral validation proofs, never persistent or nested account approvals. + struct agent_proofs { + std::vector> operations; + }; + + bool agent_operation_allowed(const database& db, const graphene::protocol::operation& op, + const graphene::protocol::account_name_type& principal, + const graphene::protocol::public_key_type& key); + + void verify_agent_transaction(const database& db, + const graphene::protocol::signed_transaction& trx, + const fc::flat_set& keys, + bool allow_unused = false, + fc::flat_set* used = nullptr, + agent_proofs* proofs = nullptr); } } // graphene::chain diff --git a/libraries/chain/include/graphene/chain/database.hpp b/libraries/chain/include/graphene/chain/database.hpp index 3f6069bb50..58da045c5c 100644 --- a/libraries/chain/include/graphene/chain/database.hpp +++ b/libraries/chain/include/graphene/chain/database.hpp @@ -21,6 +21,8 @@ namespace graphene { namespace chain { + struct agent_proofs; + /// Custom combiner for applied_block signal that logs per-slot timing. /// This allows diagnosing which plugin callback is slow without /// modifying each plugin individually. @@ -673,9 +675,9 @@ namespace graphene { namespace chain { void _apply_block(const signed_block &next_block, uint32_t skip); - void _apply_transaction(const signed_transaction &trx, uint32_t skip); + void _apply_transaction(const signed_transaction &trx, uint32_t skip, const agent_proofs* proofs = nullptr); - void _validate_transaction(const signed_transaction& trx, uint32_t skip); + void _validate_transaction(const signed_transaction& trx, uint32_t skip, agent_proofs* proofs = nullptr); void apply_operation(const operation &op, bool is_virtual = false); diff --git a/libraries/protocol/agent_operations.cpp b/libraries/protocol/agent_operations.cpp index e32dc0920b..1f2efe99cd 100644 --- a/libraries/protocol/agent_operations.cpp +++ b/libraries/protocol/agent_operations.cpp @@ -17,13 +17,10 @@ namespace graphene { namespace protocol { // principal's active authority, so an agent holding active could otherwise // re-delegate. Escalation chains are refused structurally. s.insert("set_agent_permission"); - // Proposal wrappers carry arbitrary operations whose authorities are collected at - // EXECUTION time from the wrapped ops. Delegating `proposal_create` would therefore - // not mean "may create a proposal" but "may execute anything the principal can" — - // the explicit list would be bypassed while looking narrow. - s.insert("proposal_create"); + // Creation stores proposed operations with EMPTY approvals; it cannot + // execute them. Deletion retains the evaluator's requester/veto checks. + // Updating can mint persistent account approvals, so it is never delegated. s.insert("proposal_update"); - s.insert("proposal_delete"); // Authority rotation. `account_update` is the sharp one: an op without the `master` // field is satisfied by the ACTIVE authority (see account_update_operation:: // get_required_active_authorities) and may carry a new `active` authority — so an diff --git a/libraries/protocol/include/graphene/protocol/agent_operations.hpp b/libraries/protocol/include/graphene/protocol/agent_operations.hpp index 0a7cec0df0..04ead94f2e 100644 --- a/libraries/protocol/include/graphene/protocol/agent_operations.hpp +++ b/libraries/protocol/include/graphene/protocol/agent_operations.hpp @@ -27,8 +27,9 @@ namespace graphene { namespace protocol { /// /// Rules enforced here and in the evaluator: /// - signed by the principal's ACTIVE authority; - /// - a transaction signed by an agent key passes only if every authority-requiring operation - /// in it is on that agent's list and nothing in it needs master or regular authority; + /// - an agent answers direct active/regular requirements only for explicitly listed + /// operations of its principal; master, other and nested authorities stay ordinary; + /// agent-dependent proofs are rechecked before each operation is applied; /// - never-delegable names are refused (see never_delegable_operation_names()); unknown /// or virtual names are refused too — a typo must not become a dead permission; /// - one key per agent, and a key may belong to one agent of the principal only; diff --git a/libraries/protocol/include/graphene/protocol/transaction.hpp b/libraries/protocol/include/graphene/protocol/transaction.hpp index 1981003371..3bed358a86 100644 --- a/libraries/protocol/include/graphene/protocol/transaction.hpp +++ b/libraries/protocol/include/graphene/protocol/transaction.hpp @@ -70,7 +70,8 @@ namespace graphene { const authority_getter &get_master, const authority_getter &get_regular, uint32_t max_recursion = CHAIN_MAX_SIG_CHECK_DEPTH, - const std::function& direct_active = {} + const std::function& direct_active = {}, + const std::function& direct_regular = {} ) const; void verify_authority( @@ -112,7 +113,8 @@ namespace graphene { const flat_set ®ular_approvals = flat_set(), const std::function& direct_active = {}, bool allow_unused = false, - flat_set* used = nullptr); + flat_set* used = nullptr, + const std::function& direct_operation = {}); struct annotated_signed_transaction : public signed_transaction { diff --git a/libraries/protocol/transaction.cpp b/libraries/protocol/transaction.cpp index 95c92b3db0..15f1a9455a 100644 --- a/libraries/protocol/transaction.cpp +++ b/libraries/protocol/transaction.cpp @@ -104,7 +104,8 @@ namespace graphene { const flat_set& regular_approvals, const std::function& direct_active, bool allow_unused, - flat_set* used + flat_set* used, + const std::function& direct_operation ) { try { fc::flat_set required_active; fc::flat_set required_master; @@ -118,6 +119,21 @@ namespace graphene { operation_get_required_authorities(op, required_active, required_master, required_regular, other); } + // Agent proofs cover only operations which directly require this principal. + // Keep them out of approved_by: nested account authorities and proposal + // approvals must continue to use ordinary account authority exclusively. + const auto check_delegated = [&](const account_name_type& id, bool regular, sign_state& state) { + if (!direct_operation) return false; + for (const auto& op : ops) { + flat_set a, m, r; + std::vector o; + operation_get_required_authorities(op, a, m, r, o); + if ((regular ? r : a).count(id) && !direct_operation(op, id, regular, state)) + return false; + } + return true; + }; + /** * Transactions with operations required regular authority cannot be combined * with transactions requiring active or master authority. This is for ease of @@ -137,12 +153,20 @@ namespace graphene { } for (const auto& id: required_regular) { - if (!s.check_authority(id) && - !s.check_authority(get_active(id)) && - !s.check_authority(get_master(id)) - ) { - missing_accounts.push_back(id); + sign_state ordinary = s; + if (ordinary.check_authority(id) || + ordinary.check_authority(get_active(id)) || + ordinary.check_authority(get_master(id))) { + s.provided_signatures = std::move(ordinary.provided_signatures); + s.approved_by = std::move(ordinary.approved_by); + continue; } + sign_state delegated = s; + if (check_delegated(id, true, delegated)) { + s.provided_signatures = std::move(delegated.provided_signatures); + continue; + } + missing_accounts.push_back(id); } CHAIN_CTOR_ASSERT( @@ -202,7 +226,8 @@ namespace graphene { continue; } sign_state delegated = s; - if (direct_active && direct_active(id, delegated)) { + if ((direct_active && direct_active(id, delegated)) || + check_delegated(id, false, delegated)) { s.provided_signatures = std::move(delegated.provided_signatures); s.approved_by = std::move(delegated.approved_by); continue; @@ -271,7 +296,8 @@ namespace graphene { const authority_getter &get_master, const authority_getter &get_regular, uint32_t max_recursion_depth, - const std::function& direct_active) const { + const std::function& direct_active, + const std::function& direct_regular) const { flat_set required_active; flat_set required_master; flat_set required_regular; @@ -286,7 +312,25 @@ namespace graphene { FC_ASSERT(!required_master.size()); FC_ASSERT(!required_active.size()); for (auto ®ular : required_regular) { - s.check_authority(regular); + if (!direct_regular) { + s.check_authority(regular); + continue; + } + sign_state ordinary = s; + if (ordinary.check_authority(regular) || + ordinary.check_authority(get_active(regular)) || + ordinary.check_authority(get_master(regular))) { + s.provided_signatures = std::move(ordinary.provided_signatures); + s.approved_by = std::move(ordinary.approved_by); + continue; + } + sign_state delegated = s; + if (direct_regular(regular, delegated)) { + s.provided_signatures = std::move(delegated.provided_signatures); + } else { + s.provided_signatures = std::move(ordinary.provided_signatures); + s.approved_by = std::move(ordinary.approved_by); + } } s.remove_unused_signatures(); @@ -316,7 +360,7 @@ namespace graphene { continue; } sign_state ordinary = s; - if (ordinary.check_authority(active)) { + if (ordinary.check_authority(active) || ordinary.check_authority(get_master(active))) { s.provided_signatures = std::move(ordinary.provided_signatures); s.approved_by = std::move(ordinary.approved_by); continue; diff --git a/plugins/database_api/api.cpp b/plugins/database_api/api.cpp index 8c167e1e5b..04146ad5aa 100755 --- a/plugins/database_api/api.cpp +++ b/plugins/database_api/api.cpp @@ -1,5 +1,6 @@ #include #include +#include #include #include @@ -794,10 +795,7 @@ std::set plugin::api_impl::get_required_signatures( const signed_transaction &trx, const flat_set &available_keys ) const { - flat_set candidate = available_keys; const auto signed_keys = trx.get_signature_keys(CHAIN_ID); - candidate.insert(signed_keys.begin(), signed_keys.end()); - const auto delegated = graphene::chain::delegated_active_authorities(database(), trx, CHAIN_ID, &candidate); const auto get_active = [&](const account_name_type& n) { return authority(database().get(n).active); }; @@ -807,28 +805,32 @@ std::set plugin::api_impl::get_required_signatures( const auto get_regular = [&](const account_name_type& n) { return authority(database().get(n).regular); }; - const auto direct = [&](const account_name_type& n, sign_state& state) { - const auto it = delegated.find(n); - return it != delegated.end() && state.signed_by(it->second); + // Discovery is not transaction acceptance: independent missing authorities + // must not discard usable contributions. Agent checks remain top-level only. + const auto delegated = [&](const account_name_type& principal, bool regular, sign_state& state) { + for (const auto& op : trx.operations) { + flat_set active, master, regular_auths; + std::vector other; + operation_get_required_authorities(op, active, master, regular_auths, other); + if (!(regular ? regular_auths : active).count(principal)) continue; + bool covered = false; + const auto& idx = database().get_index().indices().get(); + for (auto it = idx.lower_bound(boost::make_tuple(principal)); + it != idx.end() && it->account == principal; ++it) + if (graphene::chain::agent_operation_allowed(database(), op, principal, it->agent_key) && + state.signed_by(it->agent_key)) { + covered = true; + break; + } + if (!covered) return false; + } + return true; }; auto result = trx.get_required_signatures(CHAIN_ID, available_keys, - get_active, get_master, get_regular, - CHAIN_MAX_SIG_CHECK_DEPTH, direct); - // Complete candidates can take ordinary master fallback. Return only used - // available keys, never a failed partial active branch or existing signature. - flat_set used; - try { - graphene::protocol::verify_authority(trx.operations, candidate, - get_active, get_master, get_regular, CHAIN_MAX_SIG_CHECK_DEPTH, - false, {}, {}, {}, direct, true, &used); - result.clear(); - for (const auto& key : used) - if (available_keys.count(key)) result.insert(key); - } catch (const tx_missing_active_auth&) { - } catch (const tx_missing_master_auth&) { - } catch (const tx_missing_other_auth&) { - } catch (const tx_missing_regular_auth&) { - } + get_active, get_master, get_regular, CHAIN_MAX_SIG_CHECK_DEPTH, + [&](const account_name_type& n, sign_state& s) { return delegated(n, false, s); }, + [&](const account_name_type& n, sign_state& s) { return delegated(n, true, s); }); + for (const auto& key : signed_keys) result.erase(key); return result; } @@ -842,58 +844,46 @@ DEFINE_API(plugin, get_potential_signatures) { std::set plugin::api_impl::get_potential_signatures(const signed_transaction &trx) const { // wdump((trx)); std::set result; + // Discovery must include ordinary active/master fallback for regular + // transactions, not just the first regular getter visited by the legacy helper. + const auto collect = [&](const account_name_type& account, int role) { + const auto& row = database().get(account); + const authority auth(role == 0 ? row.active : role == 1 ? row.master : row.regular); + for (const auto& key : auth.get_keys()) result.insert(key); + return auth; + }; + flat_set required_active, required_master, required_regular; + std::vector required_other; + trx.get_required_authorities(required_active, required_master, required_regular, required_other); + for (const auto& account : required_active) { collect(account, 0); collect(account, 1); } + for (const auto& account : required_master) collect(account, 1); + for (const auto& account : required_regular) { + collect(account, 2); collect(account, 0); collect(account, 1); + } + // Candidate discovery must keep walking ordinary nested authorities even + // when an independent explicit key authority is currently unsatisfied. + // The legacy discovery helper does not stop at missing-authority errors. trx.get_required_signatures(CHAIN_ID, flat_set(), - [&](account_name_type account_name) { - const auto &auth = database().get(account_name).active; - for (const auto &k : auth.get_keys()) { - result.insert(k); - } - return authority(auth); - }, - [&](account_name_type account_name) { - const auto &auth = database().get(account_name).master; - for (const auto &k : auth.get_keys()) { - result.insert(k); - } - return authority(auth); - }, - [&](account_name_type account_name) { - const auto &auth = database().get(account_name).regular; - for (const auto &k : auth.get_keys()) { - result.insert(k); - } - return authority(auth); - }, - CHAIN_MAX_SIG_CHECK_DEPTH - ); + [&](const account_name_type& n) { return collect(n, 0); }, + [&](const account_name_type& n) { return collect(n, 1); }, + [&](const account_name_type& n) { return collect(n, 2); }, + CHAIN_MAX_SIG_CHECK_DEPTH); + // Explicit key authorities do not invoke an account getter. + for (const auto& auth : required_other) + for (const auto& key : auth.get_keys()) result.insert(key); if (database().has_hardfork(CHAIN_HARDFORK_15)) { - flat_set active, master, regular; - std::vector other; - trx.get_required_authorities(active, master, regular, other); - if (master.empty() && regular.empty()) { - flat_set names; - for (const auto& op : trx.operations) { - flat_set a, m, r; - std::vector o; - operation_get_required_authorities(op, a, m, r, o); - if (!a.empty() || !m.empty() || !r.empty() || !o.empty()) - names.insert(fc::resolve_operation_name(operation_wire_name(op))); - } - const auto& idx = database().get_index().indices().get(); + const auto& idx = database().get_index().indices().get(); + for (const auto& op : trx.operations) { + flat_set active, master, regular; + std::vector other; + operation_get_required_authorities(op, active, master, regular, other); + active.insert(regular.begin(), regular.end()); for (const auto& principal : active) { for (auto it = idx.lower_bound(boost::make_tuple(principal)); - it != idx.end() && it->account == principal; ++it) { - if (it->expiration != fc::time_point_sec() && - it->expiration <= database().head_block_time()) continue; - const auto grants = unpack_operation_names(it->operations); - bool allowed = !grants.empty(); - for (const auto& name : grants) - if (never_delegable_operation_names().count(name)) allowed = false; - for (const auto& name : names) - if (!grants.count(name)) allowed = false; - if (allowed) result.insert(it->agent_key); - } + it != idx.end() && it->account == principal; ++it) + if (graphene::chain::agent_operation_allowed(database(), op, principal, it->agent_key)) + result.insert(it->agent_key); } } } @@ -908,22 +898,7 @@ DEFINE_API(plugin, verify_authority) { } bool plugin::api_impl::verify_authority(const signed_transaction &trx) const { - const auto get_active = [&](const account_name_type& n) { - return authority(database().get(n).active); - }; - const auto get_master = [&](const account_name_type& n) { - return authority(database().get(n).master); - }; - const auto get_regular = [&](const account_name_type& n) { - return authority(database().get(n).regular); - }; - const auto delegated = graphene::chain::delegated_active_authorities(database(), trx, CHAIN_ID); - graphene::protocol::verify_authority(trx.operations, trx.get_signature_keys(CHAIN_ID), - get_active, get_master, get_regular, CHAIN_MAX_SIG_CHECK_DEPTH, - false, {}, {}, {}, [&](const account_name_type& n, sign_state& state) { - const auto it = delegated.find(n); - return it != delegated.end() && state.signed_by(it->second); - }); + graphene::chain::verify_agent_transaction(database(), trx, trx.get_signature_keys(CHAIN_ID)); return true; } diff --git a/tests/consensus_sim/scenarios/test_agent_access.cpp b/tests/consensus_sim/scenarios/test_agent_access.cpp index 30d3022331..1704450a26 100644 --- a/tests/consensus_sim/scenarios/test_agent_access.cpp +++ b/tests/consensus_sim/scenarios/test_agent_access.cpp @@ -19,6 +19,8 @@ #include #include #include +#include +#include #include #include #include @@ -307,8 +309,8 @@ BOOST_AUTO_TEST_CASE(agent_access_unknown_key_grants_nothing) { "a key that is not an agent of the principal signed for it"); } -// An agent's list is not a blank cheque: EVERY authority-requiring operation must be on it. -BOOST_AUTO_TEST_CASE(agent_access_requires_full_coverage_of_the_transaction) { +// An agent's list is not a blank cheque: every operation directly requiring its principal must be covered. +BOOST_AUTO_TEST_CASE(agent_access_requires_full_coverage_of_principal_operations) { agent_fixture f(0xAA9E19, "aa-coverage"); f.issue({"transfer"}); @@ -500,7 +502,7 @@ BOOST_AUTO_TEST_CASE(agent_access_master_change_wipes) { BOOST_CHECK_MESSAGE(!has_row(f.node, f.principal, f.bot), "master change left the agent"); } -// A regular-only change keeps the agents: they never stood on regular keys. +// A regular-only change keeps explicitly granted agents: master/active still own delegation policy. BOOST_AUTO_TEST_CASE(agent_access_regular_change_keeps_agents) { agent_fixture f(0xAA9E1E, "aa-keep-regular"); f.issue({"transfer"}); @@ -611,6 +613,119 @@ BOOST_AUTO_TEST_CASE(agent_access_cap_and_expired_sweep) { expect_rejected(f.node, sign_ops({op}, f.principal_key, f.node, 2), "agent over the cap accepted"); } +BOOST_AUTO_TEST_CASE(agent_access_direct_regular_and_scope) { + agent_fixture f(0xAC0001, "aa-feature-regular"); + f.issue({"custom", "account_metadata", "transfer"}); + custom_operation op; + op.required_regular_auths.insert(f.principal); + op.id = "feature-regular"; + op.json = "{}"; + BOOST_CHECK_NO_THROW(f.node.push_pending_transaction(sign_ops({op}, f.agent_key, f.node))); + produce(f.node, f.gp, f.when); + // Another principal's ungranted operation has its own ordinary proof. + transfer_to_vesting_operation tv; + tv.from = f.gp.initiator_name; + tv.to = f.gp.initiator_name; + tv.amount = asset(1000, TOKEN_SYMBOL); + auto scoped = sign_ops({f.pay(1000), tv}, f.agent_key, f.node); + scoped.sign(f.gp.initiator_key, f.node.chain_id()); + BOOST_CHECK_NO_THROW(f.node.push_pending_transaction(scoped)); + produce(f.node, f.gp, f.when); + tv.from = f.principal; + tv.to = f.principal; + expect_rejected(f.node, sign_ops({f.pay(1000), tv}, f.agent_key, f.node, 1), "own uncovered op accepted"); + expect_rejected(f.node, sign_ops({op, f.pay(1000)}, f.agent_key, f.node, 2), "mixed regular/active accepted"); +} + +BOOST_AUTO_TEST_CASE(agent_access_regular_proof_never_approves_nested_accounts) { + agent_fixture f(0xAC0002, "aa-feature-regular-nested"); + f.issue({"custom"}); + const auto second_key = derive_key("feature-nested-owner"); + create_account(f.node, f.gp, f.when, "second", second_key, 100000); + vest(f.node, f.gp, f.when, "second", second_key, 50000); + account_update_operation update; + update.account = "second"; + authority nested; + nested.weight_threshold = 1; + nested.account_auths[f.principal] = 1; + update.regular = nested; + f.node.push_pending_transaction(sign_ops({update}, second_key, f.node)); + produce(f.node, f.gp, f.when); + custom_operation op; + op.required_regular_auths = {f.principal, "second"}; + op.id = "nested-regular"; + op.json = "{}"; + expect_rejected(f.node, sign_ops({op}, f.agent_key, f.node), "regular agent became nested approval"); + BOOST_CHECK_NO_THROW(f.node.push_pending_transaction(sign_ops({op}, f.principal_key, f.node, 1))); +} + +BOOST_AUTO_TEST_CASE(agent_access_sale_wipe_invalidates_only_agent_proofs) { + agent_fixture f(0xAC0003, "aa-feature-apply-wipe"); + f.issue({"transfer"}); + put_on_sale(f); + // Past auction window: direct buy wipes all principal agents. + f.node.db().modify(f.node.db().get_account(f.principal), [&](account_object& a) { + a.account_on_sale_start_time = f.node.head_block_time() - fc::seconds(1); + }); + buy_account_operation buy_op; + buy_op.buyer = f.gp.initiator_name; + buy_op.account = f.principal; + buy_op.account_offer_price = asset(10000, TOKEN_SYMBOL); + buy_op.account_authorities_key = derive_key("feature-new-owner").get_public_key(); + buy_op.tokens_to_shares = f.node.db().get_validator_schedule_object().median_props.account_creation_fee; + auto agent_tx = sign_ops({buy_op, f.pay(1000)}, f.agent_key, f.node); + agent_tx.sign(f.gp.initiator_key, f.node.chain_id()); + BOOST_CHECK_NO_THROW(verify_agent_transaction(f.node.db(), agent_tx, + agent_tx.get_signature_keys(f.node.chain_id()))); + bool revoked = false; + const auto flags = f.node.db().validate_transaction(agent_tx, database::skip_apply_transaction); + // Exercise the actual chain-plugin read-validate -> returned flags -> write-push boundary. + try { f.node.db().push_transaction(agent_tx, flags); } + catch (const fc::exception& e) { + revoked = e.to_detail_string().find("Agent permission revoked before operation") != std::string::npos; + } + BOOST_CHECK_MESSAGE(revoked, "entry-time agent proof survived sale wipe or failed for an unrelated reason"); + BOOST_CHECK(has_row(f.node, f.principal, f.bot)); // rejection rolled back the buy + auto ordinary = sign_ops({buy_op, f.pay(1000)}, f.principal_key, f.node, 1); + ordinary.sign(f.gp.initiator_key, f.node.chain_id()); + const auto ordinary_flags = f.node.db().validate_transaction(ordinary, database::skip_apply_transaction); + BOOST_CHECK_NO_THROW(f.node.db().push_transaction(ordinary, ordinary_flags)); + produce(f.node, f.gp, f.when); + BOOST_CHECK(!has_row(f.node, f.principal, f.bot)); +} + +BOOST_AUTO_TEST_CASE(agent_access_proposals_create_no_approval) { + agent_fixture f(0xAC0004, "aa-feature-proposals"); + f.issue({"proposal_create", "proposal_delete"}); + proposal_create_operation create; + create.author = f.principal; + create.title = "agent-proposal"; + create.expiration_time = f.node.head_block_time() + fc::seconds(600); + create.proposed_operations.push_back(operation_wrapper{operation(f.pay(1000))}); + const auto before = liquid(f.node, f.principal); + BOOST_CHECK_NO_THROW(f.node.push_pending_transaction(sign_ops({create}, f.agent_key, f.node))); + produce(f.node, f.gp, f.when); + const auto* proposal = f.node.db().find_proposal(f.principal, create.title); + BOOST_REQUIRE(proposal); + BOOST_CHECK(proposal->available_active_approvals.empty()); + BOOST_CHECK(proposal->available_master_approvals.empty()); + BOOST_CHECK(proposal->available_regular_approvals.empty()); + BOOST_CHECK(proposal->available_key_approvals.empty()); + BOOST_CHECK_EQUAL(liquid(f.node, f.principal), before); + proposal_update_operation update; + update.author = f.principal; + update.title = create.title; + update.active_approvals_to_add.insert(f.principal); + expect_rejected(f.node, sign_ops({update}, f.agent_key, f.node, 1), "agent approved ungranted inner transfer"); + proposal_delete_operation remove; + remove.author = f.principal; + remove.title = create.title; + remove.requester = f.principal; + BOOST_CHECK_NO_THROW(f.node.push_pending_transaction(sign_ops({remove}, f.agent_key, f.node, 2))); + produce(f.node, f.gp, f.when); + BOOST_CHECK(!f.node.db().find_proposal(f.principal, create.title)); +} + // ───────────────────────── HF15 key history ───────────────────────── // Every key an account stops standing behind is kept forever: a DLT node has no past blocks, so // without these rows nobody could prove which key an account held when it signed something. diff --git a/tests/pm/agent_access_test.cpp b/tests/pm/agent_access_test.cpp index 2afaf0200e..a166a9a913 100644 --- a/tests/pm/agent_access_test.cpp +++ b/tests/pm/agent_access_test.cpp @@ -4,8 +4,8 @@ // 1. the op is APPENDED to the operation static_variant: its index (the consensus op-id, 105) // and the indices of its neighbours must not move, or old transactions re-interpret as new ops; // 2. validate() is the only gate against a delegation list that looks fine and does nothing -// (typo / virtual name) or does far more than it says (a proposal wrapper, which carries -// arbitrary operations whose authorities are collected at execution time). +// (typo / virtual name) or grants account takeover/persistent proposal approvals. +// Proposal creation/deletion alone does not grant approval of the inner operations. // // Not a consensus test: it links the protocol library only, no chain. @@ -95,11 +95,11 @@ BOOST_AUTO_TEST_CASE(validate_bounds_addons_only) { g.agent_key = public_key_type(); BOOST_CHECK(!accepts(g)); // addon-only still needs a key } -BOOST_AUTO_TEST_CASE(validate_refuses_escalation_and_wrappers) { +BOOST_AUTO_TEST_CASE(validate_refuses_escalation_but_accepts_safe_proposals) { BOOST_CHECK(!accepts(grant({"set_agent_permission"}))); // would let an agent re-delegate - BOOST_CHECK(!accepts(grant({"proposal_create"}))); // wraps arbitrary ops: bypasses the list - BOOST_CHECK(!accepts(grant({"proposal_update"}))); - BOOST_CHECK(!accepts(grant({"proposal_delete"}))); + BOOST_CHECK(accepts(grant({"proposal_create"}))); // creates no approval + BOOST_CHECK(!accepts(grant({"proposal_update"}))); // persistent approval is not delegated + BOOST_CHECK(accepts(grant({"proposal_delete"}))); // ordinary requester checks still apply // An active-signed account_update without the master field may rewrite the ACTIVE authority, // i.e. rotate it to a key the agent controls: one granted op would be ownership itself. BOOST_CHECK(!accepts(grant({"account_update"}))); @@ -212,4 +212,66 @@ BOOST_AUTO_TEST_CASE(ordinary_partial_active_then_master_keeps_legacy_acceptance }), tx_irrelevant_sig); } +BOOST_AUTO_TEST_CASE(direct_regular_discards_failed_ordinary_keys) { + const auto key = [](const char* seed) { + return public_key_type(fc::ecc::private_key::regenerate(fc::sha256::hash(std::string(seed))).get_public_key()); + }; + const auto partial = key("regular-partial"); + const auto absent = key("regular-absent"); + const auto agent = key("regular-agent"); + authority regular; + regular.weight_threshold = 2; + regular.key_auths[partial] = 1; + regular.key_auths[absent] = 1; + authority ordinary; + ordinary.weight_threshold = 1; + ordinary.key_auths[absent] = 1; + const auto get_regular = [&](const account_name_type&) { return regular; }; + const auto get_ordinary = [&](const account_name_type&) { return ordinary; }; + custom_operation op; + op.required_regular_auths.insert("alice"); + op.id = "regular-isolation"; + op.json = "{}"; + const auto direct = [&](const operation& operation, const account_name_type& principal, bool is_regular, sign_state& state) { + return operation_wire_name(operation) == "custom" && principal == "alice" && is_regular && state.signed_by(agent); + }; + BOOST_CHECK_NO_THROW(verify_authority({operation(op)}, {agent}, get_ordinary, get_ordinary, + get_regular, CHAIN_MAX_SIG_CHECK_DEPTH, false, {}, {}, {}, {}, false, nullptr, direct)); + BOOST_CHECK_THROW(verify_authority({operation(op)}, {partial, agent}, get_ordinary, get_ordinary, + get_regular, CHAIN_MAX_SIG_CHECK_DEPTH, false, {}, {}, {}, {}, false, nullptr, direct), tx_irrelevant_sig); +} + +BOOST_AUTO_TEST_CASE(discovery_keeps_independent_agent_and_ordinary_partial_contributions) { + const auto key = [](const char* seed) { + return public_key_type(fc::ecc::private_key::regenerate(fc::sha256::hash(std::string(seed))).get_public_key()); + }; + const auto agent = key("discovery-agent"); + const auto partial = key("discovery-partial"); + const auto absent = key("discovery-absent"); + authority auth; + auth.weight_threshold = 2; + auth.key_auths[partial] = 1; + auth.key_auths[absent] = 1; + const auto getter = [&](const account_name_type&) { return auth; }; + const auto direct = [&](const account_name_type& n, sign_state& state) { + return n == "alice" && state.signed_by(agent); + }; + signed_transaction tx; + custom_operation op; + op.required_active_auths = {"alice", "bob"}; + op.id = "partial-discovery"; + op.json = "{}"; + tx.operations = {operation(op)}; + const auto active = tx.get_required_signatures(CHAIN_ID, {agent, partial}, getter, getter, getter, + CHAIN_MAX_SIG_CHECK_DEPTH, direct); + BOOST_CHECK(active == std::set({agent, partial})); + op.required_active_auths.clear(); + op.required_regular_auths = {"alice", "bob"}; + tx.operations = {operation(op)}; + const auto regular = tx.get_required_signatures(CHAIN_ID, {agent, partial}, getter, getter, getter, + CHAIN_MAX_SIG_CHECK_DEPTH, {}, direct); + BOOST_CHECK(regular == std::set({agent, partial})); + BOOST_CHECK_THROW(verify_authority(tx.operations, {agent, partial}, getter, getter, getter), tx_missing_regular_auth); +} + BOOST_AUTO_TEST_SUITE_END()