Если эта цитата немецкого математика Леопольда Кронекера не оттолкнет вас от прочтения этой статьи, то я не знаю, что оттолкнет. Считай себя предупрежденным.

"Натуральные числа были созданы Богом, все остальное — дело рук человеческих".

Несколько дней назад я разместил в LinkedIn информацию о развернутом мной токене ERC20 под названием DETS, по сотне каждого из которых были разосланы по воздуху на каждый отдельный адрес Ethereum.

Фархан Хан заявил в комментарии, что это была уловка или поддельный токен, а Денис Петровчич отметил тот факт, что Etherscan сообщает, что у токена всего восемь держателей. У нас было несколько хороших дебатов по этому поводу в комментариях, и эта статья является более длинным продолжением этих дебатов.

Я не согласен с тем, что это подделка, и хотя это уловка, это не более уловка, чем стандартный способ реализации токенов Ethereum ERC20. Таким образом, в конечном итоге это не трюк.

Более того, дебаты открывают возможность исследовать разницу в вычислениях между:

  1. как что-то реализовано,
  2. как мы это интерпретируем и
  3. что это значит для нас.

Последние два на первый взгляд могут показаться идентичными, но поверьте мне — это не так.

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

Выполнение

Байт в вычислительной технике — это имя, которое мы даем последовательности из восьми битов. А бит — это значение, которое может быть равно 0 или 1.

В нашем компьютерном оборудовании биты хранятся в электронных компонентах, использующих миллиарды крошечных конденсаторов, которые вы можете представить себе как микроскопические чашки, удерживающие электроны. Если одна из этих чашек «полна» электронами, бит, который она представляет, равна 1, а если она «пуста», то чашка представляет 0. Восемь из этих чашек в ряду представляют собой байт.

Благодаря чуду современной микроэлектроники вы можете смотреть на содержимое стаканчиков и менять содержимое. Это известно как «чтение» и «письмо».

Вот пример байта, где ● указывает, что чашка полна, а ○ — что она пуста: ●○○○○○●●.

Использование черных и белых кругов становится немного неудобным, поэтому с этого момента я буду использовать 1 для обозначения полного конденсатора и 0 для обозначения пустого.

И так, наконец, вот наш байт, представленный единицами и нулями:

10000011

Как разработчики программного обеспечения, мы не заботимся о конденсаторах и электронах. Они для аппаратных инженеров.

Интерпретация

Как мы это интерпретируем? Одной из интерпретаций может быть то, что это представление числа в двоичной системе счисления. Если мы используем то, что называется «самым значащим битом» или порядком MSB, то это:

(1*2⁷) + (0*2⁶) + (0*2⁵) + (0*2⁴) + (0*2³) + (0*2²) + (1*2¹) + (1*2⁰)

Я выделил биты жирным шрифтом, и вы можете видеть, что они соответствуют битам нашего исходного байта примера. Вы можете рассчитать это значение или просто скопировать/вставить его в поиск Google и получить ответ: 131.

Или это? Существует еще один способ интерпретации байта, называемый «наименее значащим битом» или порядком LSB, при котором считается, что первый бит имеет наименьшее значение, а последний бит — наибольшее:

(1*2⁰) + (0*2¹) + (0*2²) + (0*2³) + (0*2⁴) + (0*2⁵) + (1*2⁶) + (1*2⁷)

Когда вы проверите значение этого, это 193.

Третий способ интерпретации байта заключается в том, что он представляет положительное или отрицательное число, а первый бит представляет знак: 0, если это положительное число, и 1, если оно отрицательное. Это дает нам один бит для знака числа и еще семь для значения числа, а байт может представлять любое целое число от -127 до +127.

Чтобы выяснить, каково значение байта в этом случае, если мы используем порядок MSB, мы используем:

(1 * -1) * ((0*2⁶) + (0*2⁵) + (0*2⁴) + (0*2³) + (0*2²) + (1*2¹) + (1*2⁰))

Теперь байт интерпретируется как представляющий -3. О, и если мы используем порядок LSB, сохраняя первый бит для представления знака, это -65. Или, возможно, -129, если мы используем последний бит для знака.

Один байт, пять различных значений: 131, 193, -3, -65 или -129.

Ни одна из этих интерпретаций не является объективно правильной, неправильной или даже лучше другой. Важно то, что мы везде используем одну и ту же интерпретацию и учитываем интерпретацию, когда добавляем логику для обработки арифметики — иначе мы получим неправильные ответы, когда будем использовать наши байты для выполнения сложений и вычитаний. .

Кроме того, единственный раз, когда разработчик обычно заботится о сравнении MSB и LSB, это когда они получают необработанные данные из одной системы, которая использует одну интерпретацию, и работают с этими необработанными данными в другой системе, которая использует другую интерпретацию. В этом случае мы должны конвертировать между ними, но в остальное время все это происходит под капотом, и мы на самом деле не думаем об этом.

Как разработчики программного обеспечения, мы не заботимся о реализации, если только мы не пишем компиляторы или трансляторы. Мы просто хотим, чтобы реализация была последовательной и правильной, чтобы мы получили правильный ответ.

Значение

