Solidity. Смарт контракты и аудит
2.62K subscribers
246 photos
7 videos
18 files
550 links
Обучение Solidity. Уроки, аудит, разбор кода и популярных сервисов
Download Telegram
Последний пост этого года

Хочу подвести итоги этого года и зафиксировать его как рабочий, плотный и во многом переломный. Год начался с активного участия в аудит-конкурсах и bug bounty, где было много репортов, почти ноль выплат и полезный опыт трезвого взгляда на реальность аудита. Параллельно я системно развивал каналы, писал уроки, наполнял AuditProfile протоколами и задачами, пробовал разные форматы контента и обучения, не все из которых оказались удачными. Уже в первые месяцы стало понятно, что фокус постепенно смещается от точечных конкурсов к созданию собственных инструментов, курсов и инфраструктуры.

Весной и летом основное внимание ушло в образовательные проекты и эксперименты с автоматизацией анализа. Был полностью собран и проведён курс, проданы модули, переработаны десятки уроков, заданий и практикумов. Параллельно я глубоко ушёл в анализ аудиторских отчётов, обработку PDF, чанков уязвимостей, OCR, embedding-пайплайны и первые серьёзные попытки построить системы, которые реально помогают работать с большим объёмом знаний. Именно в этот период появилось понимание ограничений локальных моделей, иллюзий вокруг «быстрого ИИ» и необходимости аккуратной инженерии вместо магического мышления.

Вторая половина года прошла под знаком ML, RAG, статистики и собственных проектов. LazyAuditor, SoliditySet, эксперименты с обучением моделей, нормализация десятков тысяч уязвимостей, сбор датасетов, API, серверы, векторные базы — всё это делалось медленно, часто без внешней отдачи, но с чётким ощущением, что закладывается фундамент. Параллельно я много читал, доучивал математику и теорию вероятностей, осознанно отказывался от направлений, которые не давали результата, и перестраивал приоритеты.

Отдельно отмечу, что год был не только про работу. Были отпуска, дом, стройка, бытовые задачи, которые неожиданно хорошо балансировали голову и напоминали, что мир не ограничивается Solidity и багами. По каналам где-то был рост, где-то стагнация или откат, и это тоже нормальное отражение реального, а не маркетингового процесса.

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

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

#summary
🎉185🔥4
Первый пост 2026 и ближайшие планы

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

Чем вообще мы будем заниматься в ближайшее время?

Это был самый сложный вопрос в профессиональном плане. Хочется оставаться в рамках web3, но и развиваться в сфере AI. Кроме того, нельзя игнорировать все более нарастающие тренды вайбкодинга (мне и самому очень нравятся идеи быстрого тестирования гипотез/проектов на рынке). Вместе с этим, работа над своими собственными проектами подсветила проблемные зоны и недостаток знаний в некоторых областях полного цикла разработки приложений. Поэтому я хочу посвятить следующие пару месяцев вот чему:

1. Изучение, повторение и анализ различных алгоритмов в программировании: начиная с сfмых простых (например, сортировка данных) и заканчивая теми, что используются в разработки ИИ систем.

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

3. Изучение современных архитектур построения высоконагруженных приложений.

4. Изучение новых протоколов в web3 и нововведений в Solidity.

Почему именно так и в таком порядке?

Вообще в web3 можно было взять немного другие направления работы: изучение zk, тестов формальной верификации и, в конечном итоге, взяться за rust. Но это уже будет либо узкая специализация (в первых двух вариантах) или переход в альтернативную сферу (в случае с rust). Однако я, пока что, не вижу явных причин двигаться в эти сферы.

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

В итоге для "разумного вайбкодинга" нам потребуется целая область знаний, начиная с Solidity, заканчивая Python, JS, архитектурой и алгоритмами.

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

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

Всех с началом нового рабочего года и ярких свершений!

#offtop
4👍13🔥3
Алгоритмы. Big O.

