Перейти к содержанию

Мультиподпись в Solidity: полномочия издателя

Один внешний издательский ключ является единственным субъектом базового реестра. Редакция может вместо него выбрать контракт, который требует согласования нескольких участников. Тогда CourseCatalog продолжает сравнивать один адрес publisher, а правила принятия решения находятся внутри этого адреса.

Разберём собственный учебный PublicationCommittee: два разных подтверждения из трёх фиксированных участников разрешают одну публикацию. Это маленькая модель для локального изучения, не кошелёк для средств и не реализация Safe. Она не использует подписи вне цепочки, восстановление и плагины.

Один субъект, несколько подтверждений

Каждый член подтверждает определённое предложение. Предложение содержит courseId и digest, а не произвольное обещание «публиковать всё». Счётчик увеличивается один раз для каждого адреса. Иначе один участник мог бы нажать кнопку дважды и получить порог без второго человека.

Издателем нового реестра становится сам комитет. Чтобы не создавать неопределённость порядка настройки, комитет создаёт собственный CourseCatalog конструктором. Базовый исходник из первого курса сохраняется в соседнем файле без изменений.

Полная ограниченная модель

// SPDX-License-Identifier: MIT
pragma solidity 0.8.30;
import {CourseCatalog} from "./CourseCatalog.sol";

contract PublicationCommittee {
    error NotMember();
    error InvalidProposal();
    error AlreadyConfirmed();
    error NotReady();
    struct Proposal {
        uint256 courseId;
        bytes32 digest;
        uint256 approvals;
        bool executed;
    }
    CourseCatalog public immutable catalog;
    mapping(address => bool) public member;
    mapping(uint256 => Proposal) public proposals;
    mapping(uint256 => mapping(address => bool)) public confirmed;
    uint256 public proposalCount;

    constructor(address[3] memory members) {
        for (uint256 i; i < 3; ++i) {
            if (members[i] == address(0) || member[members[i]]) {
                revert InvalidProposal();
            }
            member[members[i]] = true;
        }
        catalog = new CourseCatalog(address(this));
    }
    modifier onlyMember() {
        if (!member[msg.sender]) revert NotMember();
        _;
    }
    function propose(uint256 id, bytes32 digest)
        external onlyMember returns (uint256 number)
    {
        if (id == 0 || digest == bytes32(0)) revert InvalidProposal();
        number = ++proposalCount;
        proposals[number] = Proposal(id, digest, 0, false);
    }
    function confirm(uint256 number) external onlyMember {
        Proposal storage p = proposals[number];
        if (p.courseId == 0 || p.executed) revert InvalidProposal();
        if (confirmed[number][msg.sender]) revert AlreadyConfirmed();
        confirmed[number][msg.sender] = true;
        ++p.approvals;
    }
    function execute(uint256 number) external onlyMember {
        Proposal storage p = proposals[number];
        if (p.approvals < 2 || p.executed) revert NotReady();
        p.executed = true;
        catalog.publish(p.courseId, p.digest);
    }
}

Состав проверяется на нулевые адреса и повтор. Он фиксирован: внешнего setter нет. Предложение получает новый номер и неизменные через интерфейс параметры. Подтверждение связано с номером и адресом. Принятый execute сначала помечает предложение, затем вызывает собственный известный реестр.

Порог не означает исполнение

После двух подтверждений публикация ещё не произошла: требуется execute. Предложение с approvals=2 и executed=false готово по нашему правилу, но состояние курса остаётся прежним до успешного вызова. Если исполнение откатится, пометка executed также не закрепится в этой области действия.

Реестр видит msg.sender равным комитету. Отдельный член напрямую не может вызвать его publish как издатель. Это и есть композиция: внутреннее согласование комитета даёт основание внешнему действию одного субъекта, который понятен реестру.

Шаг Подтверждения Состояние курса
propose(17,H1) 0 Не меняется
Первый confirm 1 Не меняется
Второй разный confirm 2 Не меняется
Успешный execute 2 и executed=true H1,true

Ограничения учебного комитета

