openlang

Solidity откатывает переполнение, а релизный Rust его оборачивает

Начиная с Solidity 0.8 арифметика откатывает транзакцию при переполнении вверх и вниз. Отключить это можно только явно, блоком unchecked { ... }. В Rust наоборот. Отладочная сборка паникует, а с --release по умолчанию значение оборачивается в дополнительном коде. Проверки в релизе будут, только если в профиле Cargo стоит overflow-checks = true. В C знаковое переполнение - вообще неопределённое поведение, и проверку после операции компилятор может просто удалить.

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

Но дыра у обоих одна. В Solidity явное преобразование типа обрезает значение даже в режиме проверок, так что uint128(x) не откатится. as в Rust тоже молча обрезает. Проверенное сложение, за которым идёт сужающее приведение, на деле ничем не проверено.

Отсюда правило. В арифметике сумм никаких unchecked. В релизном профиле Rust проверки включены. Приведения сумм только через try_from. Кто-нибудь из вас держал overflow-checks включённым в релизных сборках? Интересно, заметна ли цена.

Разговор продолжается 3

Следуя доводам
  1. Ссылка на ответ

    Проверка в этих языках стоит в разных местах. В Solidity умолчание задаёт синтаксис, а исключение видно в коде как блок unchecked. В Rust overflow-checks задаётся в профиле, и по одной строке a + b не понять, запаникует операция или обернётся. Один и тот же исходник становится разными программами в зависимости от сборки.

    Для нескольких строк, которые переводят сумму, checked_add лучше, чем надежда на профиль. Ветка, где возвращается None, видна в месте вызова, и поведение не зависит от того, кто правит релизный профиль. Для такого узкого участка лишняя длина кода выглядит оправданной ценой. Проверки профиля остаются страховкой снизу.

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

  2. Ссылка на ответ

    Если привык к Zig, unchecked выглядит не переключателем производительности, а утверждением. Оно заявляет, что в блоке переполнения не будет, и без основания это та же проблема, что и catch unreachable в Zig. Последствия разные. В режиме ReleaseFast у Zig переполнение считается непроверяемым недопустимым поведением, а unchecked в Solidity оставляет обёрнутое значение. В обоих случаях значение неверное.

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

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

  3. Ссылка на ответ

    Два теста для тех, кто хочет увидеть строку профиля в деле, с overflow-checks = true в [profile.release].

    use std::hint::black_box;
    
    #[test]
    #[should_panic(expected = "attempt to add with overflow")]
    fn add_overflows() {
        let x: u64 = black_box(u64::MAX);
        let _ = x + 1;
    }
    
    #[test]
    fn cast_truncates() {
        let x: u64 = black_box(1 << 32);
        assert_eq!(x as u32, 0);
    }

    Без black_box это не собирается. Складываются две константы, и линт arithmetic_overflow отвергает сложение ещё при компиляции. С ним в релизе проходят оба теста. Уберите строку из профиля, и первый тест падает, потому что паники нет.

    Так что строка в профиле работает ровно так, как описал alden, а приведение обрезает значение, как и было сказано в начале темы. Путь суммы всё равно переведён на checked_add и try_from, а строку в профиле оставляю как страховку.

Другие стороны того же вопроса

  1. Типизированный merge всё равно заходит в `__proto__`

    Импорт настроек использует небольшой рекурсивный merge. Типы параметров выглядят аккуратно, но описывают лишь то, чего ждёт п…

  2. Аксиомы чужой теоремы стоит прочитать до того, как на неё ссылаться

    Теорема, которая компилируется, ещё не теорема с доказательством. sorry закрывает любую цель, и Lean принимает файл с предупр…

    Ответы 1lean-4
  3. Тест передачи через канал Go без sleep

    Большинство тестов передачи, которые мне попадаются, кончаются sleep и проверкой на удачу. Этот обходится без них. Запрос нес…

    Ответы 23go