Смарт-аккаунт: скрипт вместо одной подписи
Зачем это нужно
Обычный аккаунт работает по одному правилу: «подпись верна — пропускаем». В предыдущей главе мы написали это правило на RIDE и увидели, что оно стоит 182 единицы сложности.
А теперь главное: это правило можно заменить. Навесить на обычный аккаунт скрипт — и он начнёт пропускать транзакции по своему собственному условию. Две подписи из трёх. Никаких переводов до определённой даты. Только записи данных, но не деньги. Что угодно, что выражается в RIDE.
Такой аккаунт называется смарт-аккаунтом, и это самая недооценённая возможность Waves: в Ethereum ровно то же самое требует отдельного контракта-кошелька и куда больше кода.
Эта глава — про то, как это делается, сколько стоит и где подводные камни. Все ответы узла сняты на локальном узле Waves 1.6.4.
Скрипт-турникет
Начнём с очевидного правила — «переводы запрещены»:
{-# STDLIB_VERSION 6 #-}
{-# CONTENT_TYPE EXPRESSION #-}
{-# SCRIPT_TYPE ACCOUNT #-}
match tx {
case t: TransferTransaction => false
case _ => sigVerify(tx.bodyBytes, tx.proofs[0], tx.senderPublicKey)
}
сложность: 183
Ставим:
const { setScript } = require('@waves/waves-transactions');
const ss = setScript({ script: compiled.base64, chainId: 'R' }, seed);
в блоке: 30
Проверим, что было до и что стало после. До установки тот же перевод проходил:
ответ: 200
После:
код: 400
{"error":307,"message":"Transaction is not allowed by account-script"}
А запись данных по-прежнему проходит — её пропускает ветка case _:
код: 200 | в блоке: 30
Скрипт решает только про исходящие транзакции. Входящий перевод он остановить не может: транзакция подписана отправителем, а не вами, и ваш скрипт в ней не участвует. Задача «не принимать деньги» на смарт-аккаунте не решается — её решают смарт-активом.
Что видит скрипт
Внутри скрипта доступно одно значение — tx, сама транзакция. Что в нём есть, зависит от типа, поэтому почти каждый скрипт начинается с match.
| Поле | Что в нём |
|---|---|
tx.bodyBytes | тело транзакции в байтах — то, что подписано |
tx.proofs | массив подписей, до восьми мест |
tx.senderPublicKey | публичный ключ отправителя |
tx.fee | комиссия |
t.amount, t.recipient | у перевода — сумма и получатель |
d.data | у транзакции данных — список записей |
sigVerify(tx.bodyBytes, tx.proofs[0], tx.senderPublicKey) — это ровно поведение обычного аккаунта, записанное вручную. Полезно держать эту строку в голове как «ничего не изменилось»: любой скрипт-проверяющий отличается от неё только добавленными условиями.
Две подписи из трёх
Вот ради чего всё затевалось. Счёт, которым распоряжаются трое, и любые двое из них могут отправить транзакцию:
{-# STDLIB_VERSION 6 #-}
{-# CONTENT_TYPE EXPRESSION #-}
{-# SCRIPT_TYPE ACCOUNT #-}
let alice = base58'FmEEYNK6ZxyC…'
let boris = base58'8cbuFR33cJ9x…'
let sonya = base58'6ZwCUSYEc5CA…'
let a = if sigVerify(tx.bodyBytes, tx.proofs[0], alice) then 1 else 0
let b = if sigVerify(tx.bodyBytes, tx.proofs[1], boris) then 1 else 0
let s = if sigVerify(tx.bodyBytes, tx.proofs[2], sonya) then 1 else 0
a + b + s >= 2
сложность: 549
Одна подпись из трёх:
код: 400 | Transaction is not allowed by account-script
Две подписи:
код: 200 | принято | в блоке: 149
Две подписи, но в перепутанных местах — подпись Бориса положили в нулевое место, Алисы в первое:
код: 400 | Transaction is not allowed by account-script
Порядок в proofs — не формальность. Скрипт проверяет конкретное место конкретным ключом. Если подписант не участвует, на его место кладут пустую строку — это законное значение, означающее «этот не подписывал».
Доплата за сложность
При первой же попытке отправить транзакцию с такого счёта узел сказал неожиданное:
State check failed. Reason: Transaction sent from smart account. Requires 400000 extra fee.
Fee for TransferTransaction (100000 in WAVES) does not exceed minimal value of 500000 WAVES.
Правило такое: если сложность проверяющего больше 200, к комиссии каждой исходящей транзакции добавляется 0,004 WAVES. Проверяется это одним запросом:
curl .../addresses/scriptInfo/<адрес>
сложность проверяющего: 183 | доплата: 0
сложность проверяющего: 549 | доплата: 400000
Первый скрипт — «переводы запрещены», сложность 183, доплаты нет. Второй — две подписи из трёх, сложность 549, доплата есть.
Отсюда практический приём: если проверяющий слегка не укладывается в 200, его стоит подсократить — уложиться выгоднее, чем платить впятеро за каждую транзакцию. Например, valueOrElse стоит дороже, чем getIntegerValue, а лишний sigVerify — 182 единицы сам по себе.
Снять скрипт
Скрипт можно убрать — если он сам это разрешает:
const off = setScript({ script: null, chainId: 'R' }, seed);
код: 200
скрипт после снятия: {"script":null,"complexity":0,"extraFee":0,"publicKey":null}
Аккаунт снова стал обычным.
Скрипт может запретить сам себя снимать — достаточно ветки case s: SetScriptTransaction => false. Такой аккаунт останется со своим правилом навсегда. Это мощный приём и одновременно способ заблокировать счёт насмерть: проверяйте, что при отладке такая ветка не попала в код случайно.
Типичные ошибки
Ошибка 1. Забыть про доплату.
Transaction sent from smart account. Requires 400000 extra fee.
Библиотека считает комиссию по типу транзакции и про скрипт на счёте не знает. Ставить надо вручную.
Ошибка 2. Сдвигать подписи вместо пустых мест.
Подписант не участвует — на его месте пустая строка, а не отсутствие элемента. Сдвиг ломает соответствие.
Ошибка 3. Запретить всё, включая снятие скрипта.
match tx {
case _ => false // счёт заблокирован навсегда
}
Потерять так учебный счёт не страшно, рабочий — очень.
Ошибка 4. Ожидать, что скрипт остановит входящие.
Не остановит. Скрипт — про исходящие.
Ошибка 5. Думать, что отказ объяснится.
Transaction is not allowed by account-script — это всё, что будет сказано. Какое именно условие не сошлось, узел не сообщает: отлаживать приходится, упрощая скрипт по частям.
Шпаргалка по главе
| Что нужно | Как |
|---|---|
| Поставить скрипт | setScript({ script: base64, chainId }, seed) |
| Снять скрипт | setScript({ script: null, chainId }, seed) |
| Узнать сложность и доплату | GET /addresses/scriptInfo/<адрес> |
| Поведение обычного аккаунта | sigVerify(tx.bodyBytes, tx.proofs[0], tx.senderPublicKey) |
| Запретить вид транзакции | case t: TransferTransaction => false |
| Подписать несколькими ключами | signTx(tx, [{privateKey: a}, {privateKey: b}, null]) |
| Отметить «не подписывал» | пустая строка на своём месте в proofs |
| Доплата | 0,004 WAVES, если сложность проверяющего больше 200 |
Словарь главы
| Термин | Что это |
|---|---|
| Смарт-аккаунт | Обычный аккаунт со скриптом-проверяющим |
| Проверяющий | Скрипт-выражение, решающий про исходящие транзакции |
error: 307 | Скрипт аккаунта не разрешил транзакцию |
extraFee | Доплата за сложный проверяющий: 0,004 WAVES |
proofs[n] | Место в массиве подписей, закреплённое за конкретным ключом |
| Многоподписный счёт | Смарт-аккаунт, требующий N подписей из M |
SetScriptTransaction | Тип 13; им ставят и снимают скрипт |
Цель: скорость ≥ 90 зн/мин, точность ≥ 88%
sigVerify(tx.bodyBytes, tx.proofs[0], tx.senderPublicKey)
Что должен уметь после главы
- поставить скрипт на аккаунт и снять его;
- объяснить, что скрипт решает только про исходящие транзакции;
- написать проверяющего, запрещающего определённый вид транзакций;
- собрать счёт с двумя подписями из трёх и объяснить роль мест в
proofs; - посчитать доплату по сложности и узнать её через
scriptInfo; - назвать способ заблокировать счёт навсегда — и не сделать этого случайно.
Проверка по главе
Сводная проверка по всей главе: 10 вопросов, 5:00 на всё, без подсказок по ходу. Вопросы показываются по одному, ответ изменить нельзя. Если время выйдет — экзамен завершится с тем, что успел ответить. Пересдавать можно сколько угодно раз.
Куда дальше: проверенные ресурсы
- Смарт-аккаунты — что можно и чего нельзя.
- Скрипт аккаунта — правила и ограничения проверяющего.
- Комиссия за скрипт — когда появляется доплата.
- Waves IDE — редактор, в котором такие скрипты удобно отлаживать.
Комментарии
Обсуждение живёт в GitHub Discussions и грузится с чужого домена — поэтому только по нажатию.