Завдання Solidity-розробника, або Як готуватися до роботи в парадигмі Web3

Підписуйтеся на Telegram-канал «DOU #tech», щоб не пропустити нові технічні статті

Мене звати Олександр Дубовик, я співпрацюю з EPAM Ukraine як Golang Software Engineer, а в минулому — розробник Solidity смарт-контрактів. У цій статті я розповім про основні розділи знань, які вам потрібні для роботи Solidity-розробником: Web3, Ethereum Virtual Machine, аспекти розробки на Soliditiy і тестування за допомогою Truffle. Наприкінці матеріалу ви знайдете декілька цікавих посилань на цю тему.

Web3

На сьогодні web-оточення швидко розвивається. 7-8 років тому світ побачив парадигму Web3, яка відкриває для нас нову можливість децентралізації рішень. Що ж таке Web3? Для висвітлення цього питання повернімося трохи назад і поговорімо про Web2.

Web2 є парадигмою інтернету, що домінує і сьогодні. Люди обмінюються інформацією, створюють екаунти в соціальних мережах та відеосервісах, завантажують туди створений контент, який хоча й не є власністю цих платформ, підлягає певній цензурі. Великою проблемою є також непрозоре прийняття рішень, часткова або повна відсутність публічних записів обробки грошових транзакцій банківськими системами та багато інших питань. Я думаю, ви могли стикатися з цими ситуаціями та проблемами, пов’язаними з ними.

Web3 своєю чергою пропонує прозорий підхід до вирішення цих проблем за допомогою блокчейну. У блокчейн легко записати дані, але вкрай важко видалити. Це вирішує повністю або частково всі з вищеназваних питань. До того ж деякі рішення (зокрема Ethereum) дають можливість реалізувати власні алгоритми обробки, результат якої також буде збережений. Я б описав це так: Web2 — read-write, Web3 — read-write-own, тобто ми не лише щось записуємо/ створюємо, а також володіємо результатами своїх дій. На сьогодні ІТ-індустрія вже має низку інноваційних рішень, які із цього виросли — NFT, DAO, IoT.

Щоб реалізувати ці речі, потрібні розробники смарт-контрактів. Що ж необхідно знати для початку роботи в цій галузі? Перш за все, ООП та основні поняття кодингу: типи даних, функції тощо, мову Solidity, базові знання технології блокчейн, особливості роботи мережі Ethereum зі смарт-контрактами, JavaScript, NodeJS, web3.js. В залежності від типу роботи, не завадить трохи знань з криптографії.

Solidity

Solidity є повноцінною мовою програмування, яку ще називають тюрінг-повною мовою. Вона дозволяє нам кодити за допомогою усього спектру інструментів, які є у більшості мов програмування. Гаразд, єдине, чого нема — типу float, але на мою думку, у смарт-контрактах можна без цього обійтись використанням uint256.

Тепер давайте трохи покодимо:

pragma solidity >=0.5.16 <0.9.0
contract DataStorage {
    uint256 private counter;
    mapping(bytes32 => string) private trxContent;

    function getCounter() public view returns (uint256) {
        return counter;
    }

    function getTrxContentByHash(bytes32 hash)
    public view returns (string memory) {
        counter++;
        return trxContent[hash];
    }
}

Уявімо, що це — сховище даних транзакції. Спочатку ми задаємо версію Solidity-компілятора, або solc, який буде компілювати наш код. Версія може задаватися у діапазоні. Нижче іде опис контракту: його назва, поля та методи. Тут я оголошую два поля — ’counter’ і ’trxContextSolidity’. Перше — лічильник всіх транзакцій, друге — мапа, яка залежно від хешу транзакції містить певну інформацію про неї у вигляді рядка.

Також ми маємо два методи: ’getCounter’, який повертає нам значення лічильника та ’getTrxContentByHash’, який повертає дані транзакції за її хешем.

Обмеження функцій

Поля та функції мають модифікатори доступу — public, external, internal, private. Вони показують область видимості поля або функції:

  • Public — може бути використано де завгодно, значення за замовчуванням;
  • External — де завгодно, окрім контракту;
  • Internal — тільки у контракті або у його наступниках;
  • Private — тільки у контракті.

