diff --git a/.github/assets/revive-dev-node-polkavm-resolc.json b/.github/assets/revive-dev-node-polkavm-resolc.json index 2a49559cbd6bf..7cd6184286b8f 100644 --- a/.github/assets/revive-dev-node-polkavm-resolc.json +++ b/.github/assets/revive-dev-node-polkavm-resolc.json @@ -1,15 +1,652 @@ { - "fixtures/solidity/complex/create/create2_many/test.json::1::Y M3": "Failed", - "fixtures/solidity/complex/create/create_many/test.json::1::Y M3": "Failed", - "fixtures/solidity/simple/yul_instructions/revert.sol::19::Y M0": "Failed", - "fixtures/solidity/simple/yul_instructions/revert.sol::19::Y M3": "Failed", - "fixtures/solidity/simple/yul_instructions/revert.sol::33::Y M0": "Failed", - "fixtures/solidity/simple/yul_instructions/revert.sol::33::Y M3": "Failed", - "fixtures/solidity/simple/yul_instructions/revert.sol::47::Y M0": "Failed", - "fixtures/solidity/simple/yul_instructions/revert.sol::47::Y M3": "Failed", - "fixtures/solidity/simple/yul_instructions/revert.sol::5::Y M3": "Failed", - "fixtures/solidity/simple/yul_instructions/revert.sol::61::Y M0": "Failed", - "fixtures/solidity/simple/yul_instructions/revert.sol::61::Y M3": "Failed", - "fixtures/solidity/simple/yul_instructions/revert.sol::75::Y M0": "Failed", - "fixtures/solidity/simple/yul_instructions/revert.sol::75::Y M3": "Failed" + "fixtures/solidity/complex/create/create2_many/test.json::1::Y M3 S+": "Failed", + "fixtures/solidity/complex/create/create_many/test.json::1::Y M3 S+": "Failed", + "fixtures/solidity/complex/defi/UniswapV4/test.json::0::Y M3 S+": "Failed", + "fixtures/solidity/complex/defi/UniswapV4/test.json::0::Y M3 S-": "Failed", + "fixtures/solidity/simple/algorithm/cryptography/book_cypher.sol::0::Y M0 S-": "Failed", + "fixtures/solidity/simple/algorithm/cryptography/book_cypher.sol::1::Y M0 S-": "Failed", + "fixtures/solidity/simple/algorithm/cryptography/caesar_cypher.sol::2::Y M0 S-": "Failed", + "fixtures/solidity/simple/internal_function_pointers/hash.sol::0::Y M0 S-": "Failed", + "fixtures/solidity/simple/internal_function_pointers/sum_oddness.sol::0::Y M0 S-": "Failed", + "fixtures/solidity/simple/loop/complex/1.sol::0::Y M0 S+": "Failed", + "fixtures/solidity/simple/loop/complex/1.sol::0::Y M0 S-": "Failed", + "fixtures/solidity/simple/loop/complex/2.sol::0::Y M0 S-": "Failed", + "fixtures/solidity/simple/loop/complex/3.sol::0::Y M0 S+": "Failed", + "fixtures/solidity/simple/loop/complex/3.sol::0::Y M0 S-": "Failed", + "fixtures/solidity/simple/modular/addmod_complex.sol::0::Y M0 S+": "Failed", + "fixtures/solidity/simple/modular/addmod_complex.sol::0::Y M0 S-": "Failed", + "fixtures/solidity/simple/modular/mulmod.sol::0::Y M0 S+": "Failed", + "fixtures/solidity/simple/modular/mulmod.sol::0::Y M0 S-": "Failed", + "fixtures/solidity/simple/modular/mulmod.sol::1::Y M0 S+": "Failed", + "fixtures/solidity/simple/modular/mulmod.sol::1::Y M0 S-": "Failed", + "fixtures/solidity/simple/modular/mulmod.sol::2::Y M0 S+": "Failed", + "fixtures/solidity/simple/modular/mulmod.sol::2::Y M0 S-": "Failed", + "fixtures/solidity/simple/modular/mulmod.sol::3::Y M0 S+": "Failed", + "fixtures/solidity/simple/modular/mulmod.sol::3::Y M0 S-": "Failed", + "fixtures/solidity/simple/modular/mulmod.sol::4::Y M0 S+": "Failed", + "fixtures/solidity/simple/modular/mulmod.sol::4::Y M0 S-": "Failed", + "fixtures/solidity/simple/modular/mulmod.sol::5::Y M0 S+": "Failed", + "fixtures/solidity/simple/modular/mulmod.sol::5::Y M0 S-": "Failed", + "fixtures/solidity/simple/try_catch/revert_long_data.sol::0::Y M0 S+": "Failed", + "fixtures/solidity/simple/try_catch/revert_long_data.sol::0::Y M0 S-": "Failed", + "fixtures/solidity/simple/try_catch/revert_long_data.sol::0::Y M3 S+": "Failed", + "fixtures/solidity/simple/try_catch/revert_long_data.sol::0::Y M3 S-": "Failed", + "fixtures/solidity/simple/try_catch/revert_long_data.sol::0::Y Mz S+": "Failed", + "fixtures/solidity/simple/try_catch/revert_long_data.sol::0::Y Mz S-": "Failed", + "fixtures/solidity/simple/yul_instructions/mulmod.sol::0::Y M0 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/mulmod.sol::0::Y M0 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/mulmod.sol::1::Y M0 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/mulmod.sol::1::Y M0 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/mulmod.sol::2::Y M0 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/mulmod.sol::2::Y M0 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/mulmod.sol::3::Y M0 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/mulmod.sol::3::Y M0 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/mulmod.sol::4::Y M0 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/mulmod.sol::4::Y M0 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/mulmod.sol::5::Y M0 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/mulmod.sol::5::Y M0 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/mulmod.sol::6::Y M0 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/mulmod.sol::6::Y M0 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/mulmod.sol::7::Y M0 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/mulmod.sol::7::Y M0 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/mulmod.sol::8::Y M0 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/mulmod.sol::8::Y M0 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/mulmod.sol::9::Y M0 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/mulmod.sol::9::Y M0 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/mulmod.sol::10::Y M0 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/mulmod.sol::10::Y M0 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/mulmod.sol::11::Y M0 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/mulmod.sol::11::Y M0 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/mulmod.sol::12::Y M0 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/mulmod.sol::12::Y M0 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/mulmod.sol::13::Y M0 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/mulmod.sol::13::Y M0 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/mulmod.sol::14::Y M0 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/mulmod.sol::14::Y M0 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/mulmod.sol::15::Y M0 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/mulmod.sol::15::Y M0 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/mulmod.sol::16::Y M0 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/mulmod.sol::16::Y M0 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/mulmod.sol::17::Y M0 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/mulmod.sol::17::Y M0 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/mulmod.sol::18::Y M0 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/mulmod.sol::18::Y M0 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/mulmod.sol::19::Y M0 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/mulmod.sol::19::Y M0 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/mulmod.sol::20::Y M0 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/mulmod.sol::20::Y M0 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/mulmod.sol::21::Y M0 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/mulmod.sol::21::Y M0 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/mulmod.sol::22::Y M0 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/mulmod.sol::22::Y M0 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/mulmod.sol::23::Y M0 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/mulmod.sol::23::Y M0 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/mulmod.sol::24::Y M0 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/mulmod.sol::24::Y M0 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/mulmod.sol::25::Y M0 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/mulmod.sol::25::Y M0 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/mulmod.sol::26::Y M0 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/mulmod.sol::26::Y M0 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/mulmod.sol::27::Y M0 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/mulmod.sol::27::Y M0 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/mulmod.sol::28::Y M0 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/mulmod.sol::28::Y M0 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/mulmod.sol::29::Y M0 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/mulmod.sol::29::Y M0 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/mulmod.sol::30::Y M0 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/mulmod.sol::30::Y M0 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/mulmod.sol::31::Y M0 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/mulmod.sol::31::Y M0 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/mulmod.sol::32::Y M0 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/mulmod.sol::32::Y M0 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/mulmod.sol::33::Y M0 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/mulmod.sol::33::Y M0 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/mulmod.sol::34::Y M0 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/mulmod.sol::34::Y M0 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/mulmod.sol::35::Y M0 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/mulmod.sol::35::Y M0 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/mulmod.sol::36::Y M0 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/mulmod.sol::36::Y M0 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/mulmod.sol::37::Y M0 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/mulmod.sol::37::Y M0 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/mulmod.sol::38::Y M0 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/mulmod.sol::38::Y M0 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/mulmod.sol::39::Y M0 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/mulmod.sol::39::Y M0 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/mulmod.sol::40::Y M0 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/mulmod.sol::40::Y M0 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/mulmod.sol::41::Y M0 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/mulmod.sol::41::Y M0 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/mulmod.sol::42::Y M0 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/mulmod.sol::42::Y M0 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/mulmod.sol::43::Y M0 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/mulmod.sol::43::Y M0 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/mulmod.sol::44::Y M0 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/mulmod.sol::44::Y M0 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/mulmod.sol::45::Y M0 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/mulmod.sol::45::Y M0 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/mulmod.sol::46::Y M0 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/mulmod.sol::46::Y M0 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/mulmod.sol::47::Y M0 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/mulmod.sol::47::Y M0 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/mulmod.sol::48::Y M0 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/mulmod.sol::48::Y M0 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/mulmod.sol::49::Y M0 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/mulmod.sol::49::Y M0 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/mulmod.sol::50::Y M0 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/mulmod.sol::50::Y M0 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/mulmod.sol::51::Y M0 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/mulmod.sol::51::Y M0 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/mulmod.sol::52::Y M0 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/mulmod.sol::52::Y M0 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/mulmod.sol::53::Y M0 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/mulmod.sol::53::Y M0 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/mulmod.sol::54::Y M0 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/mulmod.sol::54::Y M0 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/mulmod.sol::55::Y M0 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/mulmod.sol::55::Y M0 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/mulmod.sol::56::Y M0 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/mulmod.sol::56::Y M0 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/mulmod.sol::57::Y M0 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/mulmod.sol::57::Y M0 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/mulmod.sol::58::Y M0 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/mulmod.sol::58::Y M0 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/mulmod.sol::59::Y M0 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/mulmod.sol::59::Y M0 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/mulmod.sol::60::Y M0 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/mulmod.sol::60::Y M0 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/mulmod.sol::61::Y M0 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/mulmod.sol::61::Y M0 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/mulmod.sol::62::Y M0 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/mulmod.sol::62::Y M0 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/mulmod.sol::63::Y M0 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/mulmod.sol::63::Y M0 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/mulmod.sol::64::Y M0 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/mulmod.sol::64::Y M0 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/mulmod.sol::65::Y M0 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/mulmod.sol::65::Y M0 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/mulmod.sol::66::Y M0 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/mulmod.sol::66::Y M0 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/mulmod.sol::67::Y M0 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/mulmod.sol::67::Y M0 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/mulmod.sol::68::Y M0 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/mulmod.sol::68::Y M0 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/mulmod.sol::69::Y M0 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/mulmod.sol::69::Y M0 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/mulmod.sol::70::Y M0 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/mulmod.sol::70::Y M0 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/mulmod.sol::71::Y M0 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/mulmod.sol::71::Y M0 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/mulmod.sol::72::Y M0 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/mulmod.sol::72::Y M0 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/mulmod.sol::73::Y M0 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/mulmod.sol::73::Y M0 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/mulmod.sol::74::Y M0 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/mulmod.sol::74::Y M0 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/mulmod.sol::75::Y M0 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/mulmod.sol::75::Y M0 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/mulmod.sol::76::Y M0 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/mulmod.sol::76::Y M0 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/mulmod.sol::77::Y M0 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/mulmod.sol::77::Y M0 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/mulmod.sol::78::Y M0 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/mulmod.sol::78::Y M0 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/mulmod.sol::79::Y M0 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/mulmod.sol::79::Y M0 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/mulmod.sol::80::Y M0 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/mulmod.sol::80::Y M0 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/mulmod.sol::81::Y M0 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/mulmod.sol::81::Y M0 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/mulmod.sol::82::Y M0 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/mulmod.sol::82::Y M0 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::11::Y M0 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::11::Y M0 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::11::Y M3 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::11::Y M3 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::11::Y Mz S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::11::Y Mz S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::12::Y M0 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::12::Y M0 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::12::Y M3 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::12::Y M3 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::12::Y Mz S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::12::Y Mz S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::13::Y M0 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::13::Y M0 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::13::Y M3 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::13::Y M3 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::13::Y Mz S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::13::Y Mz S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::25::Y M0 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::25::Y M0 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::25::Y M3 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::25::Y M3 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::25::Y Mz S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::25::Y Mz S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::26::Y M0 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::26::Y M0 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::26::Y M3 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::26::Y M3 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::26::Y Mz S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::26::Y Mz S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::27::Y M0 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::27::Y M0 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::27::Y M3 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::27::Y M3 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::27::Y Mz S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::27::Y Mz S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::39::Y M0 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::39::Y M0 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::39::Y M3 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::39::Y M3 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::39::Y Mz S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::39::Y Mz S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::40::Y M0 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::40::Y M0 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::40::Y M3 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::40::Y M3 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::40::Y Mz S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::40::Y Mz S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::41::Y M0 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::41::Y M0 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::41::Y M3 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::41::Y M3 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::41::Y Mz S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::41::Y Mz S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::53::Y M0 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::53::Y M0 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::53::Y M3 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::53::Y M3 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::53::Y Mz S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::53::Y Mz S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::54::Y M0 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::54::Y M0 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::54::Y M3 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::54::Y M3 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::54::Y Mz S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::54::Y Mz S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::55::Y M0 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::55::Y M0 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::55::Y M3 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::55::Y M3 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::55::Y Mz S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::55::Y Mz S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::67::Y M0 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::67::Y M0 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::67::Y M3 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::67::Y M3 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::67::Y Mz S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::67::Y Mz S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::68::Y M0 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::68::Y M0 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::68::Y M3 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::68::Y M3 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::68::Y Mz S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::68::Y Mz S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::69::Y M0 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::69::Y M0 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::69::Y M3 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::69::Y M3 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::69::Y Mz S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::69::Y Mz S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::81::Y M0 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::81::Y M0 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::81::Y M3 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::81::Y M3 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::81::Y Mz S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::81::Y Mz S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::82::Y M0 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::82::Y M0 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::82::Y M3 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::82::Y M3 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::82::Y Mz S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::82::Y Mz S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::83::Y M0 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::83::Y M0 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::83::Y M3 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::83::Y M3 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::83::Y Mz S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::83::Y Mz S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::95::Y M0 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::95::Y M0 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::95::Y M3 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::95::Y M3 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::95::Y Mz S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::95::Y Mz S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::96::Y M0 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::96::Y M0 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::96::Y M3 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::96::Y M3 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::96::Y Mz S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::96::Y Mz S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::97::Y M0 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::97::Y M0 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::97::Y M3 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::97::Y M3 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::97::Y Mz S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::97::Y Mz S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::109::Y M0 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::109::Y M0 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::109::Y M3 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::109::Y M3 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::109::Y Mz S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::109::Y Mz S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::110::Y M0 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::110::Y M0 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::110::Y M3 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::110::Y M3 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::110::Y Mz S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::110::Y Mz S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::111::Y M0 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::111::Y M0 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::111::Y M3 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::111::Y M3 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::111::Y Mz S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::111::Y Mz S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::123::Y M0 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::123::Y M0 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::123::Y M3 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::123::Y M3 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::123::Y Mz S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::123::Y Mz S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::124::Y M0 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::124::Y M0 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::124::Y M3 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::124::Y M3 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::124::Y Mz S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::124::Y Mz S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::125::Y M0 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::125::Y M0 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::125::Y M3 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::125::Y M3 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::125::Y Mz S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::125::Y Mz S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::137::Y M0 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::137::Y M0 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::137::Y M3 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::137::Y M3 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::137::Y Mz S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::137::Y Mz S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::138::Y M0 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::138::Y M0 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::138::Y M3 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::138::Y M3 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::138::Y Mz S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::138::Y Mz S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::139::Y M0 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::139::Y M0 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::139::Y M3 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::139::Y M3 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::139::Y Mz S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::139::Y Mz S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::151::Y M0 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::151::Y M0 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::151::Y M3 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::151::Y M3 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::151::Y Mz S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::151::Y Mz S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::152::Y M0 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::152::Y M0 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::152::Y M3 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::152::Y M3 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::152::Y Mz S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::152::Y Mz S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::153::Y M0 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::153::Y M0 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::153::Y M3 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::153::Y M3 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::153::Y Mz S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::153::Y Mz S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::154::Y M0 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::154::Y M0 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::154::Y M3 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::154::Y M3 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::154::Y Mz S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::154::Y Mz S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::155::Y M0 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::155::Y M0 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::155::Y M3 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::155::Y M3 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::155::Y Mz S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::155::Y Mz S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::156::Y M0 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::156::Y M0 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::156::Y M3 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::156::Y M3 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::156::Y Mz S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::156::Y Mz S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::157::Y M0 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::157::Y M0 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::157::Y M3 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::157::Y M3 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::157::Y Mz S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::157::Y Mz S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::158::Y M0 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::158::Y M0 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::158::Y M3 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::158::Y M3 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::158::Y Mz S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::158::Y Mz S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::159::Y M0 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::159::Y M0 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::159::Y M3 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::159::Y M3 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::159::Y Mz S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::159::Y Mz S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::160::Y M0 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::160::Y M0 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::160::Y M3 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::160::Y M3 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::160::Y Mz S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::160::Y Mz S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::161::Y M0 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::161::Y M0 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::161::Y M3 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::161::Y M3 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::161::Y Mz S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::161::Y Mz S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::162::Y M0 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::162::Y M0 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::162::Y M3 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::162::Y M3 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::162::Y Mz S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::162::Y Mz S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::163::Y M0 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::163::Y M0 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::163::Y M3 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::163::Y M3 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::163::Y Mz S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::163::Y Mz S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::164::Y M0 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::164::Y M0 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::164::Y M3 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::164::Y M3 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::164::Y Mz S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::164::Y Mz S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::165::Y M0 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::165::Y M0 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::165::Y M3 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::165::Y M3 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::165::Y Mz S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::165::Y Mz S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::166::Y M0 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::166::Y M0 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::166::Y M3 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::166::Y M3 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::166::Y Mz S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::166::Y Mz S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::167::Y M0 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::167::Y M0 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::167::Y M3 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::167::Y M3 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::167::Y Mz S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::167::Y Mz S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::168::Y M0 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::168::Y M0 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::168::Y M3 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::168::Y M3 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::168::Y Mz S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::168::Y Mz S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::169::Y M0 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::169::Y M0 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::169::Y M3 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::169::Y M3 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::169::Y Mz S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::169::Y Mz S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::170::Y M0 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::170::Y M0 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::170::Y M3 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::170::Y M3 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::170::Y Mz S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::170::Y Mz S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::171::Y M0 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::171::Y M0 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::171::Y M3 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::171::Y M3 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::171::Y Mz S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::171::Y Mz S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::172::Y M0 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::172::Y M0 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::172::Y M3 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::172::Y M3 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::172::Y Mz S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::172::Y Mz S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::173::Y M0 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::173::Y M0 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::173::Y M3 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::173::Y M3 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::173::Y Mz S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::173::Y Mz S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::174::Y M0 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::174::Y M0 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::174::Y M3 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::174::Y M3 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::174::Y Mz S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::174::Y Mz S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::175::Y M0 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::175::Y M0 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::175::Y M3 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::175::Y M3 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::175::Y Mz S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::175::Y Mz S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::176::Y M0 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::176::Y M0 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::176::Y M3 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::176::Y M3 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::176::Y Mz S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::176::Y Mz S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::177::Y M0 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::177::Y M0 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::177::Y M3 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::177::Y M3 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::177::Y Mz S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::177::Y Mz S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::178::Y M0 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::178::Y M0 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::178::Y M3 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::178::Y M3 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::178::Y Mz S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::178::Y Mz S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::179::Y M0 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::179::Y M0 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::179::Y M3 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::179::Y M3 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::179::Y Mz S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::179::Y Mz S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::180::Y M0 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::180::Y M0 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::180::Y M3 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::180::Y M3 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::180::Y Mz S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::180::Y Mz S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::181::Y M0 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::181::Y M0 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::181::Y M3 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::181::Y M3 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::181::Y Mz S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::181::Y Mz S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::182::Y M0 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::182::Y M0 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::182::Y M3 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::182::Y M3 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::182::Y Mz S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::182::Y Mz S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::183::Y M0 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::183::Y M0 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::183::Y M3 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::183::Y M3 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::183::Y Mz S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::183::Y Mz S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::184::Y M0 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::184::Y M0 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::184::Y M3 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::184::Y M3 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::184::Y Mz S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::184::Y Mz S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::185::Y M0 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::185::Y M0 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::185::Y M3 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::185::Y M3 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::185::Y Mz S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::185::Y Mz S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::186::Y M0 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::186::Y M0 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::186::Y M3 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::186::Y M3 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::186::Y Mz S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::186::Y Mz S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::187::Y M0 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::187::Y M0 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::187::Y M3 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::187::Y M3 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::187::Y Mz S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::187::Y Mz S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::188::Y M0 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::188::Y M0 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::188::Y M3 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::188::Y M3 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::188::Y Mz S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::188::Y Mz S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::189::Y M0 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::189::Y M0 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::189::Y M3 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::189::Y M3 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::189::Y Mz S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::189::Y Mz S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::190::Y M0 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::190::Y M0 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::190::Y M3 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::190::Y M3 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::190::Y Mz S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::190::Y Mz S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::191::Y M0 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::191::Y M0 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::191::Y M3 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::191::Y M3 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::191::Y Mz S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::191::Y Mz S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::192::Y M0 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::192::Y M0 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::192::Y M3 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::192::Y M3 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::192::Y Mz S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::192::Y Mz S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::193::Y M0 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::193::Y M0 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::193::Y M3 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::193::Y M3 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::193::Y Mz S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::193::Y Mz S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::194::Y M0 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::194::Y M0 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::194::Y M3 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::194::Y M3 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::194::Y Mz S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::194::Y Mz S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::195::Y M0 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::195::Y M0 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::195::Y M3 S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::195::Y M3 S-": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::195::Y Mz S+": "Failed", + "fixtures/solidity/simple/yul_instructions/revert.sol::195::Y Mz S-": "Failed" } diff --git a/.github/assets/revive-dev-node-revm-solc.json b/.github/assets/revive-dev-node-revm-solc.json index d2cc5abce5927..7fdf668c905c7 100644 --- a/.github/assets/revive-dev-node-revm-solc.json +++ b/.github/assets/revive-dev-node-revm-solc.json @@ -1,3 +1,13 @@ { - "fixtures/solidity/complex/create/create2_many/test.json::1::E M3": "Failed" + "fixtures/solidity/complex/create/create2_many/test.json::1::E M3 S+": "Failed", + "fixtures/solidity/complex/create/create2_many/test.json::1::Y M3 S+": "Failed", + "fixtures/solidity/complex/create/create_many/test.json::1::E M3 S+": "Failed", + "fixtures/solidity/complex/create/create_many/test.json::1::Y M3 S+": "Failed", + "fixtures/solidity/translated_semantic_tests/array/array_storage_index_boundary_test/test.json::0::E M0 S-": "Failed", + "fixtures/solidity/translated_semantic_tests/array/array_storage_push_empty/test.json::0::E M0 S-": "Failed", + "fixtures/solidity/translated_semantic_tests/array/copying/bytes_storage_to_storage/test.json::0::E M0 S-": "Failed", + "fixtures/solidity/translated_semantic_tests/array/copying/nested_array_element_storage_to_memory/test.json::0::E M0 S-": "Failed", + "fixtures/solidity/translated_semantic_tests/array/copying/nested_array_element_storage_to_storage/test.json::0::E M0 S-": "Failed", + "fixtures/solidity/translated_semantic_tests/events/event_indexed_string/test.json::0::E M0 S-": "Failed", + "fixtures/solidity/translated_semantic_tests/storageLayoutSpecifier/mapping_storage_end/test.json::0::E M0 S-": "Failed" } diff --git a/.github/workflows/tests-evm.yml b/.github/workflows/tests-evm.yml index 62e7f7997030a..3cf8d02f4c8f4 100644 --- a/.github/workflows/tests-evm.yml +++ b/.github/workflows/tests-evm.yml @@ -42,11 +42,11 @@ jobs: id: download uses: ./.github/actions/get-resolc - name: Run revive differential tests - uses: paritytech/revive-differential-tests/.github/actions/run-differential-tests@004a36f4e88228ee1331cb598c8873ab7315e3f0 + uses: paritytech/revive-differential-tests/.github/actions/run-differential-tests@b9cff329db2c981f706297150c6cb1f4a5d490c3 with: platform: ${{ matrix.platform }} cargo-command: "forklift cargo" - revive-differential-tests-ref: "004a36f4e88228ee1331cb598c8873ab7315e3f0" + revive-differential-tests-ref: "b9cff329db2c981f706297150c6cb1f4a5d490c3" resolc-path: ${{ steps.download.outputs.resolc-path }} expectations-file-path: ./.github/assets/${{ matrix.platform }}.json diff --git a/Cargo.lock b/Cargo.lock index 7918658b28daf..86351ab2029ca 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -13348,6 +13348,7 @@ version = "0.1.0" dependencies = [ "anyhow", "clap", + "derive_more 0.99.17", "env_logger 0.11.3", "futures", "git2", @@ -13379,6 +13380,7 @@ dependencies = [ "substrate-prometheus-endpoint 0.17.0", "subxt 0.44.2", "subxt-signer 0.44.2", + "tempfile", "thiserror 1.0.65", "tokio", ] diff --git a/prdoc/pr_11153.prdoc b/prdoc/pr_11153.prdoc new file mode 100644 index 0000000000000..1b0a8845334e6 --- /dev/null +++ b/prdoc/pr_11153.prdoc @@ -0,0 +1,17 @@ +title: '[eth-rpc]: add resumable block sync and improve CLI arguments' +doc: +- audience: Runtime Dev + description: | + ### Resumable block sync + - New block_sync module syncs backward from the latest finalized block to the first EVM block, with restart-safe checkpoint tracking via a sync_state SQLite table. + - On restart, fills only the top gap (new blocks) and bottom gap (remaining backfill) without re-syncing completed ranges. + - Auto-discovers and persists `first_evm_block` — the lowest block with EVM support on the chain. + - Chain identity verification: validates stored genesis hash on startup to detect database reuse across different chains; verifies sync boundary hashes to detect pruned blocks on the connected node. + + ### CLI rework + New `--eth-pruning` flag replaces `--database-url`, `--cache-size`, `--index-last-n-blocks`, and `--earliest-receipt-block`: + - `--eth-pruning archive` (default): persistent on-disk DB with backward historical sync. + - `--eth-pruning `: in-memory DB keeping the latest N blocks. +crates: +- name: pallet-revive-eth-rpc + bump: major diff --git a/substrate/frame/revive/rpc/.sqlx/query-0b667102134cdbe0776b46a8ffa651cefd9590006ea59b8fa7490dae024c16ac.json b/substrate/frame/revive/rpc/.sqlx/query-0b667102134cdbe0776b46a8ffa651cefd9590006ea59b8fa7490dae024c16ac.json new file mode 100644 index 0000000000000..231f4301bad18 --- /dev/null +++ b/substrate/frame/revive/rpc/.sqlx/query-0b667102134cdbe0776b46a8ffa651cefd9590006ea59b8fa7490dae024c16ac.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "\n\t\t\tDELETE FROM sync_state WHERE label = $1\n\t\t\t", + "describe": { + "columns": [], + "parameters": { + "Right": 1 + }, + "nullable": [] + }, + "hash": "0b667102134cdbe0776b46a8ffa651cefd9590006ea59b8fa7490dae024c16ac" +} diff --git a/substrate/frame/revive/rpc/.sqlx/query-2e21c8b990a008294d9cbaa229d521a7a2e2fa33b3d93b5c2c7aece64a4fe79b.json b/substrate/frame/revive/rpc/.sqlx/query-2e21c8b990a008294d9cbaa229d521a7a2e2fa33b3d93b5c2c7aece64a4fe79b.json new file mode 100644 index 0000000000000..77c4234dd0240 --- /dev/null +++ b/substrate/frame/revive/rpc/.sqlx/query-2e21c8b990a008294d9cbaa229d521a7a2e2fa33b3d93b5c2c7aece64a4fe79b.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "INSERT INTO sync_state (label, block_number, block_hash)\n\t\t\tVALUES ($1, $2, $3)\n\t\t\tON CONFLICT(label) DO UPDATE\n\t\t\t\tSET block_number = excluded.block_number, block_hash = excluded.block_hash\n\t\t\tWHERE sync_state.block_number > excluded.block_number\n\t\t\t", + "describe": { + "columns": [], + "parameters": { + "Right": 3 + }, + "nullable": [] + }, + "hash": "2e21c8b990a008294d9cbaa229d521a7a2e2fa33b3d93b5c2c7aece64a4fe79b" +} diff --git a/substrate/frame/revive/rpc/.sqlx/query-481e21860552d88454b30e7e5cb31748994229fa131fa9b608c987cbd97b3b6c.json b/substrate/frame/revive/rpc/.sqlx/query-481e21860552d88454b30e7e5cb31748994229fa131fa9b608c987cbd97b3b6c.json new file mode 100644 index 0000000000000..b6d2ae6d6d771 --- /dev/null +++ b/substrate/frame/revive/rpc/.sqlx/query-481e21860552d88454b30e7e5cb31748994229fa131fa9b608c987cbd97b3b6c.json @@ -0,0 +1,26 @@ +{ + "db_name": "SQLite", + "query": "\n\t\t\tSELECT block_number, block_hash\n\t\t\tFROM sync_state\n\t\t\tWHERE label = $1\n\t\t\t", + "describe": { + "columns": [ + { + "name": "block_number", + "ordinal": 0, + "type_info": "Integer" + }, + { + "name": "block_hash", + "ordinal": 1, + "type_info": "Blob" + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false, + true + ] + }, + "hash": "481e21860552d88454b30e7e5cb31748994229fa131fa9b608c987cbd97b3b6c" +} diff --git a/substrate/frame/revive/rpc/.sqlx/query-872ea7628d20866aa6e9e701e8c2f5d5d045f26ad888d26ff07d4cd5130c398f.json b/substrate/frame/revive/rpc/.sqlx/query-872ea7628d20866aa6e9e701e8c2f5d5d045f26ad888d26ff07d4cd5130c398f.json new file mode 100644 index 0000000000000..4f1db61d221d1 --- /dev/null +++ b/substrate/frame/revive/rpc/.sqlx/query-872ea7628d20866aa6e9e701e8c2f5d5d045f26ad888d26ff07d4cd5130c398f.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "INSERT INTO sync_state (label, block_number, block_hash)\n\t\t\tVALUES ($1, $2, $3)\n\t\t\tON CONFLICT(label) DO UPDATE\n\t\t\t\tSET block_number = excluded.block_number, block_hash = excluded.block_hash\n\t\t\tWHERE sync_state.block_number < excluded.block_number\n\t\t\t", + "describe": { + "columns": [], + "parameters": { + "Right": 3 + }, + "nullable": [] + }, + "hash": "872ea7628d20866aa6e9e701e8c2f5d5d045f26ad888d26ff07d4cd5130c398f" +} diff --git a/substrate/frame/revive/rpc/.sqlx/query-fa3e8dbe73656b1dfee46df2deee48cae1255f7d77bd8d1b403ddc057a5d0b2c.json b/substrate/frame/revive/rpc/.sqlx/query-fa3e8dbe73656b1dfee46df2deee48cae1255f7d77bd8d1b403ddc057a5d0b2c.json new file mode 100644 index 0000000000000..641dc55731688 --- /dev/null +++ b/substrate/frame/revive/rpc/.sqlx/query-fa3e8dbe73656b1dfee46df2deee48cae1255f7d77bd8d1b403ddc057a5d0b2c.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "\n\t\t\tINSERT OR REPLACE INTO sync_state (label, block_number, block_hash)\n\t\t\tVALUES ($1, $2, $3)\n\t\t\t", + "describe": { + "columns": [], + "parameters": { + "Right": 3 + }, + "nullable": [] + }, + "hash": "fa3e8dbe73656b1dfee46df2deee48cae1255f7d77bd8d1b403ddc057a5d0b2c" +} diff --git a/substrate/frame/revive/rpc/Cargo.toml b/substrate/frame/revive/rpc/Cargo.toml index 88ee4a464ee40..87d3eaa32361b 100644 --- a/substrate/frame/revive/rpc/Cargo.toml +++ b/substrate/frame/revive/rpc/Cargo.toml @@ -23,6 +23,7 @@ path = "src/main.rs" anyhow = { workspace = true } clap = { workspace = true, features = ["derive", "env"] } codec = { workspace = true, features = ["derive"] } +derive_more = { workspace = true } futures = { workspace = true, features = ["thread-pool"] } hex = { workspace = true } jsonrpsee = { workspace = true, features = ["full"] } @@ -55,6 +56,7 @@ pallet-revive-fixtures = { workspace = true, default-features = true } pretty_assertions = { workspace = true } revive-dev-node = { workspace = true } sp-io = { workspace = true, default-features = true } +tempfile = { workspace = true } [build-dependencies] git2 = { workspace = true } diff --git a/substrate/frame/revive/rpc/migrations/0004_create_sync_state.sql b/substrate/frame/revive/rpc/migrations/0004_create_sync_state.sql new file mode 100644 index 0000000000000..e9618028210cf --- /dev/null +++ b/substrate/frame/revive/rpc/migrations/0004_create_sync_state.sql @@ -0,0 +1,5 @@ +CREATE TABLE IF NOT EXISTS sync_state ( + label TEXT NOT NULL PRIMARY KEY, + block_number INTEGER NOT NULL, + block_hash BLOB +); diff --git a/substrate/frame/revive/rpc/src/block_sync.rs b/substrate/frame/revive/rpc/src/block_sync.rs new file mode 100644 index 0000000000000..3c83551de1781 --- /dev/null +++ b/substrate/frame/revive/rpc/src/block_sync.rs @@ -0,0 +1,388 @@ +// This file is part of Substrate. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Historic block syncing logic for the Ethereum JSON-RPC server. + +use crate::{ + BlockInfoProvider, + client::{Client, ClientError, SubstrateBlockNumber}, +}; +use pallet_revive::evm::H256; + +const LOG_TARGET: &str = "eth-rpc::block-sync"; + +/// Trait for types that can be used as keys in the `sync_state` table. +pub trait SyncStateKey: std::fmt::Display {} + +/// Labels used to track sync progress in the `sync_state` table. +#[derive(Debug, Clone, Copy, derive_more::Display)] +pub enum SyncLabel { + /// Lowest synced block. Only decreases. + #[display(fmt = "sync-tail")] + Tail, + /// Highest synced block. Absent means no sync has started. + /// During backfill: upper boundary being filled. + /// After backfill: advanced by the finalized-block subscription. + #[display(fmt = "sync-head")] + Head, +} + +/// Chain metadata stored in the `sync_state` table. +#[derive(Debug, Clone, Copy, derive_more::Display)] +pub enum ChainMetadata { + /// Genesis block hash — used for chain identity verification. + #[display(fmt = "chain-genesis")] + Genesis, + /// Auto-discovered first EVM block on the chain. + #[display(fmt = "chain-first-evm-block")] + FirstEvmBlock, +} + +impl SyncStateKey for SyncLabel {} +impl SyncStateKey for ChainMetadata {} + +/// Sync checkpoint persisted in the `sync_state` table to allow resuming after a restart. +#[derive(Debug, Clone, Copy, Eq, PartialEq)] +pub struct SyncCheckpoint { + pub block_number: SubstrateBlockNumber, + pub block_hash: Option, +} + +impl SyncCheckpoint { + /// Create a checkpoint with a known block hash. + pub fn new(block_number: SubstrateBlockNumber, block_hash: H256) -> Self { + Self { block_number, block_hash: Some(block_hash) } + } + + /// Create a checkpoint with only a block number (no hash). + pub fn from_number(block_number: SubstrateBlockNumber) -> Self { + Self { block_number, block_hash: None } + } +} + +/// How often (in blocks) the backward sync checkpoints are persisted to the database. +const BLOCK_INTERVAL: u32 = 128; + +/// Options for [`Client::sync_backward_range`]. +struct BackwardSyncRange { + from: SubstrateBlockNumber, + to: SubstrateBlockNumber, + /// Set `Head` label after syncing the first block. + set_head: bool, + /// Checkpoint `Tail` label periodically and at end. + checkpoint_tail: bool, +} + +impl Client { + /// Verify that the stored genesis hash matches the connected chain. + async fn validate_chain_identity(&self) -> Result { + let genesis_hash: H256 = self.api().genesis_hash(); + + if let Some(checkpoint) = + self.receipt_provider().get_sync_label(ChainMetadata::Genesis).await? + { + if let Some(stored) = checkpoint.block_hash { + if stored != genesis_hash { + return Err(ClientError::ChainMismatch); + } + } + } + + Ok(genesis_hash) + } + + /// Verify that a stored boundary block still exists on the finalized chain. + async fn verify_boundary(&self, checkpoint: &SyncCheckpoint) -> Result<(), ClientError> { + let num = checkpoint.block_number; + let hash = checkpoint.block_hash; + match (num, hash) { + (_, None) => { + log::error!(target: LOG_TARGET, + "Boundary #{num}: missing stored hash"); + Err(ClientError::SyncBoundaryMismatch) + }, + (_, Some(stored_hash)) => { + let block = self.block_provider().block_by_number(num).await?.ok_or_else(|| { + log::error!(target: LOG_TARGET, + "Boundary #{num}: block not found on chain \ + (node may have pruned it — use an archive node with --eth-pruning archive)"); + ClientError::SyncBoundaryMismatch + })?; + if block.hash() != stored_hash { + log::error!(target: LOG_TARGET, + "Boundary #{num}: hash mismatch — stored {stored_hash:?}, \ + chain {:?}", block.hash()); + return Err(ClientError::SyncBoundaryMismatch); + } + Ok(()) + }, + } + } + + /// Checkpoint the given sync label to the DB. + async fn checkpoint_sync_label(&self, label: SyncLabel, num: SubstrateBlockNumber, hash: H256) { + let cp = SyncCheckpoint::new(num, hash); + let result = match label { + SyncLabel::Head => self.receipt_provider().advance_sync_label(label, cp).await, + SyncLabel::Tail => self.receipt_provider().recede_sync_label(label, cp).await, + }; + if let Err(err) = result { + log::warn!(target: LOG_TARGET, "Failed to update sync_label[{label}]: {err:?}"); + } + } + + /// Backward sync historical blocks from the latest finalized block to the first EVM block. + /// Resumes from the last checkpoint if a previous sync was interrupted. + /// Fatal errors (chain/DB mismatch) are propagated; transient errors are swallowed + /// to avoid taking down the RPC server. + pub async fn sync_backward(&self) -> Result<(), ClientError> { + log::info!(target: LOG_TARGET, + "🔄 Historical block sync enabled. \ + For a complete sync, the connected node should be an archive node."); + match self.sync_backward_inner().await { + Ok(()) => Ok(()), + Err(err) if err.is_chain_validation_error() => Err(err), + Err(err) => { + log::error!(target: LOG_TARGET, "🗄️ Sync stopped due to {err}."); + Ok(()) + }, + } + } + + async fn sync_backward_inner(&self) -> Result<(), ClientError> { + let genesis_hash = self.validate_chain_identity().await?; + let latest_finalized_block = self.latest_finalized_block().await; + let latest_finalized = + SyncCheckpoint::new(latest_finalized_block.number(), latest_finalized_block.hash()); + + // Store genesis (idempotent). + self.receipt_provider() + .set_sync_label(ChainMetadata::Genesis, SyncCheckpoint::new(0, genesis_hash)) + .await?; + + let (head, tail) = tokio::try_join!( + self.receipt_provider().get_sync_label(SyncLabel::Head), + self.receipt_provider().get_sync_label(SyncLabel::Tail), + )?; + + match (tail, head) { + (Some(tail), Some(head)) => { + // Verify boundary hashes still match the finalized chain. + tokio::try_join!(self.verify_boundary(&tail), self.verify_boundary(&head),)?; + self.sync_backward_resume(tail, head, latest_finalized).await?; + }, + (Some(_), None) => { + log::warn!(target: LOG_TARGET, + "🗄️ Tail exists without Head — possible partial corruption, \ + starting fresh sync from #{}", latest_finalized.block_number); + self.sync_backward_fresh(latest_finalized.block_number).await?; + }, + _ => { + log::info!(target: LOG_TARGET, + "🗄️ Fresh sync: syncing backward from #{}", latest_finalized.block_number); + self.sync_backward_fresh(latest_finalized.block_number).await?; + }, + } + + self.mark_backfill_complete(); + + log::info!(target: LOG_TARGET, "🗄️ Historic sync complete"); + Ok(()) + } + + /// Backward sync from `latest_finalized` down to the first EVM block. + async fn sync_backward_fresh( + &self, + latest_finalized: SubstrateBlockNumber, + ) -> Result<(), ClientError> { + let first_evm = self.receipt_provider().first_evm_block().unwrap_or(0); + self.sync_backward_range(BackwardSyncRange { + from: latest_finalized, + to: first_evm, + set_head: true, + checkpoint_tail: true, + }) + .await + } + + /// Resume backward sync by filling the top gap (new blocks) and bottom gap (backfill). + async fn sync_backward_resume( + &self, + tail: SyncCheckpoint, + head: SyncCheckpoint, + latest_finalized: SyncCheckpoint, + ) -> Result<(), ClientError> { + log::info!(target: LOG_TARGET, + "🗄️ Resuming sync: DB has blocks #{}..#{}, chain head is #{}", + tail.block_number, head.block_number, latest_finalized.block_number); + + let top_gap = async { + // Top gap: sync from latest_finalized down to head + 1. + if head.block_number < latest_finalized.block_number { + self.sync_backward_range(BackwardSyncRange { + from: latest_finalized.block_number, + to: head.block_number.saturating_add(1), + set_head: false, + checkpoint_tail: false, + }) + .await?; + + // Mark top gap complete so a restart won't redo it. + self.receipt_provider() + .advance_sync_label(SyncLabel::Head, latest_finalized) + .await?; + } + Ok::<_, ClientError>(()) + }; + + let bottom_gap = async { + // Bottom gap: sync from tail - 1 down to the first EVM block. + let first_evm = self.receipt_provider().first_evm_block().unwrap_or(0); + if tail.block_number > first_evm { + self.sync_backward_range(BackwardSyncRange { + from: tail.block_number.saturating_sub(1), + to: first_evm, + set_head: false, + checkpoint_tail: true, + }) + .await?; + } else { + log::debug!(target: LOG_TARGET, "🗄️ No backward gap to fill"); + } + Ok::<_, ClientError>(()) + }; + + tokio::try_join!(top_gap, bottom_gap)?; + + Ok(()) + } + + /// Backward sync from block `from` down to block `to` (inclusive). + /// Stops early if a non-EVM block is discovered (auto-discovery of first EVM block). + async fn sync_backward_range( + &self, + BackwardSyncRange { from, to, set_head, checkpoint_tail }: BackwardSyncRange, + ) -> Result<(), ClientError> { + if from < to { + log::debug!(target: LOG_TARGET, "⬇️ Backward sync: nothing to sync (#{from}..#{to})"); + return Ok(()); + } + + log::info!(target: LOG_TARGET, "⬇️ Backward sync: #{from} down to #{to}"); + + let mut block = self + .block_provider() + .block_by_number(from) + .await? + .ok_or(ClientError::BlockNotFound)?; + + let mut blocks_synced = 0u64; + let mut last_synced: Option<(SubstrateBlockNumber, H256)> = None; + let at_checkpoint = + |synced: u64| synced <= 1 || synced.is_multiple_of(u64::from(BLOCK_INTERVAL)); + + let loop_result: Result<(), ClientError> = loop { + let block_number = block.number(); + let block_hash = block.hash(); + + let ethereum_hash = match self + .runtime_api(block_hash) + .eth_block_hash(pallet_revive::evm::U256::from(block_number)) + .await + { + Ok(h) => h, + Err(err) => { + log::error!(target: LOG_TARGET, "⚠️ eth_block_hash failed for #{block_number}: {err:?}, stopping"); + break Err(err.into()); + }, + }; + + match ethereum_hash { + Some(hash) => { + if let Err(err) = + self.receipt_provider().insert_block_receipts_past(&block, &hash).await + { + log::error!(target: LOG_TARGET, + "⚠️ Insert failed for #{block_number}: {err:?}, stopping"); + break Err(err); + } + + last_synced = Some((block_number, block_hash)); + blocks_synced += 1; + + if blocks_synced == 1 && set_head { + self.checkpoint_sync_label(SyncLabel::Head, block_number, block_hash).await; + } + + if at_checkpoint(blocks_synced) { + log::debug!(target: LOG_TARGET, + "⬇️ Backward sync progress: #{block_number} ({blocks_synced} blocks synced)"); + if checkpoint_tail { + self.checkpoint_sync_label(SyncLabel::Tail, block_number, block_hash) + .await; + } + } + }, + None => { + let first_evm_block = block_number.saturating_add(1); + log::debug!(target: LOG_TARGET, + "🔍 No EVM hash at #{block_number}, setting first_evm_block to #{first_evm_block}"); + if let Err(err) = + self.receipt_provider().set_first_evm_block(first_evm_block).await + { + log::warn!(target: LOG_TARGET, "Failed to persist first-evm-block: {err:?}"); + } + + break Ok(()); + }, + } + + if block_number > to { + let parent_hash = block.header().parent_hash; + match self + .block_provider() + .block_by_hash(&parent_hash) + .await + .map_err(Into::into) + .and_then(|opt| opt.ok_or(ClientError::BlockNotFound)) + { + Ok(b) => block = b, + Err(err) => { + log::error!(target: LOG_TARGET, + "⚠️ Could not fetch parent of #{block_number}: {err:?}, stopping"); + break Err(err); + }, + } + } else { + break Ok(()); + } + }; + + // Checkpoint the last synced block if it wasn't already at a checkpoint interval. + if loop_result.is_ok() && checkpoint_tail && !at_checkpoint(blocks_synced) { + if let Some((num, hash)) = last_synced { + self.checkpoint_sync_label(SyncLabel::Tail, num, hash).await; + } + } + + log::info!(target: LOG_TARGET, + "⬇️ Backward sync: {blocks_synced} blocks synced \ + (requested #{from}..#{to})"); + + loop_result + } +} diff --git a/substrate/frame/revive/rpc/src/cli.rs b/substrate/frame/revive/rpc/src/cli.rs index 049512a440dc2..3bb7fb3ae1d03 100644 --- a/substrate/frame/revive/rpc/src/cli.rs +++ b/substrate/frame/revive/rpc/src/cli.rs @@ -19,18 +19,66 @@ use crate::{ DebugRpcServer, DebugRpcServerImpl, EthRpcServer, EthRpcServerImpl, LOG_TARGET, PolkadotRpcServer, PolkadotRpcServerImpl, ReceiptExtractor, ReceiptProvider, SubxtBlockInfoProvider, SystemHealthRpcServer, SystemHealthRpcServerImpl, - client::{Client, SubscriptionType, SubstrateBlockNumber, connect}, + client::{Client, SubscriptionType, connect}, }; -use clap::Parser; +use clap::{CommandFactory, FromArgMatches, Parser}; use futures::{FutureExt, future::BoxFuture, pin_mut}; use jsonrpsee::server::RpcModule; use sc_cli::{PrometheusParams, RpcParams, SharedParams, Signals}; use sc_service::{ TaskManager, - config::{PrometheusConfig, RpcConfiguration}, + config::{BasePath, PrometheusConfig, RpcConfiguration}, start_rpc_servers, }; -use sqlx::sqlite::SqlitePoolOptions; +use sqlx::sqlite::{SqliteConnectOptions, SqliteJournalMode, SqlitePoolOptions}; +use std::path::PathBuf; + +/// Specifies the eth-rpc pruning mode. +#[derive(Debug, Clone, Copy, PartialEq, Eq, derive_more::Display)] +pub enum EthPruningMode { + /// Persistent on-disk database with backward historical sync of all blocks. + #[display(fmt = "archive")] + Archive, + /// In-memory database keeping only the latest N blocks. + #[display(fmt = "{_0}")] + KeepLatest(usize), +} + +impl EthPruningMode { + /// Returns `true` if this mode enables historical block sync. + pub fn is_archive(&self) -> bool { + matches!(self, Self::Archive) + } + + /// Returns the number of blocks to keep, if in `KeepLatest` mode. + pub fn keep_latest(&self) -> Option { + match self { + Self::KeepLatest(n) => Some(*n), + _ => None, + } + } +} + +impl std::str::FromStr for EthPruningMode { + type Err = String; + + fn from_str(input: &str) -> Result { + match input { + "archive" => Ok(Self::Archive), + n => { + n.parse::() + .ok() + .filter(|&v| v >= 1) + .map(Self::KeepLatest) + .ok_or_else(|| { + format!( + "Invalid pruning mode '{n}': expected 'archive' or a positive integer" + ) + }) + }, + } + } +} // Default port if --prometheus-port is not specified const DEFAULT_PROMETHEUS_PORT: u16 = 9616; @@ -38,7 +86,7 @@ const DEFAULT_PROMETHEUS_PORT: u16 = 9616; // Default port if --rpc-port is not specified const DEFAULT_RPC_PORT: u16 = 8545; -const IN_MEMORY_DB: &str = "sqlite::memory:"; +const DEFAULT_DATABASE_NAME: &str = "eth-rpc.db"; // Parsed command instructions from the command line #[derive(Parser, Debug)] @@ -48,23 +96,12 @@ pub struct CliCommand { #[clap(long, default_value = "ws://127.0.0.1:9944")] pub node_rpc_url: String, - /// The maximum number of blocks to cache in memory. - #[clap(long, default_value = "256")] - pub cache_size: usize, - - /// Earliest block number to consider when searching for transaction receipts. - #[clap(long)] - pub earliest_receipt_block: Option, - - /// The database used to store Ethereum transaction hashes. - /// This is only useful if the node needs to act as an archive node and respond to Ethereum RPC - /// queries for transactions that are not in the in memory cache. - #[clap(long, env = "DATABASE_URL", default_value = IN_MEMORY_DB)] - pub database_url: String, - - /// If provided, index the last n blocks - #[clap(long)] - pub index_last_n_blocks: Option, + /// Pruning mode for the eth-rpc receipt database. + /// + /// - archive (default): Sync all historical blocks (requires an archive node). + /// - N (>= 1): In-memory database keeping only the latest N blocks. + #[clap(long, default_value = "archive")] + pub eth_pruning: EthPruningMode, #[allow(missing_docs)] #[clap(flatten)] @@ -85,6 +122,40 @@ pub struct CliCommand { pub allow_unprotected_txs: bool, } +impl CliCommand { + /// Parse CLI args, rejecting any removed flags with a helpful message. + pub fn parse_cli() -> anyhow::Result { + let removed_flags = + ["database-url", "cache-size", "index-last-n-blocks", "earliest-receipt-block"]; + + let cmd = removed_flags.iter().fold(Self::command(), |cmd, name| { + cmd.arg( + clap::Arg::new(*name) + .long(*name) + .num_args(0..=1) + .hide(true) + .action(clap::ArgAction::Set), + ) + }); + let matches = cmd.get_matches(); + + let used: Vec<_> = removed_flags + .iter() + .filter(|f| matches.contains_id(f)) + .map(|f| format!("--{f}")) + .collect(); + if !used.is_empty() { + anyhow::bail!( + "[{}] have been removed. \ + Check polkadot-sdk PR #11153 for the CLI migration guide.", + used.join(", "), + ); + } + + Ok(Self::from_arg_matches(&matches).expect("already validated by clap")) + } +} + /// Initialize the logger #[cfg(not(test))] fn init_logger(params: &SharedParams) -> anyhow::Result<()> { @@ -106,49 +177,94 @@ fn init_logger(params: &SharedParams) -> anyhow::Result<()> { Ok(()) } +/// Resolve the base directory for persistent database storage. +/// +/// - If `base_path` is `Some` (explicit `--base-path` or `--dev` temp dir), use it directly. +/// - If `base_path` is `None`, use the platform default: +/// - macOS: `~/Library/Application Support/eth-rpc/` +/// - Linux: `~/.local/share/eth-rpc/` +/// - Windows: `%APPDATA%\eth-rpc\` +fn resolve_db_dir(base_path: Option) -> PathBuf { + match base_path { + Some(path) => path.path().to_path_buf(), + None => BasePath::from_project("", "", "eth-rpc").path().to_path_buf(), + } +} + +/// Resolve SQLite connection options from CLI arguments. +fn resolve_db_options( + eth_pruning: EthPruningMode, + base_path: Option, +) -> anyhow::Result { + if eth_pruning.is_archive() { + let db_dir = resolve_db_dir(base_path); + std::fs::create_dir_all(&db_dir).map_err(|e| { + anyhow::anyhow!("Failed to create database directory {}: {e}", db_dir.display()) + })?; + let db_path = db_dir.join(DEFAULT_DATABASE_NAME); + log::info!(target: LOG_TARGET, "💾 Database path: {}", db_path.display()); + // WAL mode allows concurrent writes from the live subscription + // and the backward sync without SQLITE_BUSY errors. + Ok(SqliteConnectOptions::new() + .filename(&db_path) + .create_if_missing(true) + .journal_mode(SqliteJournalMode::Wal)) + } else { + Ok(SqliteConnectOptions::new().in_memory(true)) + } +} + fn build_client( tokio_handle: &tokio::runtime::Handle, - cache_size: usize, - earliest_receipt_block: Option, + eth_pruning: EthPruningMode, node_rpc_url: &str, - database_url: &str, + db_options: SqliteConnectOptions, max_request_size: u32, max_response_size: u32, abort_signal: Signals, ) -> anyhow::Result { let fut = async { - let (api, rpc_client, rpc) = connect(node_rpc_url, max_request_size, max_response_size).await?; - let block_provider = SubxtBlockInfoProvider::new( api.clone(), rpc.clone()).await?; - - let (pool, keep_latest_n_blocks) = if database_url == IN_MEMORY_DB { - log::warn!( target: LOG_TARGET, "💾 Using in-memory database, keeping only {cache_size} blocks in memory"); - // see sqlite in-memory issue: https://github.com/launchbadge/sqlx/issues/2510 - let pool = SqlitePoolOptions::new() + let (api, rpc_client, rpc) = + connect(node_rpc_url, max_request_size, max_response_size).await?; + let block_provider = SubxtBlockInfoProvider::new(api.clone(), rpc.clone()).await?; + + let (pool, keep_latest_n_blocks) = match eth_pruning { + EthPruningMode::Archive => { + (SqlitePoolOptions::new().connect_with(db_options).await?, None) + }, + EthPruningMode::KeepLatest(max_blocks) => { + log::info!(target: LOG_TARGET, + "💾 Using in-memory database, keeping only {max_blocks} blocks"); + // see sqlite in-memory issue: https://github.com/launchbadge/sqlx/issues/2510 + let pool = SqlitePoolOptions::new() .max_connections(1) .idle_timeout(None) .max_lifetime(None) - .connect(database_url).await?; - - (pool, Some(cache_size)) - } else { - (SqlitePoolOptions::new().connect(database_url).await?, None) + .connect_with(db_options) + .await?; + (pool, Some(max_blocks)) + }, }; - let receipt_extractor = ReceiptExtractor::new( - api.clone(), - earliest_receipt_block, - ).await?; + let receipt_extractor = ReceiptExtractor::new(api.clone()).await?; let receipt_provider = ReceiptProvider::new( - pool, - block_provider.clone(), - receipt_extractor.clone(), - keep_latest_n_blocks, - ) - .await?; - - let client = - Client::new(api, rpc_client, rpc, block_provider, receipt_provider).await?; + pool, + block_provider.clone(), + receipt_extractor.clone(), + keep_latest_n_blocks, + ) + .await?; + + let client = Client::new( + api, + rpc_client, + rpc, + block_provider, + receipt_provider, + eth_pruning.is_archive(), + ) + .await?; Ok(client) } @@ -168,10 +284,7 @@ pub fn run(cmd: CliCommand) -> anyhow::Result<()> { rpc_params, prometheus_params, node_rpc_url, - cache_size, - database_url, - earliest_receipt_block, - index_last_n_blocks, + eth_pruning, shared_params, allow_unprotected_txs, .. @@ -180,8 +293,22 @@ pub fn run(cmd: CliCommand) -> anyhow::Result<()> { #[cfg(not(test))] init_logger(&shared_params)?; let is_dev = shared_params.dev; + let explicit_base_path = shared_params.base_path.is_some(); + let base_path = shared_params.base_path()?; + + if is_dev && eth_pruning.is_archive() && !explicit_base_path { + log::warn!( + target: LOG_TARGET, + "⚠️ Running in --dev mode with --eth-pruning=archive but no --base-path. \ + The database will be stored in a temporary directory and lost on exit. \ + Use --base-path to persist the database." + ); + } + + let db_options = resolve_db_options(eth_pruning, base_path)?; + let rpc_addrs: Option> = rpc_params - .rpc_addr(is_dev, false, 8545)? + .rpc_addr(is_dev, false, DEFAULT_RPC_PORT)? .map(|addrs| addrs.into_iter().map(Into::into).collect()); let rpc_config = RpcConfiguration { @@ -212,10 +339,9 @@ pub fn run(cmd: CliCommand) -> anyhow::Result<()> { let client = build_client( tokio_handle, - cache_size, - earliest_receipt_block, + eth_pruning, &node_rpc_url, - &database_url, + db_options, rpc_config.max_request_size * 1024 * 1024, rpc_config.max_response_size * 1024 * 1024, tokio_runtime.block_on(async { Signals::capture() })?, @@ -246,8 +372,8 @@ pub fn run(cmd: CliCommand) -> anyhow::Result<()> { Box::pin(client.subscribe_and_cache_new_blocks(SubscriptionType::FinalizedBlocks)), ]; - if let Some(index_last_n_blocks) = index_last_n_blocks { - futures.push(Box::pin(client.subscribe_and_cache_blocks(index_last_n_blocks))); + if eth_pruning.is_archive() { + futures.push(Box::pin(client.sync_backward())); } if let Err(err) = futures::future::try_join_all(futures).await { @@ -296,3 +422,57 @@ fn rpc_module( .map_err(|e| sc_service::Error::Application(e.into()))?; Ok(module) } + +#[cfg(test)] +mod tests { + use super::*; + use tempfile::TempDir; + + #[test] + fn in_memory_returns_memory_options() { + let opts = resolve_db_options(EthPruningMode::KeepLatest(256), None).unwrap(); + // In-memory options produce `:memory:` filename. + let filename = opts.get_filename(); + assert_eq!(filename, std::path::Path::new(":memory:")); + } + + #[test] + fn persistent_with_explicit_base_path() { + let tmp = TempDir::new().unwrap(); + let base = BasePath::new(tmp.path()); + let opts = resolve_db_options(EthPruningMode::Archive, Some(base)).unwrap(); + assert_eq!(opts.get_filename(), tmp.path().join(DEFAULT_DATABASE_NAME)); + assert!(tmp.path().exists()); + } + + #[test] + fn persistent_default_path() { + let opts = resolve_db_options(EthPruningMode::Archive, None).unwrap(); + let filename = opts.get_filename().to_string_lossy().to_string(); + assert!(filename.contains("eth-rpc")); + assert!(filename.contains(DEFAULT_DATABASE_NAME)); + } + + #[test] + fn persistent_creates_nested_directories() { + let tmp = TempDir::new().unwrap(); + let nested = tmp.path().join("a").join("b"); + let base = BasePath::new(&nested); + resolve_db_options(EthPruningMode::Archive, Some(base)).unwrap(); + assert!(nested.exists()); + } + + #[test] + fn eth_pruning_mode() { + // CLI parsing + let cmd = CliCommand::try_parse_from(["eth-rpc", "--eth-pruning", "archive"]).unwrap(); + assert_eq!(cmd.eth_pruning, EthPruningMode::Archive); + + let cmd = CliCommand::try_parse_from(["eth-rpc", "--eth-pruning", "256"]).unwrap(); + assert_eq!(cmd.eth_pruning, EthPruningMode::KeepLatest(256)); + + // Default is archive + let cmd = CliCommand::try_parse_from(["eth-rpc"]).unwrap(); + assert_eq!(cmd.eth_pruning, EthPruningMode::Archive); + } +} diff --git a/substrate/frame/revive/rpc/src/client.rs b/substrate/frame/revive/rpc/src/client.rs index 1286cfe835ef2..a6ee7f7fddc66 100644 --- a/substrate/frame/revive/rpc/src/client.rs +++ b/substrate/frame/revive/rpc/src/client.rs @@ -19,9 +19,11 @@ pub(crate) mod runtime_api; pub(crate) mod storage_api; + use crate::{ BlockInfoProvider, BlockTag, FeeHistoryProvider, ReceiptProvider, SubxtBlockInfoProvider, - TracerType, TransactionInfo, + SyncLabel, TracerType, TransactionInfo, + block_sync::SyncCheckpoint, subxt_client::{self, SrcChainConfig, revive::calls::types::EthTransact}, }; use futures::TryStreamExt; @@ -31,13 +33,19 @@ use pallet_revive::{ evm::{ Block, BlockNumberOrTag, BlockNumberOrTagOrHash, FeeHistoryResult, Filter, GenericTransaction, H256, HashesOrTransactionInfos, Log, ReceiptInfo, SyncingProgress, - SyncingStatus, Trace, TransactionSigned, TransactionTrace, decode_revert_reason, + SyncingStatus, Trace, TransactionSigned, TransactionTrace, U256, decode_revert_reason, }, }; use runtime_api::RuntimeApi; use sp_runtime::traits::Block as BlockT; use sp_weights::Weight; -use std::{ops::Range, sync::Arc, time::Duration}; +use std::{ + sync::{ + Arc, + atomic::{AtomicBool, Ordering}, + }, + time::Duration, +}; use storage_api::StorageApi; use subxt::{ Config, OnlineClient, @@ -133,7 +141,7 @@ pub enum ClientError { /// author_submitExtrinsic failed. #[error("Invalid transaction: {0}")] SubmitError(SubmitError), - /// Transcact call failed. + /// Transact call failed. #[error("contract reverted: {0:?}")] TransactError(EthTransactError), /// A decimal conversion failed. @@ -171,8 +179,23 @@ pub enum ClientError { /// Transaction submission timeout. #[error("Transaction submission timeout")] Timeout, + /// Chain identity mismatch between stored genesis and connected node. + #[error("Genesis hash mismatch")] + ChainMismatch, + /// Stored sync boundary does not match the connected node. + #[error("Sync boundary mismatch")] + SyncBoundaryMismatch, +} + +impl ClientError { + /// Errors that indicate a mismatch between the stored sync state and the connected node. + pub(crate) fn is_chain_validation_error(&self) -> bool { + matches!(self, Self::ChainMismatch | Self::SyncBoundaryMismatch) + } } + const LOG_TARGET: &str = "eth-rpc::client"; +const LOG_TARGET_SUBSCRIPTION: &str = "eth-rpc::subscription"; const REVERT_CODE: i32 = 3; @@ -205,7 +228,7 @@ impl From for ErrorObjectOwned { } } -/// A client connect to a node and maintains a cache of the last `CACHE_SIZE` blocks. +/// A client that connects to a substrate node and provides Ethereum-compatible RPC functionality. #[derive(Clone)] pub struct Client { api: OnlineClient, @@ -222,6 +245,10 @@ pub struct Client { block_notifier: Option>, /// A lock to ensure only one subscription can perform write operations at a time. subscription_lock: Arc>, + /// Whether archive mode is enabled + is_archive: bool, + /// Whether historic backfill has completed. `false` if not started or in progress. + backfill_complete: Arc, } /// Fetch the chain ID from the substrate chain. @@ -280,6 +307,7 @@ impl Client { rpc: LegacyRpcMethods, block_provider: SubxtBlockInfoProvider, receipt_provider: ReceiptProvider, + is_archive: bool, ) -> Result { let (chain_id, max_block_weight, automine) = tokio::try_join!(chain_id(&api), max_block_weight(&api), async { @@ -299,11 +327,23 @@ impl Client { block_notifier: automine .then(|| tokio::sync::broadcast::channel::(NOTIFIER_CAPACITY).0), subscription_lock: Arc::new(Mutex::new(())), + is_archive, + backfill_complete: Arc::new(AtomicBool::new(false)), }; Ok(client) } + /// Mark historic backfill as complete. + pub(crate) fn mark_backfill_complete(&self) { + self.backfill_complete.store(true, Ordering::Release); + } + + /// Whether archive sync is active and backfill has completed. + fn should_advance_head(&self) -> bool { + self.is_archive && self.backfill_complete.load(Ordering::Acquire) + } + /// Creates a block notifier instance. pub fn create_block_notifier(&mut self) { self.block_notifier = Some(tokio::sync::broadcast::channel::(NOTIFIER_CAPACITY).0); @@ -314,41 +354,16 @@ impl Client { self.block_notifier = notifier; } - /// Subscribe to past blocks executing the callback for each block in `range`. - async fn subscribe_past_blocks( - &self, - range: Range, - callback: F, - ) -> Result<(), ClientError> - where - F: Fn(Arc) -> Fut + Send + Sync, - Fut: std::future::Future> + Send, - { - let mut block = self - .block_provider - .block_by_number(range.end) - .await? - .ok_or(ClientError::BlockNotFound)?; + pub(crate) fn api(&self) -> &OnlineClient { + &self.api + } - loop { - let block_number = block.number(); - log::trace!(target: "eth-rpc::subscription", "Processing past block #{block_number}"); - - let parent_hash = block.header().parent_hash; - callback(block.clone()).await.inspect_err(|err| { - log::error!(target: "eth-rpc::subscription", "Failed to process past block #{block_number}: {err:?}"); - })?; - - if range.start < block_number { - block = self - .block_provider - .block_by_hash(&parent_hash) - .await? - .ok_or(ClientError::BlockNotFound)?; - } else { - return Ok(()); - } - } + pub(crate) fn receipt_provider(&self) -> &ReceiptProvider { + &self.receipt_provider + } + + pub(crate) fn block_provider(&self) -> &SubxtBlockInfoProvider { + &self.block_provider } /// Subscribe to new blocks, and execute the async closure for each block. @@ -390,11 +405,11 @@ impl Client { let _guard = self.subscription_lock.lock().await; let block_number = block.number(); - log::trace!(target: "eth-rpc::subscription", "⏳ Processing {subscription_type:?} block: {block_number}"); + log::trace!(target: LOG_TARGET_SUBSCRIPTION, "⏳ Processing {subscription_type:?} block: {block_number}"); if let Err(err) = callback(block).await { log::error!(target: LOG_TARGET, "Failed to process block {block_number}: {err:?}"); } else { - log::trace!(target: "eth-rpc::subscription", "✅ Processed {subscription_type:?} block: {block_number}"); + log::trace!(target: LOG_TARGET_SUBSCRIPTION, "✅ Processed {subscription_type:?} block: {block_number}"); } } @@ -410,7 +425,9 @@ impl Client { log::info!(target: LOG_TARGET, "🔌 Subscribing to new blocks ({subscription_type:?})"); self.subscribe_new_blocks(subscription_type, |block| async { let hash = block.hash(); + let block_number = block.number(); let evm_block = self.runtime_api(hash).eth_block().await?; + let (_, receipts): (Vec<_>, Vec<_>) = self .receipt_provider .insert_block_receipts(&block, &evm_block.hash) @@ -421,8 +438,23 @@ impl Client { self.block_provider.update_latest(Arc::new(block), subscription_type).await; self.fee_history_provider.update_fee_history(&evm_block, &receipts).await; - // Only broadcast for best blocks to avoid duplicate notifications. match (subscription_type, &self.block_notifier) { + (SubscriptionType::FinalizedBlocks, _) if self.should_advance_head() => { + // Track finalized block in sync_state + if let Err(err) = self + .receipt_provider + .advance_sync_label( + SyncLabel::Head, + SyncCheckpoint::new(block_number, hash), + ) + .await + { + log::warn!(target: LOG_TARGET, + "Failed to update sync_label[{}]: {err:?}", + SyncLabel::Head); + } + }, + // Only broadcast for best blocks to avoid duplicate notifications. (SubscriptionType::BestBlocks, Some(sender)) if sender.receiver_count() > 0 => { let _ = sender.send(hash); }, @@ -433,30 +465,6 @@ impl Client { .await } - /// Cache old blocks up to the given block number. - pub async fn subscribe_and_cache_blocks( - &self, - index_last_n_blocks: SubstrateBlockNumber, - ) -> Result<(), ClientError> { - let last = self.latest_block().await.number().saturating_sub(1); - let range = last.saturating_sub(index_last_n_blocks)..last; - log::info!(target: LOG_TARGET, "🗄️ Indexing past blocks in range {range:?}"); - - self.subscribe_past_blocks(range, |block| async move { - let ethereum_hash = self - .runtime_api(block.hash()) - .eth_block_hash(pallet_revive::evm::U256::from(block.number())) - .await? - .ok_or(ClientError::EthereumBlockNotFound)?; - self.receipt_provider.insert_block_receipts(&block, ðereum_hash).await?; - Ok(()) - }) - .await?; - - log::info!(target: LOG_TARGET, "🗄️ Finished indexing past blocks"); - Ok(()) - } - /// Get the block hash for the given block number or tag. pub async fn block_hash_for_tag( &self, @@ -821,6 +829,15 @@ impl Client { ) -> Option { log::trace!(target: LOG_TARGET, "Get Ethereum block for hash {:?}", block.hash()); + if self + .receipt_provider + .is_before_earliest_block(&BlockNumberOrTag::U256(U256::from(block.number()))) + { + log::trace!(target: LOG_TARGET, + "Block #{} is before receipt floor, skipping", block.number()); + return None; + } + // This could potentially fail under below circumstances: // - state has been pruned // - the block author cannot be obtained from the digest logs (highly unlikely) diff --git a/substrate/frame/revive/rpc/src/lib.rs b/substrate/frame/revive/rpc/src/lib.rs index 30595a372d5e3..3b755b88790cb 100644 --- a/substrate/frame/revive/rpc/src/lib.rs +++ b/substrate/frame/revive/rpc/src/lib.rs @@ -27,6 +27,8 @@ use sp_core::{H160, H256, U256, keccak_256}; use subxt::backend::legacy::rpc_methods::TransactionStatus; use thiserror::Error; +mod block_sync; +pub(crate) use block_sync::{ChainMetadata, SyncLabel, SyncStateKey}; pub mod cli; pub mod client; pub mod example; diff --git a/substrate/frame/revive/rpc/src/main.rs b/substrate/frame/revive/rpc/src/main.rs index 3376b9b10be26..ab2a5d445c19d 100644 --- a/substrate/frame/revive/rpc/src/main.rs +++ b/substrate/frame/revive/rpc/src/main.rs @@ -15,10 +15,9 @@ // See the License for the specific language governing permissions and // limitations under the License. //! The Ethereum JSON-RPC server. -use clap::Parser; use pallet_revive_eth_rpc::cli; fn main() -> anyhow::Result<()> { - let cmd = cli::CliCommand::parse(); + let cmd = cli::CliCommand::parse_cli()?; cli::run(cmd) } diff --git a/substrate/frame/revive/rpc/src/receipt_extractor.rs b/substrate/frame/revive/rpc/src/receipt_extractor.rs index a404231849e6f..f5720887f55a7 100644 --- a/substrate/frame/revive/rpc/src/receipt_extractor.rs +++ b/substrate/frame/revive/rpc/src/receipt_extractor.rs @@ -32,7 +32,14 @@ use pallet_revive::{ evm::{GenericTransaction, H256, Log, ReceiptGasInfo, ReceiptInfo, TransactionSigned, U256}, }; use sp_core::keccak_256; -use std::{future::Future, pin::Pin, sync::Arc}; +use std::{ + future::Future, + pin::Pin, + sync::{ + Arc, + atomic::{AtomicU32, Ordering}, + }, +}; use subxt::{OnlineClient, blocks::ExtrinsicDetails}; type FetchReceiptDataFn = Arc< @@ -53,40 +60,31 @@ pub struct ReceiptExtractor { /// Fetch ethereum block hash. fetch_eth_block_hash: FetchEthBlockHashFn, - /// Earliest block number to consider when searching for transaction receipts. - earliest_receipt_block: Option, + /// Auto-discovered first EVM block on the chain. + /// Set once during backward sync when the first non-EVM block is encountered. + /// Uses `u32::MAX` as sentinel for "not yet discovered". + first_evm_block: Arc, /// Recover the ethereum address from a transaction signature. recover_eth_address: RecoverEthAddressFn, } impl ReceiptExtractor { - /// Check if the block is before the earliest block. - pub fn is_before_earliest_block(&self, block_number: SubstrateBlockNumber) -> bool { - block_number < self.earliest_receipt_block.unwrap_or_default() - } - - /// Create a new `ReceiptExtractor` with the given native to eth ratio. - pub async fn new( - api: OnlineClient, - earliest_receipt_block: Option, - ) -> Result { + /// Create a new `ReceiptExtractor`. + pub async fn new(api: OnlineClient) -> Result { Self::new_with_custom_address_recovery( api, - earliest_receipt_block, Arc::new(|signed_tx: &TransactionSigned| signed_tx.recover_eth_address()), ) .await } - /// Create a new `ReceiptExtractor` with the given native to eth ratio. + /// Create a new `ReceiptExtractor` with custom Ethereum address recovery logic. /// - /// Specify also a custom Ethereum address recovery logic. /// Use `ReceiptExtractor::new` if the default Ethereum address recovery /// logic ([`TransactionSigned::recover_eth_address`] based) is enough. pub async fn new_with_custom_address_recovery( api: OnlineClient, - earliest_receipt_block: Option, recover_eth_address_fn: RecoverEthAddressFn, ) -> Result { let api_inner = api.clone(); @@ -116,7 +114,7 @@ impl ReceiptExtractor { Ok(Self { fetch_receipt_data, fetch_eth_block_hash, - earliest_receipt_block, + first_evm_block: Arc::new(AtomicU32::new(u32::MAX)), recover_eth_address: recover_eth_address_fn, }) } @@ -135,13 +133,35 @@ impl ReceiptExtractor { Self { fetch_receipt_data, fetch_eth_block_hash, - earliest_receipt_block: None, + first_evm_block: Arc::new(AtomicU32::new(u32::MAX)), recover_eth_address: Arc::new(|signed_tx: &TransactionSigned| { signed_tx.recover_eth_address() }), } } + /// Check if the block is before the `first_evm_block` floor. + /// When sentinel (`u32::MAX`), no blocks are rejected (permissive default). + pub fn is_before_first_evm_block(&self, block_number: SubstrateBlockNumber) -> bool { + let val = self.first_evm_block.load(Ordering::Acquire); + val != u32::MAX && block_number < val + } + + /// Set the first EVM block. Only stores if lower than the current value. + pub fn set_first_evm_block(&self, block_number: SubstrateBlockNumber) { + let prev = self.first_evm_block.fetch_min(block_number, Ordering::AcqRel); + if block_number > prev { + log::debug!(target: LOG_TARGET, + "Ignored attempt to raise first_evm_block to #{block_number}, current is #{prev}"); + } + } + + /// The auto-discovered first EVM block, or `None` if not yet discovered. + pub fn first_evm_block(&self) -> Option { + let val = self.first_evm_block.load(Ordering::Acquire); + (val != u32::MAX).then_some(val) + } + /// Extract a [`TransactionSigned`] and a [`ReceiptInfo`] from an extrinsic. async fn extract_from_extrinsic( &self, @@ -233,7 +253,7 @@ impl ReceiptExtractor { &self, block: &SubstrateBlock, ) -> Result, ClientError> { - if self.is_before_earliest_block(block.number()) { + if self.is_before_first_evm_block(block.number()) { return Ok(vec![]); } @@ -353,3 +373,26 @@ impl ReceiptExtractor { (self.fetch_eth_block_hash)(*block_hash, block_number).await } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn defaults_and_first_evm_block_only_decreases() { + let extractor = ReceiptExtractor::new_mock(); + + assert!(extractor.first_evm_block().is_none()); + + // first_evm_block only decreases + extractor.set_first_evm_block(100); + assert_eq!(extractor.first_evm_block(), Some(100)); + + extractor.set_first_evm_block(50); + assert_eq!(extractor.first_evm_block(), Some(50)); + + // Higher value is ignored + extractor.set_first_evm_block(100); + assert_eq!(extractor.first_evm_block(), Some(50)); + } +} diff --git a/substrate/frame/revive/rpc/src/receipt_provider.rs b/substrate/frame/revive/rpc/src/receipt_provider.rs index cb8270b866ec7..00660715de4f4 100644 --- a/substrate/frame/revive/rpc/src/receipt_provider.rs +++ b/substrate/frame/revive/rpc/src/receipt_provider.rs @@ -15,8 +15,10 @@ // See the License for the specific language governing permissions and // limitations under the License. use crate::{ - Address, AddressOrAddresses, BlockInfoProvider, BlockNumberOrTag, BlockTag, Bytes, ClientError, - FilterTopic, ReceiptExtractor, SubxtBlockInfoProvider, + Address, AddressOrAddresses, BlockInfoProvider, BlockNumberOrTag, BlockTag, Bytes, + ChainMetadata, ClientError, FilterTopic, ReceiptExtractor, SubxtBlockInfoProvider, SyncLabel, + SyncStateKey, + block_sync::SyncCheckpoint, client::{SubstrateBlock, SubstrateBlockNumber}, }; use pallet_revive::evm::{Filter, Log, ReceiptInfo, TransactionSigned}; @@ -59,7 +61,7 @@ impl BlockHashMap { } /// Provides information about a block, -/// This is an abstratction on top of [`SubstrateBlock`] that can't be mocked in tests. +/// This is an abstraction on top of [`SubstrateBlock`] that can't be mocked in tests. /// Can be removed once is fixed. pub trait BlockInfo { /// Returns the block hash. @@ -78,7 +80,31 @@ impl BlockInfo for SubstrateBlock { } /// Maximum number of entries kept in the block to hash map. -const MAX_CACHED_BLOCKS: usize = 256; +pub const MAX_CACHED_BLOCKS: usize = 256; + +/// Upsert a sync label row, updating only when the existing `block_number` +/// compares with `$op` against the new value. `$op` must be `"<"` or `">"`. +macro_rules! upsert_sync_label { + ($pool:expr, $op:literal, $label:expr, $checkpoint:expr) => {{ + let label_str = $label.to_string(); + let block_number = $checkpoint.block_number as i64; + let block_hash = $checkpoint.block_hash.map(|h| h.as_bytes().to_vec()); + query!( + "INSERT INTO sync_state (label, block_number, block_hash) + VALUES ($1, $2, $3) + ON CONFLICT(label) DO UPDATE + SET block_number = excluded.block_number, block_hash = excluded.block_hash + WHERE sync_state.block_number " + + $op + " excluded.block_number + ", + label_str, + block_number, + block_hash + ) + .execute($pool) + .await?; + }}; +} impl ReceiptProvider { /// Create a new `ReceiptProvider` with the given database URL and block provider. @@ -87,18 +113,89 @@ impl ReceiptProvider { block_provider: B, receipt_extractor: ReceiptExtractor, keep_latest_n_blocks: Option, - ) -> Result { - sqlx::migrate!().run(&pool).await?; - Ok(Self { + ) -> Result { + sqlx::migrate!().run(&pool).await.map_err(|e| sqlx::Error::Migrate(e.into()))?; + + let provider = Self { pool, block_provider, receipt_extractor, keep_latest_n_blocks, block_number_to_hashes: Default::default(), - }) + }; + provider.restore_first_evm_block().await?; + + Ok(provider) + } + + /// Returns `true` if the block is before the auto-discovered `first_evm_block`. + pub fn is_before_earliest_block(&self, at: &BlockNumberOrTag) -> bool { + match at { + BlockNumberOrTag::U256(block_number) => { + if *block_number > U256::from(u32::MAX) { + return false; + } + self.receipt_extractor.is_before_first_evm_block(block_number.as_u32()) + }, + BlockNumberOrTag::BlockTag(_) => false, + } + } + + /// The auto-discovered first EVM block, or `None` if not yet discovered. + pub fn first_evm_block(&self) -> Option { + self.receipt_extractor.first_evm_block() + } + + /// Set the auto-discovered first EVM block (in-memory + persisted to DB). + pub async fn set_first_evm_block( + &self, + block_number: SubstrateBlockNumber, + ) -> Result<(), ClientError> { + self.receipt_extractor.set_first_evm_block(block_number); + self.set_sync_label(ChainMetadata::FirstEvmBlock, SyncCheckpoint::from_number(block_number)) + .await } - // Get block hash and transaction index by transaction hash + /// Restore `first_evm_block` from DB, clearing it if the boundary has shifted. + async fn restore_first_evm_block(&self) -> Result<(), ClientError> { + let Some(evm_first) = + self.get_sync_label(ChainMetadata::FirstEvmBlock).await?.map(|c| c.block_number) + else { + return Ok(()); + }; + + let has_evm_hash = |block_number: SubstrateBlockNumber| async move { + match self.block_provider.block_by_number(block_number).await.ok().flatten() { + Some(block) => self + .receipt_extractor + .get_ethereum_block_hash(&block.hash(), block_number as u64) + .await + .is_some(), + None => false, + } + }; + + // Stale if evm_first no longer has an EVM hash, or its predecessor now does. + let current_has_evm = has_evm_hash(evm_first).await; + let predecessor_has_evm = + if evm_first > 0 { has_evm_hash(evm_first - 1).await } else { false }; + + if !current_has_evm || predecessor_has_evm { + log::warn!(target: LOG_TARGET, + "🗄️ Stored first-evm-block=#{evm_first} is stale \ + (has_evm={current_has_evm}, predecessor_has_evm={predecessor_has_evm}), \ + clearing."); + if let Err(e) = self.delete_sync_label(ChainMetadata::FirstEvmBlock).await { + log::error!(target: LOG_TARGET, + "🗄️ Failed to clear stale first-evm-block from DB: {e:?}"); + } + } else { + self.receipt_extractor.set_first_evm_block(evm_first); + } + Ok(()) + } + + // Get block hash and transaction index by transaction hash pub async fn find_transaction(&self, transaction_hash: &H256) -> Option<(H256, usize)> { let transaction_hash = transaction_hash.as_ref(); let result = query!( @@ -214,7 +311,7 @@ impl ReceiptProvider { for block_map in block_mappings { delete_tx_query = delete_tx_query.bind(block_map.substrate_hash.as_ref()); delete_mappings_query = delete_mappings_query.bind(block_map.substrate_hash.as_ref()); - // logs table uses ethereum block hash + // logs table uses ethereum block hash delete_logs_query = delete_logs_query.bind(block_map.ethereum_hash.as_ref()); } @@ -225,16 +322,104 @@ impl ReceiptProvider { Ok(()) } - /// Check if the block is before the earliest block. - pub fn is_before_earliest_block(&self, at: &BlockNumberOrTag) -> bool { - match at { - BlockNumberOrTag::U256(block_number) => { - self.receipt_extractor.is_before_earliest_block(block_number.as_u32()) + /// Read a sync label entry. + pub async fn get_sync_label( + &self, + label: impl SyncStateKey, + ) -> Result, ClientError> { + let label_str = label.to_string(); + let row = query!( + r#" + SELECT block_number, block_hash + FROM sync_state + WHERE label = $1 + "#, + label_str + ) + .fetch_optional(&self.pool) + .await?; + + match row { + Some(row) => { + let block_number: SubstrateBlockNumber = + row.block_number.try_into().map_err(|_| { + sqlx::Error::Decode( + format!("block_number {} overflows u32", row.block_number).into(), + ) + })?; + Ok(Some(SyncCheckpoint { + block_number, + block_hash: row + .block_hash + .filter(|b| b.len() == 32) + .map(|b| H256::from_slice(&b)), + })) }, - BlockNumberOrTag::BlockTag(_) => false, + None => Ok(None), } } + /// Upsert a sync label entry. + pub async fn set_sync_label( + &self, + label: impl SyncStateKey, + checkpoint: SyncCheckpoint, + ) -> Result<(), ClientError> { + let label_str = label.to_string(); + let block_number = checkpoint.block_number as i64; + let block_hash = checkpoint.block_hash.map(|h| h.as_bytes().to_vec()); + query!( + r#" + INSERT OR REPLACE INTO sync_state (label, block_number, block_hash) + VALUES ($1, $2, $3) + "#, + label_str, + block_number, + block_hash, + ) + .execute(&self.pool) + .await?; + Ok(()) + } + + /// Delete a sync label entry. + pub async fn delete_sync_label(&self, label: impl SyncStateKey) -> Result<(), ClientError> { + let label_str = label.to_string(); + query!( + r#" + DELETE FROM sync_state WHERE label = $1 + "#, + label_str, + ) + .execute(&self.pool) + .await?; + Ok(()) + } + + /// Atomically update a sync label entry only if the new block number is strictly higher. + /// + /// Inserts the row if it doesn't exist yet. + pub async fn advance_sync_label( + &self, + label: SyncLabel, + checkpoint: SyncCheckpoint, + ) -> Result<(), ClientError> { + upsert_sync_label!(&self.pool, "<", label, checkpoint); + Ok(()) + } + + /// Atomically update a sync label entry only if the new block number is lower. + /// + /// Inserts the row if it doesn't exist yet. + pub async fn recede_sync_label( + &self, + label: SyncLabel, + checkpoint: SyncCheckpoint, + ) -> Result<(), ClientError> { + upsert_sync_label!(&self.pool, ">", label, checkpoint); + Ok(()) + } + /// Fetch receipts from the given block. pub async fn receipts_from_block( &self, @@ -243,7 +428,19 @@ impl ReceiptProvider { self.receipt_extractor.extract_from_block(block).await } - /// Extract and insert receipts from the given block. + /// Like [`Self::insert_block_receipts`] but writes only to the DB (no cache update). + /// Used for historic sync where fork detection is unnecessary. + pub async fn insert_block_receipts_past( + &self, + block: &SubstrateBlock, + ethereum_hash: &H256, + ) -> Result<(), ClientError> { + let receipts = self.receipts_from_block(block).await?; + self.insert_into_db(block, &receipts, ethereum_hash).await?; + Ok(()) + } + + /// Extract receipts from the given block, insert them, and update the block cache. pub async fn insert_block_receipts( &self, block: &SubstrateBlock, @@ -254,7 +451,23 @@ impl ReceiptProvider { Ok(receipts) } - /// Prune blocks older blocks. + /// Insert receipts into the provider, updating the in-memory block cache for fork detection. + /// + /// Note: Can be merged into `insert_block_receipts` once is fixed and subxt let + /// us create Mock `SubstrateBlock` + async fn insert( + &self, + block: &impl BlockInfo, + receipts: &[(TransactionSigned, ReceiptInfo)], + ethereum_hash: &H256, + ) -> Result<(), ClientError> { + let block_map = BlockHashMap::new(block.hash(), *ethereum_hash); + self.prune_blocks(block.number(), &block_map).await?; + self.insert_into_db(block, receipts, ethereum_hash).await?; + Ok(()) + } + + /// Handle fork detection (always) and DB pruning (temporary mode only). async fn prune_blocks( &self, block_number: SubstrateBlockNumber, @@ -307,11 +520,8 @@ impl ReceiptProvider { Ok(()) } - /// Insert receipts into the provider. - /// - /// Note: Can be merged into `insert_block_receipts` once is fixed and subxt let - /// us create Mock `SubstrateBlock` - async fn insert( + /// Insert receipts into the database without updating the in-memory block cache. + async fn insert_into_db( &self, block: &impl BlockInfo, receipts: &[(TransactionSigned, ReceiptInfo)], @@ -320,13 +530,9 @@ impl ReceiptProvider { let substrate_block_hash = block.hash(); let substrate_hash_ref = substrate_block_hash.as_ref(); let block_number = block.number() as i64; - let ethereum_hash_ref = ethereum_hash.as_ref(); - let block_map = BlockHashMap::new(substrate_block_hash, *ethereum_hash); log::trace!(target: LOG_TARGET, "Insert receipts for substrate block #{block_number} {:?}", substrate_block_hash); - self.prune_blocks(block.number(), &block_map).await?; - // Check if mapping already exists (eg. added when processing best block and we are now // processing finalized block) let result = sqlx::query!( @@ -338,6 +544,7 @@ impl ReceiptProvider { // Assuming that if no mapping exists then no relevant entries in transaction_hashes and // logs exist if !result.exists { + let ethereum_hash_ref = ethereum_hash.as_ref(); for (_, receipt) in receipts { let transaction_hash: &[u8] = receipt.transaction_hash.as_ref(); let transaction_index = receipt.transaction_index.as_u32() as i32; @@ -394,6 +601,7 @@ impl ReceiptProvider { } } // Insert block mapping from Ethereum to Substrate hash + let block_map = BlockHashMap::new(substrate_block_hash, *ethereum_hash); self.insert_block_mapping(&block_map).await?; } @@ -644,16 +852,37 @@ mod tests { count as _ } - async fn setup_sqlite_provider(pool: SqlitePool) -> ReceiptProvider { + fn mock_provider() -> ReceiptProvider { ReceiptProvider { - pool, + pool: SqlitePool::connect_lazy("sqlite::memory:").unwrap(), block_provider: MockBlockInfoProvider {}, receipt_extractor: ReceiptExtractor::new_mock(), - keep_latest_n_blocks: Some(10), + keep_latest_n_blocks: None, block_number_to_hashes: Default::default(), } } + impl ReceiptProvider { + fn with_pool(mut self, pool: SqlitePool) -> Self { + self.pool = pool; + self + } + + fn with_extractor(mut self, extractor: ReceiptExtractor) -> Self { + self.receipt_extractor = extractor; + self + } + + fn with_keep_latest(mut self, n: Option) -> Self { + self.keep_latest_n_blocks = n; + self + } + } + + async fn setup_sqlite_provider(pool: SqlitePool) -> ReceiptProvider { + mock_provider().with_pool(pool).with_keep_latest(Some(10)) + } + #[sqlx::test] async fn test_insert_remove(pool: SqlitePool) -> anyhow::Result<()> { let provider = setup_sqlite_provider(pool).await; @@ -1147,16 +1376,124 @@ mod tests { Ok(()) } + #[sqlx::test] + async fn restore_first_evm_block_clears_stale(pool: SqlitePool) -> anyhow::Result<()> { + let provider = setup_sqlite_provider(pool).await; + + // Persist first_evm_block = 42. + provider + .set_sync_label(ChainMetadata::FirstEvmBlock, SyncCheckpoint::from_number(42)) + .await?; + + // MockBlockInfoProvider returns no blocks, so has_evm_hash is always false. + // This means evm_first=42 is stale (no longer has an EVM hash). + provider.restore_first_evm_block().await?; + + // The value should have been cleared (not restored to the extractor). + assert_eq!(provider.first_evm_block(), None); + + // DB row should have been deleted. + let checkpoint = provider.get_sync_label(ChainMetadata::FirstEvmBlock).await?; + assert!(checkpoint.is_none()); + Ok(()) + } + + #[sqlx::test] + async fn advance_sync_label_only_increases(pool: SqlitePool) -> anyhow::Result<()> { + let provider = setup_sqlite_provider(pool).await; + let hash_a = H256::repeat_byte(0xAA); + let hash_b = H256::repeat_byte(0xBB); + + // First insert creates the row. + provider + .advance_sync_label(SyncLabel::Head, SyncCheckpoint::new(100, hash_a)) + .await?; + let checkpoint = provider.get_sync_label(SyncLabel::Head).await?.unwrap(); + assert_eq!((checkpoint.block_number, checkpoint.block_hash), (100, Some(hash_a))); + + // Higher value advances. + provider + .advance_sync_label(SyncLabel::Head, SyncCheckpoint::new(200, hash_b)) + .await?; + let checkpoint = provider.get_sync_label(SyncLabel::Head).await?.unwrap(); + assert_eq!((checkpoint.block_number, checkpoint.block_hash), (200, Some(hash_b))); + + // Lower and equal values are ignored (strict >). + provider + .advance_sync_label(SyncLabel::Head, SyncCheckpoint::new(50, hash_a)) + .await?; + provider + .advance_sync_label(SyncLabel::Head, SyncCheckpoint::new(200, hash_a)) + .await?; + let checkpoint = provider.get_sync_label(SyncLabel::Head).await?.unwrap(); + assert_eq!((checkpoint.block_number, checkpoint.block_hash), (200, Some(hash_b))); + + Ok(()) + } + + #[sqlx::test] + async fn recede_sync_label_only_decreases(pool: SqlitePool) -> anyhow::Result<()> { + let provider = setup_sqlite_provider(pool).await; + let hash_a = H256::repeat_byte(0xAA); + let hash_b = H256::repeat_byte(0xBB); + + // First insert creates the row. + provider + .recede_sync_label(SyncLabel::Tail, SyncCheckpoint::new(100, hash_a)) + .await?; + let checkpoint = provider.get_sync_label(SyncLabel::Tail).await?.unwrap(); + assert_eq!((checkpoint.block_number, checkpoint.block_hash), (100, Some(hash_a))); + + // Lower value recedes. + provider + .recede_sync_label(SyncLabel::Tail, SyncCheckpoint::new(50, hash_b)) + .await?; + let checkpoint = provider.get_sync_label(SyncLabel::Tail).await?.unwrap(); + assert_eq!((checkpoint.block_number, checkpoint.block_hash), (50, Some(hash_b))); + + // Higher and equal values are ignored (strict <). + provider + .recede_sync_label(SyncLabel::Tail, SyncCheckpoint::new(200, hash_a)) + .await?; + provider + .recede_sync_label(SyncLabel::Tail, SyncCheckpoint::new(50, hash_a)) + .await?; + let checkpoint = provider.get_sync_label(SyncLabel::Tail).await?.unwrap(); + assert_eq!((checkpoint.block_number, checkpoint.block_hash), (50, Some(hash_b))); + + Ok(()) + } + + #[tokio::test] + async fn is_before_earliest_block_edge_cases() { + // U256 > u32::MAX should never be considered "before floor" + let extractor = ReceiptExtractor::new_mock(); + extractor.set_first_evm_block(10); + let provider = mock_provider().with_extractor(extractor); + + let huge = BlockNumberOrTag::U256(U256::from(u64::MAX)); + assert!(!provider.is_before_earliest_block(&huge)); + + let just_over = BlockNumberOrTag::U256(U256::from(u32::MAX as u64 + 1)); + assert!(!provider.is_before_earliest_block(&just_over)); + + // Sentinel first_evm_block (u32::MAX) is permissive — no queries rejected. + let provider = mock_provider(); + assert!(!provider.is_before_earliest_block(&BlockNumberOrTag::U256(U256::from(0u32)))); + assert!( + !provider.is_before_earliest_block(&BlockNumberOrTag::U256(U256::from(1_000_000u32))) + ); + + // Tag-based queries are never rejected. + assert!(!provider.is_before_earliest_block(&BlockNumberOrTag::BlockTag( + pallet_revive::evm::BlockTag::Latest + ))); + } + #[sqlx::test] async fn persistent_mode_caps_in_memory_map(pool: SqlitePool) -> anyhow::Result<()> { // Persistent DB mode: keep_latest_n_blocks = None - let provider = ReceiptProvider { - pool, - block_provider: MockBlockInfoProvider {}, - receipt_extractor: ReceiptExtractor::new_mock(), - keep_latest_n_blocks: None, - block_number_to_hashes: Default::default(), - }; + let provider = mock_provider().with_pool(pool); // Insert more than MAX_CACHED_BLOCKS blocks. let start_block: u64 = 1; diff --git a/substrate/frame/revive/rpc/src/tests.rs b/substrate/frame/revive/rpc/src/tests.rs index faad8875de3f5..594f0480797c2 100644 --- a/substrate/frame/revive/rpc/src/tests.rs +++ b/substrate/frame/revive/rpc/src/tests.rs @@ -19,8 +19,10 @@ //! [evm-test-suite](https://github.com/paritytech/evm-test-suite) repository. use crate::{ - EthRpcClient, + BlockInfoProvider, ChainMetadata, EthRpcClient, ReceiptExtractor, ReceiptProvider, + SubxtBlockInfoProvider, SyncLabel, cli::{self, CliCommand}, + client::{Client, connect}, example::TransactionBuilder, subxt_client::{ self, SrcChainConfig, src_chain::runtime_types::pallet_revive::primitives::Code, @@ -36,6 +38,7 @@ use pallet_revive::{ HashesOrTransactionInfos, TransactionInfo, TransactionUnsigned, U256, }, }; +use sqlx::sqlite::{SqliteConnectOptions, SqlitePoolOptions}; use std::{sync::Arc, thread}; use subxt::{ OnlineClient, @@ -87,6 +90,7 @@ impl SharedResources { "--node-rpc-url=ws://localhost:45789", "--no-prometheus", "-linfo,eth-rpc=debug", + "--eth-pruning=256", ]); let _rpc_handle = thread::spawn(move || { @@ -317,6 +321,10 @@ async fn run_all_eth_rpc_tests_inner() -> anyhow::Result<()> { test_multiple_transactions_in_block, test_mixed_evm_substrate_transactions, test_runtime_pallets_address_upload_code, + test_block_sync_fresh, + test_block_sync_resume_interrupted, + test_block_sync_detects_corruption, + test_block_sync_picks_up_new_blocks, ); log::debug!(target: LOG_TARGET, "All tests completed successfully!"); @@ -954,3 +962,317 @@ async fn test_fibonacci_call_via_runtime_api() -> anyhow::Result<()> { Ok(()) } + +/// Submit `count` EVM transfer transactions and wait for inclusion. +async fn submit_evm_transfers(count: usize) -> anyhow::Result<()> { + let ws_client = Arc::new(SharedResources::client().await); + let ethan = Account::from(subxt_signer::eth::dev::ethan()); + let transactions = prepare_evm_transactions( + ws_client.clone(), + Account::default(), + ethan.address(), + U256::from(1_000_000_000_000u128), + count, + ) + .await?; + let submitted = submit_evm_transactions(transactions).await?; + submitted[0].2.wait_for_receipt().await?; + Ok(()) +} + +/// Create a [`Client`] for block-sync testing. +/// +/// Connects to the same dev-node that [`SharedResources`] started, but uses its own +/// in-memory SQLite database so that sync labels written by the test do not interfere +/// with the eth-rpc server's internal database (and vice versa). +async fn create_sync_test_client() -> anyhow::Result { + use sc_cli::{RPC_DEFAULT_MAX_REQUEST_SIZE_MB, RPC_DEFAULT_MAX_RESPONSE_SIZE_MB}; + + let node_url = SharedResources::node_rpc_url(); + let max_request_size = RPC_DEFAULT_MAX_REQUEST_SIZE_MB * 1024 * 1024; + let max_response_size = RPC_DEFAULT_MAX_RESPONSE_SIZE_MB * 1024 * 1024; + let (api, rpc_client, rpc) = connect(node_url, max_request_size, max_response_size).await?; + let block_provider = SubxtBlockInfoProvider::new(api.clone(), rpc.clone()).await?; + + let pool = SqlitePoolOptions::new() + .max_connections(1) + .idle_timeout(None) + .max_lifetime(None) + .connect_with(SqliteConnectOptions::new().in_memory(true)) + .await?; + + let receipt_extractor = ReceiptExtractor::new(api.clone()).await?; + let receipt_provider = + ReceiptProvider::new(pool, block_provider.clone(), receipt_extractor, None).await?; + + let client = Client::new(api, rpc_client, rpc, block_provider, receipt_provider, true).await?; + Ok(client) +} + +/// Fresh sync: labels, hash mappings, and re-sync idempotency. +async fn test_block_sync_fresh() -> anyhow::Result<()> { + use crate::block_sync::SyncCheckpoint; + + // Submit a transaction so the chain has at least one block with EVM data to sync. + submit_evm_transfers(1).await?; + + let client = create_sync_test_client().await?; + + // Fresh DB — sync_state table should be empty. + for label in [SyncLabel::Tail, SyncLabel::Head] { + assert!( + client.receipt_provider().get_sync_label(label).await?.is_none(), + "sync_state[{label}] should be absent on fresh DB" + ); + } + for label in [ChainMetadata::Genesis, ChainMetadata::FirstEvmBlock] { + assert!( + client.receipt_provider().get_sync_label(label).await?.is_none(), + "sync_state[{label}] should be absent on fresh DB" + ); + } + + // Capture finalized before sync — Head will be set to this snapshot. + let finalized_before_sync = client.latest_finalized_block().await.number(); + + // Run the full backward sync. + client.sync_backward().await?; + + // Genesis label must match the chain. + let genesis = client + .receipt_provider() + .get_sync_label(ChainMetadata::Genesis) + .await? + .expect("Genesis label should be set after sync"); + assert_eq!( + genesis, + SyncCheckpoint::new(0, client.api().genesis_hash()), + "Stored genesis should match chain genesis" + ); + + // Head should be exactly the finalized block at sync start. + let sync_head = client + .receipt_provider() + .get_sync_label(SyncLabel::Head) + .await? + .expect("Head should be set after sync"); + assert_eq!( + sync_head.block_number, finalized_before_sync, + "Head should equal finalized at sync start" + ); + + // Tail should be genesis (block 0) — on the dev node all blocks have EVM hashes. + let sync_tail = client + .receipt_provider() + .get_sync_label(SyncLabel::Tail) + .await? + .expect("Tail should be set after sync"); + assert_eq!(sync_tail, genesis, "Tail should be genesis"); + + // On the dev node all blocks (including genesis) have EVM hashes + let evm_first = client.receipt_provider().get_sync_label(ChainMetadata::FirstEvmBlock).await?; + assert!(evm_first.is_none(), "FirstEvmBlock should not be set when all blocks are EVM"); + assert_eq!(client.receipt_provider().first_evm_block(), None); + + // Block hash mappings should be queryable after sync. + let finalized = client.latest_finalized_block().await; + let substrate_hash = finalized.hash(); + let ethereum_hash = client.receipt_provider().get_ethereum_hash(&substrate_hash).await; + assert!( + ethereum_hash.is_some(), + "Finalized block #{} should have an ethereum hash mapping after sync", + finalized.number(), + ); + assert_eq!( + client.receipt_provider().get_substrate_hash(ðereum_hash.unwrap()).await, + Some(substrate_hash), + "Reverse mapping should resolve back to the substrate hash" + ); + + // Re-syncing should complete without errors (exercises the resume path). + client.sync_backward().await?; + let sync_head_after = client + .receipt_provider() + .get_sync_label(SyncLabel::Head) + .await? + .expect("Head should exist after re-sync"); + assert!( + sync_head_after.block_number >= sync_head.block_number, + "Head should not regress after re-sync" + ); + + Ok(()) +} + +/// Simulate an interrupted sync by manually setting both Head and Tail +/// to create a top gap and a bottom gap, then verify that `resume_sync` fills both. +async fn test_block_sync_resume_interrupted() -> anyhow::Result<()> { + use crate::block_sync::SyncCheckpoint; + + // Submit transactions so the chain has enough blocks for the 1/3 and 2/3 split. + submit_evm_transfers(6).await?; + + let client = create_sync_test_client().await?; + + // Complete a fresh sync so the DB has all blocks and labels. + client.sync_backward().await?; + + // Pick two blocks to simulate partial coverage: tail at 1/3, head at 2/3. + let chain_len = client.latest_finalized_block().await.number(); + + let tail_num = chain_len / 3; + let tail_block = client + .block_provider() + .block_by_number(tail_num) + .await? + .expect("Tail block should exist"); + + let head_num = chain_len * 2 / 3; + let head_block = client + .block_provider() + .block_by_number(head_num) + .await? + .expect("Head block should exist"); + + // Overwrite both labels to simulate an interrupted sync with a partial range. + let interrupted_tail = SyncCheckpoint::new(tail_block.number(), tail_block.hash()); + let interrupted_head = SyncCheckpoint::new(head_block.number(), head_block.hash()); + + client + .receipt_provider() + .set_sync_label(SyncLabel::Tail, interrupted_tail) + .await?; + client + .receipt_provider() + .set_sync_label(SyncLabel::Head, interrupted_head) + .await?; + + // Capture finalized before resume — Head will be set to this snapshot. + let finalized_before_resume = client.latest_finalized_block().await.number(); + + // Resume sync — fills top gap and bottom gap. + client.sync_backward().await?; + + // After resume, Head should be at the finalized block that was current at resume start. + let new_head = client + .receipt_provider() + .get_sync_label(SyncLabel::Head) + .await? + .expect("Head should exist after resume"); + assert_eq!( + new_head.block_number, finalized_before_resume, + "Head should equal finalized at resume start", + ); + + // Tail should reach genesis (bottom gap fully filled). + let new_tail = client + .receipt_provider() + .get_sync_label(SyncLabel::Tail) + .await? + .expect("Tail should exist after resume"); + assert_eq!( + new_tail.block_number, 0, + "Tail should be 0 after resume fills the bottom gap, got #{}", + new_tail.block_number, + ); + + log::debug!( + target: LOG_TARGET, + "Resume interrupted OK: simulated partial range #{}..#{}, \ + after resume tail=#{}, head=#{}", + interrupted_tail.block_number, + interrupted_head.block_number, + new_tail.block_number, + new_head.block_number, + ); + + Ok(()) +} + +/// Corrupted sync labels should be detected on resume: +/// - Fake Genesis hash → `ChainMismatch` +/// - Fake Head hash → `SyncBoundaryMismatch` +async fn test_block_sync_detects_corruption() -> anyhow::Result<()> { + use crate::{block_sync::SyncCheckpoint, client::ClientError}; + + // Submit transactions so the chain has enough blocks for the boundary test. + submit_evm_transfers(2).await?; + + let client = create_sync_test_client().await?; + + // Complete a fresh sync so all labels are stored. + client.sync_backward().await?; + + // --- ChainMismatch: overwrite Genesis with a fake hash --- + let fake_genesis = SyncCheckpoint::new(0, H256::from([0xdeu8; 32])); + client + .receipt_provider() + .set_sync_label(ChainMetadata::Genesis, fake_genesis) + .await?; + + let err = client.sync_backward().await.unwrap_err(); + assert!(matches!(err, ClientError::ChainMismatch), "Expected ChainMismatch, got: {err:?}"); + + // Restore the real genesis so we can test the next corruption. + let real_genesis = SyncCheckpoint::new(0, client.api().genesis_hash()); + client + .receipt_provider() + .set_sync_label(ChainMetadata::Genesis, real_genesis) + .await?; + + // --- SyncBoundaryMismatch: corrupted Head hash --- + let chain_len = client.latest_finalized_block().await.number(); + let corrupted_upper = SyncCheckpoint::new(chain_len / 2, H256::from([0xbau8; 32])); + client + .receipt_provider() + .set_sync_label(SyncLabel::Head, corrupted_upper) + .await?; + + let err = client.sync_backward().await.unwrap_err(); + assert!( + matches!(err, ClientError::SyncBoundaryMismatch), + "Expected SyncBoundaryMismatch, got: {err:?}" + ); + + Ok(()) +} + +/// Syncing a second client after new transactions have been submitted +/// should include the newer blocks. +async fn test_block_sync_picks_up_new_blocks() -> anyhow::Result<()> { + // First sync: snapshot the current chain state. + let client1 = create_sync_test_client().await?; + let finalized1 = client1.latest_finalized_block().await.number(); + + client1.sync_backward().await?; + + // Submit a transaction to produce at least one new block. + submit_evm_transfers(1).await?; + + // Second sync: new client with fresh DB should see the new blocks. + let client2 = create_sync_test_client().await?; + let finalized2 = client2.latest_finalized_block().await; + + client2.sync_backward().await?; + assert!( + finalized2.number() > finalized1, + "Second finalized #{} should be higher than first #{finalized1}", + finalized2.number(), + ); + + // The new block should have an ethereum hash mapping in client2's DB. + assert!( + client2.receipt_provider().get_ethereum_hash(&finalized2.hash()).await.is_some(), + "New finalized block #{} should be synced in client2", + finalized2.number(), + ); + + log::debug!( + target: LOG_TARGET, + "Picks up new blocks OK: client2 synced up to #{}, earliest=#{}", + finalized2.number(), + client2.receipt_provider().first_evm_block().unwrap_or(0), + ); + + Ok(()) +}