Потихоньку начинаем разбирать алгоритмы в программировании, как я и писал ранее. Начнем с самых базовых для повторения и перейдем к более сложным и продвинутым. Я буду давать код алгоритмов на языке Python, так как более продвинутые решения удобнее писать именно на нем. На Solidity не все из них можно будет реализовать "без костылей", но вы сами вполне сможете поэкспериментировать с этим с помощью нейронок.

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

Введение в алгоритмы и асимптотический (по сути означает "для больших значений") анализ начинается с понимания самой сути алгоритма. Алгоритм представляет собой последовательность четко определенных шагов для решения конкретной задачи, подобно кулинарному рецепту или инструкции по сборке. Например, алгоритм приготовления бутерброда можно описать так: взять два ломтика хлеба, намазать один из них маслом, положить сверху сыр и накрыть вторым ломтиком. Важно, что любой алгоритм предполагает некий набор инструкций, выполнение которых гарантированно приводит к результату.

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

Скорость работы алгоритма напрямую зависит от количества обрабатываемых данных. Если в небольшом архиве из десяти дел поиск наугад займет всего несколько секунд, то в хранилище с миллионом документов такой подход потребует непозволительно много времени. Поэтому для оценки эффективности алгоритма используется понятие временной сложности, которая показывает, как количество необходимых операций растет с увеличением размера входных данных. Например, для поиска числа 9 в списке [3, 7, 1, 9, 5] методом последовательного перебора потребовалось четыре шага. Для списка из ста элементов в худшем случае потребуется сто операций. Эта прямая зависимость описывается линейной сложностью.

Для универсального описания скорости алгоритмов используется О-нотация (Big O). Она позволяет классифицировать алгоритмы по их «аппетиту» к ресурсам, предоставляя асимптотическую оценку роста времени выполнения или потребляемой памяти. Основные классы сложности, от наиболее к наименее эффективным, выглядят следующим образом.

1. O(1) — постоянная сложность. Время выполнения не зависит от объема данных.

def get_first_letter(name):
return name[0]


Операция получения первого символа строки всегда выполняется за одно действие, будь то имя «Аня» или «Александр».

2. O(n) — линейная сложность. Время выполнения растет прямо пропорционально размеру входных данных.

def find_element(arr, target):
steps = 0
for item in arr:
steps += 1
if item == target:
return True
return False


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

3. O(n²) — квадратичная сложность. Время выполнения пропорционально квадрату количества элементов, что характерно для алгоритмов с вложенными циклами.

def find_all_pairs(arr):
pairs = []
for i in arr:
for j in arr:
pairs.append((i, j))
return pairs


Для массива из пяти элементов будет выполнено 25 итераций, а для тысячи — уже миллион.

4. O(log n) — логарифмическая сложность. Очень эффективный класс, где на каждом шаге объем обрабатываемых данных уменьшается вдвое. Яркий пример — бинарный поиск в отсортированном массиве.
def binary_search(sorted_arr, target):
left, right = 0, len(sorted_arr) - 1
steps = 0
while left <= right:
steps += 1
mid = (left + right) // 2
if sorted_arr[mid] == target:
return mid
elif sorted_arr[mid] < target:
left = mid + 1
else:
right = mid - 1
return -1


Поиск среди 16 отсортированных элементов займет не более 4 шагов.

Помимо временной, важна и пространственная сложность, которая оценивает объем дополнительной памяти, требуемой алгоритмом. Например, алгоритм нахождения максимума в массиве использует фиксированный объем памяти O(1), тогда как создание полной копии списка потребует памяти O(n).

Практическое значение асимптотического анализа становится очевидным при сравнении алгоритмов. Рассмотрим задачу поиска дубликатов. Наивный подход с двойным циклом имеет сложность O(n²):

def find_duplicates_slow(arr):
duplicates = []
for i in range(len(arr)):
for j in range(i + 1, len(arr)):
if arr[i] == arr[j] and arr[i] not in duplicates:
duplicates.append(arr[i])
return duplicates


