Перейти к основному содержимому
Блокчейн03ГЛАВА 03Первый контракт: из чего состоит код0x37…364 байтатекст → байты

Первый контракт: из чего состоит код

Прочитано0%
Квизы0/6
Тренажёрыне пройден0/1
Уровень 1 · Новичок

Зачем это нужно​

Это первая глава, в которой появляется код. До неё были блокчейн как идея и редактор как инструмент; отсюда начинается то, за что на чемпионате ставят баллы.

Первый шаг демонстрации в модуле А сформулирован так: «развернуть частную сеть и опубликовать в сети смарт-контракты». Контракт, который не компилируется, опубликовать нельзя. А «шаги засчитываются только при полном выполнении» — значит, одна пропущенная точка с запятой за пять минут до сдачи обнуляет модуль на 37 баллов.

Поэтому глава начинается не с блокчейна, а с самого кода: из чего собрана строка, почему компилятор отказывается её читать и как понять, что именно ему не понравилось. Язык — Solidity, тот, на котором пишут смарт-контракты для сетей семейства Ethereum. Это первый язык программирования в треке, и никаких других знать не нужно.

Программа — список приказов для машины​

Код — это текст, в котором каждая строка приказывает машине ровно одно действие: «запомни число», «прибавь единицу», «верни значение». Машина не догадывается, что имелось в виду, и не прощает: пропущенный знак — отказ выполнять весь файл.

Между текстом и машиной стоит компилятор (compiler) — программа, которая читает наш текст и делает одно из двух: либо переводит его в машинные байты, либо возвращает сообщение об ошибке с номером строки. Весь трек — про то, как писать текст, который компилятор принимает, и понимать, что этот текст сделает в сети.

Строка кода: слова-ключи, имена и знаки​

Возьмём одну строку — она появится в нашем первом контракте:

uint256 public count;
Нажми на часть выражения

В любой строке кода три сорта элементов. Слова-ключи (keywords) — словарь языка: contract, function, public, view, uint256, returns. Имена (identifiers) — то, что придумываем мы: Counter, count, inc. Знаки — разметка.

Компилятор различает регистр и не угадывает опечатки. unit256 вместо uint256 — для него не тип с перестановкой букв, а неизвестное имя. Кириллица в имени запрещена вовсе: uint256 public счётчик; даёт ParserError: Expected identifier but got 'ILLEGAL'. Кириллица остаётся для комментариев и текстов сообщений — об этом ниже.

Точка с запятой заканчивает приказ, фигурные скобки делают коробку​

Знак ; — точка в конце предложения. Без неё компилятор читает две строки как одну и спотыкается уже на следующем слове. Отсюда первая ловушка чтения ошибок: сообщение Expected ';' but got 'function' указывает не на строку с пропуском, а на строку после неё — туда, где компилятор споткнулся.

Пара { … } — коробка. Всё внутри принадлежит тому, что написано перед открывающей скобкой:

contract Counter { // коробка контракта
function inc() public { // коробка функции внутри коробки контракта
count = count + 1;
}
}

Скобок всегда чётное число. Потерянная } даёт ошибку в самой последней строке файла — там, где компилятор ждал закрытия и дошёл до конца.

Отступы и комментарии — для человека​

Четыре пробела перед строкой внутри коробки — соглашение для глаз. Компилятор пробелы и переводы строк не считает: контракт, записанный в одну строку, компилируется так же. Отступы нужны, чтобы человек видел, какая строка в какой коробке лежит.

Комментарий начинается с // и живёт до конца строки; многострочный — между /* и */. В комментариях можно писать кириллицу — и нужно: по конкурсному заданию «код разработанного решения должен быть задокументирован», а эксперт читает пояснения к каждой функции.

В третьей модели этой главы контракт с комментариями на 656 байт исходника даёт 364 байта байткода — ни одна буква комментария туда не попадает. Комментарии существуют только для читателя.

Важно

