diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml deleted file mode 100644 index d737e724..00000000 --- a/.github/workflows/integration.yml +++ /dev/null @@ -1,50 +0,0 @@ -name: Integration Tests - -on: - push: - branches: - - main - pull_request: - -jobs: - build: - runs-on: ubuntu-latest - - steps: - - name: Checkout code - uses: actions/checkout@v2 - - - name: Set up Rust - uses: actions-rs/toolchain@v1 - with: - toolchain: stable - - - name: Install op-reth - uses: flashbots/flashbots-toolchain@v0.2 - with: - op-reth: latest - - - name: Log Op-reth version - run: | - op-reth --version - - - name: Build - run: cargo build - - - name: Run tests - run: cargo test --features integration -- integration::integration_test::tests - - - name: Create tar archive of integration logs - if: ${{ failure() }} - run: | - # Find and archive only the logs directories. There were some issues archiving the entire directory. - find integration_logs -type d -name "logs" | tar -czvf integration_logs.tar.gz -T - - - - name: Archive integration logs - uses: actions/upload-artifact@v4 - if: ${{ failure() }} - with: - name: integration-logs - path: integration_logs.tar.gz - retention-days: 5 - if-no-files-found: error diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b37b8f3b..a85aeee8 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -2,25 +2,22 @@ name: Tests on: push: - branches: - - main + branches: [main] pull_request: +env: + CARGO_TERM_COLOR: always + jobs: - build: + run-tests: + name: Run tests runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v2 - - - name: Set up Rust - uses: actions-rs/toolchain@v1 + - uses: actions/checkout@v4 + - uses: actions-rs/toolchain@v1 with: toolchain: stable - - - name: Build - run: cargo build - - - name: Run tests - run: cargo test -- --test-threads=1 + - name: Install cargo-nextest + uses: taiki-e/install-action@nextest + - name: Run nextest + run: cargo nextest run diff --git a/Cargo.lock b/Cargo.lock index e86146da..4d2e0076 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -23,7 +23,7 @@ version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", "once_cell", "version_check", "zerocopy 0.7.35", @@ -181,7 +181,7 @@ checksum = "8c77490fe91a0ce933a1f219029521f20fc28c2c0ca95d53fa4da9c00b8d9d4e" dependencies = [ "alloy-rlp", "bytes", - "cfg-if 1.0.0", + "cfg-if", "const-hex", "derive_more 2.0.1", "foldhash", @@ -739,7 +739,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" dependencies = [ "addr2line", - "cfg-if 1.0.0", + "cfg-if", "libc", "miniz_oxide", "object", @@ -753,6 +753,12 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + [[package]] name = "base64" version = "0.22.1" @@ -848,6 +854,56 @@ dependencies = [ "zeroize", ] +[[package]] +name = "bollard" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97ccca1260af6a459d75994ad5acc1651bcabcbdbc41467cc9786519ab854c30" +dependencies = [ + "base64 0.22.1", + "bollard-stubs", + "bytes", + "futures-core", + "futures-util", + "hex", + "home", + "http", + "http-body-util", + "hyper", + "hyper-named-pipe", + "hyper-rustls", + "hyper-util", + "hyperlocal", + "log", + "pin-project-lite", + "rustls", + "rustls-native-certs", + "rustls-pemfile", + "rustls-pki-types", + "serde", + "serde_derive", + "serde_json", + "serde_repr", + "serde_urlencoded", + "thiserror 2.0.12", + "tokio", + "tokio-util", + "tower-service", + "url", + "winapi", +] + +[[package]] +name = "bollard-stubs" +version = "1.47.1-rc.27.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f179cfbddb6e77a5472703d4b30436bff32929c0aa8a9008ecf23d1d3cdd0da" +dependencies = [ + "serde", + "serde_repr", + "serde_with", +] + [[package]] name = "brotli" version = "7.0.0" @@ -949,12 +1005,6 @@ dependencies = [ "nom", ] -[[package]] -name = "cfg-if" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" - [[package]] name = "cfg-if" version = "1.0.0" @@ -1056,7 +1106,7 @@ version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4b0485bab839b018a8f1723fc5391819fea5f8f0f32288ef8a735fd096b6160c" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", "cpufeatures", "hex", "proptest", @@ -1135,7 +1185,7 @@ version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", ] [[package]] @@ -1368,6 +1418,17 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" +[[package]] +name = "docker_credential" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31951f49556e34d90ed28342e1df7e1cb7a229c4cab0aecc627b5d91edd41d07" +dependencies = [ + "base64 0.21.7", + "serde", + "serde_json", +] + [[package]] name = "dotenv" version = "0.15.0" @@ -1461,6 +1522,17 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "etcetera" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943" +dependencies = [ + "cfg-if", + "home", + "windows-sys 0.48.0", +] + [[package]] name = "ethereum_serde_utils" version = "0.7.0" @@ -1549,6 +1621,18 @@ dependencies = [ "subtle", ] +[[package]] +name = "filetime" +version = "0.2.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35c0522e981e68cbfa8c3f978441a5f34b30b96e146b33cd3359176b50fe8586" +dependencies = [ + "cfg-if", + "libc", + "libredox", + "windows-sys 0.59.0", +] + [[package]] name = "fixed-hash" version = "0.8.0" @@ -1708,7 +1792,7 @@ version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cc6bd114ceda131d3b1d665eba35788690ad37f5916457286b32ab6fd3c438dd" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", "libc", "log", "rustversion", @@ -1732,7 +1816,7 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", "js-sys", "libc", "wasi 0.11.0+wasi-snapshot-preview1", @@ -1745,7 +1829,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73fea8450eea4bac3940448fb7ae50d91f034f941199fcd9d909a5a07aa455f0" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", "libc", "r-efi", "wasi 0.14.2+wasi-0.2.4", @@ -1921,6 +2005,21 @@ dependencies = [ "want", ] +[[package]] +name = "hyper-named-pipe" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73b7d8abf35697b81a825e386fc151e0d503e8cb5fcb93cc8669c376dfd6f278" +dependencies = [ + "hex", + "hyper", + "hyper-util", + "pin-project-lite", + "tokio", + "tower-service", + "winapi", +] + [[package]] name = "hyper-rustls" version = "0.27.5" @@ -1973,6 +2072,21 @@ dependencies = [ "tracing", ] +[[package]] +name = "hyperlocal" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "986c5ce3b994526b3cd75578e62554abd09f0899d6206de48b3e96ab34ccc8c7" +dependencies = [ + "hex", + "http-body-util", + "hyper", + "hyper-util", + "pin-project-lite", + "tokio", + "tower-service", +] + [[package]] name = "iana-time-zone" version = "0.1.63" @@ -2261,7 +2375,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" dependencies = [ "cesu8", - "cfg-if 1.0.0", + "cfg-if", "combine", "jni-sys", "log", @@ -2341,7 +2455,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c872b6c9961a4ccc543e321bb5b89f6b2d2c7fe8b61906918273a3333c95400c" dependencies = [ "async-trait", - "base64", + "base64 0.22.1", "http-body", "hyper", "hyper-rustls", @@ -2417,7 +2531,7 @@ version = "9.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a87cc7a48537badeae96744432de36f4be2b4a34a05a5ef32e9dd8a1c169dde" dependencies = [ - "base64", + "base64 0.22.1", "js-sys", "pem", "ring", @@ -2432,7 +2546,7 @@ version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6e3919bbaa2945715f0bb6d3934a173d1e9a59ac23767fbaaef277265a7411b" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", "ecdsa", "elliptic-curve", "once_cell", @@ -2483,7 +2597,7 @@ version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", "windows-targets 0.52.6", ] @@ -2493,6 +2607,17 @@ version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8355be11b20d696c8f18f6cc018c4e372165b1fa8126cef092399c9951984ffa" +[[package]] +name = "libredox" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" +dependencies = [ + "bitflags 2.9.0", + "libc", + "redox_syscall 0.5.10", +] + [[package]] name = "linux-raw-sys" version = "0.4.15" @@ -2533,7 +2658,7 @@ version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "419e0dc8046cb947daa77eb95ae174acfbddb7673b4151f56d1eed8e93fbfaca" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", "generator", "scoped-tls", "tracing", @@ -2588,7 +2713,7 @@ version = "0.16.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd7399781913e5393588a8d8c6a2867bf85fb38eaf2502fdce465aad2dc6f034" dependencies = [ - "base64", + "base64 0.22.1", "http-body-util", "hyper", "hyper-rustls", @@ -2647,9 +2772,9 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" -version = "0.8.5" +version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e3e04debbb59698c15bacbb6d93584a8c0ca9cc3213cb423d31f760d8843ce5" +checksum = "ff70ce3e48ae43fa075863cef62e8b43b71a4f2382229920e0df362592919430" dependencies = [ "adler2", ] @@ -2693,19 +2818,6 @@ dependencies = [ "smallvec", ] -[[package]] -name = "nix" -version = "0.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b2e0b4f3320ed72aaedb9a5ac838690a8047c7b275da22711fddff4f8a14229" -dependencies = [ - "bitflags 1.3.2", - "cc", - "cfg-if 0.1.10", - "libc", - "void", -] - [[package]] name = "nom" version = "7.1.3" @@ -2902,7 +3014,7 @@ version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56f8870d3024727e99212eb3bb1762ec16e255e3e6f58eeb3dc8db1aa226746d" dependencies = [ - "base64", + "base64 0.22.1", "hex", "opentelemetry", "opentelemetry_sdk", @@ -2991,13 +3103,38 @@ version = "0.9.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", "libc", - "redox_syscall", + "redox_syscall 0.5.10", "smallvec", "windows-targets 0.52.6", ] +[[package]] +name = "parse-display" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "914a1c2265c98e2446911282c6ac86d8524f495792c38c5bd884f80499c7538a" +dependencies = [ + "parse-display-derive", + "regex", + "regex-syntax 0.8.5", +] + +[[package]] +name = "parse-display-derive" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ae7800a4c974efd12df917266338e79a7a74415173caf7e70aa0a0707345281" +dependencies = [ + "proc-macro2", + "quote", + "regex", + "regex-syntax 0.8.5", + "structmeta", + "syn 2.0.100", +] + [[package]] name = "paste" version = "1.0.15" @@ -3010,7 +3147,7 @@ version = "3.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38af38e8470ac9dee3ce1bae1af9c1671fffc44ddfd8bd1d0a3445bf349a8ef3" dependencies = [ - "base64", + "base64 0.22.1", "serde", ] @@ -3374,6 +3511,15 @@ dependencies = [ "bitflags 2.9.0", ] +[[package]] +name = "redox_syscall" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" +dependencies = [ + "bitflags 1.3.2", +] + [[package]] name = "redox_syscall" version = "0.5.10" @@ -3433,7 +3579,7 @@ version = "0.12.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d19c46a6fdd48bc4dab94b6103fccc55d34c67cc0ad04653aad4ea2a07cd7bbb" dependencies = [ - "base64", + "base64 0.22.1", "bytes", "futures-channel", "futures-core", @@ -3495,7 +3641,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" dependencies = [ "cc", - "cfg-if 1.0.0", + "cfg-if", "getrandom 0.2.15", "libc", "untrusted", @@ -3534,12 +3680,10 @@ dependencies = [ "hyper-rustls", "hyper-util", "jsonrpsee", - "lazy_static", "metrics", "metrics-exporter-prometheus", "metrics-util", "moka", - "nix", "op-alloy-consensus", "op-alloy-rpc-types-engine", "opentelemetry", @@ -3548,10 +3692,12 @@ dependencies = [ "parking_lot", "paste", "predicates", + "rand 0.9.0", "reth-rpc-layer", "rustls", "serde", "serde_json", + "testcontainers", "thiserror 2.0.12", "time", "tokio", @@ -3701,6 +3847,15 @@ dependencies = [ "security-framework", ] +[[package]] +name = "rustls-pemfile" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" +dependencies = [ + "rustls-pki-types", +] + [[package]] name = "rustls-pki-types" version = "1.11.0" @@ -3894,6 +4049,17 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_repr" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -3912,7 +4078,7 @@ version = "3.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6b6f7f2fcb69f747921f79f3926bd1e203fce4fef62c268dd3abfb6d86029aa" dependencies = [ - "base64", + "base64 0.22.1", "chrono", "hex", "indexmap 1.9.3", @@ -3952,7 +4118,7 @@ version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", "cpufeatures", "digest 0.10.7", ] @@ -3963,7 +4129,7 @@ version = "0.10.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", "cpufeatures", "digest 0.10.7", ] @@ -3985,7 +4151,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c28efc5e327c837aa837c59eae585fc250715ef939ac32881bcc11677cd02d46" dependencies = [ "cc", - "cfg-if 1.0.0", + "cfg-if", ] [[package]] @@ -4080,7 +4246,7 @@ version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2e859df029d160cb88608f5d7df7fb4753fd20fdfb4de5644f3d8b8440841721" dependencies = [ - "base64", + "base64 0.22.1", "bytes", "futures", "http", @@ -4118,6 +4284,29 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +[[package]] +name = "structmeta" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e1575d8d40908d70f6fd05537266b90ae71b15dbbe7a8b7dffa2b759306d329" +dependencies = [ + "proc-macro2", + "quote", + "structmeta-derive", + "syn 2.0.100", +] + +[[package]] +name = "structmeta-derive" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "152a0b65a590ff6c3da95cabe2353ee04e6167c896b28e3b14478c2636c922fc" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + [[package]] name = "strum" version = "0.27.1" @@ -4231,6 +4420,35 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f50febec83f5ee1df3015341d8bd429f2d1cc62bcba7ea2076759d315084683" +[[package]] +name = "testcontainers" +version = "0.23.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59a4f01f39bb10fc2a5ab23eb0d888b1e2bb168c157f61a1b98e6c501c639c74" +dependencies = [ + "async-trait", + "bollard", + "bollard-stubs", + "bytes", + "docker_credential", + "either", + "etcetera", + "futures", + "log", + "memchr", + "parse-display", + "pin-project-lite", + "serde", + "serde_json", + "serde_with", + "thiserror 2.0.12", + "tokio", + "tokio-stream", + "tokio-tar", + "tokio-util", + "url", +] + [[package]] name = "thiserror" version = "1.0.69" @@ -4277,7 +4495,7 @@ version = "1.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", "once_cell", ] @@ -4391,6 +4609,21 @@ dependencies = [ "tokio-util", ] +[[package]] +name = "tokio-tar" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d5714c010ca3e5c27114c1cdeb9d14641ace49874aa5626d7149e47aedace75" +dependencies = [ + "filetime", + "futures-core", + "libc", + "redox_syscall 0.3.5", + "tokio", + "tokio-stream", + "xattr", +] + [[package]] name = "tokio-util" version = "0.7.14" @@ -4431,7 +4664,7 @@ dependencies = [ "async-stream", "async-trait", "axum", - "base64", + "base64 0.22.1", "bytes", "h2", "http", @@ -4514,7 +4747,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "403fa3b783d4b626a8ad51d766ab03cb6d2dbfc46b1c5d4448395e6628dc9697" dependencies = [ "async-compression", - "base64", + "base64 0.22.1", "bitflags 2.9.0", "bytes", "futures-core", @@ -4712,6 +4945,7 @@ dependencies = [ "form_urlencoded", "idna", "percent-encoding", + "serde", ] [[package]] @@ -4753,12 +4987,6 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" -[[package]] -name = "void" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d" - [[package]] name = "wait-timeout" version = "0.2.1" @@ -4808,7 +5036,7 @@ version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", "once_cell", "rustversion", "wasm-bindgen-macro", @@ -4834,7 +5062,7 @@ version = "0.4.50" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", "js-sys", "once_cell", "wasm-bindgen", @@ -5097,6 +5325,15 @@ dependencies = [ "windows-targets 0.42.2", ] +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + [[package]] name = "windows-sys" version = "0.52.0" @@ -5130,6 +5367,21 @@ dependencies = [ "windows_x86_64_msvc 0.42.2", ] +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + [[package]] name = "windows-targets" version = "0.52.6" @@ -5168,6 +5420,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" @@ -5186,6 +5444,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + [[package]] name = "windows_aarch64_msvc" version = "0.52.6" @@ -5204,6 +5468,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + [[package]] name = "windows_i686_gnu" version = "0.52.6" @@ -5234,6 +5504,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + [[package]] name = "windows_i686_msvc" version = "0.52.6" @@ -5252,6 +5528,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + [[package]] name = "windows_x86_64_gnu" version = "0.52.6" @@ -5270,6 +5552,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" @@ -5288,6 +5576,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + [[package]] name = "windows_x86_64_msvc" version = "0.52.6" @@ -5339,6 +5633,16 @@ dependencies = [ "tap", ] +[[package]] +name = "xattr" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d65cbf2f12c15564212d48f4e3dfb87923d25d611f2aed18f4cb23f0413d89e" +dependencies = [ + "libc", + "rustix 1.0.5", +] + [[package]] name = "yoke" version = "0.7.5" diff --git a/Cargo.toml b/Cargo.toml index e1aabb10..ce177d89 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,7 +6,6 @@ edition = "2024" [dependencies] op-alloy-rpc-types-engine = "0.12.0" alloy-rpc-types-engine = "0.13.0" -alloy-eips = { version = "0.13.0", features = ["serde"], optional = true } alloy-primitives = { version = "0.8.10", features = ["rand"] } tokio = { version = "1", features = ["full"] } tracing = "0.1.4" @@ -42,31 +41,23 @@ metrics-exporter-prometheus = "0.16.0" metrics-util = "0.19.0" eyre = "0.6.12" paste = "1.0.15" - -# dev dependencies for integration tests parking_lot = "0.12.3" -time = { version = "0.3.36", features = ["macros", "formatting", "parsing"], optional = true } -lazy_static = {version = "1.5.0", optional = true } [dev-dependencies] +rand = "0.9.0" +time = { version = "0.3.36", features = ["macros", "formatting", "parsing"] } op-alloy-consensus = "0.12.0" +alloy-eips = { version = "0.13.0", features = ["serde"] } alloy-rpc-types-eth = "0.13.0" anyhow = "1.0" +testcontainers = { version = "0.23.3" } assert_cmd = "2.0.10" predicates = "3.1.2" tokio-util = { version = "0.7.13" } -nix = "0.15.0" bytes = "1.2" reth-rpc-layer = { git = "https://github.com/paradigmxyz/reth.git", rev = "v1.3.7" } ctor = "0.4.1" -[features] -integration = [ - "dep:lazy_static", - "dep:time", - "dep:alloy-eips" -] - [[bin]] name = "rollup-boost" path = "src/bin/main.rs" diff --git a/Makefile b/Makefile index 61b48ea9..abec98aa 100644 --- a/Makefile +++ b/Makefile @@ -41,10 +41,6 @@ lint: ## Run the linters test: ## Run the tests for rollup-boost cargo test --verbose --features "$(FEATURES)" -.PHONY: test-integration -test-integration: ## Run the integration tests for rollup-boost - cargo test --verbose --features integration -- integration::integration_test::tests - .PHONY: lt lt: lint test ## Run "lint" and "test" diff --git a/RELEASE.md b/RELEASE.md index 4e1e4a32..206b1d03 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -18,7 +18,6 @@ Then run the following commands to check the code is working fine: ```bash make lint make test -make test-integration git status # should show no changes # Start rollup-boost with the example .env config diff --git a/docs/design-philosophy-testing-strategy.md b/docs/design-philosophy-testing-strategy.md index 7cce6e6a..394e299f 100644 --- a/docs/design-philosophy-testing-strategy.md +++ b/docs/design-philosophy-testing-strategy.md @@ -20,7 +20,7 @@ We employ a layered testing strategy that provides defense in depth: **Unit Tests** verify individual components, but as this is a distributed system, they only get us so far. -**Integration Tests** serve as our most critical testing layer. Located in `src/integration/`, these tests use a simulated environment to verify system behavior under various conditions: +**Integration Tests** serve as our most critical testing layer. Located in `tests`, these tests use a simulated environment to verify system behavior under various conditions: - How does the system respond when the builder produces invalid blocks? - What happens when the builder experiences high latency? diff --git a/src/bin/main.rs b/src/bin/main.rs index b4538743..0208fb03 100644 --- a/src/bin/main.rs +++ b/src/bin/main.rs @@ -1,143 +1,10 @@ -#![allow(clippy::complexity)] -use ::tracing::info; use clap::Parser; -use rollup_boost::{ - Args, Commands, DebugClient, DebugCommands, PayloadSource, ProxyLayer, RollupBoostServer, - RpcClient, init_metrics, init_tracing, -}; -use std::net::SocketAddr; +use rollup_boost::Args; -use alloy_rpc_types_engine::JwtSecret; use dotenv::dotenv; -use eyre::bail; -use jsonrpsee::RpcModule; -use jsonrpsee::server::Server; - -use tokio::signal::unix::{SignalKind, signal as unix_signal}; #[tokio::main] async fn main() -> eyre::Result<()> { - // Load .env file dotenv().ok(); - rustls::crypto::ring::default_provider() - .install_default() - .expect("Failed to install TLS ring CryptoProvider"); - let args: Args = Args::parse(); - - let debug_addr = format!("{}:{}", args.debug_host, args.debug_server_port); - - // Handle commands if present - if let Some(cmd) = args.command { - let debug_addr = format!("http://{}", debug_addr); - return match cmd { - Commands::Debug { command } => match command { - DebugCommands::SetExecutionMode { execution_mode } => { - let client = DebugClient::new(debug_addr.as_str())?; - let result = client.set_execution_mode(execution_mode).await.unwrap(); - println!("Response: {:?}", result.execution_mode); - - Ok(()) - } - DebugCommands::ExecutionMode {} => { - let client = DebugClient::new(debug_addr.as_str())?; - let result = client.get_execution_mode().await?; - println!("Execution mode: {:?}", result.execution_mode); - - Ok(()) - } - }, - }; - } - - init_tracing(&args)?; - init_metrics(&args)?; - - let l2_client_args = args.l2_client; - - let l2_auth_jwt = if let Some(secret) = l2_client_args.l2_jwt_token { - secret - } else if let Some(path) = l2_client_args.l2_jwt_path.as_ref() { - JwtSecret::from_file(path)? - } else { - bail!("Missing L2 Client JWT secret"); - }; - - let l2_client = RpcClient::new( - l2_client_args.l2_url.clone(), - l2_auth_jwt, - l2_client_args.l2_timeout, - PayloadSource::L2, - )?; - - let builder_args = args.builder; - let builder_auth_jwt = if let Some(secret) = builder_args.builder_jwt_token { - secret - } else if let Some(path) = builder_args.builder_jwt_path.as_ref() { - JwtSecret::from_file(path)? - } else { - bail!("Missing Builder JWT secret"); - }; - - let builder_client = RpcClient::new( - builder_args.builder_url.clone(), - builder_auth_jwt, - builder_args.builder_timeout, - PayloadSource::Builder, - )?; - - let boost_sync_enabled = !args.no_boost_sync; - if boost_sync_enabled { - info!("Boost sync enabled"); - } - - let rollup_boost = RollupBoostServer::new( - l2_client, - builder_client, - boost_sync_enabled, - args.execution_mode, - ); - - // Spawn the debug server - rollup_boost.start_debug_server(debug_addr.as_str()).await?; - - let module: RpcModule<()> = rollup_boost.try_into()?; - - // Build and start the server - info!("Starting server on :{}", args.rpc_port); - - let http_middleware = tower::ServiceBuilder::new().layer(ProxyLayer::new( - l2_client_args.l2_url, - l2_auth_jwt, - builder_args.builder_url, - builder_auth_jwt, - )); - - let server = Server::builder() - .set_http_middleware(http_middleware) - .build(format!("{}:{}", args.rpc_host, args.rpc_port).parse::()?) - .await?; - let handle = server.start(module); - - let stop_handle = handle.clone(); - - // Capture SIGINT and SIGTERM - let mut sigint = unix_signal(SignalKind::interrupt())?; - let mut sigterm = unix_signal(SignalKind::terminate())?; - - tokio::select! { - _ = handle.stopped() => { - // The server has already shut down by itself - info!("Server stopped"); - } - _ = sigint.recv() => { - info!("Received SIGINT, shutting down gracefully..."); - let _ = stop_handle.stop(); - } - _ = sigterm.recv() => { - info!("Received SIGTERM, shutting down gracefully..."); - let _ = stop_handle.stop(); - } - } - - Ok(()) + Args::parse().run().await } diff --git a/src/cli.rs b/src/cli.rs index e973fb91..54291fc1 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -1,12 +1,20 @@ +use std::{net::SocketAddr, path::PathBuf}; + +use alloy_rpc_types_engine::JwtSecret; use clap::{Parser, Subcommand}; -use tracing::Level; +use eyre::bail; +use jsonrpsee::{RpcModule, server::Server}; +use tokio::signal::unix::{SignalKind, signal as unix_signal}; +use tracing::{Level, info}; use crate::{ + DebugClient, PayloadSource, ProxyLayer, RollupBoostServer, RpcClient, client::rpc::{BuilderArgs, L2ClientArgs}, + init_metrics, init_tracing, server::ExecutionMode, }; -#[derive(Parser, Debug)] +#[derive(Clone, Parser, Debug)] #[clap(author, version, about)] pub struct Args { #[command(subcommand)] @@ -58,6 +66,10 @@ pub struct Args { #[arg(long, env, default_value = "text")] pub log_format: LogFormat, + /// Redirect logs to a file + #[arg(long, env)] + pub log_file: Option, + /// Host to run the debug server on #[arg(long, env, default_value = "127.0.0.1")] pub debug_host: String, @@ -71,6 +83,131 @@ pub struct Args { pub execution_mode: ExecutionMode, } +impl Args { + pub async fn run(self) -> eyre::Result<()> { + rustls::crypto::ring::default_provider() + .install_default() + .expect("Failed to install TLS ring CryptoProvider"); + + let debug_addr = format!("{}:{}", self.debug_host, self.debug_server_port); + + // Handle commands if present + if let Some(cmd) = self.command { + let debug_addr = format!("http://{}", debug_addr); + return match cmd { + Commands::Debug { command } => match command { + DebugCommands::SetExecutionMode { execution_mode } => { + let client = DebugClient::new(debug_addr.as_str())?; + let result = client.set_execution_mode(execution_mode).await.unwrap(); + println!("Response: {:?}", result.execution_mode); + + Ok(()) + } + DebugCommands::ExecutionMode {} => { + let client = DebugClient::new(debug_addr.as_str())?; + let result = client.get_execution_mode().await?; + println!("Execution mode: {:?}", result.execution_mode); + + Ok(()) + } + }, + }; + } + + init_tracing(&self)?; + init_metrics(&self)?; + + let l2_client_args = self.l2_client; + + let l2_auth_jwt = if let Some(secret) = l2_client_args.l2_jwt_token { + secret + } else if let Some(path) = l2_client_args.l2_jwt_path.as_ref() { + JwtSecret::from_file(path)? + } else { + bail!("Missing L2 Client JWT secret"); + }; + + let l2_client = RpcClient::new( + l2_client_args.l2_url.clone(), + l2_auth_jwt, + l2_client_args.l2_timeout, + PayloadSource::L2, + )?; + + let builder_args = self.builder; + let builder_auth_jwt = if let Some(secret) = builder_args.builder_jwt_token { + secret + } else if let Some(path) = builder_args.builder_jwt_path.as_ref() { + JwtSecret::from_file(path)? + } else { + bail!("Missing Builder JWT secret"); + }; + + let builder_client = RpcClient::new( + builder_args.builder_url.clone(), + builder_auth_jwt, + builder_args.builder_timeout, + PayloadSource::Builder, + )?; + + let boost_sync_enabled = !self.no_boost_sync; + if boost_sync_enabled { + info!("Boost sync enabled"); + } + + let rollup_boost = RollupBoostServer::new( + l2_client, + builder_client, + boost_sync_enabled, + self.execution_mode, + ); + + // Spawn the debug server + rollup_boost.start_debug_server(debug_addr.as_str()).await?; + + let module: RpcModule<()> = rollup_boost.try_into()?; + + // Build and start the server + info!("Starting server on :{}", self.rpc_port); + + let http_middleware = tower::ServiceBuilder::new().layer(ProxyLayer::new( + l2_client_args.l2_url, + l2_auth_jwt, + builder_args.builder_url, + builder_auth_jwt, + )); + + let server = Server::builder() + .set_http_middleware(http_middleware) + .build(format!("{}:{}", self.rpc_host, self.rpc_port).parse::()?) + .await?; + let handle = server.start(module); + + let stop_handle = handle.clone(); + + // Capture SIGINT and SIGTERM + let mut sigint = unix_signal(SignalKind::interrupt())?; + let mut sigterm = unix_signal(SignalKind::terminate())?; + + tokio::select! { + _ = handle.stopped() => { + // The server has already shut down by itself + info!("Server stopped"); + } + _ = sigint.recv() => { + info!("Received SIGINT, shutting down gracefully..."); + let _ = stop_handle.stop(); + } + _ = sigterm.recv() => { + info!("Received SIGTERM, shutting down gracefully..."); + let _ = stop_handle.stop(); + } + } + + Ok(()) + } +} + #[derive(Clone, Debug)] pub enum LogFormat { Json, @@ -89,7 +226,7 @@ impl std::str::FromStr for LogFormat { } } -#[derive(Subcommand, Debug)] +#[derive(Clone, Subcommand, Debug)] pub enum Commands { /// Debug commands Debug { @@ -98,7 +235,7 @@ pub enum Commands { }, } -#[derive(Subcommand, Debug)] +#[derive(Clone, Subcommand, Debug)] pub enum DebugCommands { /// Set the execution mode SetExecutionMode { execution_mode: ExecutionMode }, diff --git a/src/client/rpc.rs b/src/client/rpc.rs index bdf863e7..37a7a786 100644 --- a/src/client/rpc.rs +++ b/src/client/rpc.rs @@ -351,10 +351,11 @@ macro_rules! define_rpc_args { define_rpc_args!((BuilderArgs, builder), (L2ClientArgs, l2)); #[cfg(test)] -mod tests { +pub mod tests { use assert_cmd::Command; use http::Uri; use jsonrpsee::core::client::ClientT; + use parking_lot::Mutex; use crate::client::auth::AuthClientService; use crate::server::PayloadSource; @@ -370,16 +371,29 @@ mod tests { }; use predicates::prelude::*; use reth_rpc_layer::{AuthLayer, JwtAuthValidator}; + use std::collections::HashSet; use std::net::SocketAddr; + use std::net::TcpListener; use std::result::Result; use std::str::FromStr; + use std::sync::LazyLock; use super::*; - const AUTH_PORT: u32 = 8550; const AUTH_ADDR: &str = "0.0.0.0"; const SECRET: &str = "f79ae8046bc11c9927afe911db7143c51a806c4a537cc08e0d37140b0192f430"; + pub fn get_available_port() -> u16 { + static CLAIMED_PORTS: LazyLock>> = + LazyLock::new(|| Mutex::new(HashSet::new())); + loop { + let port: u16 = rand::random_range(1000..20000); + if TcpListener::bind(("127.0.0.1", port)).is_ok() && CLAIMED_PORTS.lock().insert(port) { + return port; + } + } + } + #[test] fn test_invalid_args() { let mut cmd = Command::cargo_bin("rollup-boost").unwrap(); @@ -392,21 +406,22 @@ mod tests { #[tokio::test] async fn valid_jwt() { + let port = get_available_port(); let secret = JwtSecret::from_hex(SECRET).unwrap(); - - let auth_rpc = Uri::from_str(&format!("http://{}:{}", AUTH_ADDR, AUTH_PORT)).unwrap(); + let auth_rpc = Uri::from_str(&format!("http://{}:{}", AUTH_ADDR, port)).unwrap(); let client = RpcClient::new(auth_rpc, secret, 1000, PayloadSource::L2).unwrap(); - let response = send_request(client.auth_client).await; + let response = send_request(client.auth_client, port).await; assert!(response.is_ok()); assert_eq!(response.unwrap(), "You are the dark lord"); } #[tokio::test] async fn invalid_jwt() { + let port = get_available_port(); let secret = JwtSecret::random(); - let auth_rpc = Uri::from_str(&format!("http://{}:{}", AUTH_ADDR, AUTH_PORT)).unwrap(); + let auth_rpc = Uri::from_str(&format!("http://{}:{}", AUTH_ADDR, port)).unwrap(); let client = RpcClient::new(auth_rpc, secret, 1000, PayloadSource::L2).unwrap(); - let response = send_request(client.auth_client).await; + let response = send_request(client.auth_client, port).await; assert!(response.is_err()); assert!(matches!( response.unwrap_err(), @@ -417,8 +432,9 @@ mod tests { async fn send_request( client: HttpClient>, + port: u16, ) -> Result { - let server = spawn_server().await; + let server = spawn_server(port).await; let response = client .request::("greet_melkor", rpc_params![]) @@ -431,9 +447,9 @@ mod tests { } /// Spawn a new RPC server equipped with a `JwtLayer` auth middleware. - async fn spawn_server() -> ServerHandle { + async fn spawn_server(port: u16) -> ServerHandle { let secret = JwtSecret::from_hex(SECRET).unwrap(); - let addr = format!("{AUTH_ADDR}:{AUTH_PORT}"); + let addr = format!("{AUTH_ADDR}:{port}"); let validator = JwtAuthValidator::new(secret); let layer = AuthLayer::new(validator); let middleware = tower::ServiceBuilder::default().layer(layer); diff --git a/src/integration/integration_test.rs b/src/integration/integration_test.rs deleted file mode 100644 index b2b6b6a9..00000000 --- a/src/integration/integration_test.rs +++ /dev/null @@ -1,287 +0,0 @@ -#[cfg(test)] -mod tests { - use alloy_primitives::B256; - use serde_json::Value; - use std::sync::{Arc, Mutex}; - use std::time::Duration; - - use crate::integration::RollupBoostTestHarnessBuilder; - use crate::server::ExecutionMode; - use op_alloy_rpc_types_engine::OpExecutionPayloadEnvelopeV3; - - #[tokio::test] - async fn test_integration_simple() -> eyre::Result<()> { - let harness = RollupBoostTestHarnessBuilder::new("test_integration_simple") - .build() - .await?; - let mut block_generator = harness.get_block_generator().await?; - - for _ in 0..5 { - let (_block, block_creator) = block_generator.generate_block(false).await?; - assert!( - block_creator.is_builder(), - "Block creator should be the builder" - ); - } - - Ok(()) - } - - #[tokio::test] - async fn test_integration_no_tx_pool() -> eyre::Result<()> { - let harness = RollupBoostTestHarnessBuilder::new("test_integration_no_tx_pool") - .build() - .await?; - let mut block_generator = harness.get_block_generator().await?; - - // start creating 5 empty blocks which are processed by the L2 builder - for _ in 0..5 { - let (_block, block_creator) = block_generator.generate_block(true).await?; - assert!(block_creator.is_l2(), "Block creator should be l2"); - } - - // process 5 more non empty blocks which are processed by the builder. - // The builder should be on sync because it has received the new payload requests from rollup-boost. - for _ in 0..5 { - let (_block, block_creator) = block_generator.generate_block(false).await?; - assert!( - block_creator.is_builder(), - "Block creator should be the builder" - ); - } - - Ok(()) - } - - #[tokio::test] - async fn test_integration_execution_mode() -> eyre::Result<()> { - // Create a counter that increases whenever we receive a new RPC call in the builder - let counter = Arc::new(Mutex::new(0)); - - let counter_for_handler = counter.clone(); - let handler = Box::new(move |_method: &str, _params: Value, _result: Value| { - let mut counter = counter_for_handler.lock().unwrap(); - - *counter += 1; - None - }); - - let harness = RollupBoostTestHarnessBuilder::new("test_integration_dry_run") - .proxy_handler(handler) - .build() - .await?; - let mut block_generator = harness.get_block_generator().await?; - - // start creating 5 empty blocks which are processed by the builder - for _ in 0..5 { - let (_block, block_creator) = block_generator.generate_block(false).await?; - assert!( - block_creator.is_builder(), - "Block creator should be the builder" - ); - } - - let client = harness.get_client().await; - - // enable dry run mode - { - let response = client - .set_execution_mode(ExecutionMode::DryRun) - .await - .unwrap(); - assert_eq!(response.execution_mode, ExecutionMode::DryRun); - - // the new valid block should be created the the l2 builder - let (_block, block_creator) = block_generator.generate_block(false).await?; - assert!(block_creator.is_l2(), "Block creator should be l2"); - } - - // toggle again dry run mode - { - let response = client - .set_execution_mode(ExecutionMode::Enabled) - .await - .unwrap(); - assert_eq!(response.execution_mode, ExecutionMode::Enabled); - - // the new valid block should be created the the builder - let (_block, block_creator) = block_generator.generate_block(false).await?; - assert!( - block_creator.is_builder(), - "Block creator should be the builder" - ); - } - - // sleep for 1 second so that it has time to send the last FCU request to the builder - // and there is not a race condition with the disable call - std::thread::sleep(Duration::from_secs(1)); - - tracing::info!("Setting execution mode to disabled"); - - // Set the execution mode to disabled and reset the counter in the proxy to 0 - // to track the number of calls to the builder during the disabled mode which - // should be 0 - { - let response = client - .set_execution_mode(ExecutionMode::Disabled) - .await - .unwrap(); - assert_eq!(response.execution_mode, ExecutionMode::Disabled); - - // reset the counter in the proxy - *counter.lock().unwrap() = 0; - - // create 5 blocks which are processed by the l2 clients - for _ in 0..5 { - let (_block, block_creator) = block_generator.generate_block(false).await?; - assert!(block_creator.is_l2(), "Block creator should be l2"); - } - - assert_eq!( - *counter.lock().unwrap(), - 0, - "Number of calls to the builder should be 0", - ); - } - - Ok(()) - } - - #[tokio::test] - async fn test_integration_remote_builder_down() -> eyre::Result<()> { - let mut harness = - RollupBoostTestHarnessBuilder::new("test_integration_remote_builder_down") - .build() - .await?; - let mut block_generator = harness.get_block_generator().await?; - - for _ in 0..3 { - let (_block, block_creator) = block_generator.generate_block(false).await?; - assert!( - block_creator.is_builder(), - "Block creator should be the builder" - ); - } - - // stop the builder - let builder_service = harness._framework.get_mut_service("builder")?; - builder_service.stop()?; - - // create 3 new blocks that are processed by the l2 builder - for _ in 0..3 { - let (_block, block_creator) = block_generator.generate_block(false).await?; - assert!(block_creator.is_l2(), "Block creator should be l2"); - } - - // start the builder again - builder_service.start_and_ready()?; - - // the next block is computed by the l2 builder because the builder is not synced with the previous 3 blocks - // But, once the builder receives the FCU request from rollup-boost, it will sync up the blocks with the - // L2 block builder and be ready again. - let (_block, block_creator) = block_generator.generate_block(false).await?; - assert!(block_creator.is_l2(), "Block creator should be l2"); - - // Note: We might add some sleep here if the builder is not synced in time. I have not seen this happen yet. - - // create 3 new blocks that are processed by the l2 builder because the builder is not synced with the previous 3 blocks - for _ in 0..3 { - let (_block, block_creator) = block_generator.generate_block(false).await?; - assert!(block_creator.is_builder(), "Block creator should be l2"); - } - - Ok(()) - } - - #[tokio::test] - async fn test_integration_builder_full_delay() -> eyre::Result<()> { - // Create a dynamic handler that delays all the calls by 2 seconds - let delay = Arc::new(Mutex::new(Duration::from_secs(0))); - - let delay_for_handler = delay.clone(); - let handler = Box::new(move |_method: &str, _params: Value, _result: Value| { - let delay = delay_for_handler.lock().unwrap(); - // sleep the amount of time specified in the delay - std::thread::sleep(*delay); - None - }); - - // This integration test checks that if the builder has a general delay in processing ANY of the requests, - // rollup-boost does not stop building blocks. - let harness = RollupBoostTestHarnessBuilder::new("test_integration_builder_full_delay") - .proxy_handler(handler) - .build() - .await?; - - let mut block_generator = harness.get_block_generator().await?; - - // create 3 blocks that are processed by the builder - for _ in 0..3 { - let (_block, block_creator) = block_generator.generate_block(false).await?; - assert!( - block_creator.is_builder(), - "Block creator should be the builder" - ); - } - - // add the delay - *delay.lock().unwrap() = Duration::from_secs(2); - - // create 3 blocks that are processed by the builder - for _ in 0..3 { - let (_block, block_creator) = block_generator.generate_block(false).await?; - assert!(block_creator.is_l2(), "Block creator should be the builder"); - } - - Ok(()) - } - - #[tokio::test] - async fn test_integration_builder_returns_incorrect_block() -> eyre::Result<()> { - // Test that the builder returns a block with an incorrect state root and that rollup-boost - // does not process it. - let handler = Box::new(move |method: &str, _params: Value, _result: Value| { - if method != "engine_getPayloadV3" { - return None; - } - - let mut payload = - serde_json::from_value::(_result).unwrap(); - - // modify the state root field - payload - .execution_payload - .payload_inner - .payload_inner - .state_root = B256::ZERO; - - let result = serde_json::to_value(&payload).unwrap(); - Some(result) - }); - - let mut harness = - RollupBoostTestHarnessBuilder::new("test_integration_builder_returns_incorrect_block") - .proxy_handler(handler) - .build() - .await?; - - let mut block_generator = harness.get_block_generator().await?; - - // create 3 blocks that are processed by the builder - for _ in 0..3 { - let (_block, block_creator) = block_generator.generate_block(false).await?; - assert!(block_creator.is_l2(), "Block creator should be the builder"); - } - // check that at some point we had the log "builder payload was not valid" which signals - // that the builder returned a payload that was not valid and rollup-boost did not process it. - let rb_service = harness._framework.get_mut_service("rollup-boost")?; - - let logs = rb_service.get_logs()?; - assert!( - logs.contains("Invalid payload"), - "Logs should contain the message 'builder payload was not valid'" - ); - - Ok(()) - } -} diff --git a/src/integration/mod.rs b/src/integration/mod.rs deleted file mode 100644 index 3f90c7b2..00000000 --- a/src/integration/mod.rs +++ /dev/null @@ -1,873 +0,0 @@ -use crate::client::auth::{AuthClientLayer, AuthClientService}; -use crate::debug_api::DebugClient; -use crate::server::{EngineApiClient, OpExecutionPayloadEnvelope, Version}; -use crate::server::{NewPayload, PayloadSource}; -use alloy_eips::BlockNumberOrTag; -use alloy_eips::eip2718::Encodable2718; -use alloy_primitives::{B256, Bytes, TxKind, U256, address, hex}; -use alloy_rpc_types_engine::{ExecutionPayload, JwtSecret}; -use alloy_rpc_types_engine::{ - ForkchoiceState, ForkchoiceUpdated, PayloadAttributes, PayloadId, PayloadStatus, - PayloadStatusEnum, -}; -use bytes::BytesMut; -use jsonrpsee::http_client::{HttpClient, transport::HttpBackend}; -use jsonrpsee::proc_macros::rpc; -use lazy_static::lazy_static; -use op_alloy_consensus::TxDeposit; -use op_alloy_rpc_types_engine::OpPayloadAttributes; -use proxy::{DynHandlerFn, start_proxy_server}; -use serde_json::Value; -use std::collections::{HashMap, HashSet}; -use std::path::{Path, PathBuf}; -use std::str::FromStr; -use std::sync::Mutex; -use std::time::UNIX_EPOCH; -use std::{ - fs::{File, OpenOptions}, - io, - io::prelude::*, - process::{Child, Command}, - time::{Duration, SystemTime}, -}; -use thiserror::Error; -use time::{OffsetDateTime, format_description}; - -/// Default JWT token for testing purposes -pub const DEFAULT_JWT_TOKEN: &str = - "688f5d737bad920bdfb2fc2f488d6b6209eebda1dae949a8de91398d932c517a"; - -mod integration_test; -mod proxy; -mod service_rb; -mod service_reth; - -#[derive(Debug, Error)] -pub enum IntegrationError { - #[error("Failed to spawn process")] - SpawnError, - #[error("Binary not found")] - BinaryNotFound, - #[error("Failed to setup integration framework")] - SetupError, - #[error("Log error")] - LogError, - #[error("Service already running")] - ServiceAlreadyRunning, - #[error("Service stopped")] - ServiceStopped, - #[error(transparent)] - AddrParseError(#[from] std::net::AddrParseError), -} - -#[derive(Debug, Clone)] -pub enum Arg { - Port { name: String, preferred: u16 }, - Dir { name: String }, - Value(String), - // FilePath is an argument that writes the given content to a file in the test directory - // and returns the path to the file as an argument - FilePath { name: String, content: String }, -} - -impl From for Arg { - fn from(s: String) -> Self { - Arg::Value(s) - } -} - -impl From<&str> for Arg { - fn from(s: &str) -> Self { - Arg::Value(s.to_string()) - } -} - -impl From for Arg { - fn from(path: PathBuf) -> Self { - Arg::Value( - path.to_str() - .expect("Failed to convert path to string") - .to_string(), - ) - } -} - -impl From<&Path> for Arg { - fn from(path: &Path) -> Self { - Arg::Value( - path.to_str() - .expect("Failed to convert path to string") - .to_string(), - ) - } -} - -impl From<&String> for Arg { - fn from(s: &String) -> Self { - Arg::Value(s.clone()) - } -} - -impl From<&PathBuf> for Arg { - fn from(path: &PathBuf) -> Self { - Arg::Value( - path.to_str() - .expect("Failed to convert path to string") - .to_string(), - ) - } -} - -pub struct ServiceCommand { - program: String, - args: Vec, -} - -impl ServiceCommand { - pub fn new(program: impl Into) -> Self { - Self { - program: program.into(), - args: Vec::new(), - } - } - - pub fn arg(mut self, arg: impl Into) -> Self { - self.args.push(arg.into()); - self - } -} - -pub struct ReadyParams { - pub log_pattern: String, - pub duration: Duration, -} - -pub trait Service { - fn command(&self) -> ServiceCommand; - fn ready(&self) -> ReadyParams; -} - -pub struct ServiceInstance { - command_config: (String, Vec), - process: Option, - log_path: PathBuf, - service: Box, - allocated_ports: HashMap, -} - -lazy_static! { - static ref GLOBAL_ALLOCATED_PORTS: Mutex> = Mutex::new(HashSet::new()); -} - -pub struct IntegrationFramework { - test_dir: PathBuf, - logs_dir: PathBuf, - services: HashMap, -} - -impl ServiceInstance { - pub fn new( - name: String, - command_config: (String, Vec), - logs_dir: PathBuf, - allocated_ports: HashMap, - service: Box, - ) -> Self { - let log_path = logs_dir.join(format!("{}.log", name)); - Self { - process: None, - command_config, - log_path, - allocated_ports, - service, - } - } - - pub fn start(&mut self) -> Result<(), IntegrationError> { - if self.process.is_some() { - return Err(IntegrationError::ServiceAlreadyRunning); - } - - let mut log = open_log_file(&self.log_path)?; - let stdout = log.try_clone().map_err(|_| IntegrationError::LogError)?; - let stderr = log.try_clone().map_err(|_| IntegrationError::LogError)?; - - // print the command config on the log file - log.write_all(format!("Command config: {:?}\n", self.command_config).as_bytes()) - .map_err(|_| IntegrationError::LogError)?; - - // build the command from the command config - let mut cmd = { - let command_config = self.command_config.clone(); - let mut cmd = Command::new(command_config.0.clone()); - cmd.args(&command_config.1); - cmd - }; - cmd.stdout(stdout).stderr(stderr); - - let child = match cmd.spawn() { - Ok(child) => Ok(child), - Err(e) => match e.kind() { - io::ErrorKind::NotFound => Err(IntegrationError::BinaryNotFound), - _ => Err(IntegrationError::SpawnError), - }, - }?; - - self.process = Some(child); - Ok(()) - } - - pub fn stop(&mut self) -> Result<(), IntegrationError> { - if let Some(mut process) = self.process.take() { - nix::sys::signal::kill( - nix::unistd::Pid::from_raw(process.id() as i32), - nix::sys::signal::SIGINT, - ) - .map_err(|_| IntegrationError::SpawnError)?; - - // wait for the process to exit - process.wait().unwrap(); - } - Ok(()) - } - - /// Start a service using its configuration and wait for it to be ready - pub fn start_and_ready(&mut self) -> Result<(), IntegrationError> { - self.start()?; - - let params = self.service.ready(); - self.wait_for_log(¶ms.log_pattern, params.duration)?; - - Ok(()) - } - - pub fn get_port(&self, name: &str) -> u16 { - *self.allocated_ports.get(name).unwrap_or_else(|| { - panic!("Port for {} not found", name); - }) - } - - pub fn get_endpoint(&self, name: &str) -> String { - format!("http://localhost:{}", self.get_port(name)) - } - - pub fn get_logs(&self) -> Result { - let mut file = File::open(&self.log_path).map_err(|_| IntegrationError::LogError)?; - let mut contents = String::new(); - file.read_to_string(&mut contents) - .map_err(|_| IntegrationError::LogError)?; - Ok(contents) - } - - pub fn wait_for_log( - &mut self, - pattern: &str, - timeout: Duration, - ) -> Result<(), IntegrationError> { - let start = std::time::Instant::now(); - - loop { - // Check if process has stopped - if let Some(ref mut process) = self.process { - match process.try_wait() { - Ok(None) => {} - Ok(Some(_status)) => { - // Process has exited - return Err(IntegrationError::ServiceStopped); - } - Err(_) => { - return Err(IntegrationError::ServiceStopped); - } - } - } - - if start.elapsed() > timeout { - return Err(IntegrationError::SpawnError); - } - - let mut contents = self.get_logs()?; - - // Since we share the same log file for different executions of the same service during the lifespan - // of the test, we need to filter the logs and only consider the logs of the current execution. - // We can do this because we print at each service start the log "Command config: " - // So, we are going to search for the command config in the log and only consider the logs after that - if let Some(index) = contents.rfind("Command config:") { - contents = contents[index..].to_string(); - } - - if contents.contains(pattern) { - return Ok(()); - } - - std::thread::sleep(Duration::from_millis(100)); - } - } -} - -impl IntegrationFramework { - pub fn new(test_name: &str) -> Result { - let dt: OffsetDateTime = SystemTime::now().into(); - let format = format_description::parse("[year]_[month]_[day]_[hour]_[minute]_[second]") - .map_err(|_| IntegrationError::SetupError)?; - - let timestamp = dt - .format(&format) - .map_err(|_| IntegrationError::SetupError)?; - - let test_name = format!("{}_{}", timestamp, test_name); - - let mut test_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")); - test_dir.push("./integration_logs"); - test_dir.push(test_name); - - // Create logs subdirectory - let logs_dir = test_dir.join("logs"); - std::fs::create_dir_all(&logs_dir).map_err(|_| IntegrationError::SetupError)?; - - Ok(Self { - test_dir, - logs_dir, - services: HashMap::new(), - }) - } - - fn get_mut_service(&mut self, name: &str) -> eyre::Result<&mut ServiceInstance> { - self.services - .get_mut(name) - .ok_or(eyre::eyre!("Service not found")) - } - - fn build_command( - &mut self, - service_name: &str, - cmd: ServiceCommand, - ) -> Result<(HashMap, (String, Vec)), IntegrationError> { - let mut allocated_ports = HashMap::new(); - let mut command_args = Vec::new(); - - for arg in cmd.args { - match arg { - Arg::Port { name, preferred } => { - let port = self.find_available_port(preferred)?; - allocated_ports.insert(name, port); - command_args.push(port.to_string()); - } - Arg::Dir { name } => { - let dir_path = self.test_dir.join(service_name).join(name); - std::fs::create_dir_all(&dir_path).map_err(|_| IntegrationError::SetupError)?; - command_args.push( - dir_path - .to_str() - .expect("Failed to convert path to string") - .to_string(), - ); - } - Arg::FilePath { name, content } => { - let file_path = self.test_dir.join(service_name).join(name); - std::fs::write(&file_path, content) - .map_err(|_| IntegrationError::SetupError)?; - command_args.push( - file_path - .to_str() - .expect("Failed to convert path to string") - .to_string(), - ); - } - Arg::Value(value) => { - command_args.push(value); - } - } - } - - Ok((allocated_ports, (cmd.program, command_args))) - } - - fn find_available_port(&self, start: u16) -> Result { - let mut global_ports = GLOBAL_ALLOCATED_PORTS - .lock() - .expect("Failed to acquire lock"); - - (start..start + 100) - .find(|&port| { - if global_ports.contains(&port) { - return false; - } - if std::net::TcpListener::bind(("127.0.0.1", port)).is_ok() { - global_ports.insert(port); - return true; - } - false - }) - .ok_or(IntegrationError::SetupError) - } - - pub async fn start( - &mut self, - name: &str, - config: Box, - ) -> Result<&mut ServiceInstance, IntegrationError> { - let (allocated_ports, command_config) = self.build_command(name, config.command())?; - - // Store the service instance in the framework - let service = ServiceInstance::new( - name.to_string(), - command_config, - self.logs_dir.clone(), - allocated_ports, - config, - ); - self.services.insert(name.to_string(), service); - let service = self.services.get_mut(name).unwrap(); - - service.start_and_ready()?; - Ok(service) - } - - /// Writes content to a file in the test directory and returns its absolute path - pub fn write_file( - &self, - name: &str, - content: impl AsRef<[u8]>, - ) -> Result { - let file_path = self.test_dir.join(name); - if let Some(parent) = file_path.parent() { - std::fs::create_dir_all(parent).map_err(|_| IntegrationError::SetupError)?; - } - std::fs::write(&file_path, content).map_err(|_| IntegrationError::SetupError)?; - Ok(file_path) - } -} - -fn open_log_file(path: &PathBuf) -> Result { - let prefix = path.parent().unwrap(); - std::fs::create_dir_all(prefix).map_err(|_| IntegrationError::LogError)?; - - OpenOptions::new() - .append(true) - .create(true) - .open(path) - .map_err(|_| IntegrationError::LogError) -} - -impl Drop for IntegrationFramework { - fn drop(&mut self) { - // Stop all services first - for service in &mut self.services { - let _ = service.1.stop(); - } - - // Release allocated ports from global registry - let mut global_ports = GLOBAL_ALLOCATED_PORTS - .lock() - .expect("Failed to acquire lock"); - for service in &self.services { - for port in service.1.allocated_ports.values() { - global_ports.remove(port); - } - } - } -} - -pub struct EngineApi { - pub engine_api_client: HttpClient>, -} - -// TODO: Use client/rpc.rs instead -impl EngineApi { - pub fn new(url: &str, secret: &str) -> Result> { - let secret_layer = AuthClientLayer::new(JwtSecret::from_str(secret)?); - let middleware = tower::ServiceBuilder::default().layer(secret_layer); - let client = jsonrpsee::http_client::HttpClientBuilder::default() - .set_http_middleware(middleware) - .build(url) - .expect("Failed to create http client"); - - Ok(Self { - engine_api_client: client, - }) - } - - pub async fn get_payload( - &self, - version: Version, - payload_id: PayloadId, - ) -> eyre::Result { - match version { - Version::V3 => Ok(OpExecutionPayloadEnvelope::V3( - EngineApiClient::get_payload_v3(&self.engine_api_client, payload_id).await?, - )), - Version::V4 => Ok(OpExecutionPayloadEnvelope::V4( - EngineApiClient::get_payload_v4(&self.engine_api_client, payload_id).await?, - )), - } - } - - pub async fn new_payload(&self, payload: NewPayload) -> eyre::Result { - match payload { - NewPayload::V3(new_payload) => Ok(EngineApiClient::new_payload_v3( - &self.engine_api_client, - new_payload.payload, - new_payload.versioned_hashes, - new_payload.parent_beacon_block_root, - ) - .await?), - NewPayload::V4(new_payload) => Ok(EngineApiClient::new_payload_v4( - &self.engine_api_client, - new_payload.payload, - new_payload.versioned_hashes, - new_payload.parent_beacon_block_root, - new_payload.execution_requests, - ) - .await?), - } - } - - pub async fn update_forkchoice( - &self, - current_head: B256, - new_head: B256, - payload_attributes: Option, - ) -> eyre::Result { - Ok(EngineApiClient::fork_choice_updated_v3( - &self.engine_api_client, - ForkchoiceState { - head_block_hash: new_head, - safe_block_hash: current_head, - finalized_block_hash: current_head, - }, - payload_attributes, - ) - .await?) - } - - pub async fn latest(&self) -> eyre::Result> { - Ok(BlockApiClient::get_block_by_number( - &self.engine_api_client, - BlockNumberOrTag::Latest, - false, - ) - .await?) - } -} - -#[rpc(client, namespace = "eth")] -pub trait BlockApi { - #[method(name = "getBlockByNumber")] - async fn get_block_by_number( - &self, - block_number: BlockNumberOrTag, - include_txs: bool, - ) -> RpcResult>; -} - -/// Test flavor that sets up one Rollup-boost instance connected to two Reth nodes -pub struct RollupBoostTestHarness { - _framework: IntegrationFramework, // Keep framework alive to maintain service ownership -} - -/// Test node P2P configuration (private_key, enode_address) -pub const TEST_NODE_P2P_ADDR: (&str, &str) = ( - "a11ac89899cd86e36b6fb881ec1255b8a92a688790b7d950f8b7d8dd626671fb", - "3479db4d9217fb5d7a8ed4d61ac36e120b05d36c2eefb795dc42ff2e971f251a2315f5649ea1833271e020b9adc98d5db9973c7ed92d6b2f1f2223088c3d852f", -); - -const PROXY_START_PORT: u16 = 4444; - -pub struct RollupBoostTestHarnessBuilder { - test_name: String, - proxy_handler: Option, -} - -impl RollupBoostTestHarnessBuilder { - pub fn new(test_name: &str) -> Self { - Self { - test_name: test_name.to_string(), - proxy_handler: None, - } - } - - pub fn proxy_handler(mut self, proxy_handler: DynHandlerFn) -> Self { - self.proxy_handler = Some(proxy_handler); - self - } - - async fn build(self) -> Result { - let mut framework = IntegrationFramework::new(self.test_name.as_str())?; - - let jwt_path = framework.write_file("jwt.hex", DEFAULT_JWT_TOKEN)?; - - // Write the genesis file to the test directory and update the timestamp to the current time - let genesis_path = { - // Read the template file - let template = include_str!("testdata/genesis.json"); - - // Parse the JSON - let mut genesis: Value = serde_json::from_str(template).unwrap(); - - // Update the timestamp field - let timestamp = SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap() - .as_secs(); - if let Some(config) = genesis.as_object_mut() { - // Assuming timestamp is at the root level - adjust path as needed - config["timestamp"] = Value::String(format!("0x{:x}", timestamp)); - } - - framework.write_file( - "genesis.json", - serde_json::to_string_pretty(&genesis).unwrap(), - )? - }; - - // Start L2 Reth instance - let l2_reth_config = service_reth::RethConfig::new() - .jwt_secret_path(jwt_path.clone()) - .chain_config_path(genesis_path.clone()) - .p2p_secret_key(TEST_NODE_P2P_ADDR.0.to_string()); - - let l2_service = { - let service = framework.start("l2-reth", Box::new(l2_reth_config)).await?; - (service.get_endpoint("authrpc"), service.get_port("p2p")) - }; - - // Start Builder Reth instance - - // The enode address depends on the p2p port of the L2 Reth instance - // TODO: We could also query the logs of the L2 Reth instance for the enode address and avoid this - let enode_address = format!( - "enode://{}@127.0.0.1:{}", - TEST_NODE_P2P_ADDR.1, l2_service.1 - ); - - let builder_reth_config = service_reth::RethConfig::new() - .jwt_secret_path(jwt_path.clone()) - .chain_config_path(genesis_path) - .trusted_peer(enode_address); - - let builder_authrpc_port = { - let service = framework - .start("builder", Box::new(builder_reth_config)) - .await?; - service.get_port("authrpc") - }; - - // run a proxy in between the builder and the rollup-boost if the proxy_handler is set - let builder_authrpc_port = if let Some(proxy_handler) = self.proxy_handler { - let proxy_port = framework.find_available_port(PROXY_START_PORT)?; - let _ = start_proxy_server(proxy_handler, proxy_port, builder_authrpc_port).await; - proxy_port - } else { - builder_authrpc_port - }; - let builder_service = format!("http://127.0.0.1:{}", builder_authrpc_port); - - // Start Rollup-boost instance - let rb_config = service_rb::RollupBoostConfig::new() - .jwt_path(jwt_path) - .l2_url(l2_service.0) - .builder_url(builder_service); - - let _ = framework.start("rollup-boost", Box::new(rb_config)).await?; - - Ok(RollupBoostTestHarness { - _framework: framework, - }) - } -} - -impl RollupBoostTestHarness { - pub async fn get_block_generator(&self) -> eyre::Result { - let rb_service = self._framework.services.get("rollup-boost").unwrap(); - let validator = BlockBuilderCreatorValidator::new(rb_service.log_path.clone()); - - let engine_api = EngineApi::new(&rb_service.get_endpoint("rpc"), DEFAULT_JWT_TOKEN) - .map_err(|_| IntegrationError::SetupError)?; - - let mut block_creator = SimpleBlockGenerator::new(validator, engine_api); - block_creator.init().await?; - Ok(block_creator) - } - - pub async fn get_client(&self) -> DebugClient { - let rb_service = self._framework.services.get("rollup-boost").unwrap(); - let endpoint = rb_service.get_endpoint("debug"); - - DebugClient::new(&endpoint).unwrap() - } -} - -/// A simple system that continuously generates empty blocks using the engine API -pub struct SimpleBlockGenerator { - validator: BlockBuilderCreatorValidator, - engine_api: EngineApi, - latest_hash: B256, - timestamp: u64, - version: Version, -} - -impl SimpleBlockGenerator { - pub fn new(validator: BlockBuilderCreatorValidator, engine_api: EngineApi) -> Self { - Self { - validator, - engine_api, - latest_hash: B256::ZERO, // temporary value - timestamp: 0, // temporary value - version: Version::V3, - } - } - - /// Initialize the block generator by fetching the latest block - pub async fn init(&mut self) -> eyre::Result<()> { - let latest_block = self.engine_api.latest().await?.expect("block not found"); - self.latest_hash = latest_block.header.hash; - self.timestamp = latest_block.header.timestamp; - Ok(()) - } - - /// Generate a single new block and return its hash - pub async fn generate_block( - &mut self, - empty_blocks: bool, - ) -> eyre::Result<(B256, PayloadSource)> { - let txns = match self.version { - Version::V4 => { - let tx = create_deposit_tx(); - Some(vec![tx]) - } - _ => None, - }; - - // Submit forkchoice update with payload attributes for the next block - let result = self - .engine_api - .update_forkchoice( - self.latest_hash, - self.latest_hash, - Some(OpPayloadAttributes { - payload_attributes: PayloadAttributes { - withdrawals: Some(vec![]), - parent_beacon_block_root: Some(B256::ZERO), - timestamp: self.timestamp + 1000, // 1 second later - prev_randao: B256::ZERO, - suggested_fee_recipient: Default::default(), - }, - transactions: txns, - no_tx_pool: Some(empty_blocks), - gas_limit: Some(10000000000), - eip_1559_params: None, - }), - ) - .await?; - - let payload_id = result.payload_id.expect("missing payload id"); - - if !empty_blocks { - tokio::time::sleep(tokio::time::Duration::from_secs(1)).await; - } - - let payload = self - .engine_api - .get_payload(self.version, payload_id) - .await?; - - // Submit the new payload to the node - let validation_status = self - .engine_api - .new_payload(NewPayload::from(payload.clone())) - .await?; - - if validation_status.status != PayloadStatusEnum::Valid { - return Err(eyre::eyre!("Invalid payload status")); - } - - let execution_payload = ExecutionPayload::from(payload); - let new_block_hash = execution_payload.block_hash(); - - // Update the chain's head - self.engine_api - .update_forkchoice(self.latest_hash, new_block_hash, None) - .await?; - - // Update internal state - self.latest_hash = new_block_hash; - self.timestamp = execution_payload.timestamp(); - - // Check who built the block in the rollup-boost logs - let block_creator = self - .validator - .get_block_creator(new_block_hash)? - .expect("block creator not found"); - - Ok((new_block_hash, block_creator)) - } -} - -pub struct BlockBuilderCreatorValidator { - log_path: PathBuf, -} - -impl BlockBuilderCreatorValidator { - pub fn new(log_path: PathBuf) -> Self { - Self { log_path } - } -} - -impl BlockBuilderCreatorValidator { - pub fn get_block_creator(&self, block_hash: B256) -> eyre::Result> { - let mut file = File::open(&self.log_path).map_err(|_| IntegrationError::LogError)?; - let mut contents = String::new(); - file.read_to_string(&mut contents) - .map_err(|_| IntegrationError::LogError)?; - - let search_query = format!("returning block hash={:#x}", block_hash); - - // Find the log line containing the block hash - for line in contents.lines() { - if line.contains(&search_query) { - // Extract the context=X part - if let Some(context_start) = line.find("context=") { - let context = line[context_start..] - .split_whitespace() - .next() - .ok_or(eyre::eyre!("no context found"))? - .split('=') - .nth(1) - .ok_or(eyre::eyre!("no context found"))?; - - match context { - "builder" => return Ok(Some(PayloadSource::Builder)), - "l2" => return Ok(Some(PayloadSource::L2)), - _ => panic!("Unknown context: {}", context), - } - } else { - panic!("no context found"); - } - } - } - - Ok(None) - } -} - -fn create_deposit_tx() -> Bytes { - const ISTHMUS_DATA: &[u8] = &hex!( - "098999be00000558000c5fc500000000000000030000000067a9f765000000000000002900000000000000000000000000000000000000000000000000000000006a6d09000000000000000000000000000000000000000000000000000000000000000172fcc8e8886636bdbe96ba0e4baab67ea7e7811633f52b52e8cf7a5123213b6f000000000000000000000000d3f2c5afb2d76f5579f326b0cd7da5f5a4126c3500004e2000000000000001f4" - ); - - let deposit_tx = TxDeposit { - source_hash: B256::default(), - from: address!("DeaDDEaDDeAdDeAdDEAdDEaddeAddEAdDEAd0001"), - to: TxKind::Call(address!("4200000000000000000000000000000000000015")), - mint: None, - value: U256::default(), - gas_limit: 210000, - is_system_transaction: true, - input: ISTHMUS_DATA.into(), - }; - - let mut buffer_without_header = BytesMut::new(); - deposit_tx.encode_2718(&mut buffer_without_header); - - buffer_without_header.to_vec().into() -} diff --git a/src/integration/service_rb.rs b/src/integration/service_rb.rs deleted file mode 100644 index e3cd2d47..00000000 --- a/src/integration/service_rb.rs +++ /dev/null @@ -1,72 +0,0 @@ -use crate::integration::{Arg, ReadyParams, Service, ServiceCommand}; -use std::{path::PathBuf, time::Duration}; - -#[derive(Default)] -pub struct RollupBoostConfig { - jwt_path: Option, - l2_url: Option, - builder_url: Option, -} - -impl RollupBoostConfig { - pub fn new() -> Self { - Self::default() - } - - pub fn jwt_path>(mut self, path: P) -> Self { - self.jwt_path = Some(path.into()); - self - } - - pub fn l2_url(mut self, url: String) -> Self { - self.l2_url = Some(url); - self - } - - pub fn builder_url(mut self, url: String) -> Self { - self.builder_url = Some(url); - self - } -} - -impl Service for RollupBoostConfig { - fn command(&self) -> ServiceCommand { - let mut bin_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); - bin_path.push("./target/debug/rollup-boost"); - - let jwt_path = self.jwt_path.as_ref().expect("jwt_path not set"); - - let cmd = ServiceCommand::new(bin_path.to_str().unwrap()) - .arg("--l2-jwt-path") - .arg(jwt_path.clone()) - .arg("--builder-jwt-path") - .arg(jwt_path.clone()) - .arg("--l2-url") - .arg(self.l2_url.as_ref().expect("l2_url not set")) - .arg("--builder-url") - .arg(self.builder_url.as_ref().expect("builder_url not set")) - .arg("--rpc-port") - .arg(Arg::Port { - name: "rpc".into(), - preferred: 8112, - }) - .arg("--debug-server-port") - .arg(Arg::Port { - name: "debug".into(), - preferred: 5555, - }) - .arg("--tracing") - .arg("--metrics") - .arg("--log-level") - .arg("trace"); - - cmd - } - - fn ready(&self) -> ReadyParams { - ReadyParams { - log_pattern: "Starting server on".to_string(), - duration: Duration::from_secs(5), - } - } -} diff --git a/src/integration/service_reth.rs b/src/integration/service_reth.rs deleted file mode 100644 index 4c7321ef..00000000 --- a/src/integration/service_reth.rs +++ /dev/null @@ -1,93 +0,0 @@ -use crate::integration::{Arg, ReadyParams, Service, ServiceCommand}; -use std::{path::PathBuf, time::Duration}; - -#[derive(Default)] -pub struct RethConfig { - jwt_secret_path: Option, - chain_config_path: Option, - p2p_secret_key: Option, - trusted_peer: Option, -} - -impl RethConfig { - pub fn new() -> Self { - Self::default() - } - - pub fn jwt_secret_path>(mut self, path: P) -> Self { - self.jwt_secret_path = Some(path.into()); - self - } - - pub fn chain_config_path>(mut self, path: P) -> Self { - self.chain_config_path = Some(path.into()); - self - } - - pub fn p2p_secret_key(mut self, key: String) -> Self { - self.p2p_secret_key = Some(key); - self - } - - pub fn trusted_peer(mut self, trusted_peer: String) -> Self { - self.trusted_peer = Some(trusted_peer); - self - } -} - -impl Service for RethConfig { - fn command(&self) -> ServiceCommand { - let mut cmd = ServiceCommand::new("op-reth") - .arg("node") - .arg("--authrpc.port") - .arg(Arg::Port { - name: "authrpc".into(), - preferred: 8551, - }) - .arg("--authrpc.jwtsecret") - .arg( - self.jwt_secret_path - .as_ref() - .expect("jwt_secret_path not set"), - ) - .arg("--chain") - .arg( - self.chain_config_path - .as_ref() - .expect("chain_config_path not set"), - ) - .arg("--datadir") - .arg(Arg::Dir { - name: "data".into(), - }) - .arg("--disable-discovery") - .arg("--port") - .arg(Arg::Port { - name: "p2p".into(), - preferred: 30303, // We do not use this port but it cannot be disabled - }) - .arg("--color") - .arg("never") - .arg("--ipcdisable"); - - if let Some(p2p_secret_key) = &self.p2p_secret_key { - cmd = cmd.arg("--p2p-secret-key").arg(Arg::FilePath { - name: "p2p_secret_key".into(), - content: p2p_secret_key.clone(), - }); - } - - if let Some(trusted_peer) = &self.trusted_peer { - cmd = cmd.arg("--trusted-peers").arg(trusted_peer); - } - - cmd - } - - fn ready(&self) -> ReadyParams { - ReadyParams { - log_pattern: "Starting consensus".to_string(), - duration: Duration::from_secs(5), - } - } -} diff --git a/src/lib.rs b/src/lib.rs index 7cf19d5c..0d131b74 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,10 +1,5 @@ -#![cfg_attr( - not(any(test, feature = "integration")), - warn(unused_crate_dependencies) -)] - +#![cfg_attr(not(test), warn(unused_crate_dependencies))] use dotenv as _; -use rustls as _; mod client; pub use client::{auth::*, http::*, rpc::*}; @@ -18,9 +13,6 @@ pub use debug_api::*; mod health; pub use health::{HealthLayer, HealthService}; -#[cfg(all(feature = "integration", test))] -mod integration; - mod metrics; pub use metrics::*; diff --git a/src/proxy.rs b/src/proxy.rs index 7afd478d..d0bfcd40 100644 --- a/src/proxy.rs +++ b/src/proxy.rs @@ -639,7 +639,6 @@ mod tests { #[tokio::test] async fn test_forward_miner_set_gas_price() -> eyre::Result<()> { - tokio::time::sleep(tokio::time::Duration::from_secs(1)).await; let test_harness = TestHarness::new().await?; let gas_price = U128::ZERO; @@ -651,6 +650,7 @@ mod tests { .await?; let expected_price = json!(gas_price); + tokio::time::sleep(tokio::time::Duration::from_secs(3)).await; // Assert the builder received the correct payload let builder = &test_harness.builder; @@ -673,7 +673,6 @@ mod tests { #[tokio::test] async fn test_forward_miner_set_gas_limit() -> eyre::Result<()> { - tokio::time::sleep(tokio::time::Duration::from_secs(1)).await; let test_harness = TestHarness::new().await?; let gas_limit = U128::ZERO; @@ -686,6 +685,8 @@ mod tests { let expected_price = json!(gas_limit); + tokio::time::sleep(tokio::time::Duration::from_secs(2)).await; + // Assert the builder received the correct payload let builder = &test_harness.builder; let builder_requests = builder.requests.lock().unwrap(); diff --git a/src/tracing.rs b/src/tracing.rs index fdd917c7..17664c48 100644 --- a/src/tracing.rs +++ b/src/tracing.rs @@ -9,6 +9,7 @@ use tracing::level_filters::LevelFilter; use tracing_opentelemetry::OpenTelemetryLayer; use tracing_subscriber::Layer; use tracing_subscriber::filter::Targets; +use tracing_subscriber::fmt::writer::BoxMakeWriter; use tracing_subscriber::layer::SubscriberExt; use crate::cli::{Args, LogFormat}; @@ -83,6 +84,17 @@ pub fn init_tracing(args: &Args) -> eyre::Result<()> { .with_default(LevelFilter::INFO) .with_target(&filter_name, args.log_level); + let writer = if let Some(path) = &args.log_file { + let file = std::fs::OpenOptions::new() + .create(true) + .append(true) + .open(path) + .context("Failed to open log file")?; + BoxMakeWriter::new(file) + } else { + BoxMakeWriter::new(std::io::stdout) + }; + // Weird control flow here is required because of type system if args.tracing { global::set_text_map_propagator(TraceContextPropagator::new()); @@ -120,6 +132,7 @@ pub fn init_tracing(args: &Args) -> eyre::Result<()> { tracing_subscriber::fmt::layer() .json() .with_ansi(false) + .with_writer(writer) .with_filter(log_filter.clone()), ), )?; @@ -129,6 +142,7 @@ pub fn init_tracing(args: &Args) -> eyre::Result<()> { registry.with( tracing_subscriber::fmt::layer() .with_ansi(false) + .with_writer(writer) .with_filter(log_filter.clone()), ), )?; @@ -142,6 +156,7 @@ pub fn init_tracing(args: &Args) -> eyre::Result<()> { tracing_subscriber::fmt::layer() .json() .with_ansi(false) + .with_writer(writer) .with_filter(log_filter.clone()), ), )?; @@ -151,6 +166,7 @@ pub fn init_tracing(args: &Args) -> eyre::Result<()> { registry.with( tracing_subscriber::fmt::layer() .with_ansi(false) + .with_writer(writer) .with_filter(log_filter.clone()), ), )?; diff --git a/tests/builder_full_delay.rs b/tests/builder_full_delay.rs new file mode 100644 index 00000000..79aa2c04 --- /dev/null +++ b/tests/builder_full_delay.rs @@ -0,0 +1,77 @@ +use common::RollupBoostTestHarnessBuilder; +use common::proxy::ProxyHandler; +use futures::FutureExt; +use serde_json::Value; +use std::pin::Pin; +use std::sync::{Arc, Mutex}; +use std::time::Duration; + +mod common; + +// Create a dynamic handler that delays all the calls by 2 seconds +struct DelayHandler { + delay: Arc>, +} + +impl ProxyHandler for DelayHandler { + fn handle( + &self, + _method: String, + _params: Value, + _result: Value, + ) -> Pin> + Send>> { + let delay = *self.delay.lock().unwrap(); + async move { + tokio::time::sleep(delay).await; + None + } + .boxed() + } +} + +#[tokio::test] +async fn builder_full_delay() -> eyre::Result<()> { + let delay = Arc::new(Mutex::new(Duration::from_secs(0))); + + let handler = Arc::new(DelayHandler { + delay: delay.clone(), + }); + + // This integration test checks that if the builder has a general delay in processing ANY of the requests, + // rollup-boost does not stop building blocks. + let harness = RollupBoostTestHarnessBuilder::new("builder_full_delay") + .proxy_handler(handler) + .build() + .await?; + + let mut block_generator = harness.block_generator().await?; + + // create 3 blocks that are processed by the builder + for _ in 0..3 { + let (_block, block_creator) = block_generator.generate_block(false).await?; + assert!( + block_creator.is_builder(), + "Block creator should be the builder" + ); + } + + // create 3 blocks that are processed by the builder + for _ in 0..3 { + let (_block, block_creator) = block_generator.generate_block(false).await?; + assert!( + block_creator.is_builder(), + "Block creator should be the builder" + ); + } + + // add the delay + *delay.lock().unwrap() = Duration::from_secs(5); + + // create 3 blocks that are processed by the builder + for _ in 0..3 { + let (_block, block_creator) = block_generator.generate_block(false).await?; + assert!(block_creator.is_l2(), "Block creator should be the l2"); + } + + Ok(()) +} diff --git a/tests/builder_returns_incorrect_block.rs b/tests/builder_returns_incorrect_block.rs new file mode 100644 index 00000000..d1ad4746 --- /dev/null +++ b/tests/builder_returns_incorrect_block.rs @@ -0,0 +1,69 @@ +use std::{pin::Pin, sync::Arc}; + +use alloy_primitives::B256; +use common::{RollupBoostTestHarnessBuilder, proxy::ProxyHandler}; +use futures::FutureExt as _; +use serde_json::Value; + +use op_alloy_rpc_types_engine::OpExecutionPayloadEnvelopeV3; + +mod common; + +struct Handler; + +impl ProxyHandler for Handler { + fn handle( + &self, + method: String, + _params: Value, + _result: Value, + ) -> Pin> + Send>> { + async move { + if method != "engine_getPayloadV3" { + return None; + } + + let mut payload = + serde_json::from_value::(_result).unwrap(); + + // modify the state root field + payload + .execution_payload + .payload_inner + .payload_inner + .state_root = B256::ZERO; + + let result = serde_json::to_value(&payload).unwrap(); + Some(result) + } + .boxed() + } +} + +#[tokio::test] +async fn builder_returns_incorrect_block() -> eyre::Result<()> { + // Test that the builder returns a block with an incorrect state root and that rollup-boost + // does not process it. + let harness = RollupBoostTestHarnessBuilder::new("builder_returns_incorrect_block") + .proxy_handler(Arc::new(Handler)) + .build() + .await?; + + let mut block_generator = harness.block_generator().await?; + + // create 3 blocks that are processed by the builder + for _ in 0..3 { + let (_block, block_creator) = block_generator.generate_block(false).await?; + assert!(block_creator.is_l2(), "Block creator should be the l2"); + } + // check that at some point we had the log "builder payload was not valid" which signals + // that the builder returned a payload that was not valid and rollup-boost did not process it. + // read lines + let logs = std::fs::read_to_string(harness.rollup_boost.args().log_file.clone().unwrap())?; + assert!( + logs.contains("Invalid payload"), + "Logs should contain the message 'builder payload was not valid'" + ); + + Ok(()) +} diff --git a/tests/common/mod.rs b/tests/common/mod.rs new file mode 100644 index 00000000..929f71dd --- /dev/null +++ b/tests/common/mod.rs @@ -0,0 +1,491 @@ +#![allow(dead_code)] +use alloy_eips::Encodable2718; +use alloy_primitives::{B256, Bytes, TxKind, U256, address, hex}; +use alloy_rpc_types_engine::{ExecutionPayload, JwtSecret}; +use alloy_rpc_types_engine::{ + ForkchoiceState, ForkchoiceUpdated, PayloadAttributes, PayloadId, PayloadStatus, + PayloadStatusEnum, +}; +use alloy_rpc_types_eth::BlockNumberOrTag; +use bytes::BytesMut; +use futures::FutureExt; +use futures::future::BoxFuture; +use jsonrpsee::http_client::{HttpClient, transport::HttpBackend}; +use jsonrpsee::proc_macros::rpc; +use op_alloy_consensus::TxDeposit; +use op_alloy_rpc_types_engine::OpPayloadAttributes; +use parking_lot::Mutex; +use proxy::{ProxyHandler, start_proxy_server}; +use rollup_boost::DebugClient; +use rollup_boost::{AuthClientLayer, AuthClientService}; +use rollup_boost::{EngineApiClient, OpExecutionPayloadEnvelope, Version}; +use rollup_boost::{NewPayload, PayloadSource}; +use services::op_reth::{AUTH_RPC_PORT, OpRethConfig, OpRethImage, OpRethMehods, P2P_PORT}; +use services::rollup_boost::{RollupBoost, RollupBoostConfig}; +use std::collections::HashSet; +use std::net::TcpListener; +use std::path::PathBuf; +use std::str::FromStr; +use std::sync::{Arc, LazyLock}; +use std::time::SystemTime; +use testcontainers::core::ContainerPort; +use testcontainers::core::client::docker_client_instance; +use testcontainers::core::logs::LogFrame; +use testcontainers::core::logs::consumer::LogConsumer; +use testcontainers::runners::AsyncRunner; +use testcontainers::{ContainerAsync, ImageExt}; +use time::{OffsetDateTime, format_description}; +use tokio::io::AsyncWriteExt as _; +use tracing::info; + +/// Default JWT token for testing purposes +pub const JWT_SECRET: &str = "688f5d737bad920bdfb2fc2f488d6b6209eebda1dae949a8de91398d932c517a"; +pub const L2_P2P_ENODE: &str = "3479db4d9217fb5d7a8ed4d61ac36e120b05d36c2eefb795dc42ff2e971f251a2315f5649ea1833271e020b9adc98d5db9973c7ed92d6b2f1f2223088c3d852f"; +pub static TEST_DATA: LazyLock = + LazyLock::new(|| format!("{}/tests/common/test_data", env!("CARGO_MANIFEST_DIR"))); + +pub mod proxy; +pub mod services; + +pub struct LoggingConsumer { + target: String, + log_file: tokio::sync::Mutex, +} + +impl LogConsumer for LoggingConsumer { + fn accept<'a>(&'a self, record: &'a LogFrame) -> BoxFuture<'a, ()> { + async move { + match record { + testcontainers::core::logs::LogFrame::StdOut(bytes) => { + info!(target = self.target, "{}", String::from_utf8_lossy(bytes)); + self.log_file.lock().await.write_all(bytes).await.unwrap(); + } + testcontainers::core::logs::LogFrame::StdErr(bytes) => { + info!(target = self.target, "{}", String::from_utf8_lossy(bytes)); + self.log_file.lock().await.write_all(bytes).await.unwrap(); + } + } + } + .boxed() + } +} + +pub struct EngineApi { + pub engine_api_client: HttpClient>, +} + +// TODO: Use client/rpc.rs instead +impl EngineApi { + pub fn new(url: &str, secret: &str) -> eyre::Result { + let secret_layer = AuthClientLayer::new(JwtSecret::from_str(secret)?); + let middleware = tower::ServiceBuilder::default().layer(secret_layer); + let client = jsonrpsee::http_client::HttpClientBuilder::default() + .set_http_middleware(middleware) + .build(url) + .expect("Failed to create http client"); + + Ok(Self { + engine_api_client: client, + }) + } + + pub async fn get_payload( + &self, + version: Version, + payload_id: PayloadId, + ) -> eyre::Result { + match version { + Version::V3 => Ok(OpExecutionPayloadEnvelope::V3( + EngineApiClient::get_payload_v3(&self.engine_api_client, payload_id).await?, + )), + Version::V4 => Ok(OpExecutionPayloadEnvelope::V4( + EngineApiClient::get_payload_v4(&self.engine_api_client, payload_id).await?, + )), + } + } + + pub async fn new_payload(&self, payload: NewPayload) -> eyre::Result { + match payload { + NewPayload::V3(new_payload) => Ok(EngineApiClient::new_payload_v3( + &self.engine_api_client, + new_payload.payload, + new_payload.versioned_hashes, + new_payload.parent_beacon_block_root, + ) + .await?), + NewPayload::V4(new_payload) => Ok(EngineApiClient::new_payload_v4( + &self.engine_api_client, + new_payload.payload, + new_payload.versioned_hashes, + new_payload.parent_beacon_block_root, + new_payload.execution_requests, + ) + .await?), + } + } + + pub async fn update_forkchoice( + &self, + current_head: B256, + new_head: B256, + payload_attributes: Option, + ) -> eyre::Result { + Ok(EngineApiClient::fork_choice_updated_v3( + &self.engine_api_client, + ForkchoiceState { + head_block_hash: new_head, + safe_block_hash: current_head, + finalized_block_hash: current_head, + }, + payload_attributes, + ) + .await?) + } + + pub async fn latest(&self) -> eyre::Result> { + Ok(BlockApiClient::get_block_by_number( + &self.engine_api_client, + BlockNumberOrTag::Latest, + false, + ) + .await?) + } +} + +#[rpc(client, namespace = "eth")] +pub trait BlockApi { + #[method(name = "getBlockByNumber")] + async fn get_block_by_number( + &self, + block_number: BlockNumberOrTag, + include_txs: bool, + ) -> RpcResult>; +} + +/// Test flavor that sets up one Rollup-boost instance connected to two Reth nodes +pub struct RollupBoostTestHarness { + pub l2: ContainerAsync, + pub builder: ContainerAsync, + pub rollup_boost: RollupBoost, +} + +pub struct RollupBoostTestHarnessBuilder { + test_name: String, + proxy_handler: Option>, +} + +impl RollupBoostTestHarnessBuilder { + pub fn new(test_name: &str) -> Self { + Self { + test_name: test_name.to_string(), + proxy_handler: None, + } + } + + pub fn file_path(&self, service_name: &str) -> eyre::Result { + let dt: OffsetDateTime = SystemTime::now().into(); + let format = format_description::parse("[year]_[month]_[day]_[hour]_[minute]_[second]")?; + let timestamp = dt.format(&format)?; + + let dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("integration_logs") + .join(self.test_name.clone()) + .join(timestamp); + std::fs::create_dir_all(&dir)?; + + let file_name = format!("{service_name}.log"); + Ok(dir.join(file_name)) + } + + pub async fn async_log_file(&self, service_name: &str) -> eyre::Result { + let file_path = self.file_path(service_name)?; + Ok(tokio::fs::OpenOptions::new() + .append(true) + .create(true) + .open(file_path) + .await?) + } + + pub async fn log_consumer(&self, service_name: &str) -> eyre::Result { + let file = self.async_log_file(service_name).await?; + Ok(LoggingConsumer { + target: service_name.to_string(), + log_file: tokio::sync::Mutex::new(file), + }) + } + + pub fn proxy_handler(mut self, proxy_handler: Arc) -> Self { + self.proxy_handler = Some(proxy_handler); + self + } + + pub async fn build(self) -> eyre::Result { + let network = rand::random::().to_string(); + let l2_log_consumer = self.log_consumer("l2").await?; + let builder_log_consumer = self.log_consumer("builder").await?; + let rollup_boost_log_file_path = self.file_path("rollup_boost")?; + + let l2_p2p_port = get_available_port(); + let l2 = OpRethConfig::default() + .set_p2p_secret(Some(PathBuf::from(format!( + "{}/p2p_secret.hex", + *TEST_DATA + )))) + .build()? + .with_mapped_port(l2_p2p_port, ContainerPort::Tcp(P2P_PORT)) + .with_mapped_port(l2_p2p_port, ContainerPort::Udp(P2P_PORT)) + .with_mapped_port(get_available_port(), ContainerPort::Tcp(AUTH_RPC_PORT)) + .with_network(&network) + .with_log_consumer(l2_log_consumer) + .start() + .await?; + + let client = docker_client_instance().await?; + let res = client.inspect_container(l2.id(), None).await?; + let name = res.name.unwrap()[1..].to_string(); // remove the leading '/' + + let l2_enode = format!("enode://{}@{}:{}", L2_P2P_ENODE, name, P2P_PORT); + + let builder_p2p_port = get_available_port(); + let builder = OpRethConfig::default() + .set_trusted_peers(vec![l2_enode]) + .build()? + .with_mapped_port(builder_p2p_port, ContainerPort::Tcp(P2P_PORT)) + .with_mapped_port(builder_p2p_port, ContainerPort::Udp(P2P_PORT)) + .with_mapped_port(get_available_port(), ContainerPort::Tcp(AUTH_RPC_PORT)) + .with_network(&network) + .with_log_consumer(builder_log_consumer) + .start() + .await?; + + println!("l2 authrpc: {}", l2.auth_rpc().await?); + println!("builder authrpc: {}", builder.auth_rpc().await?); + + // run a proxy in between the builder and the rollup-boost if the proxy_handler is set + let mut builder_authrpc_port = builder.auth_rpc_port().await?; + if let Some(proxy_handler) = self.proxy_handler { + println!("starting proxy server"); + let proxy_port = get_available_port(); + start_proxy_server(proxy_handler, proxy_port, builder_authrpc_port).await?; + builder_authrpc_port = proxy_port + }; + let builder_url = format!("http://localhost:{}/", builder_authrpc_port); + println!("proxy authrpc: {}", builder_url); + + // Start Rollup-boost instance + let mut rollup_boost = RollupBoostConfig::default(); + rollup_boost.args.l2_client.l2_url = l2.auth_rpc().await?; + rollup_boost.args.builder.builder_url = builder_url.try_into().unwrap(); + rollup_boost.args.log_file = Some(rollup_boost_log_file_path); + let rollup_boost = rollup_boost.start().await; + println!("rollup-boost authrpc: {}", rollup_boost.rpc_endpoint()); + println!("rollup-boost metrics: {}", rollup_boost.metrics_endpoint()); + + Ok(RollupBoostTestHarness { + l2, + builder, + rollup_boost, + }) + } +} + +impl RollupBoostTestHarness { + pub async fn block_generator(&self) -> eyre::Result { + let validator = + BlockBuilderCreatorValidator::new(self.rollup_boost.args().log_file.clone().unwrap()); + + let engine_api = EngineApi::new(&self.rollup_boost.rpc_endpoint(), JWT_SECRET)?; + + let mut block_creator = SimpleBlockGenerator::new(validator, engine_api); + block_creator.init().await?; + Ok(block_creator) + } + + pub async fn debug_client(&self) -> DebugClient { + DebugClient::new(&self.rollup_boost.debug_endpoint()).unwrap() + } +} + +/// A simple system that continuously generates empty blocks using the engine API +pub struct SimpleBlockGenerator { + validator: BlockBuilderCreatorValidator, + engine_api: EngineApi, + latest_hash: B256, + timestamp: u64, + version: Version, +} + +impl SimpleBlockGenerator { + pub fn new(validator: BlockBuilderCreatorValidator, engine_api: EngineApi) -> Self { + Self { + validator, + engine_api, + latest_hash: B256::ZERO, // temporary value + timestamp: 0, // temporary value + version: Version::V3, + } + } + + /// Initialize the block generator by fetching the latest block + pub async fn init(&mut self) -> eyre::Result<()> { + let latest_block = self.engine_api.latest().await?.expect("block not found"); + self.latest_hash = latest_block.header.hash; + self.timestamp = latest_block.header.timestamp; + Ok(()) + } + + /// Generate a single new block and return its hash + pub async fn generate_block( + &mut self, + empty_blocks: bool, + ) -> eyre::Result<(B256, PayloadSource)> { + let txns = match self.version { + Version::V4 => { + let tx = create_deposit_tx(); + Some(vec![tx]) + } + _ => None, + }; + + // Submit forkchoice update with payload attributes for the next block + let result = self + .engine_api + .update_forkchoice( + self.latest_hash, + self.latest_hash, + Some(OpPayloadAttributes { + payload_attributes: PayloadAttributes { + withdrawals: Some(vec![]), + parent_beacon_block_root: Some(B256::ZERO), + timestamp: self.timestamp + 1000, // 1 second later + prev_randao: B256::ZERO, + suggested_fee_recipient: Default::default(), + }, + transactions: txns, + no_tx_pool: Some(empty_blocks), + gas_limit: Some(10000000000), + eip_1559_params: None, + }), + ) + .await?; + + let payload_id = result.payload_id.expect("missing payload id"); + + if !empty_blocks { + tokio::time::sleep(tokio::time::Duration::from_secs(1)).await; + } + + let payload = self + .engine_api + .get_payload(self.version, payload_id) + .await?; + + // Submit the new payload to the node + let validation_status = self + .engine_api + .new_payload(NewPayload::from(payload.clone())) + .await?; + + if validation_status.status != PayloadStatusEnum::Valid { + return Err(eyre::eyre!("Invalid payload status")); + } + + let execution_payload = ExecutionPayload::from(payload); + let new_block_hash = execution_payload.block_hash(); + + // Update the chain's head + self.engine_api + .update_forkchoice(self.latest_hash, new_block_hash, None) + .await?; + + // Update internal state + self.latest_hash = new_block_hash; + self.timestamp = execution_payload.timestamp(); + + // Check who built the block in the rollup-boost logs + let block_creator = self + .validator + .get_block_creator(new_block_hash) + .await? + .expect("block creator not found"); + + Ok((new_block_hash, block_creator)) + } +} + +pub struct BlockBuilderCreatorValidator { + file: PathBuf, +} + +impl BlockBuilderCreatorValidator { + pub fn new(file: PathBuf) -> Self { + Self { file } + } +} + +impl BlockBuilderCreatorValidator { + pub async fn get_block_creator(&self, block_hash: B256) -> eyre::Result> { + let contents = std::fs::read_to_string(&self.file)?; + + let search_query = format!("returning block hash={:#x}", block_hash); + + // Find the log line containing the block hash + for line in contents.lines() { + if line.contains(&search_query) { + // Extract the context=X part + if let Some(context_start) = line.find("context=") { + let context = line[context_start..] + .split_whitespace() + .next() + .ok_or(eyre::eyre!("no context found"))? + .split('=') + .nth(1) + .ok_or(eyre::eyre!("no context found"))?; + + match context { + "builder" => return Ok(Some(PayloadSource::Builder)), + "l2" => return Ok(Some(PayloadSource::L2)), + _ => panic!("Unknown context: {}", context), + } + } else { + panic!("no context found"); + } + } + } + + Ok(None) + } +} + +fn create_deposit_tx() -> Bytes { + const ISTHMUS_DATA: &[u8] = &hex!( + "098999be00000558000c5fc500000000000000030000000067a9f765000000000000002900000000000000000000000000000000000000000000000000000000006a6d09000000000000000000000000000000000000000000000000000000000000000172fcc8e8886636bdbe96ba0e4baab67ea7e7811633f52b52e8cf7a5123213b6f000000000000000000000000d3f2c5afb2d76f5579f326b0cd7da5f5a4126c3500004e2000000000000001f4" + ); + + let deposit_tx = TxDeposit { + source_hash: B256::default(), + from: address!("DeaDDEaDDeAdDeAdDEAdDEaddeAddEAdDEAd0001"), + to: TxKind::Call(address!("4200000000000000000000000000000000000015")), + mint: None, + value: U256::default(), + gas_limit: 210000, + is_system_transaction: true, + input: ISTHMUS_DATA.into(), + }; + + let mut buffer_without_header = BytesMut::new(); + deposit_tx.encode_2718(&mut buffer_without_header); + + buffer_without_header.to_vec().into() +} + +pub fn get_available_port() -> u16 { + static CLAIMED_PORTS: LazyLock>> = + LazyLock::new(|| Mutex::new(HashSet::new())); + loop { + let port: u16 = rand::random_range(1000..20000); + if TcpListener::bind(("127.0.0.1", port)).is_ok() && CLAIMED_PORTS.lock().insert(port) { + return port; + } + } +} diff --git a/src/integration/proxy.rs b/tests/common/proxy.rs similarity index 89% rename from src/integration/proxy.rs rename to tests/common/proxy.rs index cf3cce5f..356729b5 100644 --- a/src/integration/proxy.rs +++ b/tests/common/proxy.rs @@ -9,6 +9,7 @@ use hyper_util::rt::TokioIo; use serde::{Deserialize, Serialize}; use serde_json::Value; use std::net::SocketAddr; +use std::pin::Pin; use std::sync::Arc; use tokio::net::{TcpListener, TcpStream}; @@ -37,13 +38,20 @@ pub struct JsonRpcError { data: Option, } -pub type DynHandlerFn = Box Option + Send + Sync>; +pub trait ProxyHandler: Send + Sync + 'static { + fn handle( + &self, + method: String, + params: Value, + result: Value, + ) -> Pin> + Send>>; +} // Structure to hold the target address that we'll pass to the proxy function #[derive(Clone)] struct ProxyConfig { target_addr: SocketAddr, - handler: Arc, + handler: Arc, } async fn proxy( @@ -78,7 +86,10 @@ async fn proxy( let json_rpc_response = serde_json::from_slice::(&bytes).unwrap(); let bytes = if let Some(result) = json_rpc_response.clone().result { - let value = (config.handler)(&json_rpc_request.method, json_rpc_request.params, result); + let value = config + .handler + .handle(json_rpc_request.method, json_rpc_request.params, result) + .await; if let Some(value) = value { // If the handler returns a value, we replace the result with the new value // The callback only returns the result of the jsonrpc request so we have to wrap it up @@ -105,16 +116,16 @@ async fn proxy( } pub async fn start_proxy_server( - handler: DynHandlerFn, + handler: Arc, listen_port: u16, target_port: u16, -) -> Result<(), Box> { +) -> eyre::Result<()> { let listen_addr = SocketAddr::from(([127, 0, 0, 1], listen_port)); let target_addr = SocketAddr::from(([127, 0, 0, 1], target_port)); let config = ProxyConfig { target_addr, - handler: Arc::new(handler), + handler, }; let listener = TcpListener::bind(listen_addr).await?; diff --git a/tests/common/services/mod.rs b/tests/common/services/mod.rs new file mode 100644 index 00000000..41ef9049 --- /dev/null +++ b/tests/common/services/mod.rs @@ -0,0 +1,2 @@ +pub mod op_reth; +pub mod rollup_boost; diff --git a/tests/common/services/op_reth.rs b/tests/common/services/op_reth.rs new file mode 100644 index 00000000..f0921a78 --- /dev/null +++ b/tests/common/services/op_reth.rs @@ -0,0 +1,207 @@ +use std::{ + borrow::Cow, + collections::HashMap, + fs::File, + io::BufReader, + path::PathBuf, + sync::LazyLock, + time::{SystemTime, UNIX_EPOCH}, +}; + +use http::Uri; +use serde_json::Value; +use testcontainers::{ + ContainerAsync, CopyToContainer, Image, + core::{ContainerPort, WaitFor}, +}; + +use crate::common::TEST_DATA; + +const NAME: &str = "ghcr.io/paradigmxyz/op-reth"; +const TAG: &str = "v1.3.4"; + +pub const AUTH_RPC_PORT: u16 = 8551; +pub const P2P_PORT: u16 = 30303; + +// Genesis should only be created once and reused for +// each instance +static GENESIS: LazyLock = LazyLock::new(|| { + let file = File::open(PathBuf::from(format!("{}/genesis.json", *TEST_DATA))).unwrap(); + let reader = BufReader::new(file); + let mut genesis: Value = serde_json::from_reader(reader).unwrap(); + + // Update the timestamp field + let timestamp = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_secs(); + + if let Some(config) = genesis.as_object_mut() { + // Assuming timestamp is at the root level - adjust path as needed + config["timestamp"] = Value::String(format!("0x{:x}", timestamp)); + } + + serde_json::to_string_pretty(&genesis).unwrap() +}); + +#[derive(Debug, Clone)] +pub struct OpRethConfig { + jwt_secret: PathBuf, + p2p_secret: Option, + pub trusted_peers: Vec, + pub color: String, + pub ipcdisable: bool, + pub env_vars: HashMap, +} + +impl Default for OpRethConfig { + fn default() -> Self { + Self { + jwt_secret: PathBuf::from(format!("{}/jwt_secret.hex", *TEST_DATA)), + p2p_secret: None, + trusted_peers: vec![], + color: "never".to_string(), + ipcdisable: true, + env_vars: Default::default(), + } + } +} + +impl OpRethConfig { + pub fn set_trusted_peers(mut self, trusted_peers: Vec) -> Self { + self.trusted_peers = trusted_peers; + self + } + + pub fn set_jwt_secret(mut self, jwt_secret: PathBuf) -> Self { + self.jwt_secret = jwt_secret; + self + } + + pub fn set_p2p_secret(mut self, p2p_secret: Option) -> Self { + self.p2p_secret = p2p_secret; + self + } + + pub fn build(self) -> eyre::Result { + let mut copy_to_sources = vec![ + CopyToContainer::new( + std::fs::read_to_string(&self.jwt_secret)?.into_bytes(), + "/jwt_secret.hex".to_string(), + ), + CopyToContainer::new(GENESIS.clone().into_bytes(), "/genesis.json".to_string()), + ]; + + if let Some(p2p_secret) = &self.p2p_secret { + let p2p_string = std::fs::read_to_string(p2p_secret) + .unwrap() + .replace("\n", ""); + copy_to_sources.push(CopyToContainer::new( + p2p_string.into_bytes(), + "/p2p_secret.hex".to_string(), + )); + } + + let expose_ports = vec![]; + + Ok(OpRethImage { + config: self, + copy_to_sources, + expose_ports, + }) + } +} + +impl OpRethImage { + pub fn config(&self) -> &OpRethConfig { + &self.config + } +} + +#[derive(Debug, Clone)] +pub struct OpRethImage { + config: OpRethConfig, + copy_to_sources: Vec, + expose_ports: Vec, +} + +impl Image for OpRethImage { + fn name(&self) -> &str { + NAME + } + + fn tag(&self) -> &str { + TAG + } + + fn ready_conditions(&self) -> Vec { + vec![WaitFor::message_on_stdout("Starting consensus")] + } + + fn env_vars( + &self, + ) -> impl IntoIterator>, impl Into>)> { + &self.config.env_vars + } + + fn copy_to_sources(&self) -> impl IntoIterator { + self.copy_to_sources.iter() + } + + fn cmd(&self) -> impl IntoIterator>> { + let mut cmd = vec![ + "node".to_string(), + "--port=30303".to_string(), + "--addr=0.0.0.0".to_string(), + "--http".to_string(), + "--http.addr=0.0.0.0".to_string(), + "--authrpc.port=8551".to_string(), + "--authrpc.addr=0.0.0.0".to_string(), + "--authrpc.jwtsecret=/jwt_secret.hex".to_string(), + "--chain=/genesis.json".to_string(), + "--log.stdout.filter=trace".to_string(), + "-vvvvv".to_string(), + "--disable-discovery".to_string(), + "--color".to_string(), + self.config.color.clone(), + ]; + if self.config.p2p_secret.is_some() { + cmd.push("--p2p-secret-key=/p2p_secret.hex".to_string()); + } + if !self.config.trusted_peers.is_empty() { + println!("Trusted peers: {:?}", self.config.trusted_peers); + cmd.extend([ + "--trusted-peers".to_string(), + self.config.trusted_peers.join(","), + ]); + } + if self.config.ipcdisable { + cmd.push("--ipcdisable".to_string()); + } + cmd + } + + fn expose_ports(&self) -> &[ContainerPort] { + &self.expose_ports + } +} + +pub trait OpRethMehods { + async fn auth_rpc(&self) -> eyre::Result; + async fn auth_rpc_port(&self) -> eyre::Result; +} + +impl OpRethMehods for ContainerAsync { + async fn auth_rpc_port(&self) -> eyre::Result { + Ok(self.get_host_port_ipv4(AUTH_RPC_PORT).await?) + } + + async fn auth_rpc(&self) -> eyre::Result { + Ok(format!( + "http://{}:{}", + self.get_host().await?, + self.get_host_port_ipv4(AUTH_RPC_PORT).await? + ) + .parse()?) + } +} diff --git a/tests/common/services/rollup_boost.rs b/tests/common/services/rollup_boost.rs new file mode 100644 index 00000000..4efd3f5c --- /dev/null +++ b/tests/common/services/rollup_boost.rs @@ -0,0 +1,76 @@ +use std::time::Duration; + +use clap::Parser; +use rollup_boost::Args; +use tokio::task::JoinHandle; + +use crate::common::{TEST_DATA, get_available_port}; + +#[derive(Debug)] +pub struct RollupBoost { + args: Args, + pub _handle: JoinHandle>, +} + +impl RollupBoost { + pub fn args(&self) -> &Args { + &self.args + } + + pub fn rpc_endpoint(&self) -> String { + format!("http://localhost:{}", self.args.rpc_port) + } + + pub fn metrics_endpoint(&self) -> String { + format!("http://localhost:{}", self.args.metrics_port) + } + + pub fn debug_endpoint(&self) -> String { + format!("http://localhost:{}", self.args.debug_server_port) + } +} + +#[derive(Clone, Debug)] +pub struct RollupBoostConfig { + pub args: Args, +} + +impl Default for RollupBoostConfig { + fn default() -> Self { + let mut args = Args::parse_from([ + "rollup-boost", + &format!("--l2-jwt-path={}/jwt_secret.hex", *TEST_DATA), + &format!("--builder-jwt-path={}/jwt_secret.hex", *TEST_DATA), + "--log-level=trace", + "--tracing", + "--metrics", + ]); + + args.rpc_port = get_available_port(); + args.metrics_port = get_available_port(); + args.debug_server_port = get_available_port(); + + Self { args } + } +} + +impl RollupBoostConfig { + pub async fn start(self) -> RollupBoost { + let args = self.args.clone(); + let _handle = tokio::spawn(async move { + let res = args.clone().run().await; + if let Err(e) = &res { + eprintln!("Error: {:?}", e); + } + res + }); + + // Allow some time for the app to startup + tokio::time::sleep(Duration::from_secs(4)).await; + + RollupBoost { + args: self.args, + _handle, + } + } +} diff --git a/src/integration/testdata/genesis.json b/tests/common/test_data/genesis.json similarity index 100% rename from src/integration/testdata/genesis.json rename to tests/common/test_data/genesis.json diff --git a/tests/common/test_data/jwt_secret.hex b/tests/common/test_data/jwt_secret.hex new file mode 100644 index 00000000..6e72091c --- /dev/null +++ b/tests/common/test_data/jwt_secret.hex @@ -0,0 +1 @@ +688f5d737bad920bdfb2fc2f488d6b6209eebda1dae949a8de91398d932c517a diff --git a/tests/common/test_data/p2p_secret.hex b/tests/common/test_data/p2p_secret.hex new file mode 100644 index 00000000..499ea6cd --- /dev/null +++ b/tests/common/test_data/p2p_secret.hex @@ -0,0 +1 @@ +a11ac89899cd86e36b6fb881ec1255b8a92a688790b7d950f8b7d8dd626671fb diff --git a/tests/execution_mode.rs b/tests/execution_mode.rs new file mode 100644 index 00000000..ead3884d --- /dev/null +++ b/tests/execution_mode.rs @@ -0,0 +1,116 @@ +use common::proxy::ProxyHandler; +use futures::FutureExt as _; +use rollup_boost::ExecutionMode; +use serde_json::Value; +use std::pin::Pin; +use std::sync::{Arc, Mutex}; +use std::time::Duration; + +mod common; + +use crate::common::RollupBoostTestHarnessBuilder; + +struct CounterHandler { + counter: Arc>, +} + +impl ProxyHandler for CounterHandler { + fn handle( + &self, + _method: String, + _params: Value, + _result: Value, + ) -> Pin> + Send>> { + *self.counter.lock().unwrap() += 1; + async move { None }.boxed() + } +} + +#[tokio::test] +async fn execution_mode() -> eyre::Result<()> { + // Create a counter that increases whenever we receive a new RPC call in the builder + let counter = Arc::new(Mutex::new(0)); + let handler = Arc::new(CounterHandler { + counter: counter.clone(), + }); + + let harness = RollupBoostTestHarnessBuilder::new("execution_mode") + .proxy_handler(handler) + .build() + .await?; + let mut block_generator = harness.block_generator().await?; + + // start creating 5 empty blocks which are processed by the builder + for _ in 0..5 { + let (_block, block_creator) = block_generator.generate_block(false).await?; + assert!( + block_creator.is_builder(), + "Block creator should be the builder" + ); + } + + let client = harness.debug_client().await; + + // enable dry run mode + { + let response = client + .set_execution_mode(ExecutionMode::DryRun) + .await + .unwrap(); + assert_eq!(response.execution_mode, ExecutionMode::DryRun); + + // the new valid block should be created the the l2 builder + let (_block, block_creator) = block_generator.generate_block(false).await?; + assert!(block_creator.is_l2(), "Block creator should be l2"); + } + + // toggle again dry run mode + { + let response = client + .set_execution_mode(ExecutionMode::Enabled) + .await + .unwrap(); + assert_eq!(response.execution_mode, ExecutionMode::Enabled); + + // the new valid block should be created the the builder + let (_block, block_creator) = block_generator.generate_block(false).await?; + assert!( + block_creator.is_builder(), + "Block creator should be the builder" + ); + } + + // sleep for 1 second so that it has time to send the last FCU request to the builder + // and there is not a race condition with the disable call + std::thread::sleep(Duration::from_secs(1)); + + tracing::info!("Setting execution mode to disabled"); + + // Set the execution mode to disabled and reset the counter in the proxy to 0 + // to track the number of calls to the builder during the disabled mode which + // should be 0 + { + let response = client + .set_execution_mode(ExecutionMode::Disabled) + .await + .unwrap(); + assert_eq!(response.execution_mode, ExecutionMode::Disabled); + + // reset the counter in the proxy + *counter.lock().unwrap() = 0; + + // create 5 blocks which are processed by the l2 clients + for _ in 0..5 { + let (_block, block_creator) = block_generator.generate_block(false).await?; + assert!(block_creator.is_l2(), "Block creator should be l2"); + } + + assert_eq!( + *counter.lock().unwrap(), + 0, + "Number of calls to the builder should be 0", + ); + } + + Ok(()) +} diff --git a/tests/no_tx_pool.rs b/tests/no_tx_pool.rs new file mode 100644 index 00000000..ac0e708c --- /dev/null +++ b/tests/no_tx_pool.rs @@ -0,0 +1,29 @@ +mod common; + +use crate::common::RollupBoostTestHarnessBuilder; + +#[tokio::test] +async fn no_tx_pool() -> eyre::Result<()> { + let harness = RollupBoostTestHarnessBuilder::new("no_tx_pool") + .build() + .await?; + let mut block_generator = harness.block_generator().await?; + + // start creating 5 empty blocks which are processed by the L2 builder + for _ in 0..5 { + let (_block, block_creator) = block_generator.generate_block(true).await?; + assert!(block_creator.is_l2(), "Block creator should be l2"); + } + + // process 5 more non empty blocks which are processed by the builder. + // The builder should be on sync because it has received the new payload requests from rollup-boost. + for _ in 0..5 { + let (_block, block_creator) = block_generator.generate_block(false).await?; + assert!( + block_creator.is_builder(), + "Block creator should be the builder" + ); + } + + Ok(()) +} diff --git a/tests/remote_builder_down.rs b/tests/remote_builder_down.rs new file mode 100644 index 00000000..cf459697 --- /dev/null +++ b/tests/remote_builder_down.rs @@ -0,0 +1,50 @@ +mod common; + +use std::time::Duration; + +use testcontainers::core::client::docker_client_instance; + +use crate::common::RollupBoostTestHarnessBuilder; + +#[tokio::test] +async fn remote_builder_down() -> eyre::Result<()> { + let harness = RollupBoostTestHarnessBuilder::new("remote_builder_down") + .build() + .await?; + let mut block_generator = harness.block_generator().await?; + + for _ in 0..3 { + let (_block, block_creator) = block_generator.generate_block(false).await?; + assert!( + block_creator.is_builder(), + "Block creator should be the builder" + ); + } + + // stop the builder + let client = docker_client_instance().await?; + client.pause_container(harness.builder.id()).await?; + tokio::time::sleep(Duration::from_secs(2)).await; + + // create 3 new blocks that are processed by the l2 builder + for _ in 0..3 { + let (_block, block_creator) = block_generator.generate_block(false).await?; + assert!(block_creator.is_l2(), "Block creator should be l2"); + } + + client.unpause_container(harness.builder.id()).await?; + + // Sleep briefly to allow the builder to sync + tokio::time::sleep(Duration::from_secs(2)).await; + + // create 3 new blocks that are processed by the l2 builder because the builder is not synced with the previous 3 blocks + for _ in 0..3 { + let (_block, block_creator) = block_generator.generate_block(false).await?; + assert!( + block_creator.is_builder(), + "Block creator should be builder" + ); + } + + Ok(()) +} diff --git a/tests/simple.rs b/tests/simple.rs new file mode 100644 index 00000000..30390272 --- /dev/null +++ b/tests/simple.rs @@ -0,0 +1,19 @@ +use common::RollupBoostTestHarnessBuilder; + +mod common; + +#[tokio::test] +async fn test_integration_simple() -> eyre::Result<()> { + let harness = RollupBoostTestHarnessBuilder::new("simple").build().await?; + let mut block_generator = harness.block_generator().await?; + + for _ in 0..5 { + let (_block, block_creator) = block_generator.generate_block(false).await?; + assert!( + block_creator.is_builder(), + "Block creator should be the builder" + ); + } + + Ok(()) +}