Более разумный подход с использованием хэш-множества имеет сложность O(n):

def find_duplicates_fast(arr):
seen = set()
duplicates = set()
for item in arr:
if item in seen:
duplicates.add(item)
else:
seen.add(item)
return list(duplicates)


На списке из тысячи элементов второй алгоритм окажется в сотни раз быстрее первого.

Для наглядности можно представить себе сводную таблицу сложностей. O(1) обозначает мгновенное выполнение, например доступ к элементу массива по индексу. O(log n) характерна для алгоритмов типа бинарного поиска. O(n) — для линейного прохода по данным. O(n log n) — это типичная сложность эффективных алгоритмов сортировки. O(n²) часто возникает при обработке матриц или использовании вложенных циклов. O(2ⁿ) — экстремально медленная сложность, присущая некоторым задачам полного перебора.

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

#algorithm
👍9
Алгоритмы. Рекурсия

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

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

Для корректной работы любой рекурсивной функции необходимы два фундаментальных компонента. Во-первых, это базовый случай — условие остановки, которое предотвращает бесконечный цикл вызовов. Его можно сравнить с последней ступенькой лестницы: достигнув её, вы прекращаете движение. Во-вторых, требуется рекурсивный шаг — вызов функцией самой себя, но с изменёнными входными данными, которые должны гарантированно приближать выполнение к базовому случаю. Это подобно каждому следующему шагу вниз по лестнице, который сокращает оставшееся до низа расстояние.

Рассмотрим простейшую иллюстрацию — функцию обратного отсчёта от заданного числа до единицы. Её логика наглядно демонстрирует оба принципа.

def count_down(n):
# Базовый случай: когда достигли 0, останавливаемся
if n == 0:
print("Готово!")
return

# Выводим текущее число
print(n)

# Рекурсивный шаг: вызываем функцию с n-1
count_down(n - 1)

count_down(5)


Вывод этой программы будет последовательным:
5
4
3
2
1
Готово!


Внутри этого процесса происходит следующее: вызов count_down(5) приводит к выводу числа 5 и новому вызову count_down(4). Этот процесс вкладывается, подобно матрёшкам, пока вызов count_down(0) не достигнет базового случая, выведет "Готово!" и не начнёт возвращать управление обратно по цепочке предыдущих вызовов.

Классическим примером, раскрывающим суть рекурсивного мышления, является вычисление факториала числа n, обозначаемого как n!. По определению, факториал — это произведение всех натуральных чисел от 1 до n, при этом 0! и 1! равны 1 (вообще не так чтобы равны, просто принято такое равенство для удобства расчетов). Ключевое наблюдение здесь — рекурсивная природа операции: факториал любого числа n можно выразить через факториал меньшего числа, а именно n! = n * (n-1)!. Это и становится основой для алгоритма.

def factorial(n):
# Базовый случай
if n == 0 or n == 1:
return 1

# Рекурсивный шаг
return n * factorial(n - 1)

print(factorial(5)) # 120

# Для значения 3

factorial(3)

├─ factorial(2) ← добавляется в стек
│ │
│ ├─ factorial(1) ← добавляется в стек
│ │ └─ возвращает 1 ← снимается со стека
│ │
│ └─ возвращает 2 ← снимается со стека

└─ возвращает 6 ← снимается со стека

# Стек вызовов для factorial(3):

Шаг 1: [factorial(3)]
Шаг 2: [factorial(3), factorial(2)]
Шаг 3: [factorial(3), factorial(2), factorial(1)]
Шаг 4: [factorial(3), factorial(2)] ← factorial(1) вернул результат
Шаг 5: [factorial(3)] ← factorial(2) вернул результат
Шаг 6: [] ← factorial(3) вернул результат


Пошаговое выполнение функции для factorial(5) раскладывается в цепочку отложенных умножений: 5 * factorial(4), затем 5 * (4 * factorial(3)), и так далее, пока вычисление не дойдёт до базового случая factorial(1), который возвращает 1. После этого цепочка начинает сворачиваться, производя последовательные умножения: 2 * 1 = 2, 3 * 2 = 6, 4 * 6 = 24 и, наконец, 5 * 24 = 120.

