Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions @l10n/ru/docs/protocol/operations/agent-access.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
9 changes: 7 additions & 2 deletions docs/protocol/operations/agent-access.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
166 changes: 46 additions & 120 deletions libraries/chain/agent_evaluator.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<string> authority_requiring_operation_names(const signed_transaction& trx) {
flat_set<string> names;
for (const auto& op : trx.operations) {
flat_set<account_name_type> active, master, regular;
std::vector<authority> 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<account_authority_object, by_account>(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<account_name_type, public_key_type>
delegated_active_authorities(const database& db, const signed_transaction& trx,
const chain_id_type& chain_id,
const flat_set<public_key_type>* candidate_keys) {
fc::flat_map<account_name_type, public_key_type> delegated;

if (!db.has_hardfork(CHAIN_HARDFORK_15))
return delegated;

flat_set<account_name_type> required_active, required_master, required_regular;
std::vector<authority> 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<public_key_type> 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<string> tx_ops = authority_requiring_operation_names(trx);
const authority_getter get_active = plain_active_authority_getter(db);
const flat_set<public_key_type> no_extra_keys; // a validating node can produce no extra keys

const auto& pidx = db.get_index<agent_permission_index>().indices().get<by_permission_account>();
const time_point_sec now = db.head_block_time();
const flat_set<string>& 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<agent_permission_index>().indices().get<by_permission_account>();
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<string> 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<public_key_type>& keys, bool allow_unused,
flat_set<public_key_type>* used, agent_proofs* proofs) {
const auto get_active = [&](const account_name_type& n) {
return authority(db.get<account_authority_object, by_account>(n).active);
};
const auto get_master = [&](const account_name_type& n) {
return authority(db.get<account_authority_object, by_account>(n).master);
};
const auto get_regular = [&](const account_name_type& n) {
return authority(db.get<account_authority_object, by_account>(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<agent_permission_index>().indices().get<by_permission_account>();
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 ────────────────────────────────────────────────────
Expand Down
Loading
Loading