Из штрафных санкций задания: «за использование ненормативной лексики… письменно в представленных к проверке файлах» — минус 5 баллов от общей суммы. Это больше, чем весит любая отдельная функция. Комментарии читают.

Смарт-контракт — программа с памятью, которая живёт в сети​

Обычная программа запускается на одном компьютере и после закрытия забывает всё. Смарт-контракт (smart contract) публикуется в блокчейн один раз, получает адрес вида 0x5FbDB2315678afecb367f032d93F642f64180aa3 — и с этого момента его код и его память хранят все узлы сети.

Память контракта называется состоянием (state). Изменить её может только транзакция, за которую платит отправитель. Прочитать — любой, бесплатно. Выключить контракт или стереть его память никто не может — в том числе автор.

Отсюда и требование первого шага демонстрации: развернуть сеть и опубликовать контракты. Всё дальнейшее — вызовы функций уже опубликованного кода.

Первые две строки файла: SPDX и pragma​

Каждый файл с контрактом начинается с двух строк, которые не делают ничего в сети, но без которых компилятор ругается:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

Первая — комментарий особого вида, машинно-читаемая метка лицензии. Без неё файл собирается, но с предупреждением: Warning: SPDX license identifier not provided in source file. Эксперт увидит небрежность.

Вторая — pragma: не приказ машине, а требование к компилятору. ^0.8.20 означает «любая версия от 0.8.20 до 0.9.0, не включая». Версии Solidity пишутся тремя числами; актуальная линия — 0.8.x, последний выпуск на сегодня — 0.8.36. Если версия компилятора в проекте под pragma не подходит, до компиляции дело не доходит: инструмент отказывается сразу.

В эталонном контракте конкурсного задания стоит pragma solidity ^0.8.0;, и текст задания это подтверждает: «написан с использованием компилятора языка программирования Solidity версии 0.8.0».

Отвечено вопросов: 0 из 2
Компилятор сообщает: «Expected ';' but got 'function'» и указывает на строку 7. Где пропущена точка с запятой?
Что делает строка pragma solidity ^0.8.20?
Отвечено верно: 0 из 2

contract Counter — коробка с именем​

Слово-ключ contract, потом имя с заглавной буквы, потом коробка. Имя контракта принято писать так же, как имя файла: Counter в Counter.sol.

Внутри коробки — только объявления: переменные и функции. Действие само по себе внутри contract не допускается. Строка count = 1; на уровне контракта даст ParserError: Expected identifier but got '=' — после слова count компилятор ждал объявление, а не присваивание. Действия живут внутри функций.

В эталонном контракте задания первая ошибка отчёта по синтаксису — именно в этой строке: contract Token are ERC20(...). Компилятор отвечает ParserError: Expected '{' but got identifier и ставит стрелки под словом are. Правильное слово — is; что оно означает, разберём в главе про наследование.

Переменная состояния: одно число, которое переживает вызов​

Переменная (variable) — именованная ячейка памяти. Вот наша:

uint256 public count;

uint256 — тип: целое без знака, от 0 до 2 в 256-й степени минус один; самый частый тип в Solidity. public — компилятор сам создаёт функцию-читалку count(), через которую число видно снаружи. Без public число хранится, но прочитать его извне нельзя — в третьей модели это видно как разница в ABI и в байтах.

Переменная, объявленная на уровне контракта, — переменная состояния (state variable). Она лежит в хранилище сети, начальное значение — 0, и каждое её изменение стоит газ. Что такое газ — двумя секциями ниже, числами.

Функция inc() — приказ, который меняет память​

Функция (function) — именованный набор приказов, который выполняется по вызову:

function inc() public {
count = count + 1;
}
Нажми на часть выражения

Функция без слова view меняет состояние, поэтому вызывается транзакцией. Если написать view и при этом писать в count, компилятор откажет: TypeError: Function cannot be declared as view because this expression (potentially) modifies the state — и поставит стрелки под тем count, в который пишут.

Целиком первый контракт:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