Эта же логика применима ко множеству других задач. Например, для вычисления суммы всех чисел от 1 до n.

def sum_numbers(n):
# Базовый случай
if n == 0:
return 0

# Рекурсивный шаг
return n + sum_numbers(n - 1)

print(sum_numbers(5)) # 15 (5+4+3+2+1)


Аналогично работает возведение числа в натуральную степень.
def power(base, exponent):
# Базовый случай
if exponent == 0:
return 1

# Рекурсивный шаг
return base * power(base, exponent - 1)

print(power(2, 3)) # 8 (2 * 2 * 2)


Или определение длины строки без использования встроенных функций.

def string_length(s):
# Базовый случай: пустая строка
if s == "":
return 0

# Рекурсивный шаг: убираем первый символ и считаем остаток
return 1 + string_length(s[1:])

print(string_length("hello")) # 5


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

Механизм, обеспечивающий возможность таких вложенных вызовов, называется стеком вызовов. Это специальная область памяти, организованная по принципу LIFO ("последним пришёл — первым ушёл"), подобно стопке тарелок. Каждый новый вызов функции помещает в стек свой контекст (аргументы, локальные переменные, место возврата). Когда функция завершает работу, её контекст извлекается из вершины стека, и выполнение продолжается с предыдущего вызова. При глубокой рекурсии стек может исчерпать свой лимит, что приводит к ошибке переполнения стека. Каждый рекурсивный вызов занимает память, поэтому важно, чтобы алгоритм гарантированно сходился к базовому случаю.

Рассмотрим практический вопрос: как написать рекурсивную функцию для вычисления n-го числа Фибоначчи? Последовательность Фибоначчи задаётся правилами: F(0) = 0, F(1) = 1, а для n > 1 каждое число равно сумме двух предыдущих: F(n) = F(n-1) + F(n-2). Это определение напрямую ложится на рекурсивный алгоритм.

def fibonacci(n):
# Базовые случаи
if n == 0:
return 0
if n == 1:
return 1

# Рекурсивный шаг
return fibonacci(n - 1) + fibonacci(n - 2)

# Примеры
print(fibonacci(0)) # 0
print(fibonacci(1)) # 1
print(fibonacci(5)) # 5
print(fibonacci(10)) # 55

# Почему так медленно?

Посмотрим на дерево вызовов для fibonacci(5):

fib(5)
/ \
fib(4) fib(3)
/ \ / \
fib(3) fib(2) fib(2) fib(1)
/ \ / \ / \
fib(2) fib(1) fib(1) fib(0) fib(1) fib(0)
/ \
fib(1) fib(0)


Однако у этой наивной реализации есть серьёзный недостаток — экспоненциальная временная сложность O(2^n). Это происходит из-за колоссального количества повторных вычислений одних и тех же значений, что хорошо видно на дереве вызовов для fibonacci(5), где, например, fibonacci(3) вычисляется несколько раз. Для оптимизации применяют технику мемоизации — сохранения результатов предыдущих вычислений в кеше (словаре), чтобы не считать их заново.

def fibonacci_memo(n, memo={}):
# Если уже вычисляли, берем из кеша
if n in memo:
return memo[n]

# Базовые случаи
if n == 0:
return 0
if n == 1:
return 1

# Вычисляем и сохраняем результат
memo[n] = fibonacci_memo(n - 1, memo) + fibonacci_memo(n - 2, memo)
return memo[n]

print(fibonacci_memo(50)) # Работает быстро!


С мемоизацией сложность снижается до линейной O(n), поскольку каждое значение вычисляется только один раз.

