ZYCORD docs
Русский

Кошелёк

Ключи, адреса и отправка — плюс поведенческий контракт, которому обязан отвечать любой кошелёк на этом протоколе. Каждое правило описывает способ потерять деньги, который протокол допускает и который кошелёк поэтому обязан предотвратить.

Команды#

zcd wallet new     --out KEYFILE                        # create an encrypted key file
zcd wallet address --key KEYFILE                        # show an address
zcd wallet balance --key KEYFILE                        # ask a node for a balance
zcd wallet send    --key KEYFILE --to ADDR --amount N
zcd wallet sweep   --key KEYFILE --to ADDR --one-shot
zcd wallet retire  --key KEYFILE [--addr ADDR]
zcd ui             --key KEYFILE                        # the same wallet in a browser

Графический кошелёк — не вторая реализация. zcd wallet, zcd ui и настольное приложение — это три интерфейса поверх одного wallet/session: каждый строит свой сертификат там, и проверки правил выполняются внутри этого вызова. Графический кошелёк поэтому структурно неспособен быть более снисходительным, чем CLI — не потому, что его автор был осторожен, а потому, что нет второго пути в коде, на котором он мог бы им быть.

Два вида адресов#

ВерсияВидДля чегоМногоразовый?
0x01ОдноразовыйОдин ожидаемый платёж. Выводите новый на каждый полученный платёж.Нет. Списание сжигает его навсегда.
0x02ПостоянныйПродавцы, адреса для пожертвований, горячие кошельки, выплаты за майнинг.Да, навсегда. Никогда не может попасть в реестр израсходованных.

Одноразовый против постоянного — это политика кошелька, вынесенная в бит адреса, а не два реестра. Кошельки по умолчанию берут 0x01 на каждый полученный платёж, потому что именно это делает платёж несвязываемым со следующим; кому нужен счёт, тот использует 0x02.

Восемь правил#

Каждое правило ниже описывает способ потерять деньги или комиссии, который протокол допускает — намеренно, потому что альтернатива была хуже — и который кошелёк поэтому обязан предотвратить. Эталонный кошелёк их реализует. Он не просто их документирует.

Правило 1 — подметайте ячейки целиком#

Расходуя с адреса 0x01, переводите весь баланс — получателю и на подконтрольный вам адрес сдачи. Сертификат несёт MARK_SPENT этого адреса, и после его применения любое чтение и любая запись под этим адресом навсегда завершаются отказом. Второй транзакции не будет.

Теперь цепочка это смягчает: всё, что сожжённый адрес ещё держит, при фиксации переходит в собственную ячейку RefundTo сертификата — и для нативного баланса адреса, и для каждой ячейки, которую сертификат сам называет. Так что неполное подметание больше не уничтожает остатки; оно доставляет их на ваш адрес сдачи.

Обязательство, которое с этим приходит

У сертификата один RefundTo, и он может сжечь одноразовые адреса, принадлежащие нескольким подписантам, поэтому остаток под вашей ячейкой может быть доставлен на их. Никогда не подписывайте совместно сертификат, который сжигает ваш одноразовый адрес и возвращает средства на адрес, который вам не подконтролен.

Чего это правило не спасёт: баланс в активе, который сертификат вовсе не упоминает. Активы под адресом не выводятся из слота, поэтому дотянуться до неназванного актива означало бы сканировать всю таблицу, где лежит каждая ячейка, на единственной стадии, которая не распараллеливается. Называйте каждый актив в сертификате, который сжигает адрес.

Правило 2 — RefundTo должен быть адресом, которым вы ещё сможете пользоваться#

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

Правило 3 — один адрес, один ожидаемый платёж#

Два обязательства, по одному на каждой стороне:

  • Получение. Выводите свежий адрес 0x01 на каждый ожидаемый платёж. Никогда не публикуйте один дважды. Не подметайте и не выводите из обращения адрес, который вы раскрыли в последние ttl_max блоков, если только не готовы к тому, что летящий к нему платёж будет пропущен со списанием с его отправителя.
  • Отправка. Откажитесь платить на одноразовый адрес, про который видно, что на него уже зачисляли или с него уже расходовали. Если получатель даёт вам адрес во второй раз, считайте это ошибкой, а не удобством.