Окрім обмежень за скоупом методи також можуть бути обмежені за характером виконання:

  • Pure — не може міняти стан блокчейну або зчитувати його;
  • View — може зчитувати;
  • Payable — може зчитувати і записувати, значення за замовчуванням.

Види сховищ

  • ’returns (string memory)’ — що ж тут значить ’memory’? А я вам зараз розкажу © :)

Memory — один з видів сховищ даних та змінних, які надає EVM (про це — трохи згодом) для використання у смарт-контрактах.

  • Storage

Змінні цього типу — поля контрактів, кожна їхня трансформація буде записана у блокчейн. ’counter’ є змінною, яка зберігається у storage. Можна сказати, що кожен контракт має своє сховище для даних.

  • Memory

Змінні типу «memory» існують тільки у пам’яті EVM під час виклику функції. Цим типом розташування можна користуватися для масивів, наприклад для ’[]uint256 memory’ чи для структур.

  • Calldata

Це — місце розташування аргументів функції. Змінні з таким типом розташування є іммутабельні та існують у межах функції. Загалом це стек, який може містити не більше 16 змінних, вхідні та вихідні параметри функції теж входять у нього. Це дефолтне значення розташування для змінної і не завжди воно може бути трансформовано. Також це означає, що у методі ви можете використовувати не більше 16 змінних одночасно. Це можна обходити за допомогою структур.

Gas

Calldata краще використовувати ніж memory тому, що це не займає так багато ресурсів. І мова тут іде не зовсім про обчислювальну потужність. Кожен рядок коду, кожен виклик змінної або функції і передача туди параметрів коштують вам такої речі як ’gas’. ’gas’ — це міра підрахунку обчислювальної потужності, яка потрібна, щоб виконати функцію. Його можна отримати, перерахувавши деяку кількість ETH на рахунок адресу контракту.

Пам’ятайте: кожен рядок коду на етапі виконання коштує грошей, тож код має бути настільки лаконічним, наскільки це можливо. Уникайте зайвої логіки там, де це не потрібно. Це не заклик видаляти важливі перевірки, все що потрібно — пильно перевіряти, чи можна щось зробити оптимальніше та з меншою кількістю коду.

Для зменшення використання ’gas’, у тому числі на стадії деплою контракту у мережу також можна користатися оптимізатором Yul Optimizer.

Якщо вас зацікавив цей шматочок Solidity, то йдемо далі і зануримося трохи глибше.

Ethereum Virtual Machine (EVM)

EVM — середовище виконання смарт-контрактів в Ethereum. Воно є повністю ізольованим, тобто код, який ви будете виконувати в середині EVM, не матиме доступу до мережі, файлової системи тощо.

EVM оперує певними поняттями для забезпечення своєї функціональності.

Для роботи зі смарт-контрактами нам потрібно робоче оточення, у якому можна буде створити екаунт та поповнити його баланс.

Ethereum node

Тож, аби побачити, що наш код працює, нам потрібно налаштувати оточення. Це можна зробити декількома способами:

  1. Remix IDE — онлайн-оточення, де можна одразу покодити. Від вас знадобиться тільки налаштувати версію компілятора.
  2. Скористатися тестнетом — створити API ключ за допомогою Alchemy, екаунт на Metamask, поповнити його тестовими ETH та деплоіти контракти на тестову мережу за допомогою Hardhat або Truffle. За посиланням є докладний гайд, як це зробити.
  3. Підняти локальну мережу, на якій ми можемо перевіряти результати своєї праці. Це можна зробити за допомогою Ganache або підняти оточення власноруч, бажано через Docker/Docker Compose.

❗️ Тут і далі під нодою розуміємо додаток Ethereum node, який є одиницею мережі Ethereum, запущеною локально або віддалено та здатною обробляти запити. У мережі може бути скільки завгодно нод, навіть одна.

Я використаю останній варіант, тому що у мене вже є таке оточення на локальній машині у вигляді докер контейнерів, ОС — Linux Ubuntu, ноди на основі реалізації go-ethereum. Для компіляції, деплою і тестування я скористаюсь Truffle.