Это подводит к обсуждению недостатков рекурсивного подхода в сравнении с итеративным (циклы). Главные минусы рекурсии — повышенный расход памяти из-за использования стека вызовов, риск его переполнения при большой глубине, более низкая скорость из-за накладных расходов на вызов функции, а также потенциальная сложность отладки. Итеративные решения обычно более эффективны по памяти и быстродействию для задач, которые можно просто выразить через циклы. Например, вычисление факториала с помощью цикла не требует хранения цепочки вызовов в стеке.
1
Тем не менее, рекурсия остаётся незаменимым инструментом для задач, имеющих естественную рекурсивную структуру данных или логики. Она идеально подходит для обхода древовидных структур (каталогов файловой системы), реализации алгоритмов "разделяй и властвуй" (быстрая сортировка, бинарный поиск) и решения таких задач, как Ханойские башни. Выбор между рекурсией и итерацией часто является компромиссом между читаемостью, простотой выражения идеи алгоритма и требованиями к производительности и ресурсам.

#algorithm
🔥4
Алгоритмы. Стек и Очередь

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

Примеры кода на Python получились достаточно объемными, поэтому пришлось поместить их в отдельный ресурс Telegraph.

P.S. Весь код вы можете протестировать в Google Colab или Jupyter Notebook.

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

Стек, или Stack, работает по принципу LIFO — Last-In, First-Out, что переводится как «последним пришел — первым ушел». Это легко представить на примере стопки книг: новую книгу всегда кладут сверху, и чтобы взять нужную, также снимают сначала верхнюю. Самую нижнюю книгу можно получить, только убрав все лежащие выше. Основные операции со стеком включают push для добавления элемента на вершину, pop для его удаления, peek или top для просмотра вершины без удаления и isEmpty для проверки на пустоту. Визуально стек можно изобразить следующим образом:
┌─────────┐
│ C │ ← Вершина (Top) - здесь происходят операции
├─────────┤
│ B │
├─────────┤
│ A │
└─────────┘

После операции pop() будет удален элемент C, а после push(D) на вершине окажется D. На практике это выглядит так, как показано в примере реализации стека на Python с использованием списка.

Пример 1

Вывод программы наглядно демонстрирует порядок операций:

Пример 2

Стек находит применение во множестве реальных задач. Например, история браузера использует стек для реализации кнопки «Назад»: каждый новый URL помещается в стек, а нажатие кнопки извлекает последний. Текстовые редакторы хранят историю изменений в стеке для функции отмены действий (Ctrl+Z). Компиляторы и интерпретаторы проверяют сбалансированность скобок в коде, используя стек для отслеживания открывающих символов. Наконец, сам механизм вызова функций в программах управляется стеком вызовов, где сохраняются контексты выполнения. Практическим примером может служить функция проверки корректности расстановки скобок.

Пример 3

В отличие от стека, очередь, или Queue, работает по принципу FIFO — First-In, First-Out, то есть «первым пришел — первым ушел». Это аналогично очереди в магазине: первый вставший в очередь покупатель первым же и обслуживается, а новые люди присоединяются к концу. Основные операции — enqueue для добавления элемента в конец, dequeue для удаления из начала, front для просмотра первого элемента и isEmpty для проверки. Визуализация очереди выглядит так:

Пример 4

При dequeue() будет удален элемент A, а после enqueue(E) в конец добавится E. В Python для эффективной реализации очереди используется структура deque из модуля collections.

Пример 5

Вывод этой программы отражает принцип FIFO:

Пример 6

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

Пример 7

Сравнивая стек и очередь, можно выделить их ключевые различия. Стек следует принципу LIFO, подобно стопке книг, где добавление и удаление происходят с одного конца (вершины). Очередь же работает по принципу FIFO, как очередь в магазине, где добавление идет в конец, а удаление — из начала. В Python стек эффективно реализуется через список (list), а для очереди предпочтительнее использовать deque из модуля collections.
Интересной задачей является реализация очереди с помощью двух стеков. Классическое решение использует один стек для добавления элементов (enqueue), а другой — для удаления (dequeue). Когда требуется удалить элемент, но второй стек пуст, все элементы перекладываются из первого стека во второй, в результате чего порядок инвертируется и первый добавленный элемент оказывается на вершине второго стека, готовый к извлечению.