// Контракт-счётчик: одно число в хранилище и одна функция, которая его увеличивает.
contract Counter {
// Переменная состояния: живёт в блокчейне между вызовами.
// public — компилятор сам сделает функцию-читалку count().
uint256 public count;

// Функция без view меняет состояние, поэтому вызывается транзакцией.
function inc() public {
count = count + 1;
}
}

Десять строк без комментариев. Это скелет, который на площадке набирают по памяти: SPDX → pragma → contract → переменная → функция.

Отвечено вопросов: 0 из 2
Что означает один знак = в строке count = count + 1?
Функция объявлена как view, но внутри пишет в переменную состояния. Что сделает компилятор?
Отвечено верно: 0 из 2

Компилятор делает из текста два продукта: байткод и ABI​

Что происходит с десятью строками после компиляции? Компилятор в этом треке вызывается через готовый проект Hardhat — «компилятор и сеть в одной команде»; как он устроен, отдельный блок впереди, а сейчас достаточно двух команд: npx hardhat compile и npx hardhat run scripts/имя.js. Скрипт-водитель написан на JavaScript — языке, до которого мы дойдём во втором блоке; читать его сейчас не нужно, важно только то, что он печатает.

Вот что напечатал скрипт, который заглянул в результат компиляции двух контрактов — нашего Counter и такого же CounterHidden, у которого переменная объявлена без public:

── Counter ──
байткод для развёртывания: 364 байт
байткод, остающийся в сети: 333 байт
записей в ABI: 2
function count() view → uint256
function inc() nonpayable

── CounterHidden ──
байткод для развёртывания: 260 байт
байткод, остающийся в сети: 230 байт
записей в ABI: 1
function inc() nonpayable

── что в сети после развёртывания ──
getCode(адрес): 333 байт
совпадает с deployedBytecode из артефакта: true

── имени функции в байткоде нет — есть 4 байта селектора ──
keccak256("inc()") первые 4 байта: 0x371303c0
селектор встречается в байткоде: true
строка "inc" (696e63) встречается в байткоде: false
селектор count(): 0x06661abd
КОМПИЛЯТОР ДЕЛАЕТ ИЗ ТЕКСТА ДВА ПРОДУКТАCounter.solcontract Counter { uint256 public count; function inc() public { count = count + 1; }}10 строк · 656 байт с комментариямиsolcбайткод — для машины364 байта на развёртывание · 333 останутся в сетиABI — для людей и кошельковcount() view → uint256 · inc() nonpayableимени inc в байткоде нет — вместо него 4 байта селектора 0x371303c0ни одна буква комментария в байткод не попадает
Компилятор превращает 10 строк в 364 байта байткода и в ABI из двух записей; имён функций в байткоде нет — только четырёхбайтовые селекторы

Продукт первый — байткод (bytecode): 364 байта для развёртывания, из них 333 останутся в сети, а 31 — код-установщик, который выполняется один раз. Имён в байткоде нет: строку inc там не найти. Вместо неё 4 байта селектора 0x371303c0 — начало хэша от подписи inc(). Именно по селектору сеть понимает, какую функцию вызвали.

Продукт второй — ABI (Application Binary Interface): список функций в читаемом формате. Без ABI ни кошелёк, ни скрипт не знают, как обратиться к байтам.

Слово public у переменной стоит 104 байта кода и одну запись ABI — это цена функции-читалки, которую компилятор написал за нас.

Развёртывание — транзакция, которая создаёт адрес​

Развёртывание (deploy) — транзакция без получателя, но с байткодом. Сеть кладёт её в блок и выдаёт адрес. Первая модель главы разворачивает Counter и печатает:

── развёртывание ──
адрес контракта: 0x5FbDB2315678afecb367f032d93F642f64180aa3
блок: 1
газ за развёртывание: 124749

Единица работы сети называется газ (gas). Каждая операция стоит фиксированное число единиц, а отправитель платит за них деньгами сети — ETH. Развёртывание обошлось в 124 749 единиц: 333 байта кода записываются навсегда, по 200 газа за байт — 66 600 только за хранение.

Адрес 0x5FbDB…0aa3 неслучаен: в локальной сети Hardhat первый контракт, развёрнутый с первого аккаунта, всегда получает его. По этому адресу видно, что запущена чистая сеть.

Чтение бесплатно и мгновенно, транзакция стоит газ и ждёт блок​

Продолжение той же модели — сначала читаем count(), потом вызываем inc() дважды:

── чтение count() — view, бесплатно ──
count = 0
новых блоков: 0
списано wei: 0

── вызов inc() — транзакция, стоит газ ──
хэш транзакции: 0x5717e37953efa3c70de9214db78909aeb4387a833613c7b00fface0bf5200b6a
блок: 2
газ потрачен: 43510
цена газа, wei: 1766079815
списано wei: 76842132750650
списано ETH: 0.00007684213275065
count = 1

── второй inc(): то же действие, другой газ ──
газ потрачен: 26410
count = 2
ЧТЕНИЕ БЕСПЛАТНО, ЗАПИСЬ СТОИТ ГАЗcount() — viewновых блоков: 0списано wei: 0узел отвечает сам, в сеть ничего не уходитответ мгновенныйinc() — транзакцияблок: 2 · газ: 43 510списано: 0,000077 ETHподпись → блок → wait() → результатвторой inc(): 26 410 — ячейка уже занятаразвёртывание — тоже транзакция: 124 749 газа, потому что 333 байта кода записываются навсегдазапись в пустую ячейку (0→1) дороже записи в занятую (1→2) на 17 100 газа
Чтение не создаёт блоков и ничего не списывает; запись создаёт блок и стоит газ — причём второй такой же вызов дешевле первого

Три факта, которые глазами не увидеть.

Чтение бесплатно и мгновенно. count() не создал ни одного блока и не списал ни одного wei (wei — наименьшая единица ETH, одна квинтиллионная). Узел ответил сам, в сеть ничего не ушло.

Запись — транзакция. inc() подписан, попал в блок 2, стоил 43 510 газа по цене 1 766 079 815 wei за единицу — итого 0,000077 ETH. До того как транзакция попала в блок, результата нет.

Второй inc() дешевле на 17 100 газа. Запись в пустую ячейку хранилища (0 → 1) дороже, чем в уже занятую (1 → 2). Из 26 410 базовые 21 000 — плата за саму транзакцию, работа контракта — остаток.

Интересный факт

Числа газа сняты на компиляторе 0.8.28 в локальной сети Hardhat. На другой версии компилятора или другой настройке сети они сдвинутся на единицы процентов; соотношения — развёртывание втрое дороже вызова, повторная запись дешевле первой — сохранятся.

Отвечено вопросов: 0 из 2
Чем вызов count() отличается от вызова inc() для сети?
Почему второй inc() стоил 26 410 газа, а первый — 43 510?
Отвечено верно: 0 из 2

Как читать сообщение компилятора​

Вторая модель берёт правильный Counter и портит его пятью разными способами, каждый раз показывая, что печатает компилятор:

── пропущена ; после count ──
ошибок: 1
ParserError: Expected ';' but got 'function'
--> Counter.sol:7:5:
|
7 | function inc() public {
| ^^^^^^^^

── опечатка в типе: unit256 ──
ошибок: 1
DeclarationError: Identifier not found or not unique.
--> Counter.sol:5:5:
|
5 | unit256 public count;
| ^^^^^^^

── опечатка в имени: cuont ──
ошибок: 1
DeclarationError: Undeclared identifier. Did you mean "count"?
--> Counter.sol:8:9:
|
8 | cuont = count + 1;
| ^^^^^

── две опечатки сразу: нет ; и cuont ──
ошибок: 1
ParserError: Expected ';' but got 'function'
--> Counter.sol:7:5:

── потеряна закрывающая } у функции ──
ошибок: 1
ParserError: Function, variable, struct or modifier declaration expected.
--> Counter.sol:10:1:
СООБЩЕНИЕ КОМПИЛЯТОРА — ТРИ СТРОКИParserError: Expected ';' but got 'function' --> Counter.sol:7:5:7 | function inc() public { | ^^^^^^^^тип ошибки и что ждалифайл : строка : столбецстрелки под виновникомпропуск в строке 5, а ошибка указывает на строку 7 — компилятор показывает, где споткнулсяпри двух опечатках показывается одна: чинить сверху вниз и компилировать после каждой правкиParserError → DeclarationError → TypeError: компилятор проверяет слоями
Три строки сообщения: тип ошибки, адрес строка:столбец, стрелки под виновником — и ловушка: при двух опечатках показывается одна

Сообщение читается по трём строкам. Первая — категория и что ждали. ParserError — текст не разобран: пропущен знак, кириллица в имени. DeclarationError — текст разобран, но имя неизвестно: unit256, cuont; здесь компилятор даже подсказывает Did you mean "count"?. TypeError — всё известно, но действие недопустимо: запись в view-функции. SyntaxError — не хватает обязательного слова, например видимости. Вторая — файл, строка, столбец. Третья — строка кода со стрелками под виновником.

Ловушка, которую показала модель: при двух опечатках сразу компилятор напечатал ровно одну ошибку — ParserError. До проверки имён он просто не дошёл: сначала разбирается текст, потом имена, потом типы. Поэтому «ошибок: 1» не значит «в файле одна ошибка». Чинить надо сверху вниз и компилировать после каждой правки.

Типичные ошибки​

Все сообщения — из живого прогона.

Пропущена ; после объявления. ParserError: Expected ';' but got 'function' — указывает на следующую строку. Смотреть на строку выше названной.

unit256 вместо uint256. DeclarationError: Identifier not found or not unique. — компилятор принял слово за неизвестное имя. uint — от unsigned integer, не unit.

Опечатка в имени внутри функции. DeclarationError: Undeclared identifier. Did you mean "count"? — принять подсказку. Регистр тоже считается: Count и count разные.

Кириллица в имени. ParserError: Expected identifier but got 'ILLEGAL' — одна стрелка под первой русской буквой. Имена — латиницей.

Русский текст в строке без префикса. ParserError: Invalid character in string. If you are trying to use Unicode characters, use a unicode"..." string literal. Писать unicode"Слишком много" — ровно так сделаны все сообщения в эталонном контракте задания.

view у функции, которая пишет. TypeError: Function cannot be declared as view because this expression (potentially) modifies the state. Убрать view.

Функция без видимости. SyntaxError: No visibility specified. Did you intend to add "public"? Дописать public; другие варианты — в главе про функции.

Pragma не совпадает с компилятором проекта. Error HH606: The project cannot be compiled — это отказ инструмента, не компилятора: ни строки, ни столбца. Писать ^0.8.20 или посмотреть версию в настройках проекта.

Слова главы​

  • Код — текст из приказов машине; компилятор (compiler) переводит его в байты или возвращает ошибку.
  • Слово-ключ (keyword) — зарезервированное слово языка; имя (identifier) — то, что придумываем сами, латиницей.
  • ; — конец приказа; { } — коробка, в которой лежит содержимое.
  • Комментарий — // до конца строки или /* … */; в байткод не попадает.
  • SPDX — метка лицензии в первой строке; pragma — требование к версии компилятора.
  • contract — коробка с именем; внутри только объявления.
  • Переменная состояния (state variable) — ячейка в хранилище сети, переживает вызовы.
  • uint256 — целое без знака до 2²⁵⁶−1; public — видно снаружи.
  • Функция (function) — именованный набор приказов; view — только читает.
  • Транзакция — подписанный вызов, который меняет состояние и попадает в блок.
  • Газ (gas) — единица работы сети; оплачивается в ETH; wei — 10⁻¹⁸ ETH.
  • Байткод (bytecode) — машинный код контракта; ABI — описание функций для людей и кошельков.
  • Селектор — 4 байта хэша подписи функции, по которым сеть находит нужную функцию.
  • Развёртывание (deploy) — транзакция, создающая контракт по адресу.

Тренажёр: набрать вслепую​

Скелет контракта на площадке набирается по памяти — интернет ограничен, шпаргалки обнуляют модуль.

Цель: скорость ≥ 100 зн/мин, точность ≥ 88%

// SPDX-License-Identifier: MIT

На чемпионате​

Модуль А, 37 баллов. Первый шаг демонстрации — развернуть сеть и опубликовать контракты. Не компилируется — не публикуется — ноль за все последующие шаги. «Код разработанного решения должен быть задокументирован» и «привести достаточные пояснения к работе всех функций смарт-контракта» — комментарии над каждой переменной и функцией эксперт читает первыми.

Модуль В, 14 баллов. Первый отчёт — «по синтаксису: указать строки, в которых допущены синтаксические ошибки, и их исправление». Проверяется сверкой с ключом. В эталонном контракте таких строк около десяти: are вместо is, enums вместо enum, 1 minute вместо 1 minutes, .lenth, userMaps, value, sender, newPrice. Компилятор здесь не помощник, а ловушка: он останавливается на первой ParserError и остальное не показывает. Искать надо глазами, строка за строкой.

Интернет на модуле А ограничен, а «использование материалов, подготовленных вне конкурсного времени, в том числе шпаргалок» обнуляет модуль. Скелет из десяти строк и две команды — по памяти.

Файловая дисциплина. Папка «2026_AХ» в центре рабочего стола, латинская раскладка; исполняемый файл «StartХ»; инструкции по запуску. Имя контракта совпадает с именем файла — та же привычка к порядку.

Отвечено вопросов: 0 из 2
В эталонном контракте около десяти синтаксических ошибок. Сколько покажет компилятор при первом запуске?
Почему на площадке скелет контракта надо уметь набрать по памяти?
Отвечено верно: 0 из 2

Что дальше​

Следующая глава — типы данных: чем uint256 отличается от int256, что такое address, bool, string, почему 0.00075 ether и 10 minutes из эталонного контракта — это обычные числа, и что происходит при переполнении. Потом — функции с входными данными и возвратом, потом — проверки require с сообщениями, которые в эталонном контракте стоят пустыми.

Отвечено вопросов: 0 из 1
Что из перечисленного попадает в байткод?
Отвечено верно: 0 из 1

Углубиться​

  • Метаданные в конце байткода — почему один и тот же контракт с другим комментарием даёт разные последние байты: там хэш метаданных, а не код.
  • Опкоды EVM — из чего собраны 333 байта; Remix во вкладке Debugger показывает их по одному.
  • pure против view — функция, которая не только не пишет, но и не читает состояние.
  • Цель EVM — компилятор собирает под конкретную версию виртуальной машины сети; в 0.8.20 умолчание сменилось на Shanghai, в 0.8.36 — Osaka; Hardhat 2 для локальной сети выбирает Paris, чтобы байткод работал везде.
  • constant и immutable — как объявить число, за хранение которого не платят.

Сводная проверка по всей главе: 8 вопросов, 5:00 на всё, без подсказок по ходу. Вопросы показываются по одному, ответ изменить нельзя. Если время выйдет — экзамен завершится с тем, что успел ответить. Пересдавать можно сколько угодно раз.

Куда дальше: проверенные ресурсы​

Видео по теме

ВидеоSolidity и смарт-контракты Ethereum, урок #1 (АПДЕЙТ В ЗАКРЕПЕ) | Интро, Remix IIlya Krukowski
ВидеоSOLIDITY – БАЗОВЫЙ КУРС [Урок 1]Блокчейн Разработчик
ВидеоОсновы программирования / Урок #1 – Введение в программирование с нуляГоша Дударь
ВидеоSolidity Tutorial for Beginners - Full Course in 4 Hours (2023)Naz Dumanskyy
ВидеоSolidity Smart Contract Tutorial for Beginners | Write Your First Smart ContractSession Bytes

Комментарии

Обсуждение живёт в GitHub Discussions и грузится с чужого домена — поэтому только по нажатию.