Детальний опис, як підняти таке оточення з нуля за посиланням. Цей варіант не є найлегшим. Якщо раніше ви не були з цим знайомі, може пройти багато часу, поки ви зробите свою локальну мережу, тож я рекомендую варіант з тестнет. Обов’язково ознайомтеся з бібліотекою web3.js, вона використовується безпосередньо чи під капотом далі у багатьох розділах.

Я опишу зміни, які нам треба внести для роботи з контрактами. Для створення екаунту необхідно зробити наступні кроки:

  1. Рядок запуску ноди:
    ./build/bin/geth --datadir=./path/to/datadir/mynodenum \
    --port=10304 –networkid=123 \
    --http –-http.addr="0.0.0.0" --http.port 9000 \
    --http.api="admin,personal,eth,net,debug,web3" \
    --mine --miner.threads=1 -–miner.gasprice=5 \
    --miner.gastarget=1000000000 \
    --nodiscover --syncmode=full --gcmode "archive" \
    --allow-insecure-unlock console
    
  2. Якщо нода(-и) не була ініціалізована, запускаємо ініціалізацію
    geth init path/to/genesis.json
  3. Запускаємо ноду(-и), заходимо у консоль geth:
    cd ~/go/src/github.com/ethereum/go-ethereum
    geth attach path/to/datadir/mynodenum/geth.ipc
  4. Створюємо запис у keystore для нашого екаунту:
    >web3.sha3('lorem ipsum')
    >0x9da9ec7069ee6ad9f4e58929462db0f04f49034a356d1a36f631ce6457101bdd
    >personal.importRawKey('9da9ec7069ee6ad9f4e58929462db0f04f49034a356d1a36f631ce6457101bdd', 'yourKey')
    >0xf20803aa557c3e65dab634415def4675ec21386
    Ми отримали адресу нашого екаунту.
  5. Зупиняємо ноди, видаляємо дані нашого локального блокчейну, окрім keystore:
    cd  path/to/datadir/mynodenum
    cp -r keystore/ path/to/datadir
    rm –rf *
    mkdir keystore
    mv path/to/datadir/keystore .
  6. Вставляємо в genesis.json нашу адресу та задаємо їй баланс:
    "alloc": {
    "0xf20803aa557c3e65dab634415def4675ec21386": { "balance": 1000000000000000000000000000000 }
    }
  7. Ініціалізуємо ноди ще раз та запускаємо. Після старту ноди у консолі ноди виконуємо miner.start()

Перевіримо, чи є наш екаунт у мережі:

>eth.accounts
>["0xf20803aa557c3e65dab634415def4675ec21386"]
>eth.getBalance(eth.accounts[0])
>1e+38

Так, екаунт є, як і баланс на ньому. Тепер для того, щоб мати можливість деплоїти та тестувати контракти, я налаштую Truffle.

Truffle Setup

Почнемо з сетапу проєкту. Truffle є пакетом для NodeJS, який ми можемо отримати за допомогою node package manager(npm). Якщо у вас нема ні NodeJS, ні npm, встановлюємо їх:

sudo apt install nodejs
sudo apt install npm

Далі ставимо глобально пакет truffle:

sudo npm i –g truffle

Я додам кілька файлів у робочу директорію, з якими ми працюватимемо далі. Зовнішній вигляд готового до роботи проєкту:

Контракти знаходяться у директорії «contracts», один у нас вже є. Аби задеплоїти його у мережу, потрібно написати скрипт деплою, який повинен знаходитись у migrations. Першим скриптом завжди йде деплой контракту Migrations від Truffle.

Він використовується при деплої інших контрактів для уникання передеплою коду, який не був змінений. На практиці це ніколи не використовується, адже скрипти деплою (далі — міграції) розробляють подібно до міграцій баз даних, тобто при наступному деплої виконуються лише нові міграції. Тести знаходяться у директорії test. Конфігурація truffle.js містить налаштування підключення, параметри компілятора та багато іншого.

truffle.js

module.exports = {
compilers: {
solc: {
version: "0.8.16"
}
},
migrations_directory: "./migrations",
    networks: {
        local: {
            host: "localhost",
            port: 9000,
            network_id: "123",
            gasPrice: 5,
            from: "0xf20803aa557c3e65dab6344e15def4675ec21386",
            gas: 1000000000,
        }
    }
};