Пример 8

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

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

Пример 9

Функция отмены действий в редакторах также полагается на стек для хранения состояний.

Пример 10

Стек вызовов, управляющий выполнением функций в программе, — еще один яркий пример. Когда функция вызывает другую, ее контекст помещается в стек, а после завершения вложенной функции — восстанавливается.

Пример 11

Проверка корректности HTML-тегов — задача, где стек отслеживает вложенность открывающих и закрывающих элементов.

Пример 12

Для реализации очереди в Python оптимально использовать класс deque из модуля collections. Это двусторонняя очередь, которая обеспечивает константную сложность O(1) для операций добавления и удаления с обоих концов, в отличие от списка, где удаление из начала (pop(0)) имеет линейную сложность O(n). Полноценная реализация очереди на deque включает все основные операции.

Пример 13

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

Пример 14

С точки зрения сложности операций, стек на основе списка Python обеспечивает O(1) для push, pop и peek, так же как и проверка на пустоту. Очередь на основе deque также гарантирует O(1) для enqueue, dequeue, front и isEmpty. Важно подчеркнуть, что использование обычного списка для очереди неэффективно из-за линейной сложности операции pop(0), что делает deque предпочтительным выбором.

В заключение, стек и очередь — это фундаментальные структуры данных, каждая со своим строгим принципом доступа: LIFO для стека, подобно стопке тарелок, и FIFO для очереди, как очередь людей. Их понимание критически важно для изучения алгоритмов и системного программирования, поскольку они лежат в основе множества механизмов — от управления памятью и планирования процессов до синтаксического анализа и работы с историей. Ключевые моменты для запоминания: стек оперирует с одним концом, очередь — с двумя; в Python для стека используется list, для очереди — deque; обе структуры обеспечивают высокую эффективность основных операций.

#algorithm
👍6
Начинаю бета-тест моего проекта

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

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

HornetMCP — это MCP/API-сервер, который реализует поиск уязвимостей, схожих с запросом пользователя. Однако, в отличие от стандартных API (таких, как Solodit), работающих на основе ключевых слов и тегов, здесь используется векторное сходство данных.

Как это работает на практике?

1. Пользователь отправляет в базу данных запрос, содержащий пример функции и контекст — что она делает, за что отвечает, что нужно проверить и т.д.

2. Запрос преобразуется в эмбеддинги — то есть переводится в векторное пространство.

3. Полученные векторы сравниваются с векторами (эмбеддингами), хранящимися в базе данных.

4. Система выбирает 5–10 наиболее похожих векторов и возвращает пользователю соответствующие отчеты, из которых эти векторы были получены.

5. В случае использования через MCP (например, с Claude) нейросеть самостоятельно проводит дальнейший анализ функции и выдает результат. При работе через API пользователь получает непосредственно отчеты. Также доступно тестовое чат-окно для разового запроса, который анализирует найденные отчеты (промт пока в доработке). Кроме того, чат использует бесплатную модель DeepSeek R1T2 Chimera - поэтому работа чуть медленнее обычного чата и немного хуже ответы, чем в Claude или ChatGPT.

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

Почему я решил создать этот проект? Было несколько причин. Во-первых, я хотел глубже погрузиться в работу с нейросетями и RAG. Во-вторых, в ходе аудита смарт-контрактов я регулярно сталкивался с вопросами вроде: «Были ли уже подобные уязвимости?», «Не упускаю ли я важный паттерн?», «Есть ли скрытые проблемы?» — иными словами, мне хотелось получать больше идей и гипотез для проверки. Solodit, например, предлагает хороший фильтр, но он работает по ключевым словам и тегам. Вставить функцию и получить релевантные отчеты на основе семантики там нельзя. А векторный поиск как раз решает эту задачу.

