Why am I sharing my travel stories?
Founder & CEO of TruStory. I have a passion for understanding things at a fundamental level and sharing it as clearly as possible.
Special thanks to Uliana Didych, Volodymyr Cherepanyak for translating this post into Ukraine.
Ви мабуть чули про Ethereum, та можливо не знаєте що це таке. Останнім часом він часто з’являється в новинах, включно з обкладинками серйозних журналів, однак, якщо не мати уявлення, чим саме є Ethereum, то ці статті виглядають як набір незрозумілих символів. То ж чим він є? По суті, це публічна база даних, яка постійно реєструє цифрові транзакції. Важливим є той фактор, що для її ведення і безпеки не потрібне централізоване управління. Натомість вона працює як система транзакцій, яка не потребує довіри, — оболонка, в якій окремі користувачі можуть здійснювати прямі транзакції, не вимагаючи довіри до третіх сторін АБО один до одного.
Все ще не зовсім зрозуміло? Саме про це цей допис. Моєю метою є пояснити, як працює Ethereum на технічному рівні, не використовуючи складну математику або відлякуючі формули. Навіть, якщо ви не програміст, сподіваюся, в результаті ви краще розумітимете механізм. Якщо деякі моменти будуть надто технічні, щоб їх грокнути, це нормально. Нема потреби розуміти всі деталі.
Рекомендую зосередитися на загальному рівні розуміння. Багато охоплених тут тем є розшифруванням понять, викладених у жовтих сторінках. Для простішого розуміння Ethereum я додав власні пояснення й діаграми. Якщо ж ви достатньо сміливі, щоб прийняти технічний виклик, почитайте також жовті сторінки Ethereum.
То ж до праці!
Блокчейн — це «криптографічно захищена одинична машина транзакцій з доступним станом». [1] ****Дещо заплутано, правда? Давайте, розкладемо на частини.
Система Ethereum втілює цю модель.
За своєю суттю блокчейн Ethereum є транзакційною машиною станів. В комп’ютерних науках машина станів це щось, що зчитує набір вхідих даних, і на їх основі переходить в інший стан.
Говорячи про машину станів Ethereum, розпочнемо зі «стану генези». Це як чистий аркуш до того, як у мережі відбулися будь-які транзакції. Після виконання транзакції цей стан генези переходить у деякий фінальний стан. У будь-який момент часу цей фінальний стан репрезентує поточний стан Ethereum.
Стан Ethereum містить мільйони транзакцій. Ці транзакції групуються у «блоки». Блок містить набір транзакцій, і кожний наступний блок пов’язаний у ланцюжок з попереднім.
Щоб система перейшла з одного стану в інший, транзакція має бути дійсною. Щоб транзакцію було визнано дійсною, вона має пройти через процедуру валідації, яку називають майнінгом. Майнінг — це процес, коли група вузлів (наприклад, комп’ютерів) витрачає свої обчислювальні ресурси для створення блоку дійсних транзакцій.
Будь-який вузол у мережі, який заявляє себе майнером, може спробувати створити і схвалити блок. Одночасно велика кількість майнерів в усьому світі намагається створювати і схвалювати блоки. Кожен майнер, відправляючи блок у блокчейн, подає математичне «підтвердження». Це підтвердження діє як гарантія: якщо воно існує, блок має бути дійсний.
Щоб блок було додано до головного ланцюжка, майнер має підтвердити його швидше за конкурентів. Процес, під час якого майнер схвалює кожний блок, надаючи математичне підтвердження, називається «доказ роботи» (proof of work).
Майнер, який схвалив блок, отримує винагороду в розмірі певної вартості виконання цієї роботи. Якою є ця вартість? Блокчейн Ethereum використовує власний цифровий токен, який називається «Ether». Щоразу, коли майнер схвалює новий блок, генеруються і присвоюються нові токени Ether.
Може виникнути питання: а де гарантія, що всі дотримуються одного ланцюжка блоків? Звідки впевненість, що не існує якась підмножина майнерів, які вирішили створити власний ланцюжок блоків?
Раніше ми визначили блокчейн, як криптографічно захищену одиничну машину транзакцій з доступним станом. Згідно цього визначення правильний поточний стан це єдина глобальна істина, яку всі мають прийняти. Наявність багатьох станів (або ланцюжків) зруйнує всю систему, оскільки буде неможливо визначити, який стан є правильним. Якщо ланцюжки розгалужуються, у вас може бути 10 монет на одному ланцюжку, 20 на другому і 40 на третьому. В такому сценарії немає можливості визначити, який ланцюжок найбільш «правильний». Якщо генерується декілька шляхів, виникає «розгалуження».
Ми зазвичай намагаємося уникати розгалужень, оскільки вони переривають систему і змушують людей вибирати, в який ланцюжок «вірити».
Щоб визначити правильний шлях і уникнути створення кількох ланцюжків, Ethereum використовує механізм, який називається «протокол GHOST».
«GHOST» = «Greedy Heaviest Observed Subtree» (візуально найважче піддерево).
Простими словами, протокол GHOST вказує вибирати той шлях, за яким здійснено набільше обчислень. Єдиним способом визначити такий шлях є використання номера останнього блоку («кінцевий блок»), який представляє загальну кількість блоків поточного шляху (без урахування генези). Що більший номер блоку, то довший шлях і більше зусиль майнінгу докладено, щоб досягнути цього кінцевого блоку. Використання цього механізму дозволяє дійти згоди щодо канонічної версії поточного стану.
Після загального знайомства з блокчейном, давайте розглянемо детальніше основні компоненти, з яких складається система Ethereum:
Для початку одне зауваження: вживаючи термін «геш» X, я маю наувазі геш KECCAK-256 , який використовує Ethereum.
Глобальний «доступний стан» Ethereum складається з багатьох дрібних об’єктів («облікових записів»), які можуть взаємодіяти між собою в оболонці для передачі повідомлень. Кожний обліковий запис має свій стан, пов’язаний з 20-байтною адресою. Адресою в Ethereum є 160-бітний ідентифікатор, який використовується для ідентифікації кожного облікового запису.
Є два типи облікових записів:
Важливо розуміти фундаментальну різницю між зовнішніми і контрактними обліковими записами. Зовнішній обліковий запис може надсилати повідомлення іншим зовнішнім обліковим записам АБО іншим контрактним обліковим записам, створивши і підписавши транзакцію за допомогою свого приватного ключа. Повідомлення між зовнішніми обліковими записами — це просто передавання вартості. Однак повідомлення з зовнішнього облікового запису на контрактний активізує код контрактного рахунку, даючи змогу виконувати різні операції (наприклад, передача токенів, запис у внутрішнє сховище, виконання обчислень, створення нових контрактів тощо).
На відміну від зовнішніх облікових записів, контрактні не можуть ініціювати нові тразакції самостійно. Натомість, контрактні облікові записи можуть лише запускати транзакції у відповідь на інші, отримані ними транзакції (від зовнішнього облікового запису або іншого контрактного облікового запису). Детальніше про виклики контракт-контракт буде розглянуто в розділі «Транзакції і повідомлення».
Тому будь-яка операція, яка виникає у блокчейні Ethereum завжди запущена транзакцією з зовнішнього облікового запису.
Стан облікового запису складається з чотирьох компонентів, наявних, не залежно від типу облікового запису:
Отже, тепер ми знаємо, що глобальний стан Ethereum складається з відображення між адресами облікових записів і станами облікових записів. Це відображення зберігається у структурі даних, відомій як дерево Меркла «Патріція».
Дерево Меркла (відоме також як префіксне дерево Меркла) є типом бінарного дерева, сформованого з набору вузлів із:
Дані в основі дерева формуються розділенням даних, які потрібно зберегти, на порції, відтак порції діляться на сегменти, а тоді створюється геш кожного сегменту. Цей процес повторюється, поки загальна кількість гешів, що залишилися, стане одиницею: кореневий геш.
Для цього дерева потрібно мати ключ для кожного значення, збереженого в ньому. Починаючи з кореневого вузла, ключ визначає, який з дочірніх елементів вибирати, щоб отримати потрібне значення, збережене у вузлах вершин. У випадку з Ethereum відображенням ключ / значення для дерева стану є відображення між адресами і пов’язаними з ними обліковими записами, включно з полями balance, nonce, codeHash і storageRoot для кожного облікового запису (де storageRoot також є деревом).
Джерело: біла книга Ethereum. Така ж структура дерева використовується для зберігання транзакцій і квитанцій. Якщо детальніше: то кожен блок має «заголовок», в якому зберігається геш кореневого вузла трьох різних структур дерев Меркле, а саме:
Можливість ефективно зберігати всю інформацію в деревах Меркле виявилася надзвичайно корисною в Ethereum для того, що ми називаємо «легкі клієнти» або «легкі вузли». В загальних рисах є два типи вузлів: повні і легкі.
Повний архівний вузол синхронізує блокчейн, завантажуючи увесь ланцюжок від блоку генези до поточного кінцевого блоку, виконуючи всі транзакції, які він містить. Як правило, майнери зберігають повний архівний вузол, бо цього вимагає процес майнінгу. Також можна завантажити повний вузол, не виконуючи кожну транзакцію. Тим не менше, будь-який повний вузол містить увесь ланцюжок.
Проте, якщо для вузла не потрібне виконання всіх транзакцій або легкого доступу до історії, немає потреби зберігати увесь ланцюжок. І тут стає в пригоді концепція легкого вузла. Замість завантажувати і зберігати увесь ланцюжок і виконувати всі транзакції, легкі вузли завантажують лише ланцюжок заголовків від блоку генези і до поточної вершини, не виконуючи жодних транзакцій і не видобуваючи пов’язані стани. Оскільки легкі вузли мають доступ до заголовків блоків, які містять геші трьох дерев, вони можуть легко генерувати й отримувати достовірні відповіді про транзакції, події, баланси тощо.
Це працює, бо геші в дереві Меркле передаються вгору — якщо зловмисник спробує вставити фейкову транзакцію внизу дерева Меркле, ця зміна викличе зміну в геші вузла вище, що в свою чергу змінить геш наступного вузла, і таким чином дійде до кореня дерева.
Будь-який вузол може перевірити частину даних, використавши процедуру з назвою «підтвердження Меркле». Підтвердження Меркле складається з:
Будь-хто, зчитавши підтвердження, може перевірити, чи гешування гілки цілісне до кореня дерева, і таким чином, чи дана порція дійсно знаходиться у вказаній позиції в дереві.
У підсумку перевагою використання дерева Меркле «Патріція» є те, що кореневий вузол структури криптографічно залежить від збережених у дереві даних, а отже може використовуватися як захищений примірник цих даних. Оскільки заголовок блоку містить кореневий геш дерев стану, транзакцій і квитанцій, будь-який вузол може перевірити невелику частину стану Ethereum без потреби зберігати увесь стан, який може мати необмежений розмір.
Одним із дуже важливих концептів Ethereum є концепт оплат. Кожне обчислення, яке виникає в результаті транзакції в мережі Ethereum, вимагає оплати — безкоштовного нічого не буває. Ця оплата здійснюється в одиницях, які називаються «gas».
Gas — це одиниця вимірювання оплат, які нараховуються за певне обчислення. Ціна gas — це кількість Ether, яку ви хочете витратити за кожну одиницю gas, вимірюється у «gwei». «Wei» — найменша частина Ether, 1⁰¹⁸ Wei = 1 Ether. 1 gwei = 1 000 000 000 Wei.
Для кожної транзакції відправник встановлює ліміт gas і ціну gas. Добуток ціни gas і ліміту gas визначає максимальну кількість Wei, яку відправник має намір заплатити за виконання транзакції.
Наприклад, скажімо, відправник встановив ліміт gas 50 000, а ціну gas 20 gwei. Це означає, що відправник хоче витратити 50 000 x 20 gwei = 1 000 000 000 000 000 Wei = 0,001 Ether за виконання транзакції.
Пам’ятаємо, що ліміт gas — це максимальна кількість gas, на яку відправник має намір витратити кошти. Якщо на балансі його облікового запису є достатньо Ether, щоб покрити цей максимум, можна виконувати транзакцію. Після завершення транзакції всі кошти за невикористані gas повертаються відправникові за початковою вартістю.
У разі, якщо відправник не надав достатню кількість gas для виконання транзакції, транзакція вважається недійсною з позначкою «недостатньо gas». У цьому випадку виконання транзакції переривається, всі зміни стану, які виникли в процесі, скасовуються, і Ethereum повертається до стану перед транзакцією. Крім того здійснюється запис про невдалу спробу транзакції із зазначенням етапу, на якому вона виявилася невдалою. А оскільки машина вже витратила зусилля для обчислень до того, як закінчилися gas, логічно, що кошти за gas відправникові не повертаються.
То ж на що саме йдуть ці кошти gas? Усі кошти, витрачені відправником на gas, надсилаються на адресу «бенефіціара», яка зазвичай є адресою майнера. Оскільки майнери витрачають зусилля для обчислень і перевірки транзакцій, вони отримують оплату за gas як винагороду.
Як правило, що вищу ціну за gas відправник готовий заплатити, то вищу вартість отримає майнер з транзакції. Таким чином, майнери охочіше вибиратимуть такі транзакції. То ж майнери можуть вільно вибирати, які транзакції схвалювати або ігнорувати. Щоб зорієнтувати відправників, яку ціну за gas встановлювати, майнери можуть оголошувати мінімальну ціну за gas, за яку вони виконають транзакції.
Gas використовуються для оплати не тільки за обчислення, але й за використання сховища. Загальна плата за сховище пропорційна найменшому кратному використаних 32 байтів.
Щодо оплати за сховище є деякі нюанси. Наприклад, якщо ви збільшуєте сховище, збільшується розмір бази даних стану Ethereum на всіх вузлах, це спонукає зберігати невеликий обсяг даних. Тому, якщо транзакція містить етап, який вилучає запис зі сховища, оплата за цю операцію не стягується І повертаються кошти за звільнення місця в сховищі.
Одним із важливих аспектів роботи Ethereum є те, що виконання кожної окремої операції в мережі одночасно здійснюється всіма повними вузлами. Проте, обчислювальні кроки на віртуальній машині Ethereum дуже дорогі. Тому смарт-контракти Ethereum краще використовувати для простих задач, як от бізнес-логіка, перевірка підписів та інших криптографічних об’єктів, аніж для складних — зберігання файлів, електронна пошта або машинне навчання, — які можуть збільшити навантаження на мережу. Застосування оплати запобігає перенавантаженню мережі.
Ethereum є мовою, повною за Тюрінгом. (Якщо коротко, то машина Тюрінга — це машина, яка може симулювати будь-який комп’ютерний алгоритм (якщо ви не знайомі з поняттям машини Тюрінга, перегляньте це і це посилання)). Це дозволяє використовувати цикли, а також робить систему Ethereum вразливою до проблеми зупинки, яка полягає в тому, що неможливо визначити, чи робота програми колись завершиться. Без оплат зловмисник може легко і без наслідків для себе зруйнувати мережу, виконавши нескінчений цикл транзакцій. Таким чином оплата захищає мережу від навмисних атак.
Може виникнути питання: «а чому потрібно платити ще й за зберігання?" Як і з обчисленнями, за зберігання даних в Ethereum відповідає вся мережа.
Раніше ми вже згадували, що Ethereum це машина стану на основі транзакцій. Іншими словами, саме транзакції між різними обліковими записами спричиняють зміну глобального стану Ethereum.
Найпростішими словами, транзакція — це криптографічно підписана інструкція, згенерована зовнішнім обліковим записом, серіалізована і надіслана у блокчейн.
Є два типи транзакцій: повідомлення і створення контракту (наприклад, транзакції, які створюють нові контракти Ethereum).
Не залежно від типу, транзакції складаються з таких компонентів:
У розділі «Облікові записи» ми з’ясували, що транзакції, — як повідомлення, так і створення контрактів, — завжди ініціюються зовнішніми обліковими записами і надсилаються у блокчейн. З іншого боку транзакції можна розглядати як місток від зовнішнього світу до внутрішнього стану Ethereum.
Однак, це не означає, що контрактні облікові записи не можуть комунікувати між собою. Контракти, які існують в межах глобального стану Ethereum, можуть комунікувати з такими ж контрактами в тих же межах. Ця комунікація відбувається через «повідомлення» або «внутрішні транзакції» з іншими контрактними обліковими записами. Повідомлення або внутрішні транзакції схожі на звичайні транзакції з однією суттєвою відмінністю: їх НЕ згенеровано зовнішніми обліковими записами. Натомість, їх генерують контрактні облікові записи. Це віртуальні об’єкти, які, на відміну від транзакцій, не серіалізуються й існують тільки в середовищі виконання Ethereum.
Коли один контрактний обліковий запис надсилає внутрішню транзакцію на інший, виконується пов’язаний код, який існує в контрактному обліковому записі отримувача.
Тут важливо зазначити, що внутрішні транзакції або повідомлення не містять параметра gasLimit. Це тому, що ліміт gas встановлюється зовнішнім відправником оригінальної транзакції (наприклад, певним зовнішнім обліковим записом). Ліміт gas, встановлений зовнішнім обліковим записом, має бути достатньо високим для здійснення транзакції, включно з усіма підзадачами, які виникають в результаті транзакції, в тому числі повідомленнями між контрактними обліковими записами. Якщо в ланцюжку транзакцій і повідомлень під час виконання певного повідомлення закінчаться gas, виконання повідомлення скасується разом з усіма наступними повідомленнями, запущеними цим виконанням. Проте, батьківське виконання не скасовується.
Усі транзакції групуються в «блоки». Блокчейн складається з набору таких блоків, пов’язаних у ланцюжок.
Блок в Ethereum складається з:
Що таке «дядько»? «Дядько» — це блок, батьківський блок якого є одночасно батьківським блоком для батьківського блоку поточного блоку. Давайте швидко ознайомимося з тим, для чого використовується поняття «дядько», і навіщо блок містить заголовки блоків-дядьків.
Завдяки способу побудови Ethereum, час, потрібний на формування блоку, набагато менший (близько 15 с.), ніж в інших блокчейнах, наприклад у Bitcoin (близько 10 хв.). Це дає змогу швидше опрацьовувати транзакції. Проте, зворотньою стороною швидшого формування блоків є те, що майнери знаходять більше рішень для формування альтернативних блоків. Ці альтернативні блоки також називають «блоками-сиротами» (тобто створені блоки не включені в головний ланцюг).
Завданням блоків-дядьків є допомгти майнерам отримати винагороду за включення цих блоків-сиріт. Блоки-дядьки, яких майнери включають в ланцюг, мають бути дійсними, тобто належати до шостого або молодшого покоління поточного блоку. Після шостого покоління на застарілі блоки-сироти посилатися неможливо (оскільки включення старіших транзакцій дещо ускладнює завдання).
Винагорода за блок-дядька менша, ніж за повний блок. Проте, для майнерів залишається стимул включати ці блоки-сироти й отримувати винагороду.
Давайте ненадовго повернемося до блоків. Ми згадували раніше, що кожний блок має «заголовок». Що це таке?
Заголовком блоку є частина блоку, яка складається з таких даних:
Зауважте, кожен заголовок блоку містить три структури для:
Ці структури є нічим іншим, як деревами Меркле «Патріція», про які йшла мова раніше.
Крім того, раніше згадувалися декілька термінів, які слід пояснити детальніше.
Ось вони.
Журнали в Ethereum дозволяють відстежувати різноманітні транзакції й повідомлення. Контрактний обліковий запис може явно генерувати журнал, визначивши «події», які слід туди внести.
Запис журналу містить:
Журнали зберігаються у фільтрі Блума, який ефективно зберігає нескінчені дані журналу.
Записи журналу в заголовку формуються з інформації журнальних записів, яка міститься в квитанції транзакції. Так, як ви отримуєте квитанцію, придбавши щось у крамниці, так і Ethereum генерує квитанцію для кожної транзакції. Зрозуміло, що кожна квитанція містить певну інформацію про транзакцію.
Квитанція містить такі елементи, як:
«Складність» блоку використовується для забезпечення часової сумісності схвалення блоків. Складність блоку генези становить 131 072, а для обчислення складності всіх наступних блоків використовується спеціальна формула. Якщо певний блок схвалюється швидше, ніж попередній, протокол Ethereum підвищує його складність.
Складність блоку впливає на параметр nonce, який є гешем, що обчислюється в процесі майнінгу блоку за допомогою алгоритму «доказ роботи».
Математично залежність між складністю блоку і параметром nonce виглядає так:
де Hd — складність.
Єдиний спосіб підібрати параметр nonce, який задовольняє умові порогу складності, є перелічення всіх можливостей за допомогою алгоритму «доказ роботи». Очікуваний час пошуку розв’язку пропорційний складності — що вища складність, то важче знайти значення nonce і важче схвалити блок, через що збільшується час схвалення нового блоку. Таким чином, змінивши складність блоку, протокол може змінити час, необхідний для його схвалення. І навпаки, якщо схвалення триває довше, протокол зменшує складність блоку.
У такий спосіб час схвалення самоналаштовується для збереження однакового значення, — в середньому один блок схвалюється що 15 секунд.
Переходимо до однієї з найскладніших частин протоколу Ethereum: виконання транзакцій. Припустимо, ви надсилаєте транзакцію в мережу Ethereum для обробки. Що відбувається для того, щоб Ethereum змінив стан і включив вашу транзакцію?
В першу чергу для того, щоб транзакція виконалася, вона має задовільняти початковому набору вимог. До них належать:
Якщо транзакція відповідає всім переліченим критеріям дійсності, відбувається перехід до наступного кроку.
Спершу з балансу відправника знімається авансова оплата за виконання, а параметр nonce облікового запису відправника збільшується на 1, щоби врахувати поточну транзакцію. Тепер обчислюється залишок gas, як загальний ліміт gas для транзакції мінус використане власне значення gas транзакції.
Після цього починається виконання транзакції. Під час виконання транзакції Ethereum відстежує «підстан». Це спосіб запису інформації, отриманої під час транзакції, яка буде потрібна відразу після завершення виконання.
Зокрема, сюди належать:
Після цього виконуються обчислення, передбачені транзакцією.
Після того, як всі кроки, необхідні для транзакції, виконані, якщо немає недійсних станів, стан фіналізується і визначається, скільки gas не використано і буде повернено відправникові. Окрім невикористаних gas, відправник також отримає певні кошти з «балансу відшкодування», описаного вище.
Після відшкодування відправникові:
В результаті ми отримуємо новий стан і набір журналів, створених транзакцією.
Після того, як ми ознайомилися з основами виконання транзакцій, давайте розглянемо відмінності між транзакціями створення контракту і повідомленнями.
Нагадаємо: що в Ethereum є два типи облікових записів: контрактні і зовнішні. Коли мова йде про транзакцію «створення контракту», мається наувазі, що метою транзакції є створення контрактного облікового запису.
Щоб створити новий контрактний обліковий запис, спершу слід задекларувати його адресу, використовуючи спеціальну формулу. Після цього ініціалізується новий обліковий запис за такою процедурою:
Після ініціалізації облікового запису його можна створити, надіславши з транзакцією код init (щоб нагадати собі, що це таке, перегляньте розділ «Транзакції і повідомлення»). Виконання цього коду відбувається по різному. Залежно від конструктора контрактів, може оновлюватися сховище облікового запису, створюватися інші контрактні облікові записи, надсилатися нові повідомлення тощо.
Під час виконання коду для ініціалізації контракту використовуються gas. Транзакція не може використати більше gas, ніж його залишилося. Якщо це трапилося, виконання переривається з винятком out-of-gas (OOG). Якщо виконання транзакції припинилося з винятком out-of-gas, стан повертається до значення безпосередньо перед транзакцією. Відправник не отримує відшкодування gas, витрачених до переривання.
Шкода.
Проте, якщо відправник надіслав з транзакцією Ether, цю суму буде повернено, навіть якщо не вдасться створити контракт.
І це добре.
Якщо код ініціалізації виконано успішно, буде стягнуто кінцеву вартість створення контракту. Це вартість сховища, вона пропорційна розміру коду створення контракту (нічого не буває безкоштовним!). Якщо для сплати цієї вартості залишилося недостатньо gas, транзакцію буде перервано з винятком out-of-gas.
Якщо все пройшло успішно без збоїв і винятків, усі невикористані gas відшкодовуються відправникові транзакції і затверджується змінений стан.
Ура!
Виконання транзакції повідомлення подібне до виконання транзакції створення контракту, проте є деякі відмінності.
Виконання повідомлення не містить коду ініціалізації, оскільки облікові записи не створюються. Проте, воно може містити вхідні дані, якщо їх надано відправником. Під час виконання повідомлення також має додатковий компонент з вихідними даними, який використовується, якщо ці дані потрібні для подальших виконань.
Як і зі створенням контракту, якщо виконання транзакції повідомлення припиняється через недостаню кількість gas або через недійсну транзакцію (наприклад, переповнення стеку, недійсний перехід, недійсна інструкція), використані gas відправникові не відшкодовуються. Натомість, використовуються всі невикористані gas: а стан повертається до стану безпосередньо перед передачею балансу.
До найновішого оновлення Ethereum не було способу зупинити або скасувати виконання транзакції так, щоб система не використала всі надані gas. Скажімо, ви авторизували контракт, який видав помилку через те, що відправник не має повноважень виконувати певну транзакцію. У попередніх версіях Ethereum залишок gas все одно був би використаний без відшкодувань відправникові. Однак оновлення Byzantium включає новий код «відкочування», який дозволяє контрактному обліковому запису зупинити виконання і відкотити зміни стану без використання решти gas, а також із можливістю повернути причину збою транзакції. Якщо транзакція припинилася через відкочування стану, невикористані gas повертаються відправникові.
Тепер ми знаємо послідовність кроків, необхідну для виконання транзакції від початку до кінця. Тепер подивимося, як насправді виконується транзакція в віртуальній машині.
Частина протоколу, яка фактично керує виконанням транзакції, є внутрішньою віртуальною машиною Ethereum, відомою як EVM (Ethereum Virtual Machine).
EVM — це віртуальна машина, повна за Тюрінгом, як зазначалося вище. Єдине обмеження, яке є в EVM, і якого немає в типової машини, повної за Тюрінгом, це те, що EVM внутрішньо обмежена за допомогою gas. Таким чином, загальна кількість обчислень, які можуть бути виконані, внутрішньо обмежена кількістю наданих gas.
Джерело: CMU Більше того, архітектура EVM базується на стеку. Машина зі стековою організацією — це комп’ютер, який для зберігання тимчасових значень використовує стек LIFO.
Розмір кожного елемента стеку EVM становить 256 біт, а максимальний розмір стеку 1024.
EVM має пам’ять, в якій елементи зберігаються як масиви байтів з послівною адресацією. Пам’ять є змінною, тобто не постійною.
Крім того EVM має власне сховище. На відміну від пам’яті, сховище є постійним і розглядається як частина стану системи. EVM зберігає програмний код окремо, в віртуальній пам’яті ROM, доступ до якої можливий лише за допомогою стеціальної інструкції. Таким чином EVM відрізняється від типової vархітектури фон Неймана, в якій прогамний код зберігається у пам’яті або сховищі.
EVM також має власну мову, «байткод EVM». Коли програмісти, як ви чи я, пише смарт-контракт для роботи в Ethereum, зазвичай, використовується мова програмування високого рівня, наприклад Solidity. Після цього його можна компілювати в байткод EVM, який може зрозуміти EVM.
А тепер перейдемо до виконання.
Перед виконанням певного обчислення процесор перевіряє доступність і дійсність такої інформації:
На початку виконання пам’ять і стек порожні, а значення лічильника програми «0».
PC: 0 STACK: [] MEM: [], STORAGE: {}
Після цього EVM рекурсивно виконує транзакцію, обчислюючи стан системи і стан машини для кожної ітерації циклу. Стан системи — це глобальний стан Ethereum.
Стан машини складається з:
Елементи стеку додаються або вилучаються з найлівішої частини ряду.
На кожній ітерації циклу від gas, що залишилися, віднімається потрібна кількість і збільшується лічильник програми.
Під час завершення кожної ітерації є три можливості:
Припускаємо, що виняткової ситуації не відбулося, досягнуто контрольованого нормального завершення, машина згенерувала результуючий стан, залишок gas після виконання, отриманий підстан і кінцевий результат.
Фух.
Ми пройшли через найскладнішу частину Ethereum. Навіть, якщо ви не повністю її зрозуміли, це не страшно. Насправді, вам не потрібно розуміти в деталях, як відбувається виконання, якщо ви не працюєте на дуже глибокому рівні.
На закінчення подивимося, як фіналізується блок з багатьох транзакцій.
Під словом «фіналізується» можна розуміти дві різні речі, залежно відтого, новий це блок, чи існуючий. Якщо це блок новий, то ми говоримо про процес, необхідний для його майнінгу. Якщо блок існуючий, то мова йтиме про процес його валідації. У будь-якому разі для «фіналізації» блоку є чотири вимоги:
1) Перевірка (чи, у разі майнінгу, визначення) блоків-дядьків
Кожний блок-дядько в заголовку блока має бути дійсним заголовком і знаходитися в межах шести поколінь від поточного блоку.
2) Валідація (чи, у разі майнінгу, визначення) транзакцій
Параметр gasUsed блоку має дорівнювати сумарній кількості gas, використаних транзакціями, переліченими в блоці. (Слід нагадати, що під час виконання транзакції ми відстежуємо лічильник gas блоку, який у свою чергу відстежує загальну кількість gas, використаних всіма транзакціями в блоці).
3) Призначення винагороди (лише в разі майнінгу)
Адреса-бенефіціар отримує 5 Ether за майнінг блоку. (За пропозицією Ethereum EIP-649 цю винагороду 5 ETH незабаром буде зменшено з 5 ETH до 3 ETH). Окрім того, за кожний блок-дядька бенефіціар поточного блоку отримує додаткову винагороду в розмірі 1/32 від винагороди за поточний блок. Нарешті, бенефіціар дядька(-ів) також отримує певну винагороду (для її обчислення існує спеціальна формула).
4) Перевірка (або, в разі майнінгу — обчислення дійсного) стану і параметра nonce.
Переконайтеся, що застосовано всі транзакції й результуючі стани, а тоді визначте новий блок як стан після того, як винагороду за блок додано до кінцевого результату транзакції. Перевірка відбувається порівнянням цього результуючого стану з деревом стану, збереженим у заголовку.
В розділі «Блоки» коротко описано поняття складності блоку. Алгоритм, за яким визначається значення складності блоку, називається «Доказ роботи» (PoW — Proof of Work).
Алгоритм доказу роботи в Ethereum називається «Ethash» (раніше — Dagger-Hashimoto).
Формально алгоритм визаначається так:
де m це mixHash, n це nonce, Hn є заголовком нового блоку (за винятком компонентів nonce і mixHash , які потрібно обчислювати), Hn — параметр nonce заголовку блоку, а d це DAG*, який є великим набором даних.
У розділі «Блоки*» ми згадували про елементи, які містяться в заголовку блоку.Два з них називалися mixHash і nonce.
Нагадаємо:
Для обчислення цих двох елементів використовується функція PoW.
Як саме обчислюються параметри mixHash і nonce за допомогою функції PoW, — питання дещо складніше. Детальніше ми його розберемо окремою статтею. Але на вищому рівні це працює так: для кожного блоку обчислюється «основа».
Ця основа відрізняється для кожної «епохи», довжина епохи становить 30 000 блоків. Для першої епохи основа є гешем послідовності з 32 байтів нулів. Для кожної наступної епохи вона є гешем гешу попередньої основи. На базі цієї основи вузол може обчислити псевдовипадковий «кеш». Цей кеш уможливлює концепцію «легких вузлів», яку ми обговорювали раніше в цій статті. Легкі вузли дають змогу певним вузлам ефективно перевіряти транзакцію, не перевантажуючи пам’ять зберіганням повного набору даних усього блокчейну. Легкий вузол може перевірити дійсність транзакції на основі виключно кешу, оскільки з кешу можна регенерувати конкретний блок, який потрібно перевірити.
За допомогою кешу вузол може генерувати «набір даних» DAG, кожен елемент якого залежить від невеликої кількості псевдовипадково вибраних елементів кешу. Щоб стати майнером, вам слід генерувати цей повний набір даних; всі повні клієнти і майнери зберігають його, і він зростає лінійно з часом.
Майнери можуть брати випадкові частини цього набору даних і за допомогою математичної функції об’єднувати його в геш у параметр «mixHash». Майнер постійно генерує mixHash, допоки на виході значення не досягає цільового значення nonce. Як тільки цю умову виконано, nonce вважається дійсним, і блок можна додати в ланцюжок.
Загалом, алгоритм PoW використовується для доведення у криптографічно захищений спосіб, що було здійснено певну кількість обчислень для генерування деякого результату (наприклад, nonce). Це тому, що найкращим способом знайти значення nonce, яке не перевищує заданого порогового значення, є перелічити всі можливі варіанти. Якщо послідовно застосовувати геш-функцію, результат матиме рівномірний розподіл, а отже, в середньому час, потрібний для знаходження такого значення nonce, залежить від порогу складності. Що вища складність, то більше часу потрібно для розв’язку nonce. У такий спосіб алгоритм PoW пояснює концепцію складності, яка використовується для гарантування безпеки блокчейну.
Що таке безпека блокчейну? Це просто: ми хочемо створити блокчейн, якому довіряє КОЖНИЙ. Як уже згадувалося раніше, якщо існує декілька ланцюжків, довіра користувачів втратиться, оскільки неможливо раціонально визначити, який з ланцюжків «дійсний». Щоб група користувачів погодилася з наявним станом, збереженим у блокчейні, потрібно мати єдиний канонічний блокчейн, якому група довіряє.
Саме це робить алгоритм PoW: він гарантує, що конкретний блокчейн залишатиметься канонічним в майбутньому, створюючи майже нездоланні перешкоди для зловмисників, які мають намір створити нові блоки, щоб переписати частину історії (наприклад, стерши певні транзакції чи створивши фейкові) або утворити розгалуження. Щоб такі блоки були валідовані першими, потрібно, щоб значення nonce було розв’язано без суперечностей і швидше, ніж будь які інші в мережі, оскільки користувачі мережі переконані, що їх ланцюжок є найтяжчим (на основі принципів протоколу GHOST, згаданого раніше). Це неможливо, якщо в зловмисника немає понад половини майнінгової потужності мережі, — сценарій, відомий як атака 51%.
Окрім захисту блокчейну, алгоритм PoW такж розподіляє прибуток між тими користувачами, які витратили свої обчислювальні потужності для забезпечення цього захисту. Нагадаємо, майнер отримує винагороду за майнінг блоку, а саме:
Щоб забезпечити довготривалість застосування механізму консенсусу алгоритму PoW для захисту і розподілу прибутку, Ethereum намагається впровадити такі дві властивості:
У мережі блокчейну Bitcoin у зв’язку з наведеними двома властивостями виникла проблема, пов’язана з тим, що алгоритмом PoW є геш-функція SHA256. Слабким місцем такого типу функцій є те, що її можна обчислювати значно ефективніше, використовуючи спеціальне обладнання, відоме як ASICs.
Щоб нейтралізувати цю проблему, алгоритм PoW в Ethereum (Ethhash) послідовно сильно залежить від пам’яті. Це означає, що алгоритм розроблено так, щоб обчислення nonce вимагало великого об’єму пам’яті І пропускної здатності. Задіювання великого об’єму пам’яті ускладнює використоання пам’яті комп’ютера для одночасного обчислення кількох nonce, а вимога до високої пропускної здатності ускладнює одночасне обчислення кількох nonce навіть для надшвидких комп’ютерів. В такий спосіб знижується ризик централізації, і вирівнюються шанси для вузлів, які виконують перевірку.
Однак слід зазначити, що зараз Ethereum переходить від механізму консенсусу PoW до іншого механізму з назвою «доказ ставки». Це окрема неприємна тема, маю надію дослідити її наступним дописом.
☺️
…Фух! Ви дійшли до кінця. Сподіваюся.
Я знаю, що в цій статті було багато інформації для переварювання. Якщо вам знадобилося перечитати її декілька разів, щоб усе зрозуміти, це чудово. Мені особисто довелося не один раз перечитати документацію, посібники і різні частини бази коду, щоби в усьому розібратися. Тим не менше, сподіваюся, цей огляд став вам у пригоді.
Якщо ви знайшли помилки або неточності, буду вдячний, якщо напишете мені про це приватним повідомленням або тут в коментарях. Обіцяю, що прочитаю всі ;)
Не забувайте, що я теж людина (так, це правда) і можу допускатися помилок. Цей допис зроблено безкоштовно, на користь спільноти. То ж будьте конструктивними у відгуках, наїзди будуть зайвими.
☺️
[1] https://github.com/ethereum/yellowpaper
Founder & CEO of TruStory. I have a passion for understanding things at a fundamental level and sharing it as clearly as possible.