Налаштування мережі беруться згідно з параметрами запуску ноди. Після цього ми можемо конектитися до працюючої ноди.

Для компіляції контракту потрібно виконати наступну команду, знаходячись у директорії проєкту:

truffle compile

Це типове повідомлення, що все успішно скомпілювалось. Також ми можемо отримати Warning або Error в залежності від ситуації.

Truffle deploy

Наступний крок — деплой. Для деплою нам необхідно написати ось такий скрипт:

2_storage.js

const DataStorage = artifacts.require("./contracts/DataStorage.sol");


module.exports = async (deployer) => {
    await deployer.deploy(DataStorage);
}

По-перше, ми беремо артефакт, що був створений після компіляції DataStorage.sol і який знаходиться у build/contracts/. Беремо саме з розширенням .sol, не .json. Далі йде команда на деплой контракту за допомогою вбудованого деплоєра.

Деплой контракту:

truffle deploy --network local

Тобто деплоїмо у нашу локальну мережу, яка була описана в truffle.js Команда deploy містить в собі команду compile. Весь список команд Truffle можна знайти за посиланням.

Ось так виглядають логи успішного деплою. Вони містять багато інформації, найбільш важлива для нас — витрати gas. Наприклад, за цей деплой будо витрачено 224510 gas.

Смарт-контракт готовий до використання, але у ньому немає ні одного сетеру для зміни стану, тож зараз я додам один:

Ого, начебто багато чого було додано. Зараз ми це розглянемо. У двох словах:

я додав сетер для мапи trxContent, де обмеження — тільки заданий адрес. Цей сетер може вносити зміни до сховища та — якщо хеш транзакції не порожній — збільшувати лічильник.

Тепер прочитаємо код трошки детальніше. Почнемо з інструкції require — ця функція дозволяє нам робити перевірки та зупиняти виконання методу, якщо перевірку було не пройдено. Також це можна побачити у блокчейні у вигляді повідомлення у логах — в такому випадку маються на увазі не тільки логи ноди, а також поле транзакції, яке містить інформацію про виконання.

Тут ми можемо перейти до поля event SetTrxContent. Це івент, який може бути записаний у логи за допомогою ключового слова emit, яке викликається останнім у сетері. Функція require при виклику скасовує всі зміни, які були ініційовані протягом виконання транзакції, і робить revert.

Constructor — метод, який виконується один раз при створенні контракту. Може приймати параметри. Отже, ми установлюємо значення адреси, яка буде мати можливість змінювати trxContent. msg.sender — адреси того, хто викликав метод. Це може бути як звичайний екаунт, так і інший смарт-контракт. Якщо метою є перевірка саме екаунту, який почав ланцюжок викликів, треба використовувати tx.origin.

І останнє — умова, за якої лічильник counter збільшується. Ми перевіряємо, чи параметр hash не порожній, і якщо ні — збільшуємо counter на один. Від типів можна брати нульове значення bytes32(0) — беремо дефолтне значення типу bytes32. Те ж саме і з address(0) у рядку 25. Не від всіх типів можна взяти таке значення, наприклад, string, масиви чи структури для цього не підходять.

Для успішного деплою додамо адресу нашого екаунту в метод деплою в 2_storage.js

await deployer.deploy(DataStorage, '0xf20803aa557c3e65dab6344e15def4675ec21386');

Truffle console

Тепер нарешті ми можемо побачити наш смарт-контракт у дії. Для цього деплоїмо нову версію та викликаємо його методи через консоль Truffle.

truffle console --network local

Використаємо рандомну адресу для тесту

Тепер викликаємо сетер. Нам повернувся результат, який є не дуже читабельним. Але найважливіше тут — status: true, тобто транзакція пройшла успішно. Також не забуваємо про gasUsed: 67828.

Окрім цього, якщо подивитися у логи ноди, ми побачимо повідомлення про успішну транзакцію: увага на tx із консолі Truffle та логами ноди: «Submitted transaction...» із «hash=0xf1b44...» — вони збігаються. Таким чином також можна дивитися повідомлення про помилки з require, якщо ви не знаєте, як інакше або це на цей час легший спосіб виявити, що не так.

І у фіналі — зчитуємо значення, які лежать у сховищі контракту. Так, все збігається — ’1′ та ’someContext’.