Сейчас я ищу участников канала, которые в своей работе используют нейросети и MCP-серверы — для разработки контрактов или для аудита. Необходимо протестировать работу проекта, его стабильность и качество векторного поиска отчетов. В идеале — если вы будете использовать проект вместе с Claude или другой LLM, поддерживающей MCP: это обеспечит максимальную эффективность.

Для регистрации потребуется подтверждение email (я параллельно тестирую сервис Resend для отправки писем). Также нужно будет сформировать APIKey для отправки запросов через MCP/API.

Сайт проекта — https://hornetmcp.com/

Инвайт — solidityset

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

Все комментарии и замечания можно направлять в группу под этим постом.

Спасибо всем, кто примет участие в тестировании!

#hornetmcp
🔥154
Еще инвайты

Добавил еще 10 инвайтов для теста моего проекта. Если кто хотел, но не успел - это ваш шанс!

Инвайт тот же - solidityset

Буду рад отзывам!

#hornetmcp
2🔥2🎉2
Алгоритмы. Пузырьковая сортировка

Продолжаем наше изучение алгоритмов и сегодня начнем разбирать пузырьковой сортировки, один из наиболее наглядных методов упорядочивания данных. Идея сортировки в целом подобна приведению в порядок перепутанной колоды карт, когда требуется расположить элементы от меньшего к большему или наоборот. В программировании эта задача возникает постоянно: будь то числа, строки или другие данные, требующие определённой последовательности. Например, изначальный список [64, 34, 25, 12, 22] после сортировки превращается в [12, 22, 25, 34, 64].

Название "пузырьковая" отражает суть процесса: более лёгкие элементы, подобно пузырькам воздуха в воде, постепенно всплывают к началу списка, в то время как тяжёлые опускаются в конец. Алгоритм построен на простом принципе: последовательно сравниваются соседние элементы, и если они расположены в неправильном порядке, происходит их обмен. Эти действия повторяются до тех пор, пока весь массив не будет упорядочен.

Для лучшего понимания разберём визуальный пример сортировки списка [5, 3, 8, 4, 2] по возрастанию. В первом проходе сравниваются 5 и 3 — поскольку 5 больше 3, они меняются местами, и список становится [3, 5, 8, 4, 2]. Далее 5 и 8 остаются на своих местах, так как 5 меньше 8. Затем 8 и 4 обмениваются, получается [3, 5, 4, 8, 2]. Наконец, 8 и 2 также меняются, и результат первого прохода — [3, 5, 4, 2, 8]. Обратите внимание, что самое большое число 8 оказалось в конце, заняв свою окончательную позицию. Второй проход перемещает 5 на предпоследнее место: после сравнений и обменов список принимает вид [3, 4, 2, 5, 8]. Третий проход ставит на место 4: [3, 2, 4, 5, 8]. Четвёртый проход завершает сортировку, обменяв 3 и 2, и итоговый результат — [2, 3, 4, 5, 8].

Теперь перейдём к программной реализации. Код функции пузырьковой сортировки на Python выглядит следующим образом:

def bubble_sort(arr):
"""Пузырьковая сортировка списка."""
n = len(arr)
for i in range(n):
swapped = False
for j in range(0, n - i - 1):
if arr[j] > arr[j + 1]:
arr[j], arr[j + 1] = arr[j + 1], arr[j]
swapped = True
if not swapped:
break
return arr


Разберём каждую часть. Функция bubble_sort принимает список arr. Переменная n хранит его длину. Внешний цикл for i in range(n) определяет количество полных проходов по списку. Внутри него устанавливается флаг swapped = False, который отслеживает, были ли совершены обмены во время текущего прохода. Это важная оптимизация: если обменов не произошло, список уже отсортирован, и дальнейшие проходы не нужны.