Для всего, что оплачивается более одного раза — продавец, адрес для пожертвований, выплата за майнинг — используйте постоянный (0x02) адрес. Это единственное правило, которое убирает подверженность риску, а не сужает её.

Правило 4 — зависимые цепочки: дождитесь подтверждения или примите риск#

Увеличивайте Seq для каждого сертификата, зависящего от предыдущего. Внутри одного блока свёртка фиксирует сертификаты подписанта в порядке Seq, поэтому зависимая цепочка в одном блоке безопасна. Между блоками — нет: рассылайте Seq = n+1 только после того, как Seq = n подтверждён, либо примите то, что зависимый может быть зафиксирован в одиночку и пропущен. Это не дефект протокола — подписант принял риск устаревания, подписав.

Правило 5 — задавайте максимум щедро, а приоритет честно#

Каждый рынок принимает две цены. Максимум — это граница платёжеспособности: как только базовая комиссия её превысит, сертификат становится невключаемым и должен быть подписан заново. Приоритет — это то, что реально получает майнер.

  • Повышение максимума ничего не стоит в комиссиях — запас прочности бесплатен.
  • Но депозит резервирует gas × max, поэтому максимум задаёт, сколько баланса заперто под один шаг свёртки. Соразмеряйте его с реально доступным балансом.
  • Сертификату с длинным TTL нужен больший запас, чем сертификату с коротким, потому что у базовой комиссии больше блоков, чтобы сдвинуться.

Выход для небольших балансов — сокращать окно, а не безопасность: подписывайте с коротким TTL и переподписывайте по истечении, меняя блокировку средств на задержку. Неверный ответ — сохранить длинный TTL и урезать максимум, что делает сертификат зависающим ровно тогда, когда рынок сдвигается — в том самом случае, ради которого запас и был.

Правило 6 — сортируйте переводы канонически#

Протокол не навязывает порядок переводов внутри перевода, поэтому без канонического порядка повтор одного и того же логического платежа даёт другой идентификатор, и кошелёк не может отличить «уже отправлено» от «отправлено дважды». wallet.Transfer сортирует по активу, источнику, получателю, затем по сумме, так что повтор воспроизводит идентификатор.

Повтор не обязан воспроизводить подпись: прообраз идентификатора исключает список подписей, поэтому повтор, переподписанный со свежим одноразовым числом, — это тот же идентификатор, и сеть отвергает его как дубликат, каковым он и является. Идемпотентность — свойство, которое даёт протокол, каждому кошельку, а не только аккуратным.

Правило 7 — никогда не отдавайте семя#

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

Файлы ключей всегда зашифрованы: Argon2id по парольной фразе, AES-256-GCM по семени, и параметры обоих хранятся в файле, чтобы пользователь, потерявший доступ к этому бинарнику, мог восстановиться стандартной библиотекой любого языка. Флага, записывающего файл незашифрованным, нет. Парольные фразы читаются с терминала без эха, никогда из флага — парольная фраза в командной строке попадает в историю оболочки и в таблицу процессов.

Запись удерживает два свойства сразу, и оба про одно и то же — потерять одно значит потерять деньги:

  • Никогда не перезаписывать молча. Файл ключа, уже лежащий по адресу назначения, остаётся нетронутым, а запись завершается отказом.
  • Никогда не оставлять надорванный. Семя пишется во временный файл в том же каталоге, там же сбрасывается на диск через fsync, и только затем публикуется под окончательным именем одной файловой операцией.

Оба свойства держатся на каждой файловой системе, на которой измеряли CLI, включая FAT32 и exFAT — форматы, в которых с наибольшей вероятностью окажется USB-носитель для холодного хранения.

Зашифрованный файл ключа на носителе с FAT защищён парольной фразой и больше ничем

У FAT32 и exFAT нет битов прав доступа Unix, поэтому 0600, с которыми создаётся файл ключа, на них не сохраняются — решают параметры монтирования. В Linux fmask по умолчанию обычно оставляет файл читаемым всем; macOS монтирует такие тома с noowners, что показывает 0700 и не обеспечивает ничего. Выбирайте парольную фразу соответственно. У них также нет журнала, поэтому если публикация прервана на FAT и сообщает о сбое, проверьте адрес назначения, прежде чем запускать заново.