Ми вже впритул підійшли до тестування, але перед цим деякі з вас могли побачити, що написаний код є неоптимізованим. Наприклад, навіщо перевіряти msg.sender на порожнє значення, якщо ми все одно перевіряємо на setterAddress, або як було б краще з точки зору використання gas перевіряти параметр hash на нульове значення: через require, або можливо залишити все як є, тільки поміняти умову на ==, додати return, щоб не виконувати дорогу операцію запису у сховище trxContent[hash] = content.

Ви можете поекспериментувати з цим та виявити найоптимальніші варіанти — код є на репозиторії за посиланням.

Truffle test

Почнемо тестування. У нас буде один негативний кейс та два позитивних. Для негативного потрібно підготувати ще один екаунт та переслати на нього ETH.

>web3.sha3('test')
>0x9c22ff5f21f0b81b113e63f7db6da94fedef11b2119b4088b89664fb9a3cb658
>personal.importRawKey('9c22ff5f21f0b81b113e63f7db6da94fedef11b2119b4088b89664fb9a3cb658', 'testKey')
>0xc08b5542d177ac6686946920409741463a15dddb
>eth.sendTransaction({from:eth.accounts[0], to:eth.accounts[1], value: 1000000})
>personal.unlockAccount(eth.accounts[1], ‘testKey’, 0)

Після створення екаунту для роботи зі смарт-контрактами його потрібно розблокувати через personal.unlockAccount. Перший параметр — адреса екаунта, другий — ключ, третій — на який час розблокувати екаунт, 0 — доки запущена нода. Після кожного рестарту ноди цю операцію потрібно повторювати з кожним екаунтом.

Я створив 3 тести:

  • get/set даних мапи trxContext. Очікувана поведінка — запис та зчитування одного і того ж значення;
  • перевірка роботи require метода setTrxContent для невідповідної адреси екаунту. Очікувана поведінка — revert транзакції;
  • get лічильника для метода getCounter.

Тести у Truffle використовують бібліотеки Chai та Mocha, також в даному тесті я використав бібліотеку truffle-assertions для перевірки на revert транзакції.

Структура тестового файлу забезпечується Mocha: функції contract (...), context (...) та it (...), де contract — головний рівень, у який при виконанні передається eth.accounts, context своєрідний тест-кейс, а it — тест (BDD, TDD).

Тести написані, отже саме час їх запустити:

truffle test --network local test/storage_test.js

Усі тести пройшли успішно. Я рекомендую у будь-якому разі писати тести до або паралельно розробці смарт-контрактів. Команда truffle test містить compile та deploy і задеплоені таким чином контракти не переписують попередні. Це дає змогу тестувати в ізольованому оточенні.

При звичайному деплої нові контракти переписують адресу старих з таким самим ім’ям. Але якщо ви знаєте адресу старих контрактів, до них ще можна звертатися та викликати функції зчитування даних.

Truffle debug

Та все ж таки тести не завжди бувають зеленими і не завжди виводять конкретну помилку, за якою ми можемо зрозуміти, що коїться. Можна годинами проводити мануальний дебаг всього і вся, інколи не знайшовши нічого, особливо якщо ваш dApp проєкт досить великий. Для вирішення таких питань існує дебагер.

Раніше ми створили контракт DataStorage та додали туди адресу, яка може записувати: 0xf20803aa557c3e65dab6344e15def4675ec21386. У Truffle console викликаємо запис даних, де у параметрі from відправником буде адреса 0xc08b5542d177ac6686946920409741463a15dddb:

await DataStorage.deployed().then(c => c.setTrxContent(web3.utils.randomHex(32), "someContent", {from: accounts[1]}))

Це дає нам невиконану транзакцію, яку ми зможемо задебажити за її хешем (поле tx):

truffle debug --network local 0xa4cf6e116c49e589e01df9efe950b8c6dd7f2dc5efa89803e7ccdd5dc308c6be

Після виклику дебагеру ми бачимо багатий набір опцій, які допоможуть нам у цій непростій справі. Ми можемо, як і у будь-якому дебагері, зміщуватися у скоупі або за нього, ставити брейкпоінти, зчитувати значення змінних на поточному моменті виконання тощо.