Внутренний цикл for j in range(0, n - i - 1) отвечает за попарное сравнение соседних элементов. Выражение n - i - 1 ограничивает диапазон, потому что после каждого прохода самый крупный элемент "всплывает" в конец, и проверять его уже не требуется. Например, для списка из пяти элементов при первом проходе (i = 0) будут сравниваться пары с индексами от 0 до 3, при втором (i = 1) — от 0 до 2, и так далее.

Внутри внутреннего цикла условие if arr[j] > arr[j + 1]: проверяет, стоит ли текущий элемент правее, чем следующий. Если да, то с помощью конструкции arr[j], arr[j + 1] = arr[j + 1], arr[j] элементы меняются местами, а флаг swapped устанавливается в True. После завершения внутреннего цикла проверяется значение флага: если он остался False, что означает отсутствие обменов, внешний цикл прерывается оператором break. В конце функция возвращает отсортированный список.

Для демонстрации работы приведём полный пример:
def bubble_sort(arr):
n = len(arr)
for i in range(n):
swapped = False
for j in range(0, n - i - 1):
if arr[j] > arr[j + 1]:
arr[j], arr[j + 1] = arr[j + 1], arr[j]
swapped = True
if not swapped:
break
return arr

# Пример 1: Обычная сортировка
data1 = [64, 34, 25, 12, 22, 11, 90]
print(bubble_sort(data1.copy()))
# Результат: [11, 12, 22, 25, 34, 64, 90]

# Пример 2: Уже отсортированный список
data2 = [1, 2, 3, 4, 5]
print(bubble_sort(data2.copy()))
# Результат: [1, 2, 3, 4, 5]

# Пример 3: Обратный порядок
data3 = [5, 4, 3, 2, 1]
print(bubble_sort(data3.copy()))
# Результат: [1, 2, 3, 4, 5]


Чтобы лучше проследить за ходом алгоритма, можно использовать визуализированную версию:

def bubble_sort_visualized(arr):
n = len(arr)
print(f"Начальный список: {arr}")
print()

for i in range(n):
print(f"=== Проход {i + 1} ===")
swapped = False

for j in range(0, n - i - 1):
print(f"Сравниваем {arr[j]} и {arr[j + 1]}", end=" ")

if arr[j] > arr[j + 1]:
arr[j], arr[j + 1] = arr[j + 1], arr[j]
swapped = True
print(f"→ Меняем! Теперь: {arr}")
else:
print(f"→ Оставляем")

if not swapped:
print("Обменов не было, список отсортирован!")
break
print()

print(f"\nИтоговый список: {arr}")
return arr

data = [5, 2, 8, 1, 9]
bubble_sort_visualized(data)


Одним из недостатков пузырьковой сортировки является её низкая эффективность на больших наборах данных. Сложность алгоритма в худшем и среднем случае оценивается как O(n²), где n — количество элементов. Это означает, что с ростом размера списка количество необходимых операций сравнения и обмена растёт квадратично. Например, для тысячи элементов может потребоваться около миллиона сравнений, в то время как более совершенные алгоритмы, такие как быстрая сортировка, справляются с этой задачей за порядка двадцати тысяч операций. Именно поэтому пузырьковая сортировка, при всей своей простоте и наглядности, не применяется в реальных проектах с большими объёмами данных.

Алгоритм можно модифицировать для сортировки по убыванию. Для этого достаточно изменить условие сравнения с > на <, чтобы более мелкие элементы перемещались вправо:

def bubble_sort_descending(arr):
"""Пузырьковая сортировка в порядке УБЫВАНИЯ."""
n = len(arr)
for i in range(n):
swapped = False
for j in range(0, n - i - 1):
if arr[j] < arr[j + 1]:
arr[j], arr[j + 1] = arr[j + 1], arr[j]
swapped = True
if not swapped:
break
return arr

data1 = [64, 34, 25, 12, 22, 11, 90]
print(bubble_sort_descending(data1.copy()))
# Результат: [90, 64, 34, 25, 22, 12, 11]


Также можно создать универсальную функцию, принимающую параметр reverse для выбора направления сортировки.

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

#algorithm
👍1