У нас есть байт, и у нас есть интерпретация, которая обрабатывает то, как байт используется для выполнения арифметических операций. Теперь главный вопрос — что это значит?

Это полностью зависит от нас, в зависимости от того, каков вариант использования байта.

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

В другой программе, скажем, это смарт-контракт на усеченной версии Ethereum, которая не может хорошо обрабатывать очень большие числа, я могу использовать байт для представления того, сколько токенов DETS у кого-то есть.

Возьмем второй случай. Пока то, что происходит под капотом, последовательно (и предпочтительно эффективно), я бы сказал, что не имеет значения, как работает реализация. При условии, что добавление 3 DETS и 4 DETS дает 7 DETS и т. д., любой, кто использует систему, не будет заботиться о том, как реализована аддитивная функция или как хранятся числа.

Как разработчики программного обеспечения, мы глубоко заботимся о значении наших структур данных, потому что, если функциональное требование и спецификация не совпадают, наш код не будет делать то, что хочет заказчик, и руководство начнет нас беспокоить.

Ничто не равно нулю

Изучив основы создания чисел и работы с ними в вычислениях, мы можем перейти на уровень выше, чтобы посмотреть, как смарт-контракты используют хранилище данных и числа, чтобы позволить нам развертывать контракты с токенами.

В Solidity разработчики обычно используют структуру данных, называемую «отображением», для хранения балансов людей. Структура данных сопоставления представляет собой список из двух столбцов, где первый столбец содержит адреса, а второй столбец содержит балансы.

Конечно, если вы углубитесь в реализацию структуры данных отображения, вы в конечном итоге получите конденсаторы, удерживающие или не удерживающие электроны, и логику для изменения состояния конденсаторов, но мы больше не думаем на этом уровне. .

Мы абстрагируем наше мышление, потому что оно намного менее громоздко.

Вернемся к сопоставлениям. Вы можете дать отображению адрес, и оно вернет баланс в виде целого числа. Например, спросите приведенное выше сопоставление, показанное на диаграмме, каков баланс адресов 0xF6B6…5D6FE, и оно вернет 100.

Запросите баланс 0xCd3e…6506a, и вы получите обратно 0.

Что произойдет, если мы запросим баланс адреса, например 0x5124…f1C34, которого нет в списке? Ethereum и Solidity по умолчанию возвращают 0. Хотя на самом деле баланса нет.

Между 0 и ничем есть тонкая разница. Первое означает «баланс есть, и этот баланс равен нулю», а второе означает «баланса нет».

Таким образом, разработчики системы могли бы вернуть что-то другое, не менее значимое, например сообщение об ошибке, или 2²⁵⁶ — 1, что является самым большим числом, которое Ethereum может обработать без дополнительной работы по программированию. Или действительно какой-то другой номер. Ни один из них не является более правильным, чем другой, за исключением того, насколько легко или сложно с ним работать.

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

Если только вы не реализуете контракт токена DETS ERC20.

Сто ничего

Так какой же трюк я использовал в контракте DETS?

Просто: в функции balanceOf контракта я перехватываю значение, возвращаемое из запроса сопоставления балансов, и если оно равно нулю, вместо этого я возвращаю 100.

Таким образом, у всех изначально на балансе 100 токенов, потому что «нет баланса» заменено на «есть баланс 100».

Теперь вы можете утверждать, что на самом деле это не баланс для вашего адреса, потому что где-то на всех компьютерах нет данных, явно фиксирующих, что у вас есть этот баланс. Но это всего лишь означает, что вы требуете, чтобы «на самом деле это не был токен, если только на всех компьютерах, на которых работают узлы Ethereum, не расположено несколько чашек электронов, расположенных в определенном порядке».

Это превращается в дискуссию, похожую на то, издает ли звук падающее в лесу дерево, когда рядом нет никого, кто мог бы его услышать.

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

Итак, пока мой DETS-контракт действует правильно, когда вы переходите к передаче некоторых из ваших 100 токенов, они у вас есть. И контракт работает, потому что я также изменил функции Transfer и TransferFrom, чтобы добиться именно этого.

Спецификация DETS соответствует функциональным требованиям для взаимозаменяемых токенов. Он просто реализует это иначе, чем стандартная реализация OpenZeppelin ERC20.

Полет фантазии

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

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

Нет (вероятно) способа узнать, основана ли основная реализация чисел во вселенной на постулатах Пеано, сюрреалистических числах Конвея или на чем-то совершенно другом.

Мы просто работаем с тем, что у нас есть, если мы инженеры, а математики и философы тем временем весело спорят обо всем этом.

Точно так же невозможно узнать, какой должна быть «единственная истинная реализация токенов в блокчейне». Почему они должны быть, если мы даже не можем понять, что такое числа на самом деле. В конечном итоге это сводится к следующему:

✅ есть ли у меня остаток по договору?

✅ ведет ли себя этот баланс общепринятым образом для токенов?

✅ Могу ли я убедиться, что код, создающий экземпляр токена, не содержит лазеек или ошибок?

Если отмечены все три поля, то во всех смыслах вы владеете этими токенами.

Даже если там нет никаких конденсаторов, удерживающих заряд, чтобы вы могли указать на них и сказать: «Вот эти электроны — мои жетоны».