В этом контракте нет отзыва предложения, срока и смены участников. Утрата двух субъектов из трёх может остановить новые действия. Также здесь нет метода деактивации: комитет служит примеру согласования публикации, а не полноценной системе управления исходным реестром. Эти ограничения должны быть видны до любой самостоятельной работы.

Два предложения для курса 17 могут быть исполнены в разном порядке. Последняя успешная публикация определит текущий hash. Порог сам не решает конкуренцию намерений. Если редакции нужен expectedHash, это дополнительное поле и условие, а не свойство слова multisig.

Фиксированное число адресов не доказывает независимость людей. Три аккаунта могут контролироваться одним устройством. Политика распределения субъектов находится в организации, а контракт сравнивает только значения. Аналогично компрометация двух участников соответствует порогу и разрешает действие по нашему коду.

Переход к настоящему инструменту управления

Полноценные смарт-аккаунты имеют собственные механизмы подписей, nonce, модулей, guard и изменения состава. Перед их выбором изучают фактические маршруты исполнения: некоторые расширения могут добавлять отдельные способы действий. Наличие порога в основной схеме не позволяет игнорировать всё подключённое управление.

В Safe порог согласования является частью модели смарт-аккаунта, а дополнительные компоненты имеют самостоятельные полномочия. Это повод читать выбранную конфигурацию, не копировать название продукта как доказательство достаточности. Наш собственный комитет служит только объяснению двух подтверждений и одного внешнего субъекта. Обзор устройства Safe помогает увидеть объём настоящей системы.

Для упражнения назначьте предложениям H1 и H2, согласуйте оба, затем поменяйте порядок execute. Запись меняется по порядку, хотя каждый порог корректен. Так мы отличаем согласование от времени действия. Следующий урок добавит ещё одну независимую границу — задержку административной операции.

Один участник и два адреса

Наш код требует разных адресов, а не разных людей. Если один человек управляет двумя членскими аккаунтами, он способен подтвердить порог самостоятельно. Для учебного исполнения это может быть удобно, но для модели организации означает другую независимость. Публичный контракт не знает распределения устройств и ответственности.

Также предложения могут быть многочисленными. Операция одного execute остаётся ограниченной и не проходит все proposals. Но постоянная история растёт. В полноценной системе добавляются обнаружение, отмена, срок и политика хранения. Мы не заменяем отсутствие этих возможностей общей надписью «безопасная мультиподпись».

Рассмотрим ошибку интерфейса: список показывает предложение 1, а подтверждение отправляется для номера 2. Подпись транзакции относится к фактическому number. Поэтому перед согласованием отображают точный courseId, полный digest и номер. Если участник не видит эти параметры, он не может осмысленно подтвердить намерение.

У нашего propose нет автоматического подтверждения автором. Сразу после создания approvals=0. Если интерфейс предположит единицу, он покажет готовность слишком рано. Это конкретный договор собственной модели, который может отличаться от другого инструмента управления. Сравнение названий кнопок не достаточно для переноса ожидаемого результата.

Можно также выполнить предложение с двумя подтверждениями спустя долгое время: срока нет. Если такое поведение нежелательно, expiresAt и его проверка должны стать частью proposal и execute. Тогда нужно решить, допускается ли обновление времени без новых подтверждений. Обычно согласованные параметры не следует менять незаметно после порога.

Получившееся упражнение показывает, как маленькая модель согласования порождает вопросы жизненного цикла. Каждый ответ относится к собственному коду и цели. Для средств и реальной администрации выбирают отдельно изученный инструмент, а этот комитет сохраняется как ограниченная лабораторная иллюстрация. При сохранении результата укажите адрес комитета и созданного им каталога отдельно. Подтверждения живут в первом состоянии, текущий hash — во втором. Их можно связать по выбранной композиции, но нельзя читать через взаимозаменяемые getter.

Для самостоятельного уточнения: smart-account-overview, contracts.html.

Оглавление курса. Листинги, расчёты и наблюдения здесь разобраны как ожидаемое поведение; компиляция, учебные программы и проверки при написании не запускались.

Оглавление серии