Я одразу піду до місця помилки:

Тепер ми чітко бачимо місце, де сталася помилка. Інколи дебагер може некоректно відпрацьовувати, збивати вказівник, але я в такі ситуації потрапляв рідко і на старих версіях. Якщо у вас виникають помилки при запуску дебагеру — оновіть Node.JS та NPM до останніх версій.

Приблизно так виглядає цикл розробки смарт-контрактів. Якщо вам сподобався цей напрямок, я думаю, саме час почати ресерч цієї теми. Good luck!

Також я зібрав корисні посилання за нашою тематикою, рекомендую ознайомитися:

Документація

ethereum.org/en/whitepaper
docs.soliditylang.org/en/latest
trufflesuite.com/docs
trufflesuite.com/ganache
web3js.readthedocs.io/en/v1.7.5
hardhat.org/...​nner/docs/getting-started
docs.infura.io/infura

Geth

github.com/ethereum/go-ethereum
geth.ethereum.org/...​interface/private-network

Деплой

support.mycrypto.com/...​ere-to-get-testnet-ether
ethereum.org/...​e-ethereum-in-javascript

Криптографія

blog.ethereum.org/...​05/zksnarks-in-a-nutshell
blog.cloudflare.com/...​iptic-curve-cryptography

👍ПодобаєтьсяСподобалось9
До обраногоВ обраному6
LinkedIn
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
я співпрацюю з EPAM Ukraine як Golang Software Engineer, а в минулому — розробник Solidity смарт-контрактів.

@Oleksander Dubovyk, чому вирішив повернутися . перейти з web3 розробки, чи працюєш теж у web3 зараз?

добавлю ещё пару мыслей пожалуй.
Геттер ’getCounter()’ в контракте описывать было не обязательно — а просто объявить counter как public. Да и вообще странно, что автор имеет привычку по-дефолту скрывать от юзера все данные. Это же смарт-контракт, а не либа с++. Юзер должен видеть данные и поэтому доверять. Но это имхо вобщем-то.

Видно, что автор также большой любитель truffle и почти ничего не рассказал про remix. Я пробовал и то, и другое, но truffle не нравится тем, что даже для выборочного деплоя контрактов надо лезть в его скрипт. Т.е., например, сначала скрипт обновляет все три контракта проекта, а потом мне надо обновлять только контракт #2 (потому что в оставшихся уже лежат нужные мне данные и код контрактов менять мне также не нужно). В remix — просто задеплоить контракт #2 ещё раз и получить только один новый адрес. Для truffle — лезть в скрипт и менять для этого его логику (или писать дополнительный отдельный скрипт, не суть). Хотя в плане тестов и дебага, как было описано, truffle очень кстати конечн.

Использовать uint32[] вместо uin256[]? Будет ли это оправданно? Повлияет ли это на цену транзакции в эфире? Если смысл использовать подобный подход?

смотря в контексте чего же. Если для дат (utc-time), например, то да

Це все добре. Онлайн-магазин можна тією мовою накодувати? :)

Теоретично можливо, але не впевнений у тому що блокчейн можна буде використовувати як повноцінну бд) Потрібен буде все ж таки сервер-гейт який буде зв’язувати логіку смарт-контрактів та базу, так буде краще

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

А у чому проблема розгорнути локально? Або ethermint, .. Написати бізнес-логіку взагалі не проблема.

«блокчейн» это не только eth/bsc и подобные сети, если что. Например, под EOS/WAX это делается и используется без проблем.
Но в целом да, принято складывать прочитанные из блокчейна/транзакций данные в отдельную БД, просто чтобы база ноды не разросталась

Так, саме це і мав на увазі

Можно конечно, например используя подобный стек технологий nodejs,truffe,web3,testrpc,vue,verticaDB. Обращайтесь у нас на 122-ой на третьем курсе любой из тех что остались, такое сделает!

если писать его под eth-совместимый блокчейн, то большой занозой в заднице будет обновление функционала. Upgradable-контракты спасают только частично, т.к. не дают возможности менять порядок и типы полей уже сохранённых данных. А это и хорошо (от юзеров больше доверия к данным) и плохо (не оставляют разрабам права на ошибку)

Підписатись на коментарі