Правило 8 — отвергайте то, что отвергнет сеть#

Кошелёк, сообщающий об успехе там, где сеть может только отбросить, перенёс отказ туда, куда пользователь никогда не заглянет. Две формы этого, обе встреченные на практике:

  • Байты, которые ни один пир не может декодировать. Кодек — самостоятельная инстанция со своими правилами, которые проверяющий не повторяет. Теперь построитель утверждает это свойство напрямую: что бы он ни выдал, декодер это принимает.
  • Перевод, который свёртка может только пропустить. Перевод сверх баланса источника не нарушает ни одного правила, поэтому его принимает каждый узел — и ничто не отказывает производителю, который его всё же включит, после чего свёртка рассчитывает его по skip_fee, сожжённой из депозита и никому не выплаченной. Плохой случай — это не «никакого эффекта», это «комиссия сожжена, а стоимость никуда не сдвинулась».

zcd wallet send --force обходит именно этот отказ и ничего больше, потому что депозит, который ожидается в пределах окна TTL, делает тот же сертификат применимым, а кошелёк не видит будущего. Он не может сделать недействительный сертификат действительным; он может лишь подать действительный, который может быть пропущен — а пропуск не бесплатен.

Доверие узлу, который вы не запускали#

zcd и кошелёк не являются полными узлами. Они верят тому, что говорит им узел, а CLI, разговаривающий по RPC с узлом, который он сам не проверяет, вынужден доверять чему-то в ответах этого узла — это не ошибка, которую надо исправить, это то, чем кошелёк является. Три меры, каждая против конкретной лжи:

МераЧто ловит
--devnet / --testnet / --paramsОператор заявляет, для какой сети он намерен подписывать, и каждый опрошенный узел сверяется с этим заявлением. Идентичность сети никогда не берётся от одного лишь узла. Это относится и к balance, а не только к подписывающим командам.
--confirm-rpc NODEНазывает второй, независимый узел. Баланс каждого адреса и флаг израсходованности должны быть сообщены обоими одинаково, прежде чем что-либо продолжится. Узел, чей chain_id расходится, отвергается, а --confirm-rpc, называющий ту же точку, что уже названа в --rpc, отвергается сразу — узел, перепроверяющий сам себя, согласен сам с собой.
Подтверждение вводомПеред подачей zcd wallet sweep печатает точные числа и требует, чтобы вы набрали sweep. --yes пропускает только этот запрос и никогда — проверки выше него.

Контрольный список для реализации кошелька#

Если вы пишете кошелёк под этот протокол, вот контракт:

  • Расходование с 0x01 переводит весь баланс, считая резервирование депозита его частью — в том числе на программах, где переводов нет вовсе.
  • RETIRE отвергает цель, которая всё ещё держит баланс.
  • Сообщение узла о балансе не считается не подлежащим сомнению: идентичность сети заявлена оператором, второй источник поддаётся перекрёстной сверке, а точные числа подтверждаются перед необратимым подметанием.
  • RefundTo проверяется на постоянность или свежесть до подписания.
  • Получение выводит свежий адрес на каждый ожидаемый платёж; отправка отвергает одноразовый адрес, на который уже зачисляли или с которого уже расходовали.
  • Адреса продавцов и выплат по умолчанию — 0x02.
  • Seq увеличивается на каждый зависимый сертификат, и кошелёк ждёт подтверждения, прежде чем рассылать следующий — либо прямо говорит, что не ждёт.
  • Максимумы комиссий рассчитываются от доступного баланса и TTL, а не зашиты в код.
  • Переводы сортируются канонически, так что повтор воспроизводит идентификатор сертификата.
  • Файлы ключей зашифрованы, записываются надёжно и без молчаливой перезаписи, а парольные фразы никогда не попадают во флаг.
  • Не подписывается ничто, из чьей собственной кодировки нельзя декодировать обратно, и не подаётся ничто, чего не покрывает баланс источника — а любое переопределение названо, узко и отражено в предпросмотре.