diff --git a/public/content/translations/ru/developers/tutorials/set-up-web3js-to-use-ethereum-in-javascript/index.md b/public/content/translations/ru/developers/tutorials/set-up-web3js-to-use-ethereum-in-javascript/index.md new file mode 100644 index 00000000000..66799126a3a --- /dev/null +++ b/public/content/translations/ru/developers/tutorials/set-up-web3js-to-use-ethereum-in-javascript/index.md @@ -0,0 +1,92 @@ +--- +title: "Настройка web3.js для использования блокчейна Ethereum в JavaScript" +description: "Узнайте, как настраивать и конфигурировать библиотеку web3.js для взаимодействия с блокчейном Ethereum из JavaScript-приложений." +author: "jdourlens" +tags: [ "web3.js", "javascript" ] +skill: beginner +lang: ru +published: 2020-04-11 +source: EthereumDev +sourceUrl: https://ethereumdev.io/setup-web3js-to-use-the-ethereum-blockchain-in-javascript/ +address: "0x19dE91Af973F404EDF5B4c093983a7c6E3EC8ccE" +--- + +В этом руководстве мы рассмотрим, как начать работу с [web3.js](https://web3js.readthedocs.io/), чтобы взаимодействовать с блокчейном Ethereum. Web3.js можно использовать как во фронтенде, так и в бэкенде, чтобы считывать данные из блокчейна, совершать транзакции и даже развертывать смарт-контракты. + +Первый шаг — это включить web3.js в ваш проект. Чтобы использовать его на веб-странице, вы можете импортировать библиотеку напрямую, используя CDN, например JSDeliver. + +```html + +``` + +Если вы предпочитаете установить библиотеку для использования в бэкенде или в проекте с фронтендом, использующим сборку, вы можете установить ее с помощью npm: + +```bash +npm install web3 --save +``` + +Затем, чтобы импортировать Web3.js в скрипт Node.js или фронтенд-проект Browserify, вы можете использовать следующую строку JavaScript: + +```js +const Web3 = require("web3") +``` + +Теперь, когда мы включили библиотеку в проект, нам нужно ее инициализировать. Ваш проект должен иметь возможность взаимодействовать с блокчейном. Большинство библиотек Ethereum взаимодействуют с [узлом](/developers/docs/nodes-and-clients/) через вызовы RPC. Чтобы инициализировать нашего провайдера Web3, мы создадим экземпляр Web3, передав в конструктор URL-адрес провайдера. Если у вас есть узел или [экземпляр ganache, запущенный на вашем компьютере](https://ethereumdev.io/testing-your-smart-contract-with-existing-protocols-ganache-fork/), это будет выглядеть так: + +```js +const web3 = new Web3("http://localhost:8545") +``` + +Если вы хотите получить прямой доступ к размещенному узлу, вы можете найти варианты на странице [«Узлы как услуга»](/developers/docs/nodes-and-clients/nodes-as-a-service). + +```js +const web3 = new Web3("https://cloudflare-eth.com") +``` + +Чтобы проверить, правильно ли мы настроили наш экземпляр Web3, мы попытаемся получить номер последнего блока с помощью функции `getBlockNumber`. Эта функция принимает в качестве параметра обратный вызов и возвращает номер блока в виде целого числа. + +```js +var Web3 = require("web3") +const web3 = new Web3("https://cloudflare-eth.com") + +web3.eth.getBlockNumber(function (error, result) { + console.log(result) +}) +``` + +Если вы выполните эту программу, она просто выведет номер последнего блока: вершину блокчейна. Вы также можете использовать вызовы функций `await/async`, чтобы избежать вложенных обратных вызовов в вашем коде: + +```js +async function getBlockNumber() { + const latestBlockNumber = await web3.eth.getBlockNumber() + console.log(latestBlockNumber) + return latestBlockNumber +} + +getBlockNumber() +``` + +Вы можете увидеть все функции, доступные в экземпляре Web3, в [официальной документации web3.js](https://docs.web3js.org/). + +Большинство библиотек Web3 асинхронны, потому что в фоновом режиме библиотека выполняет вызовы JSON-RPC к узлу, который отправляет обратно результат. + + + +Если вы работаете в браузере, некоторые кошельки напрямую внедряют экземпляр Web3, и вам следует пытаться использовать его по возможности, особенно если вы планируете взаимодействовать с адресом Ethereum пользователя для совершения транзакций. + +Вот фрагмент кода для обнаружения доступности кошелька MetaMask и попытки его включения. Это позволит вам в дальнейшем считывать баланс пользователя и дать ему возможность подтверждать транзакции, которые вы хотите, чтобы он совершал в блокчейне Ethereum: + +```js +if (window.ethereum != null) { + state.web3 = new Web3(window.ethereum) + try { + // При необходимости запросить доступ к аккаунту + await window.ethereum.enable() + // Аккаунты теперь доступны + } catch (error) { + // Пользователь отказал в доступе к аккаунту... + } +} +``` + +Существуют и широко используются альтернативы web3.js, такие как [Ethers.js](https://docs.ethers.io/). В следующем руководстве мы рассмотрим, [как легко отслеживать поступление новых блоков в блокчейне и видеть их содержимое](https://ethereumdev.io/listening-to-new-transactions-happening-on-the-blockchain/). diff --git a/public/content/translations/ru/developers/tutorials/short-abi/index.md b/public/content/translations/ru/developers/tutorials/short-abi/index.md new file mode 100644 index 00000000000..4a1b95464c6 --- /dev/null +++ b/public/content/translations/ru/developers/tutorials/short-abi/index.md @@ -0,0 +1,585 @@ +--- +title: "Краткие ABI для оптимизации calldata" +description: "Оптимизация смарт-контрактов для оптимистических ролл-апов" +author: Ori Pomerantz +lang: ru +tags: [ "уровень 2" ] +skill: intermediate +published: 2022-04-01 +--- + +## Введение {#introduction} + +В этой статье вы узнаете об [оптимистических ролл-апах](/developers/docs/scaling/optimistic-rollups), стоимости транзакций на них и о том, как эта иная структура затрат требует от нас оптимизации для других целей, чем в основной сети Ethereum. +Вы также узнаете, как реализовать эту оптимизацию. + +### Полное раскрытие информации {#full-disclosure} + +Я штатный сотрудник [Optimism](https://www.optimism.io/), поэтому примеры в этой статье будут работать на Optimism. +Однако описанная здесь техника должна работать так же хорошо и для других ролл-апов. + +### Терминология {#terminology} + +При обсуждении ролл-апов термин «уровень 1» (L1) используется для Mainnet, производственной сети Ethereum. +Термин «уровень 2» (L2) используется для ролл-апа или любой другой системы, которая полагается на L1 для обеспечения безопасности, но выполняет большую часть своей обработки вне сети. + +## Как мы можем еще больше снизить стоимость транзакций на L2? {#how-can-we-further-reduce-the-cost-of-L2-transactions} + +[Оптимистические ролл-апы](/developers/docs/scaling/optimistic-rollups) должны сохранять запись каждой исторической транзакции, чтобы любой мог просмотреть их и проверить правильность текущего состояния. +Самый дешевый способ передать данные в основную сеть Ethereum — это записать их как calldata. +Это решение было выбрано как [Optimism](https://help.optimism.io/hc/en-us/articles/4413163242779-What-is-a-rollup-), так и [Arbitrum](https://developer.offchainlabs.com/docs/rollup_basics#intro-to-rollups). + +### Стоимость транзакций на L2 {#cost-of-l2-transactions} + +Стоимость транзакций на L2 состоит из двух компонентов: + +1. Обработка на L2, которая обычно чрезвычайно дешева +2. Хранилище на L1, которое привязано к стоимости газа в Mainnet + +На момент написания этой статьи стоимость газа на L2 в Optimism составляет 0,001 [Gwei](/developers/docs/gas/#pre-london). +С другой стороны, стоимость газа на L1 составляет примерно 40 Gwei. +[Текущие цены можно посмотреть здесь](https://public-grafana.optimism.io/d/9hkhMxn7z/public-dashboard?orgId=1&refresh=5m). + +Байт calldata стоит либо 4 газа (если он нулевой), либо 16 газа (если это любое другое значение). +Одна из самых дорогостоящих операций в EVM — это запись в хранилище. +Максимальная стоимость записи 32-байтового слова в хранилище на L2 составляет 22 100 газа. На данный момент это 22,1 Gwei. +Таким образом, если мы сможем сэкономить хотя бы один нулевой байт calldata, мы сможем записать в хранилище около 200 байт и все равно остаться в выигрыше. + +### ABI {#the-abi} + +В подавляющем большинстве операций доступ к контракту осуществляется с внешнего аккаунта. +Большинство контрактов написаны на Solidity и интерпретируют поле данных в соответствии с [двоичным интерфейсом приложения (ABI)](https://docs.soliditylang.org/en/latest/abi-spec.html#formal-specification-of-the-encoding). + +Однако ABI был разработан для L1, где байт calldata стоит примерно столько же, сколько четыре арифметические операции, а не для L2, где байт calldata стоит более тысячи арифметических операций. +Calldata разделяется следующим образом: + +| Раздел | Длина | Байты | Потраченные впустую байты | Потраченный впустую газ | Необходимые байты | Необходимый газ | +| ---------------- | ----: | ----: | ------------------------: | ----------------------: | ----------------: | --------------: | +| Селектор функции | 4 | 0-3 | 3 | 48 | 1 | 16 | +| Нули | 12 | 4-15 | 12 | 48 | 0 | 0 | +| Адрес назначения | 20 | 16-35 | 0 | 0 | 20 | 320 | +| Сумма | 32 | 36-67 | 17 | 64 | 15 | 240 | +| Итого | 68 | | | 160 | | 576 | + +Пояснение: + +- **Селектор функции**: в контракте менее 256 функций, поэтому мы можем различать их по одному байту. + Эти байты обычно ненулевые, и поэтому [стоят шестнадцать газа](https://eips.ethereum.org/EIPS/eip-2028). +- **Нули**: эти байты всегда равны нулю, поскольку для хранения двадцатибайтового адреса не требуется тридцатидвухбайтовое слово. + Байты, которые содержат ноль, стоят четыре единицы газа ([см. Желтую книгу](https://ethereum.github.io/yellowpaper/paper.pdf), Приложение G, + стр. 27, значение для `G``txdatazero`). +- **Сумма**: если предположить, что в этом контракте `decimals` равно восемнадцати (стандартное значение), а максимальное количество токенов, которое мы переводим, составит 1018, то мы получим максимальную сумму в 1036. + 25615 > 1036, так что пятнадцати байт достаточно. + +Потеря 160 единиц газа на L1 обычно незначительна. Транзакция стоит как минимум [21 000 газа](https://yakkomajuri.medium.com/blockchain-definition-of-the-week-ethereum-gas-2f976af774ed), так что лишние 0,8% не имеют значения. +Однако на L2 все иначе. Почти вся стоимость транзакции — это ее запись на L1. +В дополнение к calldata транзакции существует 109 байт заголовка транзакции (адрес назначения, подпись и т. д.). +Таким образом, общая стоимость составляет `109*16+576+160=2480`, и мы тратим впустую около 6,5% от этой суммы. + +## Снижение затрат, когда вы не контролируете назначение {#reducing-costs-when-you-dont-control-the-destination} + +Если предположить, что у вас нет контроля над контрактом назначения, вы все равно можете использовать решение, подобное [этому](https://github.com/qbzzt/ethereum.org-20220330-shortABI). +Давайте рассмотрим соответствующие файлы. + +### Token.sol {#token-sol} + +[Это контракт назначения](https://github.com/qbzzt/ethereum.org-20220330-shortABI/blob/master/contracts/Token.sol). +Это стандартный контракт ERC-20 с одной дополнительной функцией. +Эта функция `faucet` позволяет любому пользователю получить немного токенов для использования. +Это сделало бы производственный контракт ERC-20 бесполезным, но это облегчает жизнь, когда ERC-20 существует только для упрощения тестирования. + +```solidity + /** + * @dev Дает вызывающему 1000 токенов для использования + */ + function faucet() external { + _mint(msg.sender, 1000); + } // функция faucet +``` + +### CalldataInterpreter.sol {#calldatainterpreter-sol} + +[Это контракт, который транзакции должны вызывать с более короткими calldata](https://github.com/qbzzt/ethereum.org-20220330-shortABI/blob/master/contracts/CalldataInterpreter.sol). +Давайте рассмотрим его построчно. + +```solidity +//SPDX-License-Identifier: Unlicense +pragma solidity ^0.8.0; + + +import { OrisUselessToken } from "./Token.sol"; +``` + +Нам нужна функция токена, чтобы знать, как ее вызывать. + +```solidity +contract CalldataInterpreter { + + OrisUselessToken public immutable token; +``` + +Адрес токена, для которого мы являемся прокси. + +```solidity + + /** + * @dev Укажите адрес токена + * @param tokenAddr_ адрес контракта ERC-20 + */ + constructor( + address tokenAddr_ + ) { + token = OrisUselessToken(tokenAddr_); + } // конструктор +``` + +Адрес токена — единственный параметр, который нам нужно указать. + +```solidity + function calldataVal(uint startByte, uint length) + private pure returns (uint) { +``` + +Считываем значение из calldata. + +```solidity + uint _retVal; + + require(length < 0x21, + "предел длины calldataVal — 32 байта"); + + require(length + startByte <= msg.data.length, + "calldataVal пытается прочитать за пределами calldatasize"); +``` + +Мы собираемся загрузить в память одно 32-байтовое (256-битное) слово и удалить байты, которые не являются частью нужного нам поля. +Этот алгоритм не работает для значений длиннее 32 байт, и, конечно же, мы не можем читать за концом calldata. +На L1 может быть необходимо пропустить эти тесты для экономии газа, но на L2 газ чрезвычайно дешев, что позволяет проводить любые проверки на вменяемость (sanity check), какие только можно придумать. + +```solidity + assembly { + _retVal := calldataload(startByte) + } +``` + +Мы могли бы скопировать данные из вызова в `fallback()` (см. ниже), но проще использовать [Yul](https://docs.soliditylang.org/en/v0.8.12/yul.html), язык ассемблера EVM. + +Здесь мы используем [опкод CALLDATALOAD](https://www.evm.codes/#35), чтобы считать байты с `startByte` до `startByte+31` в стек. +В общем, синтаксис опкода в Yul следующий: `<имя опкода>(<первое значение стека, если есть>,<второе значение стека, если есть>...)`. + +```solidity + + _retVal = _retVal >> (256-length*8); +``` + +Только самые старшие `length` байт являются частью поля, поэтому мы делаем [сдвиг вправо](https://en.wikipedia.org/wiki/Logical_shift), чтобы избавиться от других значений. +Это дает дополнительное преимущество, перемещая значение в правую часть поля, так что это само значение, а не значение, умноженное на 256что-то. + +```solidity + + return _retVal; + } + + + fallback() external { +``` + +Когда вызов контракта Solidity не соответствует ни одной из сигнатур функций, он вызывает [функцию `fallback()`](https://docs.soliditylang.org/en/v0.8.12/contracts.html#fallback-function) (если она есть). +В случае с `CalldataInterpreter` любой вызов попадает сюда, потому что других `external` или `public` функций нет. + +```solidity + uint _func; + + _func = calldataVal(0, 1); +``` + +Считываем первый байт calldata, который сообщает нам функцию. +Есть две причины, по которым эта функция может быть недоступна: + +1. Функции `pure` или `view` не изменяют состояние и не требуют газа (при вызове вне сети). + Нет смысла пытаться снизить их стоимость в газе. +2. Функции, которые полагаются на [`msg.sender`](https://docs.soliditylang.org/en/v0.8.12/units-and-global-variables.html#block-and-transaction-properties). + Значением `msg.sender` будет адрес `CalldataInterpreter`, а не вызывающего. + +К сожалению, [если посмотреть на спецификации ERC-20](https://eips.ethereum.org/EIPS/eip-20), остается только одна функция — `transfer`. +У нас остается только две функции: `transfer` (потому что мы можем вызвать `transferFrom`) и `faucet` (потому что мы можем перевести токены обратно тому, кто нас вызвал). + +```solidity + + // Вызов методов изменения состояния токена с использованием + // информации из calldata + + // faucet + if (_func == 1) { +``` + +Вызов `faucet()`, у которого нет параметров. + +```solidity + token.faucet(); + token.transfer(msg.sender, + token.balanceOf(address(this))); + } +``` + +После вызова `token.faucet()` мы получаем токены. Однако нам как прокси-контракту токены не **нужны**. +Они нужны EOA (внешнему аккаунту) или контракту, который нас вызвал. +Поэтому мы переводим все наши токены тому, кто нас вызвал. + +```solidity + // transfer (предполагается, что у нас есть разрешение на это) + if (_func == 2) { +``` + +Для перевода токенов требуются два параметра: адрес назначения и сумма. + +```solidity + token.transferFrom( + msg.sender, +``` + +Мы разрешаем вызывающим переводить только те токены, которыми они владеют + +```solidity + address(uint160(calldataVal(1, 20))), +``` + +Адрес назначения начинается с байта №1 (байт №0 — это функция). +Как адрес, он имеет длину 20 байт. + +```solidity + calldataVal(21, 2) +``` + +Для этого конкретного контракта мы предполагаем, что максимальное количество токенов, которое кто-либо захочет перевести, умещается в два байта (менее 65 536). + +```solidity + ); + } +``` + +В целом перевод занимает 35 байт calldata: + +| Раздел | Длина | Байты | +| ---------------- | ----: | ----: | +| Селектор функции | 1 | 0 | +| Адрес назначения | 32 | 1-32 | +| Сумма | 2 | 33-34 | + +```solidity + } // fallback + +} // контракт CalldataInterpreter +``` + +### test.js {#test-js} + +[Этот модульный тест на JavaScript](https://github.com/qbzzt/ethereum.org-20220330-shortABI/blob/master/test/test.js) показывает, как использовать этот механизм (и как проверить, что он работает правильно). +Я буду исходить из того, что вы понимаете [chai](https://www.chaijs.com/) и [ethers](https://docs.ethers.io/v5/) и объясню только те части, которые относятся непосредственно к контракту. + +```js +const { expect } = require("chai"); + +describe("CalldataInterpreter", function () { + it("Должен позволить нам использовать токены", async function () { + const Token = await ethers.getContractFactory("OrisUselessToken") + const token = await Token.deploy() + await token.deployed() + console.log("Адрес токена:", token.address) + + const Cdi = await ethers.getContractFactory("CalldataInterpreter") + const cdi = await Cdi.deploy(token.address) + await cdi.deployed() + console.log("Адрес CalldataInterpreter:", cdi.address) + + const signer = await ethers.getSigner() +``` + +Мы начинаем с развертывания обоих контрактов. + +```javascript + // Получаем токены для использования + const faucetTx = { +``` + +Мы не можем использовать высокоуровневые функции, которые обычно используем (например, `token.faucet()`) для создания транзакций, потому что мы не следуем ABI. +Вместо этого мы должны создать транзакцию самостоятельно, а затем отправить ее. + +```javascript + to: cdi.address, + data: "0x01" +``` + +Для транзакции необходимо указать два параметра: + +1. `to` — адрес назначения. + Это контракт-интерпретатор calldata. +2. `data` — отправляемые calldata. + В случае вызова faucet данные представляют собой один байт, `0x01`. + +```javascript + + } + await (await signer.sendTransaction(faucetTx)).wait() +``` + +Мы вызываем [метод `sendTransaction` подписанта](https://docs.ethers.io/v5/api/signer/#Signer-sendTransaction), потому что мы уже указали назначение (`faucetTx.to`), и нам нужно, чтобы транзакция была подписана. + +```javascript +// Проверяем, что faucet правильно предоставляет токены +expect(await token.balanceOf(signer.address)).to.equal(1000) +``` + +Здесь мы проверяем баланс. +Нет необходимости экономить на газе для функций `view`, поэтому мы просто запускаем их в обычном режиме. + +```javascript +// Даем CDI разрешение (одобрения не могут быть проксированы) +const approveTX = await token.approve(cdi.address, 10000) +await approveTX.wait() +expect(await token.allowance(signer.address, cdi.address)).to.equal(10000) +``` + +Даем интерпретатору calldata разрешение на выполнение переводов. + +```javascript +// Переводим токены +const destAddr = "0xf5a6ead936fb47f342bb63e676479bddf26ebe1d" +const transferTx = { + to: cdi.address, + data: "0x02" + destAddr.slice(2, 42) + "0100", +} +``` + +Создайте транзакцию перевода. Первый байт — «0x02», за ним следует адрес назначения и, наконец, сумма (0x0100, что равно 256 в десятичном формате). + +```javascript + await (await signer.sendTransaction(transferTx)).wait() + + // Проверяем, что у нас на 256 токенов меньше + expect (await token.balanceOf(signer.address)).to.equal(1000-256) + + // И что наш получатель получил их + expect (await token.balanceOf(destAddr)).to.equal(256) + }) // it +}) // describe +``` + +## Снижение затрат, когда вы контролируете контракт назначения {#reducing-the-cost-when-you-do-control-the-destination-contract} + +Если у вас есть контроль над контрактом назначения, вы можете создавать функции, которые обходят проверки `msg.sender`, поскольку они доверяют интерпретатору calldata. +[Вы можете увидеть пример того, как это работает, здесь, в ветке `control-contract`](https://github.com/qbzzt/ethereum.org-20220330-shortABI/tree/control-contract). + +Если бы контракт отвечал только на внешние транзакции, мы могли бы обойтись всего одним контрактом. +Однако это нарушило бы [компонуемость](/developers/docs/smart-contracts/composability/). +Гораздо лучше иметь контракт, который отвечает на обычные вызовы ERC-20, и другой контракт, который отвечает на транзакции с короткими данными вызовов. + +### Token.sol {#token-sol-2} + +В этом примере мы можем изменить `Token.sol`. +Это позволяет нам иметь ряд функций, которые может вызывать только прокси. +Вот новые части: + +```solidity + // Единственный адрес, которому разрешено указывать адрес CalldataInterpreter + address owner; + + // Адрес CalldataInterpreter + address proxy = address(0); +``` + +Контракт ERC-20 должен знать идентификатор авторизованного прокси. +Однако мы не можем установить эту переменную в конструкторе, поскольку еще не знаем ее значение. +Этот контракт создается первым, поскольку прокси-сервер ожидает адрес токена в своем конструкторе. + +```solidity + /** + * @dev Вызывает конструктор ERC20. + */ + constructor( + ) ERC20("Oris useless token-2", "OUT-2") { + owner = msg.sender; + } +``` + +Здесь хранится адрес создателя (называемого `owner`), поскольку это единственный адрес, разрешенный для установки прокси. + +```solidity + /** + * @dev установить адрес для прокси (CalldataInterpreter). + * Может быть вызван только один раз владельцем + */ + function setProxy(address _proxy) external { + require(msg.sender == owner, "Может быть вызван только владельцем"); + require(proxy == address(0), "Прокси уже установлен"); + + proxy = _proxy; + } // функция setProxy +``` + +Прокси имеет привилегированный доступ, поскольку может обходить проверки безопасности. +Чтобы убедиться, что мы можем доверять прокси, мы позволяем `owner` вызывать эту функцию, и только один раз. +Как только `proxy` имеет реальное значение (не нулевое), это значение не может измениться, поэтому даже если владелец решит стать мошенником или будет раскрыта мнемоника для него, мы все равно будем в безопасности. + +```solidity + /** + * @dev Некоторые функции могут быть вызваны только прокси. + */ + modifier onlyProxy { +``` + +Это [`модификатор` функции](https://www.tutorialspoint.com/solidity/solidity_function_modifiers.htm), он изменяет способ работы других функций. + +```solidity + require(msg.sender == proxy); +``` + +Сначала убедитесь, что вам позвонил прокси и никто другой. +Если нет, то `revert`. + +```solidity + _; + } +``` + +Если да, запустите функцию, которую мы модифицируем. + +```solidity + /* Функции, которые позволяют прокси фактически проксировать для аккаунтов */ + + function transferProxy(address from, address to, uint256 amount) + public virtual onlyProxy() returns (bool) + { + _transfer(from, to, amount); + return true; + } + + function approveProxy(address from, address spender, uint256 amount) + public virtual onlyProxy() returns (bool) + { + _approve(from, spender, amount); + return true; + } + + function transferFromProxy( + address spender, + address from, + address to, + uint256 amount + ) public virtual onlyProxy() returns (bool) + { + _spendAllowance(from, spender, amount); + _transfer(from, to, amount); + return true; + } +``` + +Для того чтобы сообщение пришло непосредственно от объекта, передающего токены или утверждающего разрешение, необходимы эти три операции. +Здесь у нас есть прокси-версия этих операций, которая: + +1. Изменяется с помощью `onlyProxy()`, поэтому никто другой не может ими управлять. +2. Получает адрес, который обычно выглядит как `msg.sender`, в качестве дополнительного параметра. + +### CalldataInterpreter.sol {#calldatainterpreter-sol-2} + +Интерпретатор calldata почти идентичен приведенному выше, за исключением того, что прокси-функции получают параметр `msg.sender` и нет необходимости в разрешении для `transfer`. + +```solidity + // transfer (разрешение не требуется) + if (_func == 2) { + token.transferProxy( + msg.sender, + address(uint160(calldataVal(1, 20))), + calldataVal(21, 2) + ); + } + + // approve + if (_func == 3) { + token.approveProxy( + msg.sender, + address(uint160(calldataVal(1, 20))), + calldataVal(21, 2) + ); + } + + // transferFrom + if (_func == 4) { + token.transferFromProxy( + msg.sender, + address(uint160(calldataVal( 1, 20))), + address(uint160(calldataVal(21, 20))), + calldataVal(41, 2) + ); + } +``` + +### Test.js {#test-js-2} + +Между предыдущим тестовым кодом и этим есть несколько изменений. + +```js +const Cdi = await ethers.getContractFactory("CalldataInterpreter") +const cdi = await Cdi.deploy(token.address) +await cdi.deployed() +await token.setProxy(cdi.address) +``` + +Нам нужно указать контракту ERC-20, какому прокси нужно доверять + +```js +console.log("Адрес CalldataInterpreter:", cdi.address) + +// Требуются два подписанта для проверки разрешений +const signers = await ethers.getSigners() +const signer = signers[0] +const poorSigner = signers[1] +``` + +Для проверки `approve()` и `transferFrom()` нам понадобится второй подписант. +Мы называем его `poorSigner`, потому что он не получает никаких наших токенов (хотя, конечно, у него должен быть ETH). + +```js +// Переводим токены +const destAddr = "0xf5a6ead936fb47f342bb63e676479bddf26ebe1d" +const transferTx = { + to: cdi.address, + data: "0x02" + destAddr.slice(2, 42) + "0100", +} +await (await signer.sendTransaction(transferTx)).wait() +``` + +Поскольку контракт ERC-20 доверяет прокси-серверу (`cdi`), нам не требуется разрешение на ретрансляцию передач. + +```js +// approval и transferFrom +const approveTx = { + to: cdi.address, + data: "0x03" + poorSigner.address.slice(2, 42) + "00FF", +} +await (await signer.sendTransaction(approveTx)).wait() + +const destAddr2 = "0xE1165C689C0c3e9642cA7606F5287e708d846206" + +const transferFromTx = { + to: cdi.address, + data: "0x04" + signer.address.slice(2, 42) + destAddr2.slice(2, 42) + "00FF", +} +await (await poorSigner.sendTransaction(transferFromTx)).wait() + +// Проверяем, что комбинация approve / transferFrom была выполнена правильно +expect(await token.balanceOf(destAddr2)).to.equal(255) +``` + +Протестируйте две новые функции. +Обратите внимание, что для `transferFromTx` требуются два параметра адреса: тот, кто выдал разрешение, и получатель. + +## Заключение {#conclusion} + +И [Optimism](https://medium.com/ethereum-optimism/the-road-to-sub-dollar-transactions-part-2-compression-edition-6bb2890e3e92), и [Arbitrum](https://developer.offchainlabs.com/docs/special_features) ищут способы уменьшить размер calldata, записываемых на L1, и, следовательно, стоимость транзакций. +Однако, поскольку мы являемся поставщиками инфраструктуры, ищущими универсальные решения, наши возможности ограничены. +Как разработчик децентрализованных приложений, вы обладаете знаниями, специфичными для приложения, что позволяет вам оптимизировать данные вызовов гораздо лучше, чем мы могли бы в обычном решении. +Надеемся, эта статья поможет вам найти идеальное решение для ваших нужд. + +[Больше моих работ смотрите здесь](https://cryptodocguy.pro/). + diff --git a/public/content/translations/ru/developers/tutorials/smart-contract-security-guidelines/index.md b/public/content/translations/ru/developers/tutorials/smart-contract-security-guidelines/index.md new file mode 100644 index 00000000000..abd1c8aeb62 --- /dev/null +++ b/public/content/translations/ru/developers/tutorials/smart-contract-security-guidelines/index.md @@ -0,0 +1,91 @@ +--- +title: "Рекомендации по обеспечению безопасности смарт-контрактов" +description: "Список рекомендаций по безопасности, которые следует учитывать при создании вашего децентрализованного приложения" +author: "Trailofbits" +tags: [ "твердость", "смарт-контракты", "безопасность" ] +skill: intermediate +lang: ru +published: 2020-09-06 +source: Building secure contracts +sourceUrl: https://github.com/crytic/building-secure-contracts/blob/master/development-guidelines/guidelines.md +--- + +Следуйте этим общим рекомендациям для создания более защищенных смарт-контрактов. + +## Руководство по дизайну {#design-guidelines} + +Разработку контракта следует обсуждать заранее, ещё до написания кода. + +### Документация и спецификации {#documentation-and-specifications} + +Документация может быть написана на разных уровнях и должна обновляться в процессе реализации контрактов: + +- **Описание системы на простом английском языке**, описывающее, что делают контракты, и любые допущения по кодовой базе. +- **Схемы и архитектурные диаграммы**, включая взаимодействия контрактов и конечный автомат системы. [Принтеры Slither](https://github.com/crytic/slither/wiki/Printer-documentation) могут помочь в создании этих схем. +- **Подробная документация кода**, для Solidity можно использовать [формат Natspec](https://docs.soliditylang.org/en/develop/natspec-format.html). + +### Ончейн- и офчейн-вычисления {#onchain-vs-offchain-computation} + +- **Старайтесь хранить как можно больше кода вне сети (офчейн).** Ончейн-уровень должен быть небольшим. Предварительно обрабатывайте данные с помощью кода офчейн таким образом, чтобы ончейн-проверка была простой. Вам нужен упорядоченный список? Сортируйте список офчейн, а затем просто проверяйте его порядок ончейн. + +### Возможность обновления {#upgradeability} + +Мы обсуждали различные решения для обновления в [нашей статье в блоге](https://blog.trailofbits.com/2018/09/05/contract-upgrade-anti-patterns/). Примите осознанное решение о поддержке возможности обновления, прежде чем писать какой-либо код. Это решение повлияет на то, как вы структурируете свой код. В целом мы рекомендуем: + +- **Отдавайте предпочтение [миграции контрактов](https://blog.trailofbits.com/2018/10/29/how-contract-migration-works/) перед возможностью обновления.** Системы миграции имеют многие из тех же преимуществ, что и обновляемые, но лишены их недостатков. +- **Используйте шаблон разделения данных вместо шаблона delegatecallproxy.** Если в вашем проекте есть четкое разделение абстракций, возможность обновления с использованием разделения данных потребует лишь нескольких корректировок. Шаблон delegatecallproxy требует экспертных знаний EVM и очень подвержен ошибкам. +- **Документируйте процедуру миграции/обновления перед развертыванием.** Если вам придется реагировать в стрессовой ситуации без каких-либо инструкций, вы совершите ошибки. Заранее опишите процедуру, которой нужно следовать. Она должна включать: + - Вызовы, которые инициируют новые контракты + - Где хранятся ключи и как получить к ним доступ + - Как проверить развертывание! Разработайте и протестируйте скрипт для запуска после развертывания. + +## Руководство по реализации {#implementation-guidelines} + +**Стремитесь к простоте.** Всегда используйте самое простое решение, которое соответствует вашей цели. Любой член вашей команды должен быть в состоянии понять ваше решение. + +### Композиция функций {#function-composition} + +Структура вашей кодовой базы должна облегчать просмотр вашего кода. Избегайте структуры, которая уменьшает возможность рассуждать о ее правильности. + +- **Разделяйте логику вашей системы** либо с помощью нескольких контрактов, либо путем группировки схожих функций (например, аутентификация, арифметика и т. д.). +- **Пишите небольшие функции с четкой целью.** Это облегчит проверку и позволит тестировать отдельные компоненты. + +### Наследование {#inheritance} + +- **Сделайте наследование управляемым.** Наследование следует использовать для разделения логики, однако в вашем проекте следует стремиться к минимизации глубины и ширины дерева наследования. +- **Используйте [принтер наследования Slither](https://github.com/crytic/slither/wiki/Printer-documentation#inheritance-graph) для проверки иерархии контрактов.** Принтер наследования поможет вам проверить размер иерархии. + +### События {#events} + +- **Записывайте в журнал все важные операции.** События помогут отладить контракт во время разработки и отслеживать его работу после развертывания. + +### Избегайте известных подводных камней {#avoid-known-pitfalls} + +- **Помните о наиболее распространенных проблемах безопасности.** Существует множество онлайн-ресурсов для изучения распространенных проблем, таких как [Ethernaut CTF](https://ethernaut.openzeppelin.com/), [Capture the Ether](https://capturetheether.com/) или [Not so smart contracts](https://github.com/crytic/not-so-smart-contracts/). +- **Обращайте внимание на разделы с предупреждениями в [документации Solidity](https://docs.soliditylang.org/en/latest/).** Разделы с предупреждениями сообщат вам о неочевидном поведении языка. + +### Зависимости {#dependencies} + +- **Используйте хорошо протестированные библиотеки.** Импорт кода из хорошо протестированных библиотек снизит вероятность написания кода с ошибками. Если вы хотите написать контракт ERC20, используйте [OpenZeppelin](https://github.com/OpenZeppelin/openzeppelin-contracts/tree/master/contracts/token/ERC20). +- **Используйте менеджер зависимостей; избегайте копирования и вставки кода.** Если вы полагаетесь на внешний источник, вы должны поддерживать его в актуальном состоянии в соответствии с оригинальным источником. + +### Тестирование и верификация {#testing-and-verification} + +- **Пишите подробные модульные тесты.** Обширный набор тестов имеет решающее значение для создания высококачественного программного обеспечения. +- **Пишите пользовательские проверки и свойства для [Slither](https://github.com/crytic/slither), [Echidna](https://github.com/crytic/echidna) и [Manticore](https://github.com/trailofbits/manticore).** Автоматизированные инструменты помогут обеспечить безопасность вашего контракта. Ознакомьтесь с остальной частью этого руководства, чтобы узнать, как писать эффективные проверки и свойства. +- **Используйте [crytic.io](https://crytic.io/).** Crytic интегрируется с GitHub, предоставляет доступ к частным детекторам Slither и запускает пользовательские проверки свойств от Echidna. + +### Solidity {#solidity} + +- **Отдавайте предпочтение Solidity 0.5 перед 0.4 и 0.6.** По нашему мнению, Solidity 0.5 является более безопасным и имеет лучшие встроенные практики, чем 0.4. Solidity 0.6 оказался слишком нестабильным для производственного использования и ему нужно время, чтобы «созреть». +- **Используйте стабильный релиз для компиляции; используйте последний релиз для проверки предупреждений.** Убедитесь, что ваш код не имеет зарегистрированных проблем с последней версией компилятора. Однако у Solidity быстрый цикл релизов и есть история ошибок компилятора, поэтому мы не рекомендуем использовать последнюю версию для развертывания (см. [рекомендацию по версии solc от Slither](https://github.com/crytic/slither/wiki/Detector-Documentation#recommendation-33)). +- **Не используйте встроенный ассемблер.** Ассемблер требует экспертных знаний EVM. Не пишите код для EVM, если вы не _освоили_ «желтую книгу». + +## Руководство по развертыванию {#deployment-guidelines} + +После разработки и развертывания контракта: + +- **Отслеживайте свои контракты.** Следите за журналами и будьте готовы отреагировать в случае компрометации контракта или кошелька. +- **Добавьте свою контактную информацию в [blockchain-security-contacts](https://github.com/crytic/blockchain-security-contacts).** Этот список поможет третьим лицам связаться с вами в случае обнаружения уязвимости в системе безопасности. +- **Обезопасьте кошельки привилегированных пользователей.** Следуйте нашим [лучшим практикам](https://blog.trailofbits.com/2018/11/27/10-rules-for-the-secure-use-of-cryptocurrency-hardware-wallets/), если вы храните ключи в аппаратных кошельках. +- **Имейте план реагирования на инциденты.** Учитывайте, что ваши умные контракты могут быть скомпрометированы. Даже если в ваших контрактах нет ошибок, злоумышленник может завладеть ключами владельца контракта. diff --git a/public/content/translations/ru/developers/tutorials/stealth-addr/index.md b/public/content/translations/ru/developers/tutorials/stealth-addr/index.md new file mode 100644 index 00000000000..8d76c475472 --- /dev/null +++ b/public/content/translations/ru/developers/tutorials/stealth-addr/index.md @@ -0,0 +1,443 @@ +--- +title: "Использование скрытых адресов" +description: "Скрытые адреса позволяют пользователям анонимно переводить активы. Прочитав эту статью, вы сможете: объяснить, что такое скрытые адреса и как они работают, понять, как использовать скрытые адреса для сохранения анонимности, а также написать веб-приложение, использующее скрытые адреса." +author: Ori Pomerantz +tags: + [ + "Скрытый адрес", + "конфиденциальность", + "криптография", + "rust", + "wasm" + ] +skill: intermediate +published: 2025-11-30 +lang: ru +sidebarDepth: 3 +--- + +Вы — Билл. По причинам, которые мы не будем рассматривать, вы хотите сделать пожертвование в кампанию "Алиса — королева мира" и хотите, чтобы Алиса знала о вашем пожертвовании, чтобы она вознаградила вас в случае своей победы. К сожалению, ее победа не гарантирована. Существует конкурирующая кампания "Кэрол — императрица Солнечной системы". Если Кэрол победит и узнает, что вы пожертвовали Алисе, у вас будут проблемы. Поэтому вы не можете просто перевести 200 ETH со своего аккаунта на аккаунт Алисы. + +Решение предлагает [ERC-5564](https://eips.ethereum.org/EIPS/eip-5564). Этот ERC объясняет, как использовать [скрытые адреса](https://nerolation.github.io/stealth-utils) для анонимных переводов. + +**Предупреждение**. Криптография, лежащая в основе скрытых адресов, насколько нам известно, надежна. Однако существуют потенциальные атаки по побочным каналам. [Ниже](#go-wrong) вы увидите, что можно сделать, чтобы снизить этот риск. + +## Как работают скрытые адреса {#how} + +В этой статье мы попытаемся объяснить скрытые адреса двумя способами. Первый — [как их использовать](#how-use). Этой части достаточно, чтобы понять остальную часть статьи. Затем следует [объяснение математических основ](#how-math). Если вы интересуетесь криптографией, прочтите и эту часть. + +### Простая версия (как использовать скрытые адреса) {#how-use} + +Алиса создает два приватных ключа и публикует соответствующие публичные ключи (которые можно объединить в один метаадрес двойной длины). Билл также создает приватный ключ и публикует соответствующий публичный ключ. + +Используя публичный ключ одной стороны и приватный ключ другой, можно получить общий секрет, известный только Алисе и Биллу (его нельзя получить только из публичных ключей). Используя этот общий секрет, Билл получает скрытый адрес и может отправлять на него активы. + +Алиса также получает адрес из общего секрета, но поскольку она знает приватные ключи к опубликованным ею публичным ключам, она также может получить приватный ключ, который позволяет ей снимать средства с этого адреса. + +### Математика (почему скрытые адреса работают именно так) {#how-math} + +Стандартные скрытые адреса используют [криптографию на эллиптических кривых (ECC)](https://blog.cloudflare.com/a-relatively-easy-to-understand-primer-on-elliptic-curve-cryptography/#elliptic-curves-building-blocks-of-a-better-trapdoor) для повышения производительности с меньшим количеством битов ключа при сохранении того же уровня безопасности. Но по большей части мы можем это игнорировать и делать вид, что используем обычную арифметику. + +Существует число, известное всем, — _G_. Вы можете умножать на _G_. Но из-за природы ECC практически невозможно делить на _G_. В Ethereum криптография с публичным ключом обычно работает так: вы можете использовать приватный ключ _Ppriv_ для подписи транзакций, которые затем проверяются публичным ключом _Ppub = GPpriv_. + +Алиса создает два приватных ключа: _Kpriv_ и _Vpriv_. _Kpriv_ будет использоваться для траты денег со скрытого адреса, а _Vpriv_ — для просмотра адресов, принадлежащих Алисе. Затем Алиса публикует публичные ключи: _Kpub = GKpriv_ и _Vpub = GVpriv_ + +Билл создает третий приватный ключ _Rpriv_ и публикует _Rpub = GRpriv_ в центральном реестре (Билл мог бы отправить его Алисе, но мы предполагаем, что Кэрол слушает). + +Билл вычисляет _RprivVpub = GRprivVpriv_, который, как он ожидает, Алиса также знает (объяснено ниже). Это значение называется _S_, общий секрет. Это дает Биллу публичный ключ _Ppub = Kpub+G\*hash(S)_. Из этого публичного ключа он может вычислить адрес и отправить на него любые ресурсы. В будущем, если Алиса победит, Билл может сообщить ей _Rpriv_, чтобы доказать, что ресурсы поступили от него. + +Алиса вычисляет _RpubVpriv = GRprivVpriv_. Это дает ей тот же общий секрет, _S_. Поскольку она знает приватный ключ _Kpriv_, она может вычислить _Ppriv = Kpriv+hash(S)_. Этот ключ позволяет ей получить доступ к активам на адресе, который является результатом _Ppub = GPpriv = GKpriv+G\*hash(S) = Kpub+G\*hash(S)_. + +У нас есть отдельный ключ просмотра, чтобы Алиса могла передать подряд Dave's World Domination Campaign Services. Алиса готова сообщить Дэйву публичные адреса и информировать ее о поступлении новых денег, но она не хочет, чтобы он тратил деньги ее кампании. + +Поскольку для просмотра и траты используются разные ключи, Алиса может дать Дэйву _Vpriv_. Тогда Дэйв может вычислить _S = RpubVpriv = GRprivVpriv_ и таким образом получить публичные ключи (_Ppub = Kpub+G\*hash(S)_). Но без _Kpriv_ Дэйв не сможет получить приватный ключ. + +Таким образом, это значения, известные разным участникам. + +| Алиса | Опубликовано | Билл | Дэйв | | +| ------------------------------------------------------------------------- | ----------------- | ------------------------------------------------------------------------- | --------------------------------------------------------------------------- | ----------------------------------------------- | +| G | G | G | G | | +| _Kpriv_ | – | – | – | | +| _Vpriv_ | – | – | _Vpriv_ | | +| _Kpub = GKpriv_ | _Kpub_ | _Kpub_ | _Kpub_ | | +| _Vpub = GVpriv_ | _Vpub_ | _Vpub_ | _Vpub_ | | +| – | – | _Rpriv_ | – | | +| _Rpub_ | _Rpub_ | _Rpub = GRpriv_ | _Rpub_ | | +| _S = RpubVpriv = GRprivVpriv_ | – | _S = RprivVpub = GRprivVpriv_ | _S = _RpubVpriv_ = GRprivVpriv_ | | +| _Ppub = Kpub+G\*hash(S)_ | – | _Ppub = Kpub+G\*hash(S)_ | _Ppub = Kpub+G\*hash(S)_ | | +| _Address=f(Ppub)_ | – | _Address=f(Ppub)_ | _Address=f(Ppub)_ | _Address=f(Ppub)_ | +| _Ppriv = Kpriv+hash(S)_ | – | – | – | | + +## Когда скрытые адреса работают не так, как надо {#go-wrong} + +_В блокчейне нет секретов_. Хотя скрытые адреса могут обеспечить вам конфиденциальность, эта конфиденциальность уязвима для анализа трафика. В качестве простого примера представьте, что Билл пополняет адрес и сразу же отправляет транзакцию для публикации значения _Rpub_. Без _Vpriv_ Алисы мы не можем быть уверены, что это скрытый адрес, но стоит сделать ставку именно на это. Затем мы видим еще одну транзакцию, которая переводит все ETH с этого адреса на адрес фонда кампании Алисы. Возможно, мы не сможем этого доказать, но, скорее всего, Билл только что сделал пожертвование в кампанию Алисы. Кэрол наверняка так и подумает. + +Биллу легко отделить публикацию _Rpub_ от финансирования скрытого адреса (делать это в разное время, с разных адресов). Однако этого недостаточно. Кэрол ищет закономерность: Билл пополняет адрес, а затем фонд кампании Алисы снимает с него средства. + +Одно из решений состоит в том, чтобы кампания Алисы не снимала деньги напрямую, а использовала их для оплаты третьей стороне. Если кампания Алисы отправит 10 ETH в Dave's World Domination Campaign Services, Кэрол будет знать только то, что Билл сделал пожертвование одному из клиентов Дэйва. Если у Дэйва достаточно клиентов, Кэрол не сможет узнать, пожертвовал ли Билл Алисе, которая с ней конкурирует, или Адаму, Альберту или Эбигейл, до которых Кэрол нет дела. Алиса может включить в платеж хэшированное значение, а затем предоставить Дэйву прообраз, чтобы доказать, что это было ее пожертвование. В качестве альтернативы, как отмечалось выше, если Алиса даст Дэйву свой _Vpriv_, он уже будет знать, от кого пришел платеж. + +Основная проблема этого решения в том, что оно требует от Алисы заботиться о секретности, когда эта секретность выгодна Биллу. Алиса может захотеть сохранить свою репутацию, чтобы друг Билла, Боб, тоже сделал ей пожертвование. Но также возможно, что она не прочь разоблачить Билла, потому что тогда он будет бояться того, что произойдет, если Кэрол победит. Билл может в итоге оказать Алисе еще большую поддержку. + +### Использование нескольких скрытых слоев {#multi-layer} + +Вместо того чтобы полагаться на Алису в сохранении конфиденциальности Билла, Билл может сделать это сам. Он может сгенерировать несколько метаадресов для вымышленных людей, Боба и Беллы. Затем Билл отправляет ETH Бобу, а "Боб" (который на самом деле Билл) отправляет их Белле. "Белла" (тоже Билл) отправляет их Алисе. + +Кэрол все еще может анализировать трафик и видеть цепочку от Билла к Бобу, от Боба к Белле, от Беллы к Алисе. Однако, если "Боб" и "Белла" также используют ETH для других целей, не будет выглядеть так, будто Билл что-то перевел Алисе, даже если Алиса немедленно снимет средства со скрытого адреса на известный адрес своей кампании. + +## Написание приложения для скрытых адресов {#write-app} + +В этой статье объясняется приложение для скрытых адресов, [доступное на GitHub](https://github.com/qbzzt/251022-stealth-addresses.git). + +### Инструменты {#tools} + +Существует [библиотека скрытых адресов на typescript](https://github.com/ScopeLift/stealth-address-sdk), которую мы могли бы использовать. Однако криптографические операции могут быть ресурсоемкими для процессора. Я предпочитаю реализовывать их на компилируемом языке, таком как [Rust](https://rust-lang.org/), и использовать [WASM](https://webassembly.org/) для запуска кода в браузере. + +Мы будем использовать [Vite](https://vite.dev/) и [React](https://react.dev/). Это стандартные отраслевые инструменты; если вы с ними не знакомы, можете воспользоваться [этим руководством](/developers/tutorials/creating-a-wagmi-ui-for-your-contract/). Для использования Vite нам нужен Node. + +### Скрытые адреса в действии {#in-action} + +1. Установите необходимые инструменты: [Rust](https://rust-lang.org/tools/install/) и [Node](https://nodejs.org/en/download). + +2. Клонируйте репозиторий GitHub. + + ```sh + git clone https://github.com/qbzzt/251022-stealth-addresses.git + cd 251022-stealth-addresses + ``` + +3. Установите необходимые компоненты и скомпилируйте код Rust. + + ```sh + cd src/rust-wasm + rustup target add wasm32-unknown-unknown + cargo install wasm-pack + wasm-pack build --target web + ``` + +4. Запустите веб-сервер. + + ```sh + cd ../.. + npm install + npm run dev + ``` + +5. Перейдите в [приложение](http://localhost:5173/). Эта страница приложения имеет два фрейма: один для пользовательского интерфейса Алисы, а другой — для Билла. Два фрейма не взаимодействуют; они находятся на одной странице только для удобства. + +6. От имени Алисы нажмите **Generate a Stealth Meta-Address**. Будет отображен новый скрытый адрес и соответствующие ему приватные ключи. Скопируйте скрытый метаадрес в буфер обмена. + +7. От имени Билла вставьте новый скрытый метаадрес и нажмите **Generate an address**. Вы получите адрес для пополнения счета Алисы. + +8. Скопируйте адрес и публичный ключ Билла и вставьте их в область "Private key for address generated by Bill" пользовательского интерфейса Алисы. После заполнения этих полей вы увидите приватный ключ для доступа к активам по этому адресу. + +9. Вы можете использовать [онлайн-калькулятор](https://iancoleman.net/ethereum-private-key-to-address/), чтобы убедиться, что приватный ключ соответствует адресу. + +### Как работает программа {#how-the-program-works} + +#### Компонент WASM {#wasm} + +Исходный код, который компилируется в WASM, написан на [Rust](https://rust-lang.org/). Вы можете увидеть его в [`src/rust_wasm/src/lib.rs`](https://github.com/qbzzt/251022-stealth-addresses/blob/main/src/rust-wasm/src/lib.rs). Этот код в основном является интерфейсом между кодом JavaScript и [библиотекой `eth-stealth-addresses`](https://github.com/kassandraoftroy/eth-stealth-addresses). + +**`Cargo.toml`** + +[`Cargo.toml`](https://doc.rust-lang.org/cargo/reference/manifest.html) в Rust является аналогом [`package.json`](https://docs.npmjs.com/cli/v9/configuring-npm/package-json) в JavaScript. Он содержит информацию о пакете, объявления зависимостей и т. д. + +```toml +[package] +name = "rust-wasm" +version = "0.1.0" +edition = "2024" + +[dependencies] +eth-stealth-addresses = "0.1.0" +hex = "0.4.3" +wasm-bindgen = "0.2.104" +getrandom = { version = "0.2", features = ["js"] } +``` + +Пакет [`getrandom`](https://docs.rs/getrandom/latest/getrandom/) необходим для генерации случайных значений. Это невозможно сделать чисто алгоритмическими средствами; для этого требуется доступ к физическому процессу как источнику энтропии. Это определение указывает, что мы будем получать эту энтропию, запрашивая ее у браузера, в котором мы работаем. + +```toml +console_error_panic_hook = "0.1.7" +``` + +[Эта библиотека](https://docs.rs/console_error_panic_hook/latest/console_error_panic_hook/) выдает более содержательные сообщения об ошибках, когда код WASM паникует и не может продолжать работу. + +```toml +[lib] +crate-type = ["cdylib", "rlib"] +``` + +Тип вывода, необходимый для создания кода WASM. + +**`lib.rs`** + +Это собственно код на Rust. + +```rust +use wasm_bindgen::prelude::*; +``` + +Определения для создания пакета WASM из Rust. Они задокументированы [здесь](https://wasm-bindgen.github.io/wasm-bindgen/reference/attributes/index.html). + +```rust +use eth_stealth_addresses::{ + generate_stealth_meta_address, + generate_stealth_address, + compute_stealth_key +}; +``` + +Функции, которые нам нужны из [библиотеки `eth-stealth-addresses`](https://github.com/kassandraoftroy/eth-stealth-addresses). + +```rust +use hex::{decode,encode}; +``` + +Rust обычно использует для значений байтовые [массивы](https://doc.rust-lang.org/std/primitive.array.html) (`[u8; ]`). Но в JavaScript мы обычно используем шестнадцатеричные строки. [Библиотека `hex`](https://docs.rs/hex/latest/hex/) переводит для нас значения из одного представления в другое. + +```rust +#[wasm_bindgen] +``` + +Сгенерируйте привязки WASM, чтобы можно было вызывать эту функцию из JavaScript. + +```rust +pub fn wasm_generate_stealth_meta_address() -> String { +``` + +Самый простой способ вернуть объект с несколькими полями — вернуть строку JSON. + +```rust + let (address, spend_private_key, view_private_key) = + generate_stealth_meta_address(); +``` + +[`generate_stealth_meta_address`](https://docs.rs/eth-stealth-addresses/latest/eth_stealth_addresses/fn.generate_stealth_meta_address.html) возвращает три поля: + +- Метаадрес (_Kpub_ и _Vpub_) +- Приватный ключ просмотра (_Vpriv_) +- Приватный ключ для траты средств (_Kpriv_) + +Синтаксис [кортежа](https://doc.rust-lang.org/std/primitive.tuple.html) позволяет нам снова разделить эти значения. + +```rust + format!("{{\"address\":\"{}\",\"view_private_key\":\"{}\",\"spend_private_key\":\"{}\"}}", + encode(address), + encode(view_private_key), + encode(spend_private_key) + ) +} +``` + +Используйте макрос [`format!`](https://doc.rust-lang.org/std/fmt/index.html) для генерации строки в формате JSON. Используйте [`hex::encode`](https://docs.rs/hex/latest/hex/fn.encode.html) для преобразования массивов в шестнадцатеричные строки. + +```rust +fn str_to_array(s: &str) -> Option<[u8; N]> { +``` + +Эта функция преобразует шестнадцатеричную строку (предоставленную JavaScript) в байтовый массив. Мы используем ее для разбора значений, предоставленных кодом JavaScript. Эта функция сложна из-за того, как Rust обрабатывает массивы и векторы. + +Выражение `` называется [обобщением (generic)](https://doc.rust-lang.org/book/ch10-01-syntax.html). `N` — это параметр, который управляет длиной возвращаемого массива. Функция на самом деле вызывается как `str_to_array::`, где `n` — длина массива. + +Возвращаемое значение — `Option<[u8; N]>`, что означает, что возвращаемый массив является [необязательным](https://doc.rust-lang.org/std/option/). Это типичный шаблон в Rust для функций, которые могут завершиться сбоем. + +Например, если мы вызовем `str_to_array::10("bad060a7")`, функция должна вернуть массив из десяти значений, но на вход подается только четыре байта. Функция должна завершиться сбоем, и она делает это, возвращая `None`. Возвращаемым значением для `str_to_array::4("bad060a7")` будет `Some<[0xba, 0xd0, 0x60, 0xa7]>`. + +```rust + // decode возвращает Result, _> + let vec = decode(s).ok()?; +``` + +Функция [`hex::decode`](https://docs.rs/hex/latest/hex/fn.decode.html) возвращает `Result, FromHexError>`. Тип [`Result`](https://doc.rust-lang.org/std/result/) может содержать либо успешный результат (`Ok(value)`), либо ошибку (`Err(error)`). + +Метод `.ok()` превращает `Result` в `Option`, значением которого является либо значение `Ok()`, если успешно, либо `None`, если нет. Наконец, [оператор вопросительного знака](https://doc.rust-lang.org/std/option/#the-question-mark-operator-) прерывает текущие функции и возвращает `None`, если `Option` пуст. В противном случае он разворачивает значение и возвращает его (в данном случае для присвоения значения `vec`). + +Это выглядит как странно запутанный метод обработки ошибок, но `Result` и `Option` гарантируют, что все ошибки будут обработаны тем или иным способом. + +```rust + if vec.len() != N { return None; } +``` + +Если количество байтов неверно, это сбой, и мы возвращаем `None`. + +```rust + // try_into поглощает vec и пытается создать [u8; N] + let array: [u8; N] = vec.try_into().ok()?; +``` + +В Rust есть два типа массивов. [Массивы](https://doc.rust-lang.org/std/primitive.array.html) имеют фиксированный размер. [Векторы](https://doc.rust-lang.org/std/vec/index.html) могут расти и сжиматься. `hex::decode` возвращает вектор, но библиотека `eth_stealth_addresses` хочет получать массивы. [`.try_into()`](https://doc.rust-lang.org/std/convert/trait.TryInto.html#required-methods) преобразует значение в другой тип, например, вектор в массив. + +```rust + Some(array) +} +``` + +Rust не требует использования ключевого слова [`return`](https://doc.rust-lang.org/std/keyword.return.html) при возврате значения в конце функции. + +```rust +#[wasm_bindgen] +pub fn wasm_generate_stealth_address(stealth_address: &str) -> Option { +``` + +Эта функция получает публичный метаадрес, который включает в себя как _Vpub_, так и _Kpub_. Она возвращает скрытый адрес, публичный ключ для публикации (_Rpub_) и однобайтовое значение сканирования, которое ускоряет идентификацию того, какие опубликованные адреса могут принадлежать Алисе. + +Значение сканирования является частью общего секрета (_S = GRprivVpriv_). Это значение доступно Алисе, и его проверка намного быстрее, чем проверка того, равно ли _f(Kpub+G\*hash(S))_ опубликованному адресу. + +```rust + let (address, r_pub, scan) = + generate_stealth_address(&str_to_array::<66>(stealth_address)?); +``` + +Мы используем [`generate_stealth_address`](https://docs.rs/eth-stealth-addresses/latest/eth_stealth_addresses/fn.generate_stealth_address.html) из библиотеки. + +```rust + format!("{{\"address\":\"{}\",\"rPub\":\"{}\",\"scan\":\"{}\"}}", + encode(address), + encode(r_pub), + encode(&[scan]) + ).into() +} +``` + +Подготовьте выходную строку в формате JSON. + +```rust +#[wasm_bindgen] +pub fn wasm_compute_stealth_key( + address: &str, + bill_pub_key: &str, + view_private_key: &str, + spend_private_key: &str +) -> Option { + . + . + . +} +``` + +Эта функция использует [`compute_stealth_key`](https://docs.rs/eth-stealth-addresses/latest/eth_stealth_addresses/fn.compute_stealth_key.html) из библиотеки для вычисления приватного ключа для снятия средств с адреса (_Rpriv_). Для этого вычисления требуются следующие значения: + +- Адрес (_Address=f(Ppub)_) +- Публичный ключ, сгенерированный Биллом (_Rpub_) +- Приватный ключ просмотра (_Vpriv_) +- Приватный ключ для траты средств (_Kpriv_) + +```rust +#[wasm_bindgen(start)] +``` + +[`#[wasm_bindgen(start)]`](https://wasm-bindgen.github.io/wasm-bindgen/reference/attributes/on-rust-exports/start.html) указывает, что функция выполняется при инициализации кода WASM. + +```rust +pub fn main() { + console_error_panic_hook::set_once(); +} +``` + +Этот код указывает, что вывод паники должен быть отправлен в консоль JavaScript. Чтобы увидеть это в действии, используйте приложение и дайте Биллу неверный метаадрес (просто измените одну шестнадцатеричную цифру). Вы увидите эту ошибку в консоли JavaScript: + +``` +rust_wasm.js:236 panicked at /home/ori/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/subtle-2.6.1/src/lib.rs:701:9: +assertion `left == right` failed + left: 0 + right: 1 +``` + +За этим последует трассировка стека. Затем дайте Биллу действительный метаадрес, а Алисе — либо неверный адрес, либо неверный публичный ключ. Вы увидите эту ошибку: + +``` +rust_wasm.js:236 panicked at /home/ori/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/eth-stealth-addresses-0.1.0/src/lib.rs:78:9: +keys do not generate stealth address +``` + +Снова последует трассировка стека. + +#### Пользовательский интерфейс {#ui} + +Пользовательский интерфейс написан с использованием [React](https://react.dev/) и обслуживается [Vite](https://vite.dev/). Вы можете узнать о них из [этого руководства](/developers/tutorials/creating-a-wagmi-ui-for-your-contract/). Здесь нет необходимости в [WAGMI](https://wagmi.sh/), потому что мы не взаимодействуем напрямую с блокчейном или кошельком. + +Единственная неочевидная часть пользовательского интерфейса — это подключение WASM. Вот как это работает. + +**`vite.config.js`** + +Этот файл содержит [конфигурацию Vite](https://vite.dev/config/). + +```js +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' +import wasm from "vite-plugin-wasm"; + +// https://vite.dev/config/ +export default defineConfig({ + plugins: [react(), wasm()], +}) +``` + +Нам нужны два плагина Vite: [react](https://www.npmjs.com/package/@vitejs/plugin-react) и [wasm](https://github.com/Menci/vite-plugin-wasm#readme). + +**`App.jsx`** + +Этот файл является основным компонентом приложения. Это контейнер, который включает два компонента: `Alice` и `Bill` — пользовательские интерфейсы для этих пользователей. Актуальная часть для WASM — это код инициализации. + +```jsx +import init from './rust-wasm/pkg/rust_wasm.js' +``` + +Когда мы используем [`wasm-pack`](https://rustwasm.github.io/docs/wasm-pack/), он создает два файла, которые мы здесь используем: файл wasm с фактическим кодом (здесь `src/rust-wasm/pkg/rust_wasm_bg.wasm`) и файл JavaScript с определениями для его использования (здесь `src/rust_wasm/pkg/rust_wasm.js`). Экспорт по умолчанию этого файла JavaScript — это код, который необходимо запустить для инициации WASM. + +```jsx +function App() { + . + . + . + useEffect(() => { + const loadWasm = async () => { + try { + await init(); + setWasmReady(true) + } catch (err) { + console.error('Error loading wasm:', err) + alert("Wasm error: " + err) + } + } + + loadWasm() + }, [] + ) +``` + +[Хук `useEffect`](https://react.dev/reference/react/useEffect) позволяет указать функцию, которая выполняется при изменении переменных состояния. Здесь список переменных состояния пуст (`[]`), поэтому эта функция выполняется только один раз при загрузке страницы. + +Функция эффекта должна возвращаться немедленно. Чтобы использовать асинхронный код, такой как `init` WASM (который должен загрузить файл `.wasm` и, следовательно, занимает время), мы определяем внутреннюю функцию [`async`](https://en.wikipedia.org/wiki/Async/await) и запускаем ее без `await`. + +**`Bill.jsx`** + +Это пользовательский интерфейс для Билла. У него есть одно действие: создание адреса на основе скрытого метаадреса, предоставленного Алисой. + +```jsx +import { wasm_generate_stealth_address } from './rust-wasm/pkg/rust_wasm.js' +``` + +В дополнение к экспорту по умолчанию, код JavaScript, сгенерированный `wasm-pack`, экспортирует функцию для каждой функции в коде WASM. + +```jsx +