Page: Использование юботов
2021-05-29 14:05
Использование юботов
Написание кода юбота
Код юбота - это обычный код JavaScript U8, в котором можно определить любые функции, а затем использовать их в качестве точек входа для юбота.
async function getRandom(max) {
let result = ...
return result;
}
Можно использовать обычные доступные JavaScript объекты, а также через require
некоторые дополнительные специфичные для U8 объекты из U8 jslib. Однако код юбота выполняется в контролируемой (изолированной) среде, поэтому доступна только ограниченная часть U8 jslib:
- crypto.js
- FastPriorityQueue.js
- timers.js
- sorted.js
- tools.js
- exceptions.js
- big.js
- contract.js
- biserializable.js
- defaultbimapper.js
- bossbimapper.js
- transactionpack.js
- quantiser.js
- boss.js
- roles.js
- constraint.js
- permissions.js
- errors.js
- config.js
- contractdelta.js
- extendedsignature.js
- exceptions.js
- yaml.js
- keyrecord.js
- deltas.js
- buffer.js
- esprima.js
Есть также некоторые функции, которые доступны исключительно для кода юбота. Эти функции подробно описаны в разделе внутреннего API.
Хранилище исполнителей (кортеж)
Это распределенное хранилище, в которое каждый (для каждого исполнителя) экземпляр юбота может записывать свои данные. Это можно рассматривать как распределенное хранилище кортежа данных, где каждый элемент кортежа хранит данные ключ-значение одного экземпляра юбота (не конфликтуя с другими экземплярами). Таких хранилищ может быть несколько, и они идентифицируются по названию. По умолчанию хранилище называется default.
При записи в хранилище данные синхронизируются между экземплярами юбота в пуле. Если кворум получен для экземпляров юбота, которые получают одинаковое значение, синхронизация считается завершенной.
Пример работы с хранилищем:
async function calculateAverage() {
// получение случайного числа
let rnd = Math.random();
// добавление в сохраняемый кортеж
await writeMultiStorage({random: rnd});
// получение полного кортежа
let records = await getMultiStorage();
// вычисление среднего значения
let reduce = 0.0;
for (let r of records) {
reduce += r.random
}
// обратите внимание, что количество записей находится между значениями размера пула и его кворума
reduce /= records.length
...
}
Хранилище пула
Хранилище с привязкой к пулу - это то место, где все экземпляры юбота в пуле пытаются записать одни и те же данные, которые проверяются согласованно. Таких хранилищ может быть несколько, и они идентифицируются по названию. По умолчанию хранилище называется default.
При записи в хранилище данные синхронизируются между экземплярами юбота в пуле. Если кворум получен для экземпляров юбота, которые получают одинаковое значение, синхронизация считается завершенной.
Пример записи в хранилище:
async function calculateAverage() {
...
await writeSingleStorage({reduce : reduce});
return "ok";
}
Пример чтения из хранилища ранее записанных данных (может быть выполнено в другом облачном методе):
async function getAverage() {
return (await getSingleStorage()).reduce;
}
Локальное хранилище юбот-сервера
Локальное хранилище не синхронизируется между экземплярами юбота в пуле. Записанные в него данные хранятся на каждом юбот-сервере отдельно и недоступны для других юбот-серверов. Таких хранилищ может быть несколько, и они идентифицируются по названию. По умолчанию хранилище называется default.
Поскольку данные хранятся локально и недоступны другим юбот-серверам, данный тип хранилища удобно использовать для хранения закрытых ключей экземпляров юбота, как в примере ниже.
Пример записи в хранилище:
async function saveKey(privateKey) {
...
await writeLocalStorage({key : privateKey});
return "ok";
}
Пример чтения из хранилища ранее записанных данных (может быть выполнено в другом облачном методе, но обязательно на том же юбот-сервере):
async function getKey() {
return (await getLocalStorage()).key;
}
Управление смарт-контрактами
Можно создать и запечатать так называемый контракт пула, который будет идентичен для каждого экземпляра юбота в пуле.
async function operateContract() {
let contract = createPoolContract();
contract.seal();
...
}
Этот контракт очень похож на тот, который создается посредством new Contract(PrivateKey key)
в библиотеке Java. Разница в том, что:
- роль эмитента настроена на новую роль
QuorumVoteRole
, ссылающуюся на роль контракта реестра юбот-серверов,ubots
где перечислены все ключи юбот-серверов. Кворум роли устанавливается в соответствии с настройкой кворума пула. - временные метки одинаковы для всех экземпляров юбота.
- случайная соль, используемая при заключении контрактов без подписи, также одинакова для всех экземпляров юбота.
Примечание. К сожалению, еще невозможно создать расширенную подпись ExtendedSignature
(и поэтому добавить его в контракт). Это связано с тем, что сама ExtendedSignature
также содержит случайную соль, которая еще не синхронизирована между экземплярами юбота.
После этого можно зарегистрировать договор в сети Universa, предварительно упаковав его в TransactionPack
.
async function operateContract() {
...
let packedContract = await contract.getPackedTransaction();
await registerContract(packedContract);
}
Процедура регистрации состоит из двух этапов:
- экземпляры юбота добавляют свои подписи в
signatures
контракта, используя новую технику голосования; - регистрирация контракта в сети.
Добавление кода юбота в исполняемый контракт
Исполняемый контракт - это обычный контракт Universa, содержащий код юбота и описание облачных методов.
Код Ubot хранится в state.data.js
поле в виде строки.
Облачные методы описаны в state.data.cloud_methods
поле и имеют следующую структуру:
calculateAverage:
pool:
size: 5
quorum:
size: 4
getAverage:
pool:
size: 2
quorum:
size: 2
В описании облачного метода могут использоваться поля:
pool
- размер пула (обязательное поле), задается точным значением в полеsize
или в процентах от общего количества юбот-серверов в полеpercentage
;quorum
- размер кворума (обязательное поле), задается точным значением в полеsize
или в процентах от размера пула в полеpercentage
;max_wait_ubot
- максимальное время ожидания ответа эеземпляра юбота в секундах (необязательное поле, если не задано - ожидание пока не закончатся средства);launcher
- роль исполняемого контракта, для которой разрешен запуск облачного метода (необязательное поле, если не задано - запуск разрешен для всех);storage_read_trust_level
- доля от пула, достаточная для доверенного чтения из хранилища юбота (необязательное поле, если не задано - 0.3);readsFrom
- список имен хранилищ юбота разрешенных методу на чтение (необязательное поле, если не задано - разрешены все);writesTo
- список имен хранилищ юбота разрешенных методу на запись (необязательное поле, если не задано - разрешены все).
Также могут быть описаны пул и кворум хранилищ юбота в state.data.cloud_storages
по аналогии с пулом и кворумом облачных методов. Их размер не должен превышать размер пула и кворума облачных методов, в которых используются эти хранилища.
Подготовка контракта-заявки
Контракт-заявка - это контракт Universa, описывающая единичный запрос на выполнение. Его структура выглядит так:
state.data.executable_contract_id
содержит идентификатор исполняемого контракта;state.data.method_name
содержит имя выполняемого облачного метода;state.data.method_args
содержит параметры для передачи облачному методу;state.data.parent_session_id
содержит идентификатор родительской сессии для запуска метода (необязательное поле);state.data.predefined_pool
содержит список номеров юбот-серверов для пула, если список больше пула - юбот-сервера выбираются случайно из списка, если меньше - сначала используются юбот-сервера из списка, а оставшиеся выбираются случайным образом из всех юбот-серверов (необязательное поле);- сам исполняемый контракт добавляется к
referenced items
пакета транзакции (TransactionPack
).
Клиент Java
Выполнение облачного метода юбота
Client client = new Client("mainnet", null, yourPrivateKey);
//requestContract - контракт-заявка, описанный выше
//true - ожидание завершения предыдущей сессии для этого исполняемого контракта
//0.3f - уровень доверия. Чем выше уровень, тем больше экземпляров юбота должны получить одинаковый результат. Максимум - размер кворума, минимум - 1 экземпляр юбота.
client.executeCloudMethod(requestContract, true, 0.3f)
Получение результата выполнения
Результат возвращается executeCloudMethod
в поле result
.
Можно запросить у юбота фактическое состояние хранилищ, написав простой облачный метод:
async getStorages() {
return [getSingleStorage(), getMultiStorage()];
}
Затем в сети Universa можно получить хэши фактического состояния хранилищ юбота, с указанием идентификатора его исполняемого контракта.
client.command(
"ubotGetStorage",
"executableContractId", id,
"storageNames", Do.listOf("default_single", "default_multi")
);
Клиент JavaScript
UBotClient
предоставляет методы, которые позволяют выполнить облачный метод и получить результат, а также по отдельности: запустить облачный метод, дождаться его завершения и получить текущее состояние выполнения облачного метода.
Метод executeCloudMethod
запускает облачный метод и ожидает результаты от кворума пула экземпляров юбота (процесс создания сессии и запуска облачного метода подробно описан в методе startCloudMethod
):
async executeCloudMethod(requestContract, payment, waitPreviousSession = false)
@param {Contract} requestContract
- контракт-заявка.@param {Contract | null} payment
- платежный контракт.@param {boolean} waitPreviousSession
- ожидать завершения предыдущей сессии или вернуть её идентификатор и завершить работу. Если true - повторные попытки запустить облачный метод с интервалом в 1 секунду, если сеанс находится в РАБОЧЕМ режиме, или через 100 мс, если сеанс находится в режиме ЗАКРЫТИЕ. По умолчанию - false.@return {Promise<Object>}
- состояние облачного метода, собравшее консенсус пула экземпляров юбота, поля в объекте:state
- статус метода,result
- результат метода,errors
- список ошибок.@throws {UBotClientException}
- исключение клиента, если консенсус пула экземпляров юбота не достигнут.
Пример вызова:
let state = await ubotClient.executeCloudMethod(requestContract, payment, true);
Метод startCloudMethod
запрашивает создание сессии для запуска облачного метода с использованием контракта-заявки. По умолчанию создает сессию с идентификатором контракта-заявки или выдает исключение, если сессия уже создана для другого контракта-заявки в рамках того же исполняемого контракта. Если waitPreviousSession = true, тогда метод будет ждать, пока можно будет создать сессию с его идентификатором. Ожидает, пока сессии не будет присвоен идентификатор и на его основе будет вычислен пул. Затем он запрашивает контракт с реестром и топологией юбот-серверов, подключается к случайному экземпляру юбота из пула, на котором он запускает облачный метод.
async startCloudMethod(requestContract, payment, waitPreviousSession = false)
@param {Contract} requestContract
- контракт-заявка.@param {Contract | null} payment
- платежный контракт.@param {boolean} waitPreviousSession
- ожидать завершения предыдущей сессии или вернуть её идентификатор и завершить работу. Если true - повторные попытки запустить облачный метод с интервалом в 1 секунду, если сеанс находится в РАБОЧЕМ режиме, или через 100 мс, если сеанс находится в режиме ЗАКРЫТИЕ. По умолчанию - false.@return {UBotSession}
- сессия облачного метода.
Пример вызова:
let session = await ubotClient.startCloudMethod(requestContract, payment);
Метод waitCloudMethod
ожидает завершения облачного метода и возвращает его окончательное состояние с результатом облачного метода от одного экземпляра юбота из пула:
async waitCloudMethod(requestContractId, ubotNumber = undefined)
@param {HashId} requestContractId
- идентификатор контракта-заявки.@param {number} ubotNumber
- номер экземпляра юбота для запроса текущего состояния. По умолчанию - номер экземпляра юбота, подключенного в методеstartCloudMethod
.@return {Promise<Object>}
- состояние облачного метода, собравшее консенсус пула экземпляров юбота, поля в объекте:state
- статус метода,result
- результат метода,errors
- список ошибок.
Пример вызова:
state = await ubotClient.waitCloudMethod(requestContract.id);
Метод getStateCloudMethod
получает текущее состояние облачного метода от одного экземпляра юбота из пула. Возможные состояния выполения облачного метода: INIT, SEND_STARTING_CONTRACT, DOWNLOAD_STARTING_CONTRACT, START_EXEC, FINISHED, FAILED
. Если состояние равно FINISHED
, то результат выполнения облачного метода находится в возвращаемых данных. Статус FAILED
означает, что облачный метод завершился с ошибками, и их можно просмотреть в поле errors
результата.
async getStateCloudMethod(requestContractId, ubotNumber = undefined)
@param {HashId} requestContractId
- идентификатор контракта-заявки.@param {number} ubotNumber
- номер экземпляра юбота для запроса текущего состояния. По умолчанию - номер экземпляра юбота, подключенного в методеstartCloudMethod
.@return {Promise<Object>}
- состояние облачного метода, собравшее консенсус пула экземпляров юбота, поля в объекте:state
- статус метода,result
- результат метода,errors
- список ошибок.
Пример вызова:
let state = await ubotClient.getStateCloudMethod(requestContract.id, ubotNumber);
Внутренний API
Методы внутреннего API можно разделить на следующие группы:
- работа с контрактами;
- работа с хранилищем;
- HTTP-запрос;
- DNS-запрос;
- работа с контекстом экземпляра юбота;
- работа с транзакциями.
Методы API для работы с контрактами
Метод registerContract
регистрирует контракт, переданный как часть пакета транзакции (TransactionPack
).
async registerContract(packedTransaction, contractIdsForPoolSign = null)
@param {Uint8Array} packedTransaction
- пакет транзакции для регистрации.@param {Array<string>} contractIdsForPoolSign
- массив идентификаторов (как строка BASE64) контрактов для подписания пулом.@return {Promise<ItemResult>}
- результат регистрации или текущее состояние регистрации (если она еще не завершена).@throws {UBotQuantiserException}
достигнут предел квантайзера (всё средства, выделенные на выполение облачного метода, потрачены).@throws {UBotClientException}
исключение клиента, в случае ошибки регистрации контракта.
Метод createPoolContract
создания контракта пула. Контракт пула - специальный контракт, формируемый и регистрируемый пулом.
async createPoolContract()
@return {Promise<Contract>}
- контракт пула.
Метод sealPoolContract
запечатывания контракта пула.
async sealPoolContract(contract)
@param {Contract} contract
- контракт пула.@return {Promise<Uint8Array>}
- пакет транзакции с запечатанным контрактом пула.
Метод preparePoolRevision
создания новой ревизии контракта пула и подготовки её к регистрации пулом.
async preparePoolRevision(packedTransaction)
@param {Uint8Array} packedTransaction
- пакет транзакции с контрактом пула.@return {Promise<Contract>}
- пакет транзакции с новой ревизией контракта пула.
Метод sealAndGetPackedTransactionByPool
запечатывания контракта пула, передаваемого в составе пакета транзакции.
async sealAndGetPackedTransactionByPool(packedTransaction)
@param {Uint8Array} packedTransaction
- пакет транзакции с контрактом пула.@return {Promise<Contract>}
- пакет транзакции с запечатанным контрактом пула.
Методы API для работы с хранилищем
Метод writeSingleStorage
записывает данные в хранилище пула. При записи результата в хранилище значения синхронизируются между экземплярами юбота пула. Запись выполняется только при наличии кворума, в противном случае выдается исключение.
async writeSingleStorage(data, storageName = "default")
@param {*} data
- Данные для записи в хранилище пула. Данные могут быть примитивными типами JS или специальными типами U8, которые могут быть упакованы с помощью Boss.@param {string} storageName
- Имя хранилища. Необязательно, если не определено - используется хранилище по умолчанию.@return {Promise<void>}
@throws {UBotQuantiserException}
достигнут предел квантайзера (всё средства, выделенные на выполение облачного метода, потрачены).@throws {UBotProcessException}
исключение процесса, если не удается записать пустые данные в хранилище пула.
Метод getSingleStorage
получения данных из хранилища пула.
async getSingleStorage(storageName = "default")
@param {string} storageName
- Имя хранилища. Необязательно, если не определено - используется хранилище по умолчанию.@return {Promise<null|*>}
данные из хранилища пула или null, если хранилище пусто.@throws {UBotQuantiserException}
достигнут предел квантайзера (всё средства, выделенные на выполение облачного метода, потрачены).@throws {UBotClientException}
исключение клиента.
Метод writeMultiStorage
записи данных в хранилище исполнителей (кортеж).
Сбор значений, полученных пулом экземпляров юбота, выполняется в течение времени, указанного в поле max_wait_ubot
в метаданных облачного метода. При записи результата в хранилище значения синхронизируются между экземплярами юбота пула. Запись выполняется только при наличии кворума, в противном случае выдается исключение.
async writeMultiStorage(data, storageName = "default")
@param {*} data
- Данные для записи в хранилище исполнителей. Данные могут быть примитивными типами JS или специальными типами U8, которые могут быть упакованы с помощью Boss.@param {string} storageName
- Имя хранилища. Необязательно, если не определено - используется хранилище по умолчанию.@return {Promise<void>}
@throws {UBotQuantiserException}
достигнут предел квантайзера (всё средства, выделенные на выполение облачного метода, потрачены).@throws {UBotProcessException}
исключение процесса, если не удается записать пустые данные в хранилище исполнителей.
Метод getMultiStorage
получения данных из хранилища исполнителей.
async getMultiStorage(storageName = "default")
@param {string} storageName
- Имя хранилища. Необязательно, если не определено - используется хранилище по умолчанию.@return {Promise<null|*>}
данные из хранилища исполнителей или null, если хранилище пусто.@throws {UBotQuantiserException}
достигнут предел квантайзера (всё средства, выделенные на выполение облачного метода, потрачены).@throws {UBotClientException}
исключение клиента.
Метод writeLocalStorage
записи данных в локальное хранилище юбот-сервера.
Запись данных выполняется каждым экземпляром юбота независимо от других.
async writeLocalStorage(data, storageName = "default")
@param {*} data
- Данные для записи в локальное хранилище. Данные могут быть примитивными типами JS или специальными типами U8, которые могут быть упакованы с помощью Boss.@param {string} storageName
- Имя хранилища. Необязательно, если не определено - используется хранилище по умолчанию.@return {Promise<void>}
@throws {UBotProcessException}
исключение процесса, если не удается записать пустые данные в локальное хранилище.
Метод getLocalStorage
получения данных из локального хранилища юбот-сервера.
async getLocalStorage(storageName = "default")
@param {string} storageName
- Имя хранилища. Необязательно, если не определено - используется хранилище по умолчанию.@return {Promise<null|*>}
данные из локального хранилища или null, если хранилище пусто.@throws {UBotClientException}
исключение клиента.
HTTP-запрос
Метод doHTTPRequest
выполняет HTTP-запрос к внешней службе по URL-адресу.
async doHTTPRequest(url)
@param {string} url
- URL-адрес внешней службы.@return {Promise<{body: тело HTTP-ответа, response_code: код HTTP-ответа}>}
- HTTP-ответ.@throws {UBotQuantiserException}
достигнут предел квантайзера (всё средства, выделенные на выполение облачного метода, потрачены).
DNS-запрос
Метод doDNSRequests
выполняет DNS-запросы к внешнему хосту.
async doDNSRequests(host, port, requests)
@param {string} host
- Хост для отправки DNS-запроса.@param {number} port
- Порт для отправки DNS-запроса.@param {Array<Object>} requests
- Массив с DNS-запросами {name
: строка,type
: номер}.@return {Array<Object>}
- Массив с ответами DNS {type
: число,value
: строка}.@throws {UBotQuantiserException}
достигнут предел квантайзера (всё средства, выделенные на выполение облачного метода, потрачены).@throws {UBotProcessException}
ошибка запросов.
Методы API для работы с контекстом экземпляра юбота
Метод getRequestContract
получения контракта-заявки в пакете транзакции (TransactionPack
).
async getRequestContract()
@return {Promise<Uint8Array>}
- пакет транзакции с контрактом-заявкой.
Метод getUBotRegistryContract
получения контракта реестра юбот-серверов в пакете транзакции.
async getUBotRegistryContract()
@return {Promise<Uint8Array>}
- пакет транзакции с контрактом реестра юбот-серверов.
Метод getUBotNumber
получения номера юбот-сервера.
getUBotNumber()
@return {number}
- номер юбот-сервера.
Метод getUBotNumberInPool
получения индекса экземпляра юбота в пуле.
getUBotNumberInPool()
@return {number}
- индекс экземпляра юбота в пуле.
Метод poolRandom
получить псевдослучайное число, синхронизированное между экземплярами юбота в пуле.
poolRandom()
@return {number}
- псевдослучайное число от 0 до 1.
Методы API для работы с транзакциями
Метод startTransaction
запуска именованной транзакции.
async startTransaction(name, waitMillis = 0)
@param {string} name
- наименование транзакции.@param {number} waitMillis
- время ожидания транзакции в миллисекундах; 0 - бессрочно; по умолчанию 0.@return {Promise<boolean>}
- флаг успешного запуска транзакции.@throws {UBotClientException}
- ошибка юбот-клиента.
Метод finishTransaction
завершения именованной транзакции.
async finishTransaction(name, waitMillis = 0)
@param {string} name
- наименование транзакции.@param {number} waitMillis
- время ожидания транзакции в миллисекундах; 0 - бессрочно; по умолчанию 0.@return {Promise<boolean>}
- флаг успешного завершения транзакции.@throws {UBotClientException}
- ошибка юбот-клиента.
Сценарии использования
Генерация безопасных случайных чисел
В настоящее время многие атаки основываются на уязвимостях в генераторах случайных чисел. Случайное число может быть создано сетью юбот-серверов, каждый из которых вносит свой вклад в общую энтропию и следит за тем, чтобы его вклад был учтен. Результат будет намного более защищенным от предсказания и обеспечит более высокую уверенность в системах, использующих этот метод.
Чтобы получить безопасное случайное число, созданное сетью юбот-серверов, необходимо выполнить следующие шаги:
- Шаг 1. Создайте исполняемый контракт для генерации безопасного случайного числа с определенным пулом и кворумом, в примере pool = 5, quorum = 4:
let executableContract = Contract.fromPrivateKey(userPrivKey);
executableContract.state.data.cloud_methods = {
getRandom: {
pool: {size: 5},
quorum: {size: 4}
}
};
executableContract.state.data.js = `
const BigDecimal = require("big").Big;
const RND_LEN = 96;
async function getRandom(max) {
//генерация случайных чисел и запись их хешей в хранилище исполнителей
let rnd = new Uint8Array(RND_LEN);
for (let i = 0; i < RND_LEN; ++i)
rnd[i] = Math.floor(Math.random() * 256);
let hash = crypto.HashId.of(rnd).base64;
await writeMultiStorage({hash : hash});
//вычисление хеша от полученных хешей и запись его в хранилище пула
let records = await getMultiStorage();
let hashes = [];
for (let r of records)
hashes.push(r.hash);
hashes.sort();
let hashesHash = crypto.HashId.of(hashes.join()).base64;
await writeSingleStorage({hashesHash : hashesHash});
//добавление случайных чисел в хранилище исполнителей
await writeMultiStorage({hash : hash, rnd : rnd});
//проверка hashesOfHash и rnd -> hash
records = await getMultiStorage();
hashes = [];
let rands = [];
for (let r of records) {
if (r.hash !== crypto.HashId.of(r.rnd).base64)
throw new Error("Хеш не соответствует случайному числу");
hashes.push(r.hash);
rands.push(r.rnd);
}
hashes.sort();
hashesHash = crypto.HashId.of(hashes.join()).base64;
let singleStorage = await getSingleStorage();
if (hashesHash !== singleStorage.hashesHash)
throw new Error("Хеш от хешей не соответствует ранее сохранённому: " + hashesHash + "!==" + singleStorage.hashesHash);
let summRandom = new BigDecimal(0);
rands.forEach(random => {
let bigRandom = new BigDecimal(0);
random.forEach(byte => bigRandom = bigRandom.mul(256).add(byte));
summRandom = summRandom.add(bigRandom);
});
let result = Number.parseInt(summRandom.mod(max).toFixed());
await writeSingleStorage({hashesHash: hashesHash, result: result});
return result;
}`;
await executableContract.seal();
В приведенном выше примере пул выбирает случайное значение в диапазоне от нуля (включительно) до числа, указанного в аргументе метода getRandom
.
- Шаг 2. Подготовьте контракт-заявку для выполнения облачного метода, указав метод
getRandom
и его аргумент (максимальную границу случайного значения). В приведенном ниже примере это 1000. Следовательно, будет сгенерировано случайное значение в диапазоне от 0 до 999:
async function generateSecureRandomRequestContract(executableContract) {
let requestContract = Contract.fromPrivateKey(userPrivKey);
requestContract.state.data.method_name = "getRandom";
requestContract.state.data.method_args = [1000];
requestContract.state.data.executable_contract_id = executableContract.id;
await cs.addConstraintToContract(requestContract, executableContract, "executableContractConstraint",
Constraint.TYPE_EXISTING_STATE, ["this.state.data.executable_contract_id == ref.id"], true);
return requestContract;
}
- Шаг 3. Выполните запуск облачного метода генерации
getRandom
. Использование Java-клиента и JavaScript-клиента для запуска облачного метода описано выше.
Пример:
let state = await ubotClient.executeCloudMethod(requestContract, payment, true);
Лотерея
С помощью юботов вы можете реализовать лотерею с очень высоким уровнем доверия, используя распределенно сгенерированную случайную последовательность для выбора победителя, исключающую любую возможность мошенничества.
В этом разделе описан простейший пример лотереи, чтобы продемонстрировать основные этапы ее реализации. Каждый пользователь переводит определенное количество токена в кворум пула юбота, и его платеж привязан к определенному номеру билета и его ключу. Кроме того, при розыгрыше лотереи случайный билет выбирается с использованием безопасного случайного числа. Затем все платежи объединяются и полученный контракт с суммой всех пользовательских платежей переносится на ключ, связанный с номером выигрышного билета.
- Шаг 1. Создайте исполняемый контракт для лотереи с определенным пулом и кворумом, в примере для
buyTicket
pool = 3, quorum = 3 иraffle
pool = 12, quorum = 10:
let lotteryContract = Contract.fromPrivateKey(lotteryKey);
lotteryContract.state.data.cloud_methods = {
buyTicket: {
pool: {size: 3},
quorum: {size: 3}
},
raffle: {
pool: {size: 12},
quorum: {size: 10}
}
};
lotteryContract.state.data.js = JScode;
lotteryContract.state.data.tokenOrigin = origin;
lotteryContract.state.data.ticketPrice = "10";
await lotteryContract.seal();
Здесь JScode
это код, содержащий облачные методы лотереи. Описание каждого облачного метода, который является частью JScode
, будет рассмотрено ниже.
- Шаг 2: формирование оплаты за билеты
Чтобы купить билет, вам необходимо подготовить платежный контракт payment
. Платеж должен быть в виде токена, указанного в лотерее (state.data.tokenOrigin
), а также в указанной сумме (state.data.ticketPrice
).
let payment = await payment.createRevision([userKey]);
// кворумная роль пула
payment.registerRole(new roles.QuorumVoteRole("owner",
"refUbotRegistry.state.roles.ubots", "10", payment));
payment.registerRole(new roles.QuorumVoteRole("creator",
"refUbotRegistry.state.roles.ubots", "3", payment));
// ограничение на контракт реестра юбот-серверов
payment.createTransactionalSection();
let constr = new Constraint(payment);
constr.name = "refUbotRegistry";
constr.type = Constraint.TYPE_TRANSACTIONAL;
let conditions = {};
conditions[Constraint.conditionsModeType.all_of] =
["ref.tag == \"universa:ubot_registry_contract\""];
constr.setConditions(conditions);
payment.addConstraint(constr);
await payment.seal();
payment = await payment.getPackedTransaction();
Параметр userKey
это ключ владельца контракта payment
. В процессе подготовки платежа за билет, платеж переходит во владение роли кворума из десяти экземпляров юбота. А также в контракт payment
добавлено ограничение на контракт реестра юбот-серверов.
- Шаг 3: покупка билета
Облачный метод buyTicket
является частью JScode
:
async function buyTicket(packedPayment, userKey) {
// проверка платёжного контракта
let payment = await Contract.fromPackedTransaction(packedPayment);
let lotteryContract = ut.getExecutableContract(await Contract.fromPackedTransaction(await getRequestContract()));
if (!lotteryContract.state.data.tokenOrigin.equals(payment.getOrigin()))
return {error: "Неподходящий токен"};
if (lotteryContract.state.data.ticketPrice !== payment.state.data.amount)
return {error: "Ticket cost = " + lotteryContract.state.data.ticketPrice};
// кворумная роль пула
if (!(payment.roles.owner instanceof roles.QuorumVoteRole) ||
payment.roles.owner.source !== "refUbotRegistry.state.roles.ubots" ||
payment.roles.owner.quorum !== "10")
return {error: "Неверный владелец платёжного контракта. Должен быть QuorumVoteRole из 10 экземпляров юбота"};
let refUbotRegistry = payment.findConstraintByName("refUbotRegistry");
if (payment.transactional === null || refUbotRegistry === null ||
refUbotRegistry.type !== Constraint.TYPE_TRANSACTIONAL ||
!refUbotRegistry.assemblyConditions(refUbotRegistry.conditions).equals(
{all_of: ["ref.tag==\"universa:ubot_registry_contract\""]}
)
)
return {error: "Неверное ограничение реестра юбот-серверов: refUbotRegistry"};
// регистрация контракта оплаты билета
let ir = await registerContract(packedPayment);
if (ir.state !== ItemState.APPROVED.val)
return {error: "Платёжный контракт не зарегистрирован, состояние: " + ir.state};
// получение хранилища пула
let storage = await getSingleStorage();
let first = false;
if (storage == null || (!storage.hasOwnProperty("tickets") &&
!storage.hasOwnProperty("payments") && !storage.hasOwnProperty("userKeys")))
first = true;
// проверка хранилища
if (!first && (
!(storage.hasOwnProperty("tickets") &&
storage.hasOwnProperty("payments") &&
storage.hasOwnProperty("userKeys")) ||
storage.payments.length !== storage.tickets ||
storage.userKeys.length !== storage.tickets)
)
throw new Error("Ошибка проверки хранилища");
// получения номера билета, сохранение платёжного контракта и ключа пользователя, а также увеличение счётчика билетов
let ticket = 0;
if (!first) {
ticket = storage.tickets;
storage.payments.push(packedPayment);
storage.userKeys.push(userKey);
storage.tickets++;
} else {
if (storage == null)
storage = {};
storage.tickets = 1;
storage.payments = [packedPayment];
storage.userKeys = [userKey];
}
await writeSingleStorage(storage);
return ticket;
}
Параметр packedPayment
пакет транзакции (TransactionPack
) с оплатой, userKey
является ключом, по которому будет переведен выигрыш, если этот билет выиграет. Облачный метод проверяет платёжный контракт, а затем регистрирует его. Затем он определяет номер билета, сохраняет платёжный контракт и ключ пользователя в хранилище. Увеличивает счетчик проданных билетов.
Подготовьте контракт-заявку на выполнение облачного метода buyTicket
:
let buyContract = Contract.fromPrivateKey(userKey);
buyContract.state.data.method_name = "buyTicket";
buyContract.state.data.method_args = [payment, userKey.publicKey];
buyContract.state.data.executable_contract_id = lotteryContract.id;
await cs.addConstraintToContract(buyContract, lotteryContract, "executableContractConstraint",
Constraint.TYPE_EXISTING_STATE, ["this.state.data.executable_contract_id == ref.id"], true);
Выполните запрос на запуск buyTicket
:
let state = await ubotClient.executeCloudMethod(buyContract, U, true);
- Шаг 4: розыгрыш лотереи
Облачный метод raffle
является частью JScode
:
async function raffle() {
let result = {};
// получение хранилища пула
let storage = await getSingleStorage();
result.winTicket = await getRandom(storage.tickets);
// сборка платежей за билеты в призовой платёж
let payments = await Promise.all(storage.payments.map(payment => Contract.fromPackedTransaction(payment)));
let prize = (await createSplitJoin(payments, null, null, null, "amount"))[0];
// создание призового контракта как ревизии контракта пула (синхронизация state.createdAt и установка роли создателя QuorumVoteRole)
prize = await Contract.fromPackedTransaction(await preparePoolRevision(await prize.getPackedTransaction()));
// передача приза на ключ победителя
prize.registerRole(new roles.SimpleRole("owner", storage.userKeys[result.winTicket], prize));
await prize.seal(true);
storage.prizeContract = result.prizeContract = await sealAndGetPackedTransactionByPool(await prize.getPackedTransaction());
// сохранение призового контракта в хранилище
await writeSingleStorage(storage);
// регистрация призового контракта
let ir = await registerContract(result.prizeContract);
if (ir.state !== ItemState.APPROVED.val)
return {error: "Призовой контракт не зарегистрирован, состояние: " + ir.state};
return result;
}
Облачный метод raffle
получает случайный номер билета путём генерации безопасного случайного числа. Собирает выигрышный контракт, объединяя все платежи и меняя их владельца на ключ, связанный с номером выигрышного билета. Методы внутреннего API preparePoolRevision
и sealAndGetPackedTransactionByPool
используются для создания идентичного выигрышного контракта всеми экземплярами юбота в пуле. Они синхронизируют временные метки контракта, устанавливают роль создателя (как роль кворума пула) и запечатывают контракт. Выигравший контракт сохраняется в хранилище, регистрируется пулом и возвращается пользователям в поле prizeContract
результата облачного метода. Также в поле winTicket
возвращается номер выигрышного билета.
Подготовьте контракт-заявку на выполнение облачного метода raffle
и выполните запрос:
let raffleContract = Contract.fromPrivateKey(lotteryInitiatorPrivKey);
raffleContract.state.data.method_name = "raffle";
raffleContract.state.data.executable_contract_id = lotteryContract.id;
await cs.addConstraintToContract(raffleContract, lotteryContract,
"executableContractConstraint", Constraint.TYPE_EXISTING_STATE,
["this.state.data.executable_contract_id == ref.id"], true);
console.log("Розыгрыш лотереи...");
let state = await ubotClient.executeCloudMethod(raffleContract, U, true);
let prize = state.prizeContract;
console.log("Win ticket: " + state.winTicket);
Оплата юботов
Оплата за выполнение юбота производится при запуске облачных методов.
Средства, переведенные на оплату выполнения облачного метода, распределяются поровну между всеми экземплярами юбота в пуле.
Сумма, получаемая каждым экземпляром юбота, является его лимитом, в пределах которого оплачиваются все выполняемые им операции.
Когда лимит исчерпан (частота проверки задается параметром конфигурации checkQuantiserPeriod
), экземпляр юбота завершает свое выполнение с ошибкой UBotQuantiserException
.
Если по какой-либо причине один экземпляр юбота в пуле зависает и достигает своего лимита, это не мешает другим экземплярам собрать кворум и успешно завершить метод.
После завершения работы всех экземпляров пула оставшиеся в пределах лимита средства сжигаются. Для последующего выполнения облачного метода необходимо произвести новую оплату.
Тарифы юбота:
Действие | Стоимость |
---|---|
Запуск облачного метода | 50 квантов |
Минута процессорного времени, используемого экземпляром юбота | 100 квантов |
Минута ожидания (включая ожидание выполнения методов внутреннего API) экземпляра юбота | 10 квантов |
Запись в хранилище исполнителей значений, полученных из экземпляров юбота | 40 квантов |
Запись в хранилище пула значения, которое получили все экземпляры юбота | 20 квантов |
Получение значения из хранилища | 4 кванта |
Выполнение внешнего HTTP-запроса | 2 кванта |
200 квантов равны 1 U
.
Юбот-кошелек
С помощью юбота вы можете реализовать кошелек для хранения токенов. Одним из преимуществ юбот-кошелька является то, что все операции, выполняемые с его использованием, подтверждаются пулом экземпляров юбота. Благодаря этому юбот-кошелек имеет повышенный уровень безопасности по сравнению с обычными кошельками.
В этом разделе описывается простейший пример кошелька для хранения одного типа токена.
Создание кошелька
Для начала вам нужно создать юбот-кошелек. Для создания кошелька вы можете использовать метод API createWallet
, который создает исполняемый контракт кошелька:
async function createWallet(ownerKey, quorum, pool, gettingQuorum = 3, gettingPool = 4)
@param {PrivateKey} ownerKey
- приватный ключ владельца кошелька.@param {number} quorum
- кворум для операций передачи токенов.@param {number} pool
- пул для операций передачи токенов.@param {number} gettingQuorum
- кворум для операций запроса баланса кошелька. По умолчанию 3.@param {number} gettingPool
- пул для операций запроса баланса кошелька. По умолчанию 4.@return {Contract}
- исполняемый контракт кошелька, готовый для регистрации в сети Universa.
Вы также можете использовать методы класса UBotWallet
, такие как init()
и register(clientKey, payment)
, для создания кошелька.
Метод init()
инициализирует юбот-кошелек:
async init()
@return {number}
- возвращает стоимость регистрации исполняемого контракта кошелька в сети Universa.
Метод register(clientKey, payment)
регистрирует юбот-кошелек:
async register(clientKey, payment)
@param {PrivateKey} clientKey
- клиентский ключ для регистрации контракта-заявки с исполняемым контрактом кошелька.@param {Contract} payment
- платеж за регистрацию.
let wallet = new UBotWallet(walletKey, 10, 12);
let cost = await wallet.init();
let payment = await U.createRevision([UOwnerKey]);
payment.state.data.transaction_units = payment.state.data.transaction_units - cost;
await payment.seal();
await wallet.register(clientKey, payment);
В приведённом примере:
walletKey
- приватный ключ владельца кошелька;U
- контракт с U, достаточный для оплаты регистрации кошелька (стоимостьcost
);UOwnerKeys
- ключи с правом владения контрактомU
.
Добавление токена
Чтобы добавить токен в юбот-кошелек, вам необходимо подготовить токен для передачи его на роль кворума пула экземпляров юбота. Подготовка токена необходима, чтобы юбот-кошелек мог управлять этим токеном после его добавления. Чтобы подготовить токен, воспользуйтесь prepareToken
методом:
prepareToken(walletContract, token, tokenOwnerKeys)
@param {Contract} walletContract
- исполняемый контракт кошелька.@param {Contract} token
- токен, который нужно положить в кошелек.@param {Iterable<crypto.PrivateKey> | null} tokenOwnerKeys
- ключи с правом владения токеном.@return {Uint8Array}
- пакет транзакции (TransactionPack
) с контрактом токена, готовым к помещению в кошелек.
Подготовленный токен можно добавить в кошелек облачным методом putTokenIntoWallet
:
async function putTokenIntoWallet(packedToken)
@param {Uint8Array} packedToken
- пакет транзакции с контрактом токена, готовым к помещению в кошелек.@return {string | object}
- новый баланс кошелька (или объект, содержащий ошибку).
Вы также можете использовать методы класса UBotWallet
, такие как put()
:
async put(tokenContract, tokenOwnerKeys, payment)
@param {Contract} tokenContract
- токен добавлен в кошелек.@param {Iterable<crypto.PrivateKey> | null} tokenOwnerKeys
- ключи с правом владения токеном.@param {Contract} payment
- платеж за выполнение юбота.@return {string}
- баланс кошелька после добавления токена.
let balance = await wallet.put(tokenContract, tokenOwnerKeys, payment);
Проверка баланса
Чтобы проверить баланс кошелька, воспользуйтесь облачным методом getBalance
:
async function getBalance()
@return {string}
- текущий баланс кошелька.
Вы также можете использовать метод getBalance
класса UBotWallet
для проверки баланса:
async getBalance(payment)
@param {Contract} payment
- платеж за выполнение юбота.@return {string}
- текущий баланс кошелька.
balance = await wallet.getBalance(payment);
Получение операций
Вы также можете использовать инструменты для получения информации об операциях, выполненных юбот-кошельком.
Получить все операции
Чтобы получить список всех операций, выполненных юбот-кошельком, воспользуйтесь облачным методом getOperations
:
async function getOperations()
@return {Array<object>}
- список всех операций с кошельком (как объект {operation: {string}
"put" или "transfer",amount: {string}
количество токенов,recipient {string}
адрес получателя в виде строки (если операция "transfer") }).
Вы также можете использовать метод getOperations
класса UBotWallet
для получения списка всех операций, выполненных юбот-кошельком:
async getOperations(payment)
@param {Contract} payment
- платеж за выполнение юбота.@return {Array<object>}
- список всех операций с кошельком (как объект {operation: {string}
"put" или "transfer",amount: {string}
количество токенов,recipient {string}
адрес получателя в виде строки (если операция "transfer") }).
let operations = await wallet.getOperations(payment);
Получить последнюю операцию
Чтобы получить последнюю операцию, выполненную кошельком, используйте облачный метод getLastOperation
:
async function getLastOperation()
@return {object | null}
- последняя операция кошелька (как объект {operation: {string}
"put" или "transfer",amount: {string}
количество токенов,recipient {string}
адрес получателя в виде строки (если операция "transfer") }) илиnull
- если в кошелёк не выполнил ещё ни одной операции.
Вы также можете использовать метод getLastOperation
класса UBotWallet
. Метод получает последнюю операцию, выполненную юбот-кошельком:
async getLastOperation(payment)
@param {Contract} payment
- платеж за выполнение юбота.@return {object | null}
- последняя операция кошелька (как объект {operation: {string}
"put" или "transfer",amount: {string}
количество токенов,recipient {string}
адрес получателя в виде строки (если операция "transfer") }) илиnull
- если в кошелёк не выполнил ещё ни одной операции.
let lastOperation = await wallet.getLastOperation(payment);
Закрытие кошелька
Если вы использовали методы класса UBotWallet
для работы с кошельком, то вам необходимо закрыть клиент кошелька. Для этого воспользуйтесь методом close()
:
await wallet.close();
Версии библиотек
Библиотека для Java: 3.14.0
UMI (последний)