diff --git a/.github/workflows/main-eest.yml b/.github/workflows/main-eest.yml index c45d97a11a..92e389b685 100644 --- a/.github/workflows/main-eest.yml +++ b/.github/workflows/main-eest.yml @@ -30,7 +30,7 @@ jobs: name: Run functional tests runs-on: ubuntu-latest needs: extract-rust-version - timeout-minutes: 120 # TODO: change to 60 once the exex witness generation is optimized. + timeout-minutes: 120 steps: - name: Checkout repository @@ -41,19 +41,6 @@ jobs: - name: Cleanup Space uses: ./.github/actions/cleanup # zizmor: ignore[unpinned-uses] - - name: Install bitcoind - env: - BITCOIND_VERSION: "30.2" - BITCOIND_ARCH: "x86_64-linux-gnu" - run: | - curl -fsSLO --proto "=https" --tlsv1.3 "https://bitcoincore.org/bin/bitcoin-core-$BITCOIND_VERSION/bitcoin-$BITCOIND_VERSION-$BITCOIND_ARCH.tar.gz" - curl -fsSLO --proto "=https" --tlsv1.3 "https://bitcoincore.org/bin/bitcoin-core-$BITCOIND_VERSION/SHA256SUMS" - sha256sum --ignore-missing --check SHA256SUMS - tar xzf "bitcoin-$BITCOIND_VERSION-$BITCOIND_ARCH.tar.gz" - sudo install -m 0755 -t /usr/local/bin bitcoin-"$BITCOIND_VERSION"/bin/* - bitcoind --version - rm -rf SHA256SUMS "bitcoin-$BITCOIND_VERSION" "bitcoin-$BITCOIND_VERSION-$BITCOIND_ARCH.tar.gz" - - name: Install uv uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0 with: @@ -81,9 +68,10 @@ jobs: uses: ./.github/actions/sp1-core-runner-override # zizmor: ignore[unpinned-uses] - name: Build Cargo project - run: cargo build --locked -F debug-utils -F sequencer --bin strata --bin strata-signer --bin strata-datatool --bin strata-test-cli + run: | + cargo build --locked -p alpen-client --no-default-features --features sequencer --bin alpen-client - - name: Run basic env in fntests + - name: Run alpen EEST env in fntests env: NO_COLOR: "1" LOG_LEVEL: "info" @@ -91,47 +79,47 @@ jobs: sudo apt-get install -y screen NEWPATH="$(realpath target/debug/)" export PATH="${NEWPATH}:${PATH}" - which strata + which alpen-client cd functional-tests - screen -dmS basic_env uv run python entry.py --keep-alive basic - sleep 20 # Wait for the service to start + screen -dmS alpen_eest_env uv run python entry.py --keep-alive alpen_eest + if ! ../contrib/wait-for-json-rpc.sh http://localhost:30303 120; then + find _dd -type f -name "service.log" -print -exec tail -100 {} \; || true + exit 1 + fi + + - name: Capture EEST start block + run: | + START_BLOCK="$(curl -sf -X POST http://localhost:30303 \ + -H "Content-Type: application/json" \ + -d '{"jsonrpc":"2.0","method":"eth_blockNumber","params":[],"id":1}' \ + | python3 -c 'import json,sys; print(int(json.load(sys.stdin)["result"], 16))')" + echo "EEST_START_BLOCK=${START_BLOCK}" >> "${GITHUB_ENV}" - name: Run tests id: runtests run: | - curl -LsSf https://astral.sh/uv/install.sh | sh - git clone https://github.com/alpenlabs/execution-spec-tests - cd execution-spec-tests - uv python install 3.11 - uv python pin 3.11 - uv sync --all-extras - uv run solc-select use 0.8.24 --always-install - uv run execute remote \ - -m state_test \ - --fork=Prague \ - --rpc-endpoint=http://localhost:12603 \ - --rpc-seed-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 \ - --rpc-chain-id 2892 \ - --tx-wait-timeout 30 \ - -v + ./contrib/run-eest-remote.sh \ + --rpc-endpoint http://localhost:30303 \ + --fork Prague \ + --tx-wait-timeout 120 continue-on-error: true - - name: Generate a proof of EE blocks execution. - working-directory: docker + - name: Assert EEST blocks reached confirmed EE/OL status + if: always() && steps.runtests.outcome != 'skipped' run: | - chmod +x test_ee_proof.sh - ./test_ee_proof.sh local + ./contrib/assert-eest-prover-pipeline.sh \ + --rpc-endpoint http://localhost:30303 \ + --bitcoin-rpc-url http://user:password@localhost:18444 \ + --bitcoin-wallet testwallet \ + --advanced-from "${EEST_START_BLOCK}" \ + --timeout 900 - name: Stop service if: always() run: | - screen -S basic_env -X quit + screen -S alpen_eest_env -X quit || true - name: Check tests execution - # the purpose of the workflow is twofold: - # - run the EF tests - # - make sure EE proof is completed with EF txns (regardless if some tests have failed) - # Thus, we check the whole logic here. if: steps.runtests.outcome == 'failure' run: | echo "Functional tests failed" diff --git a/.github/workflows/staging-eest.yml b/.github/workflows/staging-eest.yml index b36e8d5bc9..6adc4c387d 100644 --- a/.github/workflows/staging-eest.yml +++ b/.github/workflows/staging-eest.yml @@ -1,4 +1,4 @@ -name: Ethereum Execution Spec tests against staging docker. +name: Ethereum Execution Spec tests against staging alpen-client docker. on: schedule: @@ -19,12 +19,31 @@ jobs: runs-on: ubuntu-latest environment: name: development - timeout-minutes: 30 + timeout-minutes: 120 steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v5 with: persist-credentials: false + - name: Extract Rust toolchain version + id: extract + uses: ./.github/actions/extract-rust-version # zizmor: ignore[unpinned-uses] + + - name: Set up Rust + env: + RUST_NIGHTLY: ${{ steps.extract.outputs.rust-version }} + run: | + rustup toolchain install "$RUST_NIGHTLY" --profile minimal + rustup default "$RUST_NIGHTLY" + + - name: Install protoc + uses: arduino/setup-protoc@c65c819552d16ad3c9b72d9dfd5ba5237b9c906b # v3.0.0 + + - name: Rust cache + uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2 + with: + cache-on-failure: true + - name: Configure AWS ECR Details uses: aws-actions/configure-aws-credentials@ec61189d14ec14c8efccab744f656cffd0e33f37 # v4 with: @@ -58,53 +77,66 @@ jobs: docker pull "${IMG_URL}" docker tag "${IMG_URL}" "alpen-client:latest" - - name: Generate alpen-client keys + - name: Generate full-stack docker config run: | pip3 install coincurve - cd docker && ./init-alpen-client-keys.sh + cargo build --locked --release --bin strata-datatool + cd docker + ./init-network.sh --sequencer ../target/release/strata-datatool - name: Start services run: | - docker compose --env-file docker/.env.alpen-client \ + docker compose --env-file docker/.env.alpen \ -f docker/docker-compose-eest.yml up -d --build - name: Wait for alpen-client to be ready run: | - echo "Waiting for alpen-client RPC..." - for _i in $(seq 1 30); do - if curl -sf -X POST http://localhost:8545 \ - -H "Content-Type: application/json" \ - -d '{"jsonrpc":"2.0","method":"eth_blockNumber","params":[],"id":1}' > /dev/null 2>&1; then - echo "alpen-client RPC is up" - exit 0 - fi - sleep 2 - done - echo "alpen-client failed to start" - docker logs alpen_eest - exit 1 + if ! ./contrib/wait-for-json-rpc.sh http://localhost:8545 60; then + echo "alpen-client failed to start" + docker logs alpen_eest + exit 1 + fi + + - name: Capture EEST start block + run: | + START_BLOCK="$(curl -sf -X POST http://localhost:8545 \ + -H "Content-Type: application/json" \ + -d '{"jsonrpc":"2.0","method":"eth_blockNumber","params":[],"id":1}' \ + | python3 -c 'import json,sys; print(int(json.load(sys.stdin)["result"], 16))')" + echo "EEST_START_BLOCK=${START_BLOCK}" >> "${GITHUB_ENV}" - name: Run tests + id: runtests + run: | + ./contrib/run-eest-remote.sh \ + --rpc-endpoint http://localhost:8545 \ + --fork Prague \ + --tx-wait-timeout 120 + continue-on-error: true + + - name: Assert EEST blocks reached confirmed EE/OL status + if: always() && steps.runtests.outcome != 'skipped' run: | - curl -LsSf https://astral.sh/uv/install.sh | sh - git clone https://github.com/alpenlabs/execution-spec-tests - cd execution-spec-tests - uv python install 3.11 - uv python pin 3.11 - uv sync --all-extras - uv run solc-select use 0.8.24 --always-install - uv run execute remote \ - -m state_test \ - --fork=Shanghai \ - --rpc-endpoint=http://localhost:8545 \ - --rpc-seed-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 \ - --rpc-chain-id 2892 \ - --tx-wait-timeout 30 \ - -v + ./contrib/assert-eest-prover-pipeline.sh \ + --rpc-endpoint http://localhost:8545 \ + --bitcoin-rpc-url http://rpcuser:rpcpassword@localhost:18443 \ + --bitcoin-wallet default \ + --advanced-from "${EEST_START_BLOCK}" \ + --timeout 900 - name: Tear down services if: always() run: | docker logs alpen_eest || true - docker compose --env-file docker/.env.alpen-client \ - -f docker/docker-compose-eest.yml down + docker logs eest_strata || true + docker logs eest_bitcoind || true + if [ -f docker/.env.alpen ]; then + docker compose --env-file docker/.env.alpen \ + -f docker/docker-compose-eest.yml down + fi + + - name: Check tests execution + if: steps.runtests.outcome == 'failure' + run: | + echo "Functional tests failed" + exit 1 diff --git a/bin/alpen-client/Cargo.toml b/bin/alpen-client/Cargo.toml index a56ecafb5c..e3d5934ee9 100644 --- a/bin/alpen-client/Cargo.toml +++ b/bin/alpen-client/Cargo.toml @@ -23,7 +23,12 @@ sequencer = [ ] # SP1 remote proving for the EE chunk + acct provers. Required for # production; native-only test builds can drop this feature. -sp1 = ["dep:zkaleido-sp1-host", "strata-paas/remote", "strata-zkvm-hosts/sp1"] +sp1 = [ + "dep:strata-zkvm-hosts", + "dep:zkaleido-sp1-host", + "strata-paas/remote", + "strata-zkvm-hosts/sp1", +] [[bin]] name = "alpen-client" @@ -73,7 +78,7 @@ strata-proofimpl-alpen-chunk.workspace = true strata-service.workspace = true strata-snark-acct-runtime.workspace = true strata-snark-acct-types.workspace = true -strata-zkvm-hosts.workspace = true +strata-zkvm-hosts = { workspace = true, optional = true } zkaleido.workspace = true zkaleido-sp1-host = { workspace = true, optional = true } diff --git a/bin/alpen-client/src/main.rs b/bin/alpen-client/src/main.rs index 89b0898df0..4dfe1ab561 100644 --- a/bin/alpen-client/src/main.rs +++ b/bin/alpen-client/src/main.rs @@ -73,7 +73,9 @@ use strata_logging::{init_logging_from_config, LoggingInitConfig}; use strata_predicate::PredicateKey; use strata_primitives::{buf::Buf32, L1Height}; use tokio::sync::{mpsc, watch}; -use tracing::{error, info}; +#[cfg(feature = "sequencer")] +use tokio::time::sleep; +use tracing::{error, info, warn}; #[cfg(feature = "sequencer")] mod sequencer_imports { @@ -94,8 +96,8 @@ mod sequencer_imports { header_summary::RethHeaderSummaryProvider, payload_builder::AlpenRethPayloadEngine, prover::{ - AcctReceiptHook, AcctSpec, ChunkReceiptHook, ChunkSpec, EeBatchProofDbManager, - EeChunkReceiptStore, EeProverTaskDbManager, PaasBatchProver, + AcctReceiptHook, AcctSpec, ChunkReceiptHook, ChunkSpec, ChunkTask, + EeBatchProofDbManager, EeChunkReceiptStore, EeProverTaskDbManager, PaasBatchProver, }, }; } @@ -170,9 +172,12 @@ fn main() { info!(?params, sequencer = ext.sequencer, "Starting EE Node"); + #[cfg(feature = "sequencer")] + let da_args = resolve_da_args(&ext)?; + // Resolve btcio writer config up front so flag misuse surfaces before I/O. #[cfg(feature = "sequencer")] - let writer_config = if ext.sequencer { + let writer_config = if da_args.is_some() { let cfg = Arc::new(resolve_writer_config(&ext)?); log_writer_config(&cfg); Some(cfg) @@ -358,16 +363,19 @@ fn main() { } }); - // Install state diff exex for sequencer DA. + // Install state diff exex only when sequencer DA is enabled. // The exex persists per-block state diffs that the blob provider reads. #[cfg(feature = "sequencer")] - if ext.sequencer { + if da_args.is_some() { node_builder = node_builder.install_exex("state_diffs", { let state_diff_db = dbs.witness_db(); |ctx| async { Ok(StateDiffGenerator::new(ctx, state_diff_db).start()) } }); info!(target: "alpen-client", "installed StateDiffGenerator exex for DA"); + } + #[cfg(feature = "sequencer")] + if ext.sequencer { // Per-block accessed-state capture. Re-executes each // committed block with a `CacheDBProvider` to record the // read set + bytecodes, which the chunk-builder consumes @@ -467,354 +475,591 @@ fn main() { storage.clone(), ); - let (latest_batch, _) = require_latest_batch(storage.as_ref()).await?; - - let batch_sealing_policy = - FixedBlockCountSealing::new(ext.batch_sealing_block_count); - let block_data_provider = Arc::new(BlockCountDataProvider); - - // Chunk-spanning witness extractor. Single producer of - // `ChunkWitnessRecord`: the background `chunk_witness_task` - // invokes it once per chunk, off the batch builder's hot - // path. The chunk prover's `ChunkSpec::fetch_input` reads - // the persisted record from sled and has no extractor path - // of its own; a missing record returns `TransientFailure` - // and the task here retries on extraction errors (mainly - // covering the race against the `AccessedStateGenerator` - // exex). - let range_witness_extractor = Arc::new(RangeWitnessExtractor::new( - node.provider.clone(), - storage.clone(), - )); + if let Some((magic_bytes, btc_url, btc_user, btc_pass)) = da_args { + let (latest_batch, _) = require_latest_batch(storage.as_ref()).await?; + + let batch_sealing_policy = + FixedBlockCountSealing::new(ext.batch_sealing_block_count); + let block_data_provider = Arc::new(BlockCountDataProvider); + + // Chunk-spanning witness extractor. Single producer of + // `ChunkWitnessRecord`: the background `chunk_witness_task` + // invokes it once per chunk, off the batch builder's hot + // path. The chunk prover's `ChunkSpec::fetch_input` reads + // the persisted record from sled and has no extractor path + // of its own; a missing record returns `TransientFailure` + // and the task here retries on extraction errors (mainly + // covering the race against the `AccessedStateGenerator` + // exex). + let range_witness_extractor = Arc::new(RangeWitnessExtractor::new( + node.provider.clone(), + storage.clone(), + )); + + // `ChunkWitnessExtractFn` is the production-time hook: takes + // chunk endpoints, returns the persisted `ChunkWitnessRecord`. + // Reth's alloy `Header` / `Block` are RLP-encoded here so the + // record stays Borsh-friendly for sled. + let chunk_witness_extract_fn: Arc = { + let extractor = range_witness_extractor.clone(); + Arc::new(move |first_block, last_block| { + let first_b256 = alloy_primitives::B256::from(first_block.0); + let last_b256 = alloy_primitives::B256::from(last_block.0); + let data = extractor.extract_range_witness(first_b256, last_b256)?; + let prev_header_rlp = alloy_rlp::encode(&data.prev_header); + let blocks_rlp: Vec> = + data.blocks.iter().map(alloy_rlp::encode).collect(); + Ok(ChunkWitnessRecord::new( + data.raw_partial_pre_state, + prev_header_rlp, + blocks_rlp, + )) + }) + }; + + let (chunk_witness_tx, chunk_witness_rx) = chunk_witness_channel(); + let chunk_witness_store: Arc = + storage.clone(); + let chunk_witness_task_fut = chunk_witness_task( + chunk_witness_extract_fn, + chunk_witness_store, + chunk_witness_rx, + ); - // `ChunkWitnessExtractFn` is the production-time hook: takes - // chunk endpoints, returns the persisted `ChunkWitnessRecord`. - // Reth's alloy `Header` / `Block` are RLP-encoded here so the - // record stays Borsh-friendly for sled. - let chunk_witness_extract_fn: Arc = { - let extractor = range_witness_extractor.clone(); - Arc::new(move |first_block, last_block| { - let first_b256 = alloy_primitives::B256::from(first_block.0); - let last_b256 = alloy_primitives::B256::from(last_block.0); - let data = extractor.extract_range_witness(first_b256, last_b256)?; - let prev_header_rlp = alloy_rlp::encode(&data.prev_header); - let blocks_rlp: Vec> = - data.blocks.iter().map(alloy_rlp::encode).collect(); - Ok(ChunkWitnessRecord::new( - data.raw_partial_pre_state, - prev_header_rlp, - blocks_rlp, - )) - }) - }; + // Startup recovery for sealed-without-witness chunks. + // Covers crash-mid-extraction (mpsc request lost with the + // process) and any pre-existing chunks lacking a witness + // row. Spawned critical alongside `ee_chunk_witness`: + // witness assembly gates the entire proving pipeline, so + // a panic in either path warrants taking the node down + // rather than silently producing un-provable chunks. + let chunk_witness_backfill_task = { + let batch_storage: Arc = storage.clone(); + let witness_store: Arc = + storage.clone(); + let tx = chunk_witness_tx.clone(); + async move { + if let Err(e) = backfill_missing_chunk_witnesses( + batch_storage.as_ref(), + witness_store.as_ref(), + &tx, + ) + .await + { + error!(error = %e, "chunk witness backfill failed at startup"); + } + } + }; + + let (batch_builder_handle, batch_builder_task) = create_batch_builder( + latest_batch.id(), + BlockNumHash::new( + genesis_info.blockhash().0.into(), + genesis_info.blocknum(), + ), + batch_builder_state, + preconf_rx, + block_data_provider, + batch_sealing_policy, + storage.clone(), + storage.clone(), + exec_chain_handle.clone(), + Some(chunk_witness_tx), + ); - let (chunk_witness_tx, chunk_witness_rx) = chunk_witness_channel(); - let chunk_witness_store: Arc = - storage.clone(); - let chunk_witness_task_fut = chunk_witness_task( - chunk_witness_extract_fn, - chunk_witness_store, - chunk_witness_rx, - ); + // --- DA pipeline --- + // Create BtcioParams directly from CLI args. + let btcio_params = BtcioParams::new( + ext.l1_reorg_safe_depth, + magic_bytes, + ext.genesis_l1_height, + ); - // Startup recovery for sealed-without-witness chunks. - // Covers crash-mid-extraction (mpsc request lost with the - // process) and any pre-existing chunks lacking a witness - // row. Spawned critical alongside `ee_chunk_witness`: - // witness assembly gates the entire proving pipeline, so - // a panic in either path warrants taking the node down - // rather than silently producing un-provable chunks. - let chunk_witness_backfill_task = { - let batch_storage: Arc = storage.clone(); - let witness_store: Arc = - storage.clone(); - let tx = chunk_witness_tx.clone(); - async move { - if let Err(e) = backfill_missing_chunk_witnesses( - batch_storage.as_ref(), - witness_store.as_ref(), - &tx, + // Bitcoin RPC client. + let btc_client = Arc::new( + BtcClient::new( + btc_url, + Auth::UserPass(btc_user, btc_pass), + Some(ext.btcio_retry_count), + Some(ext.btcio_retry_interval), + None, ) - .await - { - error!(error = %e, "chunk witness backfill failed at startup"); - } - } - }; - - let (batch_builder_handle, batch_builder_task) = create_batch_builder( - latest_batch.id(), - BlockNumHash::new(genesis_info.blockhash().0.into(), genesis_info.blocknum()), - batch_builder_state, - preconf_rx, - block_data_provider, - batch_sealing_policy, - storage.clone(), - storage.clone(), - exec_chain_handle.clone(), - Some(chunk_witness_tx), - ); + .map_err(|e| eyre::eyre!("creating Bitcoin RPC client: {e}"))?, + ); + info!( + target: "alpen-client", + retry_count = ext.btcio_retry_count, + retry_interval_ms = ext.btcio_retry_interval, + "btcio Bitcoin RPC retry policy configured", + ); - // --- DA pipeline --- - // - // clap `requires_all` on --sequencer guarantees all DA args are present. - let magic_bytes = ext.ee_da_magic_bytes.expect("enforced by clap"); - let btc_url = ext.btc_rpc_url.as_ref().expect("enforced by clap"); - let btc_user = ext.btc_rpc_user.as_ref().expect("enforced by clap"); - let btc_pass = ext.btc_rpc_password.as_ref().expect("enforced by clap"); - - // Create BtcioParams directly from CLI args. - let btcio_params = - BtcioParams::new(ext.l1_reorg_safe_depth, magic_bytes, ext.genesis_l1_height); - - // Bitcoin RPC client. - let btc_client = Arc::new( - BtcClient::new( - btc_url.clone(), - Auth::UserPass(btc_user.clone(), btc_pass.clone()), - Some(ext.btcio_retry_count), - Some(ext.btcio_retry_interval), - None, - ) - .map_err(|e| eyre::eyre!("creating Bitcoin RPC client: {e}"))?, - ); - info!( - target: "alpen-client", - retry_count = ext.btcio_retry_count, - retry_interval_ms = ext.btcio_retry_interval, - "btcio Bitcoin RPC retry policy configured", - ); + // Sequencer address from bitcoin wallet. + let sequencer_address = btc_client + .get_new_address() + .await + .map_err(|e| eyre::eyre!("failed to get sequencer address: {e}"))?; - // Sequencer address from bitcoin wallet. - let sequencer_address = btc_client - .get_new_address() - .await - .map_err(|e| eyre::eyre!("failed to get sequencer address: {e}"))?; + // Wrap raw DBs in ops using the shared DB threadpool. + let broadcast_ops = Arc::new(dbs.broadcast_ops(db_pool.clone())); + let envelope_ops = Arc::new(dbs.chunked_envelope_ops(db_pool)); - // Wrap raw DBs in ops using the shared DB threadpool. - let broadcast_ops = Arc::new(dbs.broadcast_ops(db_pool.clone())); - let envelope_ops = Arc::new(dbs.chunked_envelope_ops(db_pool)); + // Launch broadcaster service and create chunked envelope task. + let broadcast_poll_interval = 5_000; - // Launch broadcaster service and create chunked envelope task. - let broadcast_poll_interval = 5_000; + let broadcast_handle = Arc::new( + BroadcasterBuilder::new( + btc_client.clone(), + broadcast_ops.clone(), + btcio_params, + ) + .with_broadcast_poll_interval_ms(broadcast_poll_interval) + .launch(&service_executor) + .await + .map_err(|e| eyre::eyre!("starting broadcaster service: {e}"))?, + ); - let broadcast_handle = Arc::new( - BroadcasterBuilder::new( - btc_client.clone(), - broadcast_ops.clone(), + let writer_config = writer_config + .clone() + .expect("writer_config resolved at startup when EE DA is configured"); + let sequencer_keypair = sequencer_keypair.ok_or_else(|| { + eyre::eyre!("EE sequencer DA reveal signing needs sequencer Keypair") + })?; + let (envelope_handle, envelope_watcher_task) = create_chunked_envelope_task( + btc_client, + writer_config, btcio_params, + sequencer_address, + sequencer_keypair, + envelope_ops, + broadcast_handle.clone(), ) - .with_broadcast_poll_interval_ms(broadcast_poll_interval) - .launch(&service_executor) - .await - .map_err(|e| eyre::eyre!("starting broadcaster service: {e}"))?, - ); - - let writer_config = writer_config - .clone() - .expect("writer_config resolved at startup when --sequencer is set"); - let sequencer_keypair = sequencer_keypair.ok_or_else(|| { - eyre::eyre!("EE sequencer DA reveal signing needs sequencer Keypair") - })?; - let (envelope_handle, envelope_watcher_task) = create_chunked_envelope_task( - btc_client, - writer_config, - btcio_params, - sequencer_address, - sequencer_keypair, - envelope_ops, - broadcast_handle.clone(), - ) - .map_err(|e| eyre::eyre!("creating chunked envelope task: {e}"))?; + .map_err(|e| eyre::eyre!("creating chunked envelope task: {e}"))?; + + let header_summary = + Arc::new(RethHeaderSummaryProvider::new(node.provider.clone())); + + let da_context_db = dbs.da_context_db(); + let blob_provider: Arc = + Arc::new(StateDiffBlobProvider::new( + storage.clone(), + dbs.witness_db(), + header_summary, + da_context_db.clone(), + )); - let header_summary = - Arc::new(RethHeaderSummaryProvider::new(node.provider.clone())); + let batch_da_provider = Arc::new(ChunkedEnvelopeDaProvider::new( + blob_provider.clone(), + envelope_handle, + broadcast_ops, + magic_bytes, + )); + + // Spawn btcio tasks. + node.task_executor + .spawn_critical("chunked_envelope_watcher", envelope_watcher_task); + + info!(target: "alpen-client", "btcio DA pipeline started"); + + // EE chunk + acct paas provers. Both use SP1 remote + // proving (production); native is dev-only via the + // proofimpl crates' `native_host()` for tests. + // + // Storage layout (sled-backed, own sled db under + // `/sled` — fully separate from OL's; the + // prover trees live alongside the EE node trees): + // - `task_store` — shared across both provers; task keys carry a kind tag + // (`b'c'`/`b'a'`) so chunk and batch entries don't collide in one tree. + // - `chunk_receipts` — chunk prover writes (via paas auto-store); acct + // `fetch_input` reads back. + // - `batch_proofs` — outer-proof store keyed by `BatchId`; outer hook writes, + // OL submission reads. + // + // All backed by `EeProverDbSled`; see + // `alpen_ee_database::sleddb::prover_db` for schemas. + let prover_db = dbs.prover_db(); + let task_store: Arc = + Arc::new(EeProverTaskDbManager::new(prover_db.clone())); + let chunk_receipts: Arc = + Arc::new(EeChunkReceiptStore::new(prover_db.clone())); + let batch_proofs = Arc::new(EeBatchProofDbManager::new(prover_db)); + let batch_storage_dyn: Arc = storage.clone(); + + let genesis = { + use alpen_reth_exex::alloy2reth::IntoRspChainConfig as _; + ext.custom_chain.genesis().config.clone().into_rsp() + }; + + let chunk_builder = ProverBuilder::new(ChunkSpec::new( + batch_storage_dyn.clone(), + storage.clone(), + genesis.clone(), + )) + .task_store(task_store.clone()) + .receipt_store(chunk_receipts.clone()) + .receipt_hook(ChunkReceiptHook::new(batch_storage_dyn.clone())) + .retry(RetryConfig::default()); + + let acct_builder = ProverBuilder::new(AcctSpec::new( + chunk_receipts.clone(), + batch_storage_dyn.clone(), + storage.clone(), + ol_client.clone(), + genesis, + )) + .task_store(task_store) + .receipt_hook(AcctReceiptHook::new( + batch_storage_dyn.clone(), + batch_proofs.clone(), + )) + .retry(RetryConfig::default()); + + // Dev/test escape hatch: use zkaleido NativeHost instead of + // the SP1 remote host. This skips real Groth16 proving and + // the need for compiled guest ELFs — only safe for + // functional tests. The acct program is wired with the + // chunk program's deterministic test predicate key so the + // native-host Schnorr signature actually verifies. + let (chunk_prover, acct_prover) = if ext.dev_native_prover { + info!( + target: "alpen-client", + "EE chunk + acct provers: native host (dev/test only)" + ); + let chunk = chunk_builder.native(EeChunkProgram::native_host()); + let acct_program = EeAcctProgram::new(EeChunkProgram::test_predicate_key()); + let acct = acct_builder.native(acct_program.native_host()); + (chunk, acct) + } else { + #[cfg(feature = "sp1")] + { + let deadline_secs = ext + .sp1_proof_deadline_secs + .unwrap_or(DEFAULT_SP1_DEADLINE_SECS); + let deadline = Duration::from_secs(deadline_secs); + info!( + target: "alpen-client", + deadline_secs, + "sp1 EE prover deadline configured" + ); + let sp1_config = SP1HostConfig::default().with_deadline(deadline); + let chunk_host: SP1Host = + (**alpen_chunk_host(sp1_config.clone()).await).clone(); + let acct_host: SP1Host = (**alpen_acct_host(sp1_config).await).clone(); + ( + chunk_builder.remote(chunk_host), + acct_builder.remote(acct_host), + ) + } + #[cfg(not(feature = "sp1"))] + { + return Err(eyre::eyre!( + "remote SP1 prover is not compiled in; pass --dev-native-prover \ + or build with the `sp1` feature" + )); + } + }; - let da_context_db = dbs.da_context_db(); - let blob_provider: Arc = Arc::new(StateDiffBlobProvider::new( - storage.clone(), - dbs.witness_db(), - header_summary, - da_context_db.clone(), - )); + let prover_tick = Duration::from_secs(5); + let chunk_handle = ProverServiceBuilder::new(chunk_prover) + .tick_interval(prover_tick) + .launch(&service_executor) + .await + .map_err(|e| eyre::eyre!("launching chunk prover service: {e}"))?; + let acct_handle = ProverServiceBuilder::new(acct_prover) + .tick_interval(prover_tick) + .launch(&service_executor) + .await + .map_err(|e| eyre::eyre!("launching acct prover service: {e}"))?; + + let batch_prover = Arc::new(PaasBatchProver::new( + chunk_handle, + acct_handle, + batch_storage_dyn, + batch_proofs, + )); + + info!(target: "alpen-client", "EE chunk + acct paas provers started (SP1 remote)"); + + let (batch_lifecycle_handle, batch_lifecycle_task) = + create_batch_lifecycle_task( + None, + batch_lifecycle_state, + batch_builder_handle.latest_batch_watcher(), + batch_da_provider, + batch_prover.clone(), + storage.clone(), + blob_provider, + da_context_db, + ); - let batch_da_provider = Arc::new(ChunkedEnvelopeDaProvider::new( - blob_provider.clone(), - envelope_handle, - broadcast_ops, - magic_bytes, - )); + let update_submitter_task = create_update_submitter_task( + ol_client, + storage.clone(), + storage.clone(), + batch_prover, + batch_lifecycle_handle.latest_proof_ready_watcher(), + status_watcher, + ); - // Spawn btcio tasks. - node.task_executor - .spawn_critical("chunked_envelope_watcher", envelope_watcher_task); - - info!(target: "alpen-client", "btcio DA pipeline started"); - - // EE chunk + acct paas provers. Both use SP1 remote - // proving (production); native is dev-only via the - // proofimpl crates' `native_host()` for tests. - // - // Storage layout (sled-backed, own sled db under - // `/sled` — fully separate from OL's; the - // prover trees live alongside the EE node trees): - // - `task_store` — shared across both provers; task keys carry a kind tag - // (`b'c'`/`b'a'`) so chunk and batch entries don't collide in one tree. - // - `chunk_receipts` — chunk prover writes (via paas auto-store); acct - // `fetch_input` reads back. - // - `batch_proofs` — outer-proof store keyed by `BatchId`; outer hook writes, OL - // submission reads. - // - // All backed by `EeProverDbSled`; see - // `alpen_ee_database::sleddb::prover_db` for schemas. - let prover_db = dbs.prover_db(); - let task_store: Arc = - Arc::new(EeProverTaskDbManager::new(prover_db.clone())); - let chunk_receipts: Arc = - Arc::new(EeChunkReceiptStore::new(prover_db.clone())); - let batch_proofs = Arc::new(EeBatchProofDbManager::new(prover_db)); - let batch_storage_dyn: Arc = storage.clone(); - - let genesis = { - use alpen_reth_exex::alloy2reth::IntoRspChainConfig as _; - ext.custom_chain.genesis().config.clone().into_rsp() - }; + node.task_executor + .spawn_critical("ol_chain_tracker", ol_chain_tracker_task); + node.task_executor.spawn_critical( + "block_assembly", + block_builder_task( + block_builder_config, + exec_chain_handle, + ol_chain_tracker, + payload_engine, + storage.clone(), + ), + ); - let chunk_builder = ProverBuilder::new(ChunkSpec::new( - batch_storage_dyn.clone(), - storage.clone(), - genesis.clone(), - )) - .task_store(task_store.clone()) - .receipt_store(chunk_receipts.clone()) - .receipt_hook(ChunkReceiptHook::new(batch_storage_dyn.clone())) - .retry(RetryConfig::default()); - - let acct_builder = ProverBuilder::new(AcctSpec::new( - chunk_receipts.clone(), - batch_storage_dyn.clone(), - storage.clone(), - ol_client.clone(), - genesis, - )) - .task_store(task_store) - .receipt_hook(AcctReceiptHook::new( - batch_storage_dyn.clone(), - batch_proofs.clone(), - )) - .retry(RetryConfig::default()); - - // Dev/test escape hatch: use zkaleido NativeHost instead of - // the SP1 remote host. This skips real Groth16 proving and - // the need for compiled guest ELFs — only safe for - // functional tests. The acct program is wired with the - // chunk program's deterministic test predicate key so the - // native-host Schnorr signature actually verifies. - let (chunk_prover, acct_prover) = if ext.dev_native_prover { + node.task_executor + .spawn_critical("ee_batch_builder", batch_builder_task); + node.task_executor + .spawn_critical("ee_chunk_witness", chunk_witness_task_fut); + node.task_executor + .spawn_critical("ee_chunk_witness_backfill", chunk_witness_backfill_task); + node.task_executor + .spawn_critical("ee_batch_lifecycle", batch_lifecycle_task); + node.task_executor + .spawn_critical("ee_update_submitter", update_submitter_task); + } else { info!( target: "alpen-client", - "EE chunk + acct provers: native host (dev/test only)" + "EE DA pipeline disabled; running sequencer with chunk-only EE proving" ); - let chunk = chunk_builder.native(EeChunkProgram::native_host()); - let acct_program = EeAcctProgram::new(EeChunkProgram::test_predicate_key()); - let acct = acct_builder.native(acct_program.native_host()); - (chunk, acct) - } else { - #[cfg(feature = "sp1")] - { - let deadline_secs = ext - .sp1_proof_deadline_secs - .unwrap_or(DEFAULT_SP1_DEADLINE_SECS); - let deadline = Duration::from_secs(deadline_secs); - info!( - target: "alpen-client", - deadline_secs, - "sp1 EE prover deadline configured" - ); - let sp1_config = SP1HostConfig::default().with_deadline(deadline); - let chunk_host: SP1Host = - (**alpen_chunk_host(sp1_config.clone()).await).clone(); - let acct_host: SP1Host = (**alpen_acct_host(sp1_config).await).clone(); - ( - chunk_builder.remote(chunk_host), - acct_builder.remote(acct_host), - ) - } - #[cfg(not(feature = "sp1"))] - { - return Err(eyre::eyre!( - "remote SP1 prover is not compiled in; pass --dev-native-prover \ - or build with the `sp1` feature" - )); - } - }; - let prover_tick = Duration::from_secs(5); - let chunk_handle = ProverServiceBuilder::new(chunk_prover) - .tick_interval(prover_tick) - .launch(&service_executor) - .await - .map_err(|e| eyre::eyre!("launching chunk prover service: {e}"))?; - let acct_handle = ProverServiceBuilder::new(acct_prover) - .tick_interval(prover_tick) - .launch(&service_executor) - .await - .map_err(|e| eyre::eyre!("launching acct prover service: {e}"))?; + let (latest_batch, _) = require_latest_batch(storage.as_ref()).await?; - let batch_prover = Arc::new(PaasBatchProver::new( - chunk_handle, - acct_handle, - batch_storage_dyn, - batch_proofs, - )); + let batch_sealing_policy = + FixedBlockCountSealing::new(ext.batch_sealing_block_count); + let block_data_provider = Arc::new(BlockCountDataProvider); - info!(target: "alpen-client", "EE chunk + acct paas provers started (SP1 remote)"); + let range_witness_extractor = Arc::new(RangeWitnessExtractor::new( + node.provider.clone(), + storage.clone(), + )); + + let chunk_witness_extract_fn: Arc = { + let extractor = range_witness_extractor.clone(); + Arc::new(move |first_block, last_block| { + let first_b256 = alloy_primitives::B256::from(first_block.0); + let last_b256 = alloy_primitives::B256::from(last_block.0); + let data = extractor.extract_range_witness(first_b256, last_b256)?; + let prev_header_rlp = alloy_rlp::encode(&data.prev_header); + let blocks_rlp: Vec> = + data.blocks.iter().map(alloy_rlp::encode).collect(); + Ok(ChunkWitnessRecord::new( + data.raw_partial_pre_state, + prev_header_rlp, + blocks_rlp, + )) + }) + }; + + let (chunk_witness_tx, chunk_witness_rx) = chunk_witness_channel(); + let chunk_witness_store: Arc = + storage.clone(); + let chunk_witness_task_fut = chunk_witness_task( + chunk_witness_extract_fn, + chunk_witness_store, + chunk_witness_rx, + ); - let (batch_lifecycle_handle, batch_lifecycle_task) = create_batch_lifecycle_task( - None, - batch_lifecycle_state, - batch_builder_handle.latest_batch_watcher(), - batch_da_provider, - batch_prover.clone(), - storage.clone(), - blob_provider, - da_context_db, - ); + let chunk_witness_backfill_task = { + let batch_storage: Arc = storage.clone(); + let witness_store: Arc = + storage.clone(); + let tx = chunk_witness_tx.clone(); + async move { + if let Err(e) = backfill_missing_chunk_witnesses( + batch_storage.as_ref(), + witness_store.as_ref(), + &tx, + ) + .await + { + error!(error = %e, "chunk witness backfill failed at startup"); + } + } + }; + + let (batch_builder_handle, batch_builder_task) = create_batch_builder( + latest_batch.id(), + BlockNumHash::new( + genesis_info.blockhash().0.into(), + genesis_info.blocknum(), + ), + batch_builder_state, + preconf_rx, + block_data_provider, + batch_sealing_policy, + storage.clone(), + storage.clone(), + exec_chain_handle.clone(), + Some(chunk_witness_tx), + ); - let update_submitter_task = create_update_submitter_task( - ol_client, - storage.clone(), - storage.clone(), - batch_prover, - batch_lifecycle_handle.latest_proof_ready_watcher(), - status_watcher, - ); + let prover_db = dbs.prover_db(); + let task_store: Arc = + Arc::new(EeProverTaskDbManager::new(prover_db.clone())); + let chunk_receipts: Arc = + Arc::new(EeChunkReceiptStore::new(prover_db)); + let batch_storage_dyn: Arc = storage.clone(); - node.task_executor - .spawn_critical("ol_chain_tracker", ol_chain_tracker_task); - node.task_executor.spawn_critical( - "block_assembly", - block_builder_task( - block_builder_config, - exec_chain_handle, - ol_chain_tracker, - payload_engine, + let genesis = { + use alpen_reth_exex::alloy2reth::IntoRspChainConfig as _; + ext.custom_chain.genesis().config.clone().into_rsp() + }; + + let chunk_builder = ProverBuilder::new(ChunkSpec::new( + batch_storage_dyn.clone(), storage.clone(), - ), - ); + genesis, + )) + .task_store(task_store) + .receipt_store(chunk_receipts) + .receipt_hook(ChunkReceiptHook::new(batch_storage_dyn.clone())) + .retry(RetryConfig::default()); + + let chunk_prover = if ext.dev_native_prover { + info!( + target: "alpen-client", + "EE chunk prover: native host (dev/test only)" + ); + chunk_builder.native(EeChunkProgram::native_host()) + } else { + #[cfg(feature = "sp1")] + { + let deadline_secs = ext + .sp1_proof_deadline_secs + .unwrap_or(DEFAULT_SP1_DEADLINE_SECS); + let deadline = Duration::from_secs(deadline_secs); + info!( + target: "alpen-client", + deadline_secs, + "sp1 EE chunk prover deadline configured" + ); + let sp1_config = SP1HostConfig::default().with_deadline(deadline); + let chunk_host: SP1Host = + (**alpen_chunk_host(sp1_config).await).clone(); + chunk_builder.remote(chunk_host) + } + #[cfg(not(feature = "sp1"))] + { + return Err(eyre::eyre!( + "remote SP1 chunk prover is not compiled in; pass \ + --dev-native-prover or build with the `sp1` feature" + )); + } + }; - node.task_executor - .spawn_critical("ee_batch_builder", batch_builder_task); - node.task_executor - .spawn_critical("ee_chunk_witness", chunk_witness_task_fut); - node.task_executor - .spawn_critical("ee_chunk_witness_backfill", chunk_witness_backfill_task); - node.task_executor - .spawn_critical("ee_batch_lifecycle", batch_lifecycle_task); - node.task_executor - .spawn_critical("ee_update_submitter", update_submitter_task); - // TODO: proof generation - // TODO: post update to OL + let prover_tick = Duration::from_secs(5); + let chunk_handle = ProverServiceBuilder::new(chunk_prover) + .tick_interval(prover_tick) + .launch(&service_executor) + .await + .map_err(|e| eyre::eyre!("launching chunk prover service: {e}"))?; + + let chunk_proof_submitter_task = { + let mut latest_batch_rx = batch_builder_handle.latest_batch_watcher(); + let batch_storage = batch_storage_dyn.clone(); + let witness_store: Arc = + storage.clone(); + async move { + loop { + if latest_batch_rx.changed().await.is_err() { + warn!("latest batch watcher closed; exiting chunk submitter"); + return; + } + + let batch_id = *latest_batch_rx.borrow_and_update(); + match batch_storage.get_batch_chunks(batch_id).await { + Ok(Some(chunks)) => { + info!( + %batch_id, + chunk_count = chunks.len(), + "submitting chunk proof tasks for sealed batch" + ); + for chunk_id in chunks { + let mut attempts = 0; + loop { + match witness_store + .get_chunk_witness(chunk_id) + .await + { + Ok(Some(_)) => break, + Ok(None) if attempts < 60 => { + attempts += 1; + sleep(Duration::from_millis(500)).await; + } + Ok(None) => { + warn!( + ?chunk_id, + "chunk witness still missing; \ + submitting proof task and relying on \ + prover retry" + ); + break; + } + Err(e) => { + error!( + ?chunk_id, + error = %e, + "failed to check chunk witness before \ + submitting proof task" + ); + break; + } + } + } + if let Err(e) = + chunk_handle.submit(ChunkTask(chunk_id)).await + { + error!( + %batch_id, + ?chunk_id, + error = %e, + "failed to submit chunk proof task" + ); + } + } + } + Ok(None) => warn!( + %batch_id, + "sealed batch has no chunks; skipping chunk proof submission" + ), + Err(e) => error!( + %batch_id, + error = %e, + "failed to read chunks for sealed batch" + ), + } + } + } + }; + + node.task_executor + .spawn_critical("ol_chain_tracker", ol_chain_tracker_task); + node.task_executor.spawn_critical( + "block_assembly", + block_builder_task( + block_builder_config, + exec_chain_handle, + ol_chain_tracker, + payload_engine, + storage.clone(), + ), + ); + node.task_executor + .spawn_critical("ee_batch_builder", batch_builder_task); + node.task_executor + .spawn_critical("ee_chunk_witness", chunk_witness_task_fut); + node.task_executor + .spawn_critical("ee_chunk_witness_backfill", chunk_witness_backfill_task); + node.task_executor + .spawn_critical("ee_chunk_proof_submitter", chunk_proof_submitter_task); + } } handle.node_exit_future.await @@ -907,15 +1152,12 @@ pub struct AdditionalConfig { #[arg(long, required = false)] pub db_retry_count: Option, - /// Run the node as a sequencer. Requires the `sequencer` feature, - /// a `SEQUENCER_PRIVATE_KEY` environment variable, and all DA-related - /// arguments (`--ee-da-magic-bytes`, `--btc-rpc-url`, `--btc-rpc-user`, - /// `--btc-rpc-password`). - #[arg( - long, - default_value_t = false, - requires_all = ["ee_da_magic_bytes", "btc_rpc_url", "btc_rpc_user", "btc_rpc_password"], - )] + /// Run the node as a sequencer. Requires the `sequencer` feature and a + /// `SEQUENCER_PRIVATE_KEY` environment variable. When Bitcoin DA is + /// enabled, all DA-related arguments (`--ee-da-magic-bytes`, + /// `--btc-rpc-url`, `--btc-rpc-user`, `--btc-rpc-password`) must be + /// provided together. + #[arg(long, default_value_t = false)] pub sequencer: bool, /// Sequencer's public key (hex-encoded, 32 bytes) for signature validation. @@ -928,15 +1170,15 @@ pub struct AdditionalConfig { #[arg(long, required = false, value_parser = parse_magic_bytes)] pub ee_da_magic_bytes: Option, - /// Bitcoin Core RPC URL. Required when `--sequencer` is set. + /// Bitcoin Core RPC URL. Required when enabling Bitcoin DA. #[arg(long, required = false)] pub btc_rpc_url: Option, - /// Bitcoin Core RPC username. Required when `--sequencer` is set. + /// Bitcoin Core RPC username. Required when enabling Bitcoin DA. #[arg(long, required = false)] pub btc_rpc_user: Option, - /// Bitcoin Core RPC password. Required when `--sequencer` is set. + /// Bitcoin Core RPC password. Required when enabling Bitcoin DA. #[arg(long, required = false)] pub btc_rpc_password: Option, @@ -1174,6 +1416,33 @@ impl From for MempoolExplorerFeePolicy { } } +#[cfg(feature = "sequencer")] +type DaArgs = (MagicBytes, String, String, String); + +/// Resolves optional Bitcoin DA configuration from CLI flags. +#[cfg(feature = "sequencer")] +fn resolve_da_args(ext: &AdditionalConfig) -> eyre::Result> { + if !ext.sequencer { + return Ok(None); + } + + match ( + ext.ee_da_magic_bytes, + ext.btc_rpc_url.clone(), + ext.btc_rpc_user.clone(), + ext.btc_rpc_password.clone(), + ) { + (None, None, None, None) => Ok(None), + (Some(magic_bytes), Some(btc_url), Some(btc_user), Some(btc_pass)) => { + Ok(Some((magic_bytes, btc_url, btc_user, btc_pass))) + } + _ => Err(eyre::eyre!( + "EE DA is optional, but --ee-da-magic-bytes, --btc-rpc-url, \ + --btc-rpc-user, and --btc-rpc-password must be provided together" + )), + } +} + /// Builds [`WriterConfig`] from CLI flags. Empty-string mempool URL is /// treated as absent so docker-compose `${VAR:-}` doesn't yield `Some("")`. #[cfg(feature = "sequencer")] @@ -1251,6 +1520,17 @@ fn log_writer_config(cfg: &WriterConfig) { mod resolve_writer_config_tests { use super::*; + fn sequencer_argv<'a>(extra: impl IntoIterator) -> Vec { + let mut argv = vec![ + "alpen-client".to_owned(), + "--sequencer".to_owned(), + "--sequencer-pubkey".to_owned(), + "0".repeat(64), + ]; + argv.extend(extra.into_iter().map(str::to_owned)); + argv + } + fn args( policy: BtcioFeePolicyArg, fee_rate: Option, @@ -1264,6 +1544,23 @@ mod resolve_writer_config_tests { cfg } + #[test] + fn sequencer_allows_no_da_args() { + let cfg = ::parse_from(sequencer_argv([])); + assert!(cfg.sequencer); + assert!(resolve_da_args(&cfg).unwrap().is_none()); + } + + #[test] + fn sequencer_rejects_partial_da_args() { + let cfg = ::parse_from(sequencer_argv([ + "--ee-da-magic-bytes", + "ALPN", + ])); + let err = resolve_da_args(&cfg).unwrap_err(); + assert!(err.to_string().contains("must be provided together")); + } + #[test] fn fixed_requires_fee_rate() { let err = resolve_writer_config(&args(BtcioFeePolicyArg::Fixed, None, None)).unwrap_err(); diff --git a/contrib/assert-eest-prover-pipeline.sh b/contrib/assert-eest-prover-pipeline.sh new file mode 100755 index 0000000000..b1eab1d250 --- /dev/null +++ b/contrib/assert-eest-prover-pipeline.sh @@ -0,0 +1,246 @@ +#!/usr/bin/env bash +set -euo pipefail + +usage() { + cat <<'EOF' +Usage: assert-eest-prover-pipeline.sh --rpc-endpoint URL [options] + +Options: + --rpc-endpoint URL Execution JSON-RPC endpoint. + --target-block-number N Block number whose status must become confirmed/finalized. + Defaults to eth_blockNumber at assertion start. + --advanced-from N Assert the target block is greater than this block number. + --bitcoin-rpc-url URL Optional Bitcoin RPC URL used to mine regtest blocks while polling. + --bitcoin-wallet NAME Optional wallet name appended as /wallet/NAME for Bitcoin RPC. + --bitcoin-blocks N Bitcoin blocks to mine per poll when --bitcoin-rpc-url is set. + Default: 4. + --timeout SEC Max wait for block status. Default: 900. + --poll SEC Poll interval. Default: 5. + -h, --help Show this help. +EOF +} + +RPC_ENDPOINT="" +TARGET_BLOCK_NUMBER="" +ADVANCED_FROM="" +BITCOIN_RPC_URL="" +BITCOIN_WALLET="" +BITCOIN_BLOCKS_PER_POLL="4" +TIMEOUT_SECONDS="900" +POLL_SECONDS="5" + +while (($#)); do + case "$1" in + --rpc-endpoint) + RPC_ENDPOINT="${2:?missing value for --rpc-endpoint}" + shift 2 + ;; + --target-block-number) + TARGET_BLOCK_NUMBER="${2:?missing value for --target-block-number}" + shift 2 + ;; + --advanced-from) + ADVANCED_FROM="${2:?missing value for --advanced-from}" + shift 2 + ;; + --bitcoin-rpc-url) + BITCOIN_RPC_URL="${2:?missing value for --bitcoin-rpc-url}" + shift 2 + ;; + --bitcoin-wallet) + BITCOIN_WALLET="${2:?missing value for --bitcoin-wallet}" + shift 2 + ;; + --bitcoin-blocks) + BITCOIN_BLOCKS_PER_POLL="${2:?missing value for --bitcoin-blocks}" + shift 2 + ;; + --timeout) + TIMEOUT_SECONDS="${2:?missing value for --timeout}" + shift 2 + ;; + --poll) + POLL_SECONDS="${2:?missing value for --poll}" + shift 2 + ;; + -h|--help) + usage + exit 0 + ;; + *) + echo "unknown argument: $1" >&2 + usage >&2 + exit 1 + ;; + esac +done + +if [[ -z "${RPC_ENDPOINT}" ]]; then + echo "--rpc-endpoint is required" >&2 + usage >&2 + exit 1 +fi + +if [[ ! "${BITCOIN_BLOCKS_PER_POLL}" =~ ^[0-9]+$ ]] || ((BITCOIN_BLOCKS_PER_POLL < 1)); then + echo "--bitcoin-blocks must be a positive decimal integer" >&2 + exit 1 +fi + +rpc() { + local method="$1" + local params="${2:-[]}" + + curl -sf -X POST "${RPC_ENDPOINT}" \ + -H "Content-Type: application/json" \ + -d "{\"jsonrpc\":\"2.0\",\"method\":\"${method}\",\"params\":${params},\"id\":1}" +} + +bitcoin_rpc_endpoint() { + if [[ -z "${BITCOIN_WALLET}" ]]; then + printf '%s\n' "${BITCOIN_RPC_URL}" + return + fi + + printf '%s/wallet/%s\n' "${BITCOIN_RPC_URL%/}" "${BITCOIN_WALLET}" +} + +bitcoin_rpc() { + local method="$1" + local params="${2:-[]}" + + curl -sf -X POST "$(bitcoin_rpc_endpoint)" \ + -H "Content-Type: application/json" \ + -d "{\"jsonrpc\":\"1.0\",\"method\":\"${method}\",\"params\":${params},\"id\":1}" +} + +json_result() { + RPC_RESPONSE="$1" python3 - <<'PY' +import json +import os +import sys + +response = json.loads(os.environ["RPC_RESPONSE"]) +if "error" in response and response["error"] is not None: + print(response["error"], file=sys.stderr) + sys.exit(1) +result = response.get("result") +if result is None: + print("missing JSON-RPC result", file=sys.stderr) + sys.exit(1) +if isinstance(result, (dict, list)): + print(json.dumps(result)) +else: + print(result) +PY +} + +block_number() { + local response + response="$(rpc eth_blockNumber)" + RPC_RESPONSE="${response}" python3 - <<'PY' +import json +import os +import sys + +response = json.loads(os.environ["RPC_RESPONSE"]) +if "error" in response and response["error"] is not None: + print(response["error"], file=sys.stderr) + sys.exit(1) +print(int(response["result"], 16)) +PY +} + +block_by_number() { + local block_number="$1" + local block_hex + printf -v block_hex '0x%x' "${block_number}" + json_result "$(rpc eth_getBlockByNumber "[\"${block_hex}\",false]")" +} + +block_hash_for_number() { + BLOCK_JSON="$(block_by_number "$1")" python3 - <<'PY' +import json +import os +import sys + +block = json.loads(os.environ["BLOCK_JSON"]) +if block is None: + print("block not found", file=sys.stderr) + sys.exit(1) +print(block["hash"]) +PY +} + +block_status() { + local block_hash="$1" + local response + response="$(rpc alpen_getBlockStatus "[\"${block_hash}\"]")" + RPC_RESPONSE="${response}" python3 - <<'PY' +import json +import os +import sys + +response = json.loads(os.environ["RPC_RESPONSE"]) +if "error" in response and response["error"] is not None: + print(response["error"], file=sys.stderr) + sys.exit(1) +print(response["result"]["status"]) +PY +} + +mine_bitcoin_blocks() { + if [[ -z "${BITCOIN_RPC_URL}" ]]; then + return + fi + + if [[ -z "${MINING_ADDRESS:-}" ]]; then + MINING_ADDRESS="$(json_result "$(bitcoin_rpc getnewaddress)")" + fi + + bitcoin_rpc generatetoaddress "[${BITCOIN_BLOCKS_PER_POLL},\"${MINING_ADDRESS}\"]" >/dev/null +} + +if [[ -z "${TARGET_BLOCK_NUMBER}" ]]; then + TARGET_BLOCK_NUMBER="$(block_number)" +fi + +if [[ ! "${TARGET_BLOCK_NUMBER}" =~ ^[0-9]+$ ]]; then + echo "--target-block-number must be a decimal integer" >&2 + exit 1 +fi + +if [[ -n "${ADVANCED_FROM}" ]]; then + if [[ ! "${ADVANCED_FROM}" =~ ^[0-9]+$ ]]; then + echo "--advanced-from must be a decimal integer" >&2 + exit 1 + fi + if ((TARGET_BLOCK_NUMBER <= ADVANCED_FROM)); then + echo "EEST did not advance the chain: before=${ADVANCED_FROM} after=${TARGET_BLOCK_NUMBER}" >&2 + exit 1 + fi +fi + +TARGET_BLOCK_HASH="$(block_hash_for_number "${TARGET_BLOCK_NUMBER}")" +echo "waiting for EEST target block ${TARGET_BLOCK_NUMBER} (${TARGET_BLOCK_HASH}) to become confirmed or finalized" + +START_SECONDS="$(date +%s)" + +while true; do + STATUS="$(block_status "${TARGET_BLOCK_HASH}")" + echo "EEST target block status: number=${TARGET_BLOCK_NUMBER} status=${STATUS}" + + if [[ "${STATUS}" == "confirmed" || "${STATUS}" == "finalized" ]]; then + echo "EEST-generated blocks reached externally confirmed EE/OL status" + exit 0 + fi + + NOW_SECONDS="$(date +%s)" + ELAPSED_SECONDS=$((NOW_SECONDS - START_SECONDS)) + if ((ELAPSED_SECONDS >= TIMEOUT_SECONDS)); then + echo "timed out waiting for EEST target block ${TARGET_BLOCK_NUMBER} to become confirmed/finalized" >&2 + exit 1 + fi + + mine_bitcoin_blocks + sleep "${POLL_SECONDS}" +done diff --git a/contrib/run-eest-remote.sh b/contrib/run-eest-remote.sh new file mode 100755 index 0000000000..1981e69570 --- /dev/null +++ b/contrib/run-eest-remote.sh @@ -0,0 +1,115 @@ +#!/usr/bin/env bash +set -euo pipefail + +usage() { + cat <<'EOF' +Usage: run-eest-remote.sh --rpc-endpoint URL [options] + +Options: + --rpc-endpoint URL Execution JSON-RPC endpoint. + --fork NAME EEST fork name. Default: Prague. + --rpc-chain-id ID Chain ID. Default: 2892. + --rpc-seed-key KEY Seed private key for EEST transactions. + --tx-wait-timeout SEC Transaction wait timeout. Default: 120. + --repo URL execution-spec-tests repository. + --checkout-dir DIR Clone/use this directory. Default: execution-spec-tests. + -h, --help Show this help. +EOF +} + +RPC_ENDPOINT="" +FORK="Prague" +RPC_CHAIN_ID="2892" +RPC_SEED_KEY="0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80" +TX_WAIT_TIMEOUT="120" +EEST_REPO="https://github.com/alpenlabs/execution-spec-tests" +CHECKOUT_DIR="execution-spec-tests" + +while (($#)); do + case "$1" in + --rpc-endpoint) + RPC_ENDPOINT="${2:?missing value for --rpc-endpoint}" + shift 2 + ;; + --fork) + FORK="${2:?missing value for --fork}" + shift 2 + ;; + --rpc-chain-id) + RPC_CHAIN_ID="${2:?missing value for --rpc-chain-id}" + shift 2 + ;; + --rpc-seed-key) + RPC_SEED_KEY="${2:?missing value for --rpc-seed-key}" + shift 2 + ;; + --tx-wait-timeout) + TX_WAIT_TIMEOUT="${2:?missing value for --tx-wait-timeout}" + shift 2 + ;; + --repo) + EEST_REPO="${2:?missing value for --repo}" + shift 2 + ;; + --checkout-dir) + CHECKOUT_DIR="${2:?missing value for --checkout-dir}" + shift 2 + ;; + -h|--help) + usage + exit 0 + ;; + *) + echo "unknown argument: $1" >&2 + usage >&2 + exit 1 + ;; + esac +done + +if [[ -z "${RPC_ENDPOINT}" ]]; then + echo "--rpc-endpoint is required" >&2 + usage >&2 + exit 1 +fi + +WORKSPACE_DIR="$(pwd)" +if [[ "${CHECKOUT_DIR}" != /* ]]; then + CHECKOUT_DIR="${WORKSPACE_DIR}/${CHECKOUT_DIR}" +fi + +if ! command -v uv >/dev/null 2>&1; then + curl -LsSf https://astral.sh/uv/install.sh | sh + export PATH="${HOME}/.local/bin:${PATH}" +fi + +if [[ ! -d "${CHECKOUT_DIR}/.git" ]]; then + git clone "${EEST_REPO}" "${CHECKOUT_DIR}" +fi + +cd "${CHECKOUT_DIR}" + +uv python install 3.11 +uv python pin 3.11 +uv sync --all-extras + +# The Alpen fork keeps expected mismatches in skip_tests.yaml. Fail early if +# the checked-out EEST tree is missing the Prague skip needed by CI. +if [[ "${FORK}" == "Prague" ]]; then + REQUIRED_SKIP="tests/frontier/opcodes/test_call.py::test_call_memory_expands_on_early_revert[fork_${FORK}-state_test]" + if [[ ! -f skip_tests.yaml ]] || ! grep -Fq "${REQUIRED_SKIP}" skip_tests.yaml; then + echo "execution-spec-tests skip_tests.yaml is missing required Alpen skip: ${REQUIRED_SKIP}" >&2 + exit 1 + fi +fi + +uv run --with solc-select solc-select use 0.8.24 --always-install + +uv run --with solc-select execute remote \ + -m state_test \ + "--fork=${FORK}" \ + "--rpc-endpoint=${RPC_ENDPOINT}" \ + "--rpc-seed-key=${RPC_SEED_KEY}" \ + "--rpc-chain-id=${RPC_CHAIN_ID}" \ + "--tx-wait-timeout=${TX_WAIT_TIMEOUT}" \ + -v diff --git a/contrib/wait-for-json-rpc.sh b/contrib/wait-for-json-rpc.sh new file mode 100755 index 0000000000..b0bce94751 --- /dev/null +++ b/contrib/wait-for-json-rpc.sh @@ -0,0 +1,40 @@ +#!/usr/bin/env bash +set -euo pipefail + +usage() { + cat <<'EOF' +Usage: wait-for-json-rpc.sh [timeout-seconds] [interval-seconds] + +Polls an Ethereum JSON-RPC endpoint with eth_blockNumber until it responds. +EOF +} + +if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then + usage + exit 0 +fi + +RPC_URL="${1:?missing rpc url}" +TIMEOUT_SECONDS="${2:-120}" +INTERVAL_SECONDS="${3:-2}" + +START_SECONDS="$(date +%s)" + +while true; do + if curl -sf -X POST "${RPC_URL}" \ + -H "Content-Type: application/json" \ + -d '{"jsonrpc":"2.0","method":"eth_blockNumber","params":[],"id":1}' \ + >/dev/null 2>&1; then + echo "JSON-RPC endpoint is up: ${RPC_URL}" + exit 0 + fi + + NOW_SECONDS="$(date +%s)" + ELAPSED_SECONDS=$((NOW_SECONDS - START_SECONDS)) + if ((ELAPSED_SECONDS >= TIMEOUT_SECONDS)); then + echo "JSON-RPC endpoint did not become ready within ${TIMEOUT_SECONDS}s: ${RPC_URL}" >&2 + exit 1 + fi + + sleep "${INTERVAL_SECONDS}" +done diff --git a/docker/alpen-client/entrypoint.sh b/docker/alpen-client/entrypoint.sh index 5bda15b9c2..249b801a16 100644 --- a/docker/alpen-client/entrypoint.sh +++ b/docker/alpen-client/entrypoint.sh @@ -19,10 +19,15 @@ BITCOIND_RPC_URL="${BITCOIND_RPC_URL:?BITCOIND_RPC_URL must be set}" BITCOIND_RPC_USER="${BITCOIND_RPC_USER:?BITCOIND_RPC_USER must be set}" BITCOIND_RPC_PASSWORD="${BITCOIND_RPC_PASSWORD:?BITCOIND_RPC_PASSWORD must be set}" +if [ "${DUMMY_OL_CLIENT:-0}" = "1" ]; then + set -- --dummy-ol-client "$@" +else + set -- --ol-client-url "${OL_CLIENT_URL:-ws://strata:8432}" "$@" +fi + exec alpen-client \ --sequencer \ --sequencer-pubkey "${SEQUENCER_PUBKEY}" \ - --ol-client-url "${OL_CLIENT_URL:-ws://strata:8432}" \ --custom-chain "${CHAIN_SPEC}" \ --datadir "${DATADIR:-/app/data}" \ --addr 0.0.0.0 \ diff --git a/docker/docker-compose-eest.yml b/docker/docker-compose-eest.yml index 835bdfcf80..d2e3304a57 100644 --- a/docker/docker-compose-eest.yml +++ b/docker/docker-compose-eest.yml @@ -1,9 +1,10 @@ # Setup for Ethereum Execution Spec Tests (EEST). -# Runs alpen-client in sequencer mode with dummy OL + regtest bitcoind for DA. +# Runs a full regtest stack: bitcoind + Strata OL + alpen-client sequencer. # # Usage: -# ./init-alpen-client-keys.sh -# docker compose --env-file .env.alpen-client -f docker-compose-eest.yml up +# cargo build --release --bin strata-datatool +# cd docker && ./init-network.sh --sequencer ../target/release/strata-datatool +# docker compose --env-file .env.alpen -f docker-compose-eest.yml up services: @@ -23,7 +24,7 @@ services: ports: - "18443:18443" healthcheck: - test: ["CMD-SHELL", "bitcoin-cli -regtest -rpcuser=${BITCOIND_RPC_USER} -rpcpassword=${BITCOIND_RPC_PASSWORD} getwalletinfo"] + test: ["CMD-SHELL", "bitcoin-cli -regtest -rpcuser=${BITCOIND_RPC_USER:-rpcuser} -rpcpassword=${BITCOIND_RPC_PASSWORD:-rpcpassword} getwalletinfo"] interval: 2s timeout: 5s retries: 30 @@ -31,64 +32,98 @@ services: networks: - eest + strata: + build: + context: .. + dockerfile: docker/strata/Dockerfile + image: strata:latest + container_name: eest_strata + user: "0:0" + depends_on: + bitcoind: + condition: service_healthy + command: + - --sequencer + - --sequencer-key + - /app/configs/generated/sequencer.key + - --rpc-host + - "0.0.0.0" + environment: + CONFIG_PATH: /app/configs/config.regtest.toml + PARAM_PATH: /app/configs/generated/rollup-params.json + OL_PARAMS_PATH: /app/configs/generated/ol-params.json + ASM_PARAMS_PATH: /app/configs/generated/asm-params.json + BITCOIND_RPC_URL: http://bitcoind:${BITCOIND_RPC_PORT:-18443}/wallet/default + BITCOIND_RPC_USER: ${BITCOIND_RPC_USER:-rpcuser} + BITCOIND_RPC_PASSWORD: ${BITCOIND_RPC_PASSWORD:-rpcpassword} + BITCOIN_NETWORK: regtest + OL_BLOCK_TIME_MS: ${OL_BLOCK_TIME_MS:-5000} + RUST_LOG: ${RUST_LOG:-info} + RUST_BACKTRACE: 1 + NO_COLOR: 1 + volumes: + - ./configs:/app/configs:ro + - strata-data:/app/data + ports: + - "8432:8432" + entrypoint: ["/usr/local/bin/entrypoint.sh"] + healthcheck: + test: >- + curl -sf -X POST -H 'Content-Type: application/json' + -d '{"jsonrpc":"2.0","method":"strata_protocolVersion","params":[],"id":1}' + http://localhost:8432 + interval: 5s + timeout: 5s + retries: 30 + start_period: 10s + networks: + - eest + alpen-client: image: alpen-client:latest container_name: alpen_eest depends_on: bitcoind: condition: service_healthy + strata: + condition: service_healthy command: - - --sequencer - - --sequencer-pubkey - - ${SEQUENCER_PUBKEY} - - --dummy-ol-client - - --custom-chain - - ${CHAIN_SPEC:-dev} - - --datadir - - /app/data - --p2p-secret-key - /app/keys/seq-p2p.hex - - --addr - - "0.0.0.0" - --port - "30303" - --nat - none - --disable-discovery - - --http - - --http.addr - - "0.0.0.0" - - --http.port - - "8545" - - --http.api - - eth,net,web3,txpool,admin,debug - - --ee-da-magic-bytes - - ${EE_DA_MAGIC_BYTES:-ALPN} - - --btc-rpc-url - - http://bitcoind:${BITCOIND_RPC_PORT:-18443} - - --btc-rpc-user - - ${BITCOIND_RPC_USER:-rpcuser} - - --btc-rpc-password - - ${BITCOIND_RPC_PASSWORD:-rpcpassword} - - --l1-reorg-safe-depth - - "${L1_REORG_SAFE_DEPTH:-1}" - - --genesis-l1-height - - "${GENESIS_L1_HEIGHT:-101}" - - --batch-sealing-block-count - - "${BATCH_SEALING_BLOCK_COUNT:-5}" + - --dev-native-prover - --color - never environment: SEQUENCER_PRIVATE_KEY: ${SEQUENCER_PRIVATE_KEY} + SEQUENCER_PUBKEY: ${SEQUENCER_PUBKEY} + OL_CLIENT_URL: ws://strata:8432 + CHAIN_SPEC: ${CHAIN_SPEC:-dev} + BITCOIND_RPC_URL: http://bitcoind:${BITCOIND_RPC_PORT:-18443}/wallet/default + BITCOIND_RPC_USER: ${BITCOIND_RPC_USER:-rpcuser} + BITCOIND_RPC_PASSWORD: ${BITCOIND_RPC_PASSWORD:-rpcpassword} + EE_DA_MAGIC_BYTES: ${EE_DA_MAGIC_BYTES:-ALPN} + L1_REORG_SAFE_DEPTH: "${L1_REORG_SAFE_DEPTH:-1}" + GENESIS_L1_HEIGHT: "${GENESIS_L1_HEIGHT:-0}" + BATCH_SEALING_BLOCK_COUNT: "${BATCH_SEALING_BLOCK_COUNT:-5}" + HTTP_API: ${HTTP_API:-eth,net,web3,txpool,admin,debug,alpen} RUST_LOG: ${RUST_LOG:-info} ports: - "8545:8545" volumes: - - ./configs/alpen-client/seq-p2p.hex:/app/keys/seq-p2p.hex:ro + - ./configs/generated/jwt.hex:/app/keys/jwt.hex:ro + - ./configs/generated/seq-p2p.hex:/app/keys/seq-p2p.hex:ro entrypoint: ["/usr/local/bin/entrypoint.sh"] networks: - eest +volumes: + strata-data: + networks: eest: driver: bridge diff --git a/docker/init-eest-keys.sh b/docker/init-eest-keys.sh new file mode 100755 index 0000000000..231a7f1a82 --- /dev/null +++ b/docker/init-eest-keys.sh @@ -0,0 +1,95 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Generate minimal keys and env for the EEST Docker stack. +# +# Usage: +# cd docker && ./init-eest-keys.sh +# +# Outputs: +# .env.alpen env file for docker-compose-eest.yml +# configs/alpen-client/jwt.hex engine JWT secret +# configs/alpen-client/seq-p2p.hex P2P secret key +# configs/alpen-client/sequencer-schnorr.hex +# sequencer Schnorr secret key + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +OUTPUT_DIR="${SCRIPT_DIR}/configs/alpen-client" + +PYTHON="" +for candidate in python3 python3.12 python3.11 python3.10 python; do + if command -v "${candidate}" >/dev/null 2>&1 && "${candidate}" -c "import coincurve" >/dev/null 2>&1; then + PYTHON="${candidate}" + break + fi +done + +if [ -z "${PYTHON}" ]; then + echo "error: no python with 'coincurve' found. install: pip install coincurve" >&2 + exit 1 +fi + +generate_secret_key() { + od -An -tx1 -N32 /dev/urandom | tr -d ' \n' +} + +derive_schnorr_pubkey() { + local privkey_hex="$1" + echo -n "${privkey_hex}" | "${PYTHON}" -c ' +import coincurve +import sys + +private_key = bytes.fromhex(sys.stdin.read()) +public_key = coincurve.PublicKey.from_secret(private_key) +sys.stdout.write(public_key.format(compressed=True)[1:].hex()) +' +} + +generate_key_file() { + local filepath="$1" + if [ -f "${filepath}" ]; then + echo "exists: ${filepath}" + return + fi + + generate_secret_key > "${filepath}" + echo "generated: ${filepath}" +} + +mkdir -p "${OUTPUT_DIR}" + +SCHNORR_KEY="${OUTPUT_DIR}/sequencer-schnorr.hex" +generate_key_file "${SCHNORR_KEY}" +SCHNORR_PRIVKEY="$(cat "${SCHNORR_KEY}")" +SCHNORR_PUBKEY="$(derive_schnorr_pubkey "${SCHNORR_PRIVKEY}")" + +SEQ_P2P_KEY="${OUTPUT_DIR}/seq-p2p.hex" +generate_key_file "${SEQ_P2P_KEY}" + +JWT_KEY="${OUTPUT_DIR}/jwt.hex" +generate_key_file "${JWT_KEY}" + +ENV_FILE="${SCRIPT_DIR}/.env.alpen" + +cat > "${ENV_FILE}" < int: ), # Alpen-client (EE) environments "alpen_ee": AlpenClientEnv(enable_l1_da=True), + # EEST needs the externally observable OL/EE path, not a + # test-only client surface. + "alpen_eest": EeOLEnv( + fullnode_count=0, + pre_generate_blocks=110, + batch_sealing_block_count=5, + ), "alpen_ee_discovery": AlpenClientEnv( enable_discovery=True, pure_discovery=True, enable_l1_da=True ), diff --git a/functional-tests/envconfigs/alpen_client.py b/functional-tests/envconfigs/alpen_client.py index a71b9470a4..68df128d69 100644 --- a/functional-tests/envconfigs/alpen_client.py +++ b/functional-tests/envconfigs/alpen_client.py @@ -155,6 +155,7 @@ def get_services( enable_discovery=enable_discovery, ol_endpoint=ol_endpoint, da_config=da_config, + batch_sealing_block_count=batch_sealing_block_count, dev_track_latest_epoch=dev_track_latest_epoch, ) sequencer.wait_for_ready(timeout=60) diff --git a/functional-tests/factories/alpen_client.py b/functional-tests/factories/alpen_client.py index c9de96f479..bbf7f4826b 100644 --- a/functional-tests/factories/alpen_client.py +++ b/functional-tests/factories/alpen_client.py @@ -65,6 +65,7 @@ def create_sequencer( custom_chain: str = "dev", ol_endpoint: str | None = None, da_config: EeDaConfig | None = None, + batch_sealing_block_count: int = 100, dev_track_latest_epoch: bool = False, **kwargs, ) -> AlpenClientService: @@ -111,10 +112,11 @@ def create_sequencer( "--port", str(p2p_port), "--http", "--http.port", str(http_port), - "--http.api", "eth,net,admin,debug", + "--http.api", "eth,net,admin,debug,alpen", "--authrpc.port", str(authrpc_port), "--p2p-secret-key", str(p2p_secret_key_file), "--custom-chain", custom_chain, + "--batch-sealing-block-count", str(batch_sealing_block_count), "-vvvv", # Functional tests don't ship the SP1 guest ELFs, so run the # EE chunk + acct provers on the zkaleido NativeHost. @@ -155,7 +157,6 @@ def create_sequencer( "--btc-rpc-password", da_config.btc_rpc_password, "--l1-reorg-safe-depth", str(da_config.l1_reorg_safe_depth), "--genesis-l1-height", str(da_config.genesis_l1_height), - "--batch-sealing-block-count", str(da_config.batch_sealing_block_count), ]) # fmt: on @@ -255,7 +256,7 @@ def create_fullnode( "--port", str(p2p_port), "--http", "--http.port", str(http_port), - "--http.api", "eth,net,admin,debug", + "--http.api", "eth,net,admin,debug,alpen", "--authrpc.port", str(authrpc_port), "--p2p-secret-key", str(p2p_secret_key_file), "--custom-chain", custom_chain,