Использование юботов


Написание кода юбота

Код юбота - это обычный код 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 (последний)