diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml index fc9e3c4042..61d1400d5d 100644 --- a/.github/workflows/docker-build.yml +++ b/.github/workflows/docker-build.yml @@ -182,6 +182,37 @@ jobs: run: | TAG=sha-$(git rev-parse --short HEAD) just run-smoke-test $TAG + smoke-test-evm-rollup-restart: + needs: [run_checker, composer, conductor, sequencer, sequencer-relayer, evm-bridge-withdrawer, cli] + if: (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'astriaorg/astria') && (github.event_name == 'merge_group' || needs.run_checker.outputs.run_docker == 'true') + runs-on: buildjet-8vcpu-ubuntu-2204 + steps: + - uses: actions/checkout@v4 + - name: Install just + uses: taiki-e/install-action@just + - name: Install kind + uses: helm/kind-action@v1 + with: + install_only: true + - name: Log in to GHCR + uses: docker/login-action@v2 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + - name: Setup Smoke Test Environment + timeout-minutes: 10 + run: | + TAG=sha-$(git rev-parse --short HEAD) + just deploy cluster + kubectl create secret generic regcred --from-file=.dockerconfigjson=$HOME/.docker/config.json --type=kubernetes.io/dockerconfigjson + echo -e "\n\nDeploying with astria images tagged $TAG" + just deploy evm-rollup-restart-test $TAG + - name: Run Smoke test + timeout-minutes: 3 + run: | + TAG=sha-$(git rev-parse --short HEAD) + just run-smoke-test $TAG smoke-cli: needs: [run_checker, composer, conductor, sequencer, sequencer-relayer, evm-bridge-withdrawer, cli] @@ -317,3 +348,4 @@ jobs: uses: ./.github/workflows/reusable-success.yml with: success: ${{ !contains(needs.*.result, 'failure') }} + \ No newline at end of file diff --git a/charts/deploy.just b/charts/deploy.just index 797554581b..0403ad61a1 100644 --- a/charts/deploy.just +++ b/charts/deploy.just @@ -211,6 +211,26 @@ deploy-smoke-test tag=defaultTag: --set evm-faucet.enabled=false > /dev/null @just wait-for-rollup > /dev/null +deploy-evm-rollup-restart-test tag=defaultTag: + @echo "Deploying ingress controller..." && just deploy ingress-controller > /dev/null + @just wait-for-ingress-controller > /dev/null + @echo "Deploying local celestia instance..." && just deploy celestia-local > /dev/null + @helm dependency update charts/sequencer > /dev/null + @helm dependency update charts/evm-stack > /dev/null + @echo "Setting up single astria sequencer..." && helm install \ + -n astria-validator-single single-sequencer-chart ./charts/sequencer \ + -f dev/values/validators/all.yml \ + -f dev/values/validators/single.yml \ + {{ if tag != '' { replace('--set images.sequencer.devTag=# --set sequencer-relayer.images.sequencerRelayer.devTag=#', '#', tag) } else { '' } }} \ + --create-namespace > /dev/null + @just wait-for-sequencer > /dev/null + @echo "Starting EVM rollup..." && helm install -n astria-dev-cluster astria-chain-chart ./charts/evm-stack -f dev/values/rollup/evm-restart-test.yaml \ + {{ if tag != '' { replace('--set evm-rollup.images.conductor.devTag=# --set composer.images.composer.devTag=# --set evm-bridge-withdrawer.images.evmBridgeWithdrawer.devTag=#', '#', tag) } else { '' } }} \ + --set blockscout-stack.enabled=false \ + --set postgresql.enabled=false \ + --set evm-faucet.enabled=false > /dev/null + @just wait-for-rollup > /dev/null + deploy-smoke-cli tag=defaultTag: @echo "Deploying ingress controller..." && just deploy ingress-controller > /dev/null @just wait-for-ingress-controller > /dev/null diff --git a/charts/evm-rollup/Chart.yaml b/charts/evm-rollup/Chart.yaml index 404198bebf..d266d88086 100644 --- a/charts/evm-rollup/Chart.yaml +++ b/charts/evm-rollup/Chart.yaml @@ -15,13 +15,13 @@ type: application # This is the chart version. This version number should be incremented each time you make changes # to the chart and its templates, including the app version. # Versions are expected to follow Semantic Versioning (https://semver.org/) -version: 1.1.2 +version: 1.1.3 # This is the version number of the application being deployed. This version number should be # incremented each time you make changes to the application. Versions are not expected to # follow Semantic Versioning. They should reflect the version the application is using. # It is recommended to use it with quotes. -appVersion: "1.0.0" +appVersion: "1.1.0" maintainers: - name: wafflesvonmaple diff --git a/charts/evm-rollup/files/genesis/geth-genesis.json b/charts/evm-rollup/files/genesis/geth-genesis.json index 116aa9b88c..d733a5038b 100644 --- a/charts/evm-rollup/files/genesis/geth-genesis.json +++ b/charts/evm-rollup/files/genesis/geth-genesis.json @@ -27,22 +27,98 @@ {{- range $key, $value := .Values.genesis.extra }} "{{ $key }}": {{ toPrettyJson $value | indent 8 | trim }}, {{- end }} - {{- if .Values.genesis.extraDataOverride }} - "astriaExtraDataOverride": "{{ .Values.genesis.extraDataOverride }}", - {{- end }} "astriaOverrideGenesisExtraData": {{ .Values.genesis.overrideGenesisExtraData }}, - "astriaSequencerInitialHeight": {{ toString .Values.genesis.sequencerInitialHeight | replace "\"" "" }}, "astriaRollupName": "{{ tpl .Values.genesis.rollupName . }}", - "astriaCelestiaInitialHeight": {{ toString .Values.genesis.celestiaInitialHeight | replace "\"" "" }}, - "astriaCelestiaHeightVariance": {{ toString .Values.genesis.celestiaHeightVariance | replace "\"" "" }}, - "astriaBridgeAddresses": {{ toPrettyJson .Values.genesis.bridgeAddresses | indent 8 | trim }}, - "astriaFeeCollectors": {{ toPrettyJson .Values.genesis.feeCollectors | indent 8 | trim }}, - "astriaEIP1559Params": {{ toPrettyJson .Values.genesis.eip1559Params | indent 8 | trim }}, - "astriaSequencerAddressPrefix": "{{ .Values.genesis.sequencerAddressPrefix }}" - {{- if not .Values.global.dev }} - {{- else }} - {{- end }} + "astriaForks": { + {{- $forks := .Values.genesis.forks }} + {{- $index := 0 }} + {{- $lastIndex := sub (len $forks) 1 }} + {{- range $key, $value := .Values.genesis.forks }} + "{{ $key }}": { + {{- $fields := list }} + {{- with $value }} + + {{- if .height }} + {{- $fields = append $fields (printf "\"height\": %s" (toString .height | replace "\"" "")) }} + {{- end }} + + {{- if .halt }} + {{- $fields = append $fields (printf "\"halt\": %s" (toString .halt | replace "\"" "")) }} + {{- end }} + + {{- if .snapshotChecksum }} + {{- $fields = append $fields (printf "\"snapshotChecksum\": %s" (toString .snapshotChecksum)) }} + {{- end }} + + {{- if .extraDataOverride }} + {{- $fields = append $fields (printf "\"extraDataOverride\": %s" (toString .extraDataOverride)) }} + {{- end }} + + {{- if .feeCollector }} + {{- $fields = append $fields (printf "\"feeCollector\": \"%s\"" (toString .feeCollector)) }} + {{- end }} + + {{- if .eip1559Params }} + {{- $fields = append $fields (printf "\"eip1559Params\": %s" (toPrettyJson .eip1559Params | indent 8 | trim)) }} + {{- end }} + + {{- if .sequencer }} + {{- $sequencerFields := list }} + + {{- if .sequencer.chainId }} + {{- $sequencerFields = append $sequencerFields (printf "\"chainId\": \"%s\"" (tpl .sequencer.chainId .)) }} + {{- end }} + + {{- if .sequencer.addressPrefix }} + {{- $sequencerFields = append $sequencerFields (printf "\"addressPrefix\": \"%s\"" .sequencer.addressPrefix) }} + {{- end }} + + {{- if .sequencer.startHeight }} + {{- $sequencerFields = append $sequencerFields (printf "\"startHeight\": %s" (toString .sequencer.startHeight | replace "\"" "")) }} + {{- end }} + + {{- if .sequencer.stopHeight }} + {{- $sequencerFields = append $sequencerFields (printf "\"stopHeight\": %s" (toString .sequencer.stopHeight | replace "\"" "")) }} + {{- end }} + + {{- $fields = append $fields (printf "\"sequencer\": {\n%s\n}" (join ",\n" $sequencerFields | indent 4)) }} + {{- end }} + + {{- if .celestia }} + {{- $celestiaFields := list }} + + {{- if .celestia.chainId }} + {{- $celestiaFields = append $celestiaFields (printf "\"chainId\": \"%s\"" (tpl .celestia.chainId .)) }} + {{- end }} + + {{- if .celestia.startHeight }} + {{- $celestiaFields = append $celestiaFields (printf "\"startHeight\": %s" (toString .celestia.startHeight | replace "\"" "")) }} + {{- end }} + + {{- if .celestia.searchHeightMaxLookAhead }} + {{- $celestiaFields = append $celestiaFields (printf "\"searchHeightMaxLookAhead\": %s" (toString .celestia.searchHeightMaxLookAhead | replace "\"" "")) }} + {{- end }} + + {{- if $celestiaFields | len }} + {{- $fields = append $fields (printf "\"celestia\": {\n%s\n}" (join ",\n" $celestiaFields | indent 4)) }} + {{- end }} + {{- end }} + + {{- if .bridgeAddresses }} + {{- $fields = append $fields (printf "\"bridgeAddresses\": %s" (toPrettyJson .bridgeAddresses | indent 4 | trim)) }} + {{- end }} + + {{- join ",\n" $fields | indent 16 }} + } + {{- if ne $index $lastIndex }},{{ end }} + {{- $index = add $index 1 }} + {{- end }} + {{- end }} + } }, + {{- if not .Values.global.dev }} + {{- else }} + {{- end }} "difficulty": "0", "gasLimit": "{{ toString .Values.genesis.gasLimit | replace "\"" "" }}", "alloc": { diff --git a/charts/evm-rollup/templates/configmap.yaml b/charts/evm-rollup/templates/configmap.yaml index f912d50bd0..4ec8e085d2 100644 --- a/charts/evm-rollup/templates/configmap.yaml +++ b/charts/evm-rollup/templates/configmap.yaml @@ -6,14 +6,12 @@ metadata: data: ASTRIA_CONDUCTOR_LOG: "astria_conductor={{ .Values.config.logLevel }}" ASTRIA_CONDUCTOR_CELESTIA_NODE_HTTP_URL: "{{ .Values.config.celestia.rpc }}" - ASTRIA_CONDUCTOR_EXPECTED_CELESTIA_CHAIN_ID: "{{ tpl .Values.config.conductor.celestiaChainId . }}" ASTRIA_CONDUCTOR_CELESTIA_BEARER_TOKEN: "{{ .Values.config.celestia.token }}" ASTRIA_CONDUCTOR_CELESTIA_BLOCK_TIME_MS: "{{ .Values.config.conductor.celestiaBlockTimeMs }}" ASTRIA_CONDUCTOR_EXECUTION_RPC_URL: "http://127.0.0.1:{{ .Values.ports.executionGRPC }}" ASTRIA_CONDUCTOR_EXECUTION_COMMIT_LEVEL: "{{ .Values.config.conductor.executionCommitLevel }}" ASTRIA_CONDUCTOR_SEQUENCER_GRPC_URL: "{{ tpl .Values.config.conductor.sequencerGrpc . }}" ASTRIA_CONDUCTOR_SEQUENCER_COMETBFT_URL: "{{ tpl .Values.config.conductor.sequencerRpc . }}" - ASTRIA_CONDUCTOR_EXPECTED_SEQUENCER_CHAIN_ID: "{{ tpl .Values.config.conductor.sequencerChainId . }}" ASTRIA_CONDUCTOR_SEQUENCER_BLOCK_TIME_MS: "{{ .Values.config.conductor.sequencerBlockTimeMs }}" ASTRIA_CONDUCTOR_NO_METRICS: "{{ not .Values.metrics.enabled }}" ASTRIA_CONDUCTOR_METRICS_HTTP_LISTENER_ADDR: "0.0.0.0:{{ .Values.ports.conductorMetrics }}" @@ -85,4 +83,4 @@ data: {{- end }} --- {{- end }} -{{- end }} \ No newline at end of file +{{- end }} diff --git a/charts/evm-rollup/values.yaml b/charts/evm-rollup/values.yaml index ec9cd7a9c4..3050cecb78 100644 --- a/charts/evm-rollup/values.yaml +++ b/charts/evm-rollup/values.yaml @@ -11,8 +11,8 @@ images: repo: ghcr.io/astriaorg/astria-geth pullPolicy: IfNotPresent tag: 1.0.0 - devTag: latest - overrideTag: "" + devTag: pr-59 + overrideTag: pr-59 conductor: repo: ghcr.io/astriaorg/conductor pullPolicy: IfNotPresent @@ -24,20 +24,55 @@ images: tag: 1.69.0 genesis: - ## These values are used to configure the genesis block of the rollup chain - ## no defaults as they are unique to each chain - # The name of the rollup chain, used to generate the Rollup ID rollupName: "" - # Block height to start syncing rollup from, lowest possible is 2 - sequencerInitialHeight: "" - # The first Celestia height to utilize when looking for rollup data - celestiaInitialHeight: "" - # The variance in Celestia height to allow before halting the chain - celestiaHeightVariance: "" - # Will fill the extra data in each block, can be left empty - # can also fill with something unique for your chain. - extraDataOverride: "" + + # The "forks" for upgrading the chain. Contains necessary information for starting + # and, if desired, restarting the chain at a given height. The necessary fields + # for the genesis fork are provided, and additional forks can be added as needed. + forks: + launch: + # The rollup number to start executing blocks at, lowest possible is 1 + height: 1 + # Whether to halt the rollup chain at the given height + halt: "false" + # Checksum of the snapshot to use upon restart + snapshotChecksum: "" + # Will fill the extra data in each block, can be left empty + # can also fill with something unique for your chain. + extraDataOverride: "" + # Configure the fee collector for the evm tx fees, activated at block heights. + # If not configured, all tx fees will be burned. + feeCollector: "" + # 1: "0xaC21B97d35Bf75A7dAb16f35b111a50e78A72F30" + # Configure EIP-1559 params, activated at block heights. + eip1559Params: {} + # 1: + # minBaseFee: 0 + # elasticityMultiplier: 2 + # baseFeeChangeDenominator: 8 + sequencer: + # The chain id of the sequencer chain + chainId: "" + # The hrp for bech32m addresses, unlikely to be changed + addressPrefix: "astria" + # Block height to start syncing rollup from (inclusive), lowest possible is 2 + startHeight: "" + celestia: + # The chain id of the celestia chain + chainId: "" + # The first Celestia height to utilize when looking for rollup data + startHeight: "" + # The maximum number of blocks ahead of the lowest Celestia search height + # to search for a firm commitment + searchHeightMaxLookAhead: "" + # Configure the sequencer bridge addresses and allowed assets if using + # the astria canonical bridge. Recommend removing alloc values if so. + bridgeAddresses: [] + # - address: "684ae50c49a434199199c9c698115391152d7b3f" + # assetDenom: "nria" + # senderAddress: "0x0000000000000000000000000000000000000000" + # assetPrecision: 9 ## These are general configuration values with some recommended defaults @@ -45,34 +80,7 @@ genesis: gasLimit: "50000000" # If set to true the genesis block will contain extra data overrideGenesisExtraData: true - # The hrp for bech32m addresses, unlikely to be changed - sequencerAddressPrefix: "astria" - - ## These values are used to configure astria native bridging - ## Many of the fields have commented out example fields - - # Configure the sequencer bridge addresses and allowed assets if using - # the astria canonical bridge. Recommend removing alloc values if so. - bridgeAddresses: [] - # - address: "684ae50c49a434199199c9c698115391152d7b3f" - # startHeight: 1 - # assetDenom: "nria" - # senderAddress: "0x0000000000000000000000000000000000000000" - # assetPrecision: 9 - - - ## Fee configuration - # Configure the fee collector for the evm tx fees, activated at block heights. - # If not configured, all tx fees will be burned. - feeCollectors: {} - # 1: "0xaC21B97d35Bf75A7dAb16f35b111a50e78A72F30" - # Configure EIP-1559 params, activated at block heights - eip1559Params: {} - # 1: - # minBaseFee: 0 - # elasticityMultiplier: 2 - # baseFeeChangeDenominator: 8 ## Standard Eth Genesis config values # An EVM chain number id, different from the astria rollup name @@ -191,8 +199,6 @@ config: # - "FirmOnly" -> blocks are only pulled from DA # - "SoftAndFirm" -> blocks are pulled from both the sequencer and DA executionCommitLevel: 'SoftAndFirm' - # The chain id of the Astria sequencer chain conductor communicates with - sequencerChainId: "" # The expected fastest block time possible from sequencer, determines polling # rate. sequencerBlockTimeMs: 2000 @@ -204,8 +210,6 @@ config: sequencerGrpc: "" # The maximum number of requests to make to the sequencer per second sequencerRequestsPerSecond: 500 - # The chain id of the celestia network the conductor communicates with - celestiaChainId: "" celestia: # if config.rollup.executionLevel is NOT 'SoftOnly' AND celestia-node is not enabled diff --git a/charts/evm-stack/Chart.lock b/charts/evm-stack/Chart.lock index a8e13b2518..3181331c69 100644 --- a/charts/evm-stack/Chart.lock +++ b/charts/evm-stack/Chart.lock @@ -4,7 +4,7 @@ dependencies: version: 0.4.0 - name: evm-rollup repository: file://../evm-rollup - version: 1.1.2 + version: 1.1.3 - name: flame-rollup repository: file://../flame-rollup version: 0.0.2 @@ -26,5 +26,5 @@ dependencies: - name: blockscout-stack repository: https://blockscout.github.io/helm-charts version: 1.6.8 -digest: sha256:ec55e7e1427dd7af6b3764d7cedc7b5b168ec2443fa140cb3000c8ed68d711ac -generated: "2025-02-20T10:44:04.460576+02:00" +digest: sha256:85329bcb82e89b366cb2e9c64e7b74207c7f2b9fb0a479b8f1a6f3a3123b91b5 +generated: "2025-02-27T15:06:50.890389-06:00" diff --git a/charts/evm-stack/Chart.yaml b/charts/evm-stack/Chart.yaml index 640f1f9658..59db76012b 100644 --- a/charts/evm-stack/Chart.yaml +++ b/charts/evm-stack/Chart.yaml @@ -15,14 +15,14 @@ type: application # This is the chart version. This version number should be incremented each time you make changes # to the chart and its templates, including the app version. # Versions are expected to follow Semantic Versioning (https://semver.org/) -version: 1.0.12 +version: 1.0.13 dependencies: - name: celestia-node version: 0.4.0 repository: "file://../celestia-node" condition: celestia-node.enabled - name: evm-rollup - version: 1.1.2 + version: 1.1.3 repository: "file://../evm-rollup" condition: evm-rollup.enabled - name: flame-rollup diff --git a/charts/evm-stack/values.yaml b/charts/evm-stack/values.yaml index 18ad6e4350..770211aedd 100644 --- a/charts/evm-stack/values.yaml +++ b/charts/evm-stack/values.yaml @@ -13,7 +13,6 @@ global: rollupName: "" evmChainId: "" sequencerChainId: "" - celestiaChainId: "" otel: endpoint: "" tracesEndpoint: "" @@ -29,8 +28,6 @@ evm-rollup: chainId: "{{ .Values.global.evmChainId }}" config: conductor: - sequencerChainId: "{{ .Values.global.sequencerChainId }}" - celestiaChainId: "{{ .Values.global.celestiaChainId }}" sequencerRpc: "{{ .Values.global.sequencerRpc }}" sequencerGrpc: "{{ .Values.global.sequencerGrpc }}" otel: diff --git a/crates/astria-auctioneer/src/block/mod.rs b/crates/astria-auctioneer/src/block/mod.rs index 53475a69d1..d0ba1de265 100644 --- a/crates/astria-auctioneer/src/block/mod.rs +++ b/crates/astria-auctioneer/src/block/mod.rs @@ -97,7 +97,7 @@ impl Proposed { #[derive(Debug, Clone)] pub(crate) struct Executed { /// The rollup block metadata that resulted from executing a proposed Sequencer block. - block: execution::v1::Block, + block_metadata: execution::v2::ExecutedBlockMetadata, /// The hash of the sequencer block that was executed optimistically. sequencer_block_hash: block::Hash, } @@ -106,8 +106,9 @@ impl Executed { pub(crate) fn try_from_raw( raw: optimistic_execution::ExecuteOptimisticBlockStreamResponse, ) -> eyre::Result { - let block = if let Some(raw_block) = raw.block { - execution::v1::Block::try_from_raw(raw_block).wrap_err("invalid rollup block")? + let block_metadata = if let Some(raw_block_metadata) = raw.block { + execution::v2::ExecutedBlockMetadata::try_from_raw(raw_block_metadata) + .wrap_err("invalid rollup block")? } else { return Err(eyre!("missing block")); }; @@ -119,7 +120,7 @@ impl Executed { .wrap_err("invalid block hash")?; Ok(Self { - block, + block_metadata, sequencer_block_hash, }) } @@ -129,6 +130,7 @@ impl Executed { } pub(crate) fn rollup_block_hash(&self) -> RollupBlockHash { - RollupBlockHash::new(self.block.hash().clone()) + let bytes = self.block_metadata.hash().to_string().into_bytes().into(); + RollupBlockHash::new(bytes) } } diff --git a/crates/astria-conductor/CHANGELOG.md b/crates/astria-conductor/CHANGELOG.md index 2210b7b33e..e31d3dd595 100644 --- a/crates/astria-conductor/CHANGELOG.md +++ b/crates/astria-conductor/CHANGELOG.md @@ -13,6 +13,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Update `idna` dependency to resolve cargo audit warning [#1869](https://github.com/astriaorg/astria/pull/1869). - Remove panic source on shutdown [#1919](https://github.com/astriaorg/astria/pull/1919). +- Bump to v2 Execution API, using execution sessions and removing chain id env +vars [#2006](https://github.com/astriaorg/astria/pull/2006). ## [1.0.0] - 2024-10-25 diff --git a/crates/astria-conductor/local.env.example b/crates/astria-conductor/local.env.example index 9a1eb3e43d..6237d3109b 100644 --- a/crates/astria-conductor/local.env.example +++ b/crates/astria-conductor/local.env.example @@ -73,12 +73,6 @@ ASTRIA_CONDUCTOR_SEQUENCER_BLOCK_TIME_MS=2000 # CometBFT node. ASTRIA_CONDUCTOR_SEQUENCER_REQUESTS_PER_SECOND=500 -# The chain ID of the sequencer network the conductor should be communicating with. -ASTRIA_CONDUCTOR_EXPECTED_SEQUENCER_CHAIN_ID="test-sequencer-1000" - -# The chain ID of the Celestia network the conductor should be communicating with. -ASTRIA_CONDUCTOR_EXPECTED_CELESTIA_CHAIN_ID="test-celestia-1000" - # Set to true to enable prometheus metrics. ASTRIA_CONDUCTOR_NO_METRICS=true diff --git a/crates/astria-conductor/src/block_cache.rs b/crates/astria-conductor/src/block_cache.rs index 799b31eb2d..3b3307d929 100644 --- a/crates/astria-conductor/src/block_cache.rs +++ b/crates/astria-conductor/src/block_cache.rs @@ -74,6 +74,10 @@ impl BlockCache { cache: self, } } + + pub(crate) fn next_height_to_pop(&self) -> u64 { + self.next_height + } } impl BlockCache { diff --git a/crates/astria-conductor/src/celestia/builder.rs b/crates/astria-conductor/src/celestia/builder.rs index eb03d9440c..aa200ab6d3 100644 --- a/crates/astria-conductor/src/celestia/builder.rs +++ b/crates/astria-conductor/src/celestia/builder.rs @@ -27,8 +27,6 @@ pub(crate) struct Builder { pub(crate) rollup_state: StateReceiver, pub(crate) sequencer_cometbft_client: SequencerClient, pub(crate) sequencer_requests_per_second: u32, - pub(crate) expected_celestia_chain_id: String, - pub(crate) expected_sequencer_chain_id: String, pub(crate) shutdown: CancellationToken, pub(crate) metrics: &'static Metrics, } @@ -42,8 +40,6 @@ impl Builder { celestia_token, sequencer_cometbft_client, sequencer_requests_per_second, - expected_celestia_chain_id, - expected_sequencer_chain_id, shutdown, metrics, firm_blocks, @@ -60,8 +56,6 @@ impl Builder { rollup_state, sequencer_cometbft_client, sequencer_requests_per_second, - expected_celestia_chain_id, - expected_sequencer_chain_id, shutdown, metrics, }) diff --git a/crates/astria-conductor/src/celestia/mod.rs b/crates/astria-conductor/src/celestia/mod.rs index 07b1633b96..1c1efd297b 100644 --- a/crates/astria-conductor/src/celestia/mod.rs +++ b/crates/astria-conductor/src/celestia/mod.rs @@ -144,12 +144,6 @@ pub(crate) struct Reader { /// (usually to verify block data retrieved from Celestia blobs). sequencer_requests_per_second: u32, - /// The chain ID of the Celestia network the reader should be communicating with. - expected_celestia_chain_id: String, - - /// The chain ID of the Sequencer the reader should be communicating with. - expected_sequencer_chain_id: String, - /// Token to listen for Conductor being shut down. shutdown: CancellationToken, @@ -179,13 +173,13 @@ impl Reader { #[instrument(skip_all, err)] async fn initialize(&mut self) -> eyre::Result { + let expected_celestia_chain_id = self.rollup_state.celestia_chain_id(); let validate_celestia_chain_id = async { let actual_celestia_chain_id = get_celestia_chain_id(&self.celestia_client) .await .wrap_err("failed to fetch Celestia chain ID")?; - let expected_celestia_chain_id = &self.expected_celestia_chain_id; ensure!( - self.expected_celestia_chain_id == actual_celestia_chain_id.as_str(), + expected_celestia_chain_id == actual_celestia_chain_id.as_str(), "expected Celestia chain id `{expected_celestia_chain_id}` does not match actual: \ `{actual_celestia_chain_id}`" ); @@ -193,14 +187,14 @@ impl Reader { } .in_current_span(); + let expected_sequencer_chain_id = self.rollup_state.sequencer_chain_id(); let get_and_validate_sequencer_chain_id = async { let actual_sequencer_chain_id = get_sequencer_chain_id(self.sequencer_cometbft_client.clone()) .await .wrap_err("failed to get sequencer chain ID")?; - let expected_sequencer_chain_id = &self.expected_sequencer_chain_id; ensure!( - self.expected_sequencer_chain_id == actual_sequencer_chain_id.to_string(), + expected_sequencer_chain_id == actual_sequencer_chain_id.as_str(), "expected Celestia chain id `{expected_sequencer_chain_id}` does not match \ actual: `{actual_sequencer_chain_id}`" ); @@ -281,18 +275,18 @@ struct RunningReader { /// The next Celestia height that will be fetched. celestia_next_height: u64, - /// The reference Celestia height. `celestia_reference_height` + `celestia_variance` = C is the - /// maximum Celestia height up to which Celestia's blobs will be fetched. - /// `celestia_reference_height` is initialized to the base Celestia height stored in the - /// rollup genesis. It is later advanced to that Celestia height from which the next block - /// is derived that will be executed against the rollup (only if greater than the current - /// value; it will never go down). + /// The reference Celestia height. `celestia_reference_height` + + /// `celestia_search_height_max_look_ahead` = C is the maximum Celestia height up to which + /// Celestia's blobs will be fetched. `celestia_reference_height` is initialized to the + /// base Celestia height stored in the rollup state. It is later advanced to that Celestia + /// height from which the next block is derived that will be executed against the rollup + /// (only if greater than the current value; it will never go down). celestia_reference_height: u64, - /// `celestia_variance` + `celestia_reference_height` define the maximum Celestia height from - /// Celestia blobs can be fetched. Set once during initialization to the value stored in - /// the rollup genesis. - celestia_variance: u64, + /// `celestia_search_height_max_look_ahead` + `celestia_reference_height` define the maximum + /// Celestia height from Celestia blobs that can be fetched. Set once during initialization + /// to the value stored in the rollup state. + celestia_search_height_max_look_ahead: u64, /// The rollup ID of the rollup that conductor is driving. Set once during initialization to /// the value stored in the @@ -338,9 +332,10 @@ impl RunningReader { let sequencer_namespace = astria_core::celestia::namespace_v0_from_sha256_of_bytes(sequencer_chain_id.as_bytes()); - let celestia_next_height = rollup_state.celestia_base_block_height(); - let celestia_reference_height = rollup_state.celestia_base_block_height(); - let celestia_variance = rollup_state.celestia_block_variance(); + let celestia_next_height = rollup_state.lowest_celestia_search_height(); + let celestia_reference_height = rollup_state.lowest_celestia_search_height(); + let celestia_search_height_max_look_ahead = + rollup_state.celestia_search_height_max_look_ahead(); Ok(Self { block_cache, @@ -359,7 +354,7 @@ impl RunningReader { celestia_head_height: None, celestia_next_height, celestia_reference_height, - celestia_variance, + celestia_search_height_max_look_ahead, rollup_id, rollup_namespace, @@ -374,7 +369,7 @@ impl RunningReader { info!( initial_celestia_height = self.celestia_next_height, initial_max_celestia_height = self.max_permitted_celestia_height(), - celestia_variance = self.celestia_variance, + celestia_search_height_max_look_ahead = self.celestia_search_height_max_look_ahead, rollup_namespace = %base64(&self.rollup_namespace.as_bytes()), rollup_id = %self.rollup_id, sequencer_chain_id = %self.sequencer_chain_id, @@ -384,6 +379,10 @@ impl RunningReader { }); let reason = loop { + if self.has_reached_stop_height()? { + break Ok("stop height reached"); + } + self.schedule_new_blobs(); select!( @@ -449,6 +448,19 @@ impl RunningReader { } } + /// The stop height is reached if a) the next height to be forwarded would be greater + /// than the stop height, and b) there is no block currently in flight. + fn has_reached_stop_height(&self) -> eyre::Result { + Ok(self + .rollup_state + .sequencer_stop_height() + .wrap_err("failed to obtain sequencer stop height")? + .map_or(false, |height| { + self.block_cache.next_height_to_pop() > height.get() + && self.enqueued_block.is_terminated() + })) + } + #[instrument(skip_all)] fn cache_reconstructed_blocks(&mut self, reconstructed: ReconstructedBlocks) { for block in reconstructed.blocks { @@ -533,14 +545,14 @@ impl RunningReader { /// Returns the maximum permitted Celestia height given the current state. /// - /// The maximum permitted Celestia height is calculated as `ref_height + 6 * variance`, with: + /// The maximum permitted Celestia height is calculated as `ref_height + + /// celestia_search_height_max_look_ahead`, with: /// /// - `ref_height` the height from which the last expected sequencer block was derived, - /// - `variance` the `celestia_block_variance` received from the connected rollup genesis info, - /// - and the factor 6 based on the assumption that there are up to 6 sequencer heights stored - /// per Celestia height. + /// - `celestia_search_height_max_look_ahead` received from the current rollup state, fn max_permitted_celestia_height(&self) -> u64 { - max_permitted_celestia_height(self.celestia_reference_height, self.celestia_variance) + self.celestia_reference_height + .saturating_add(self.celestia_search_height_max_look_ahead) } fn record_latest_celestia_height(&mut self, height: u64) { @@ -695,10 +707,6 @@ async fn get_sequencer_chain_id(client: SequencerClient) -> eyre::Result u64 { - reference.saturating_add(variance.saturating_mul(6)) -} - #[instrument(skip_all)] fn report_exit(exit_reason: eyre::Result<&str>, message: &str) -> eyre::Result<()> { match exit_reason { diff --git a/crates/astria-conductor/src/conductor/inner.rs b/crates/astria-conductor/src/conductor/inner.rs index 7f964a7bac..ba593270d3 100644 --- a/crates/astria-conductor/src/conductor/inner.rs +++ b/crates/astria-conductor/src/conductor/inner.rs @@ -28,7 +28,7 @@ use crate::{ /// Exit value of the inner conductor impl to signal to the outer task whether to restart or /// shutdown -#[derive(Debug)] +#[derive(Debug, PartialEq, Eq)] pub(super) enum RestartOrShutdown { Restart, Shutdown, @@ -44,14 +44,14 @@ impl std::fmt::Display for RestartOrShutdown { } } -struct ShutdownSignalReceived; - /// The business logic of Conductur. pub(super) struct Inner { /// Token to signal to all tasks to shut down gracefully. shutdown_token: CancellationToken, - executor: Option>>, + config: Config, + + executor: Option>>>, } impl Inner { @@ -67,7 +67,7 @@ impl Inner { shutdown_token: CancellationToken, ) -> eyre::Result { let executor = executor::Builder { - config, + config: config.clone(), shutdown: shutdown_token.clone(), metrics, } @@ -76,6 +76,7 @@ impl Inner { Ok(Self { shutdown_token, + config, executor: Some(tokio::spawn(executor.run_until_stopped())), }) } @@ -87,28 +88,28 @@ impl Inner { pub(super) async fn run_until_stopped(mut self) -> eyre::Result { info_span!("Conductor::run_until_stopped").in_scope(|| info!("conductor is running")); - let exit_reason = select! { + let exit_status = select! { biased; () = self.shutdown_token.cancelled() => { - Ok(ShutdownSignalReceived) + Ok(None) }, res = self.executor.as_mut().expect("task must always be set at this point") => { // XXX: must Option::take the JoinHandle to avoid polling it in the shutdown logic. self.executor.take(); match res { - Ok(Ok(())) => Err(eyre!("executor exited unexpectedly")), + Ok(Ok(state)) => Ok(state), Ok(Err(err)) => Err(err.wrap_err("executor exited with error")), Err(err) => Err(Report::new(err).wrap_err("executor panicked")), } } }; - self.restart_or_shutdown(exit_reason).await + self.restart_or_shutdown(exit_status).await } - /// Shuts down all tasks. + /// Shuts down all tasks and returns a token indicating whether to restart or not. /// /// Waits 25 seconds for all tasks to shut down before aborting them. 25 seconds /// because kubernetes issues SIGKILL 30 seconds after SIGTERM, giving 5 seconds @@ -116,20 +117,30 @@ impl Inner { #[instrument(skip_all, err, ret(Display))] async fn restart_or_shutdown( mut self, - exit_reason: eyre::Result, + exit_status: eyre::Result>, ) -> eyre::Result { - self.shutdown_token.cancel(); - let restart_or_shutdown = match exit_reason { - Ok(ShutdownSignalReceived) => Ok(RestartOrShutdown::Shutdown), - Err(error) => { - error!(%error, "executor failed; checking error chain if conductor should be restarted"); - if check_for_restart(&error) { - Ok(RestartOrShutdown::Restart) - } else { - Err(error) + let restart_or_shutdown = 'decide_restart: { + if self.shutdown_token.is_cancelled() { + break 'decide_restart Ok(RestartOrShutdown::Shutdown); + } + + match exit_status { + Ok(None) => Err(eyre!( + "executor exited with a success value but without rollup status even though \ + it was not explicitly cancelled; this shouldn't happen" + )), + Ok(Some(status)) => should_restart_or_shutdown(&self.config, &status), + Err(error) => { + error!(%error, "executor failed; checking error chain if conductor should be restarted"); + if should_restart_despite_error(&error) { + Ok(RestartOrShutdown::Restart) + } else { + Err(error) + } } } }; + self.shutdown_token.cancel(); if let Some(mut executor) = self.executor.take() { let wait_until_timeout = Duration::from_secs(25); @@ -149,7 +160,7 @@ impl Inner { } #[instrument(skip_all)] -fn check_for_restart(err: &eyre::Report) -> bool { +fn should_restart_despite_error(err: &eyre::Report) -> bool { let mut current = Some(err.as_ref() as &dyn std::error::Error); while let Some(err) = current { if let Some(status) = err.downcast_ref::() { @@ -162,17 +173,194 @@ fn check_for_restart(err: &eyre::Report) -> bool { false } +fn should_restart_or_shutdown( + config: &Config, + status: &crate::executor::State, +) -> eyre::Result { + let Some(rollup_stop_block_number) = status.rollup_end_block_number() else { + return Err(eyre!( + "executor exited with a success value even though it was not configured to run with a \ + stop height and even though it received no shutdown signal; this should not happen" + )); + }; + + match config.execution_commit_level { + crate::config::CommitLevel::FirmOnly | crate::config::CommitLevel::SoftAndFirm => { + if status.has_firm_number_reached_stop_height() { + Ok(RestartOrShutdown::Restart) + } else { + Err(eyre!( + "executor exited with a success value, but the stop height was not reached + (execution kind: `{}`, firm rollup block number: `{}`, mapped to sequencer \ + height: `{}`, rollup start height: `{}`, sequencer start height: `{}`, \ + sequencer stop height: `{}`)", + config.execution_commit_level, + status.firm_number(), + status.firm_block_number_as_sequencer_height(), + status.rollup_start_block_number(), + status.sequencer_start_block_height(), + rollup_stop_block_number, + )) + } + } + crate::config::CommitLevel::SoftOnly => { + if status.has_soft_number_reached_stop_height() { + Ok(RestartOrShutdown::Restart) + } else { + Err(eyre!( + "executor exited with a success value, but the stop height was not reached + (execution kind: `{}`, soft rollup block number: `{}`, mapped to sequencer \ + height: `{}`, rollup start height: `{}`, sequencer start height: `{}`, \ + sequencer stop height: `{}`)", + config.execution_commit_level, + status.soft_number(), + status.soft_block_number_as_sequencer_height(), + status.rollup_start_block_number(), + status.sequencer_start_block_height(), + rollup_stop_block_number, + )) + } + } + } +} + #[cfg(test)] mod tests { + use astria_core::generated::astria::execution::v2::{ + CommitmentState, + ExecutedBlockMetadata, + ExecutionSessionParameters, + }; use astria_eyre::eyre::WrapErr as _; + use pbjson_types::Timestamp; - #[test] - fn check_for_restart_ok() { - let tonic_error: Result<&str, tonic::Status> = - Err(tonic::Status::new(tonic::Code::PermissionDenied, "error")); + use super::{ + executor::State, + RestartOrShutdown, + }; + use crate::{ + config::CommitLevel, + test_utils::{ + make_commitment_state, + make_execution_session_parameters, + make_rollup_state, + }, + Config, + }; + + fn make_config() -> crate::Config { + crate::Config { + celestia_block_time_ms: 0, + celestia_node_http_url: String::new(), + no_celestia_auth: false, + celestia_bearer_token: String::new(), + sequencer_grpc_url: String::new(), + sequencer_cometbft_url: String::new(), + sequencer_block_time_ms: 0, + sequencer_requests_per_second: 0, + execution_rpc_url: String::new(), + log: String::new(), + execution_commit_level: CommitLevel::SoftAndFirm, + force_stdout: false, + no_otel: false, + no_metrics: false, + metrics_http_listener_addr: String::new(), + pretty_print: false, + } + } + + #[track_caller] + fn should_restart_despite_error_test(code: tonic::Code) { + let tonic_error: Result<&str, tonic::Status> = Err(tonic::Status::new(code, "error")); let err = tonic_error.wrap_err("wrapper_1"); let err = err.wrap_err("wrapper_2"); let err = err.wrap_err("wrapper_3"); - assert!(super::check_for_restart(&err.unwrap_err())); + assert!(super::should_restart_despite_error(&err.unwrap_err())); + } + #[test] + fn should_restart_despite_error() { + should_restart_despite_error_test(tonic::Code::PermissionDenied); + } + + #[track_caller] + fn assert_restart_or_shutdown( + config: &Config, + state: &State, + restart_or_shutdown: &RestartOrShutdown, + ) { + assert_eq!( + &super::should_restart_or_shutdown(config, state).unwrap(), + restart_or_shutdown, + ); + } + + #[test] + fn restart_or_shutdown_on_firm_height_reached() { + assert_restart_or_shutdown( + &Config { + execution_commit_level: CommitLevel::SoftAndFirm, + ..make_config() + }, + &make_rollup_state( + "test_execution_session".to_string(), + ExecutionSessionParameters { + sequencer_start_block_height: 10, + rollup_start_block_number: 10, + rollup_end_block_number: 99, + ..make_execution_session_parameters() + }, + CommitmentState { + firm_executed_block_metadata: Some(ExecutedBlockMetadata { + number: 99, + hash: hex::encode([0u8; 32]).to_string(), + parent_hash: String::new(), + timestamp: Some(Timestamp::default()), + }), + soft_executed_block_metadata: Some(ExecutedBlockMetadata { + number: 99, + hash: hex::encode([0u8; 32]).to_string(), + parent_hash: String::new(), + timestamp: Some(Timestamp::default()), + }), + ..make_commitment_state() + }, + ), + &RestartOrShutdown::Restart, + ); + } + + #[test] + fn restart_or_shutdown_on_soft_height_reached() { + assert_restart_or_shutdown( + &Config { + execution_commit_level: CommitLevel::SoftOnly, + ..make_config() + }, + &make_rollup_state( + "test_execution_session".to_string(), + ExecutionSessionParameters { + sequencer_start_block_height: 10, + rollup_start_block_number: 10, + rollup_end_block_number: 99, + ..make_execution_session_parameters() + }, + CommitmentState { + firm_executed_block_metadata: Some(ExecutedBlockMetadata { + number: 99, + hash: hex::encode([0u8; 32]).to_string(), + parent_hash: String::new(), + timestamp: Some(Timestamp::default()), + }), + soft_executed_block_metadata: Some(ExecutedBlockMetadata { + number: 99, + hash: hex::encode([0u8; 32]).to_string(), + parent_hash: String::new(), + timestamp: Some(Timestamp::default()), + }), + ..make_commitment_state() + }, + ), + &RestartOrShutdown::Restart, + ); } } diff --git a/crates/astria-conductor/src/config.rs b/crates/astria-conductor/src/config.rs index cd48c4d1e0..974d65ae48 100644 --- a/crates/astria-conductor/src/config.rs +++ b/crates/astria-conductor/src/config.rs @@ -63,12 +63,6 @@ pub struct Config { /// The number of requests per second that will be sent to Sequencer. pub sequencer_requests_per_second: u32, - /// The chain ID of the sequencer network the conductor should be communiacting with. - pub expected_sequencer_chain_id: String, - - /// The chain ID of the Celestia network the conductor should be communicating with. - pub expected_celestia_chain_id: String, - /// Address of the RPC server for execution pub execution_rpc_url: String, diff --git a/crates/astria-conductor/src/executor/client.rs b/crates/astria-conductor/src/executor/client.rs index 1bab377d97..f2b0b180f8 100644 --- a/crates/astria-conductor/src/executor/client.rs +++ b/crates/astria-conductor/src/executor/client.rs @@ -1,15 +1,15 @@ use std::time::Duration; use astria_core::{ - execution::v1::{ - Block, + execution::v2::{ CommitmentState, - GenesisInfo, + ExecutedBlockMetadata, + ExecutionSession, }, generated::astria::{ - execution::{ - v1 as raw, - v1::execution_service_client::ExecutionServiceClient, + execution::v2::{ + self as raw, + execution_service_client::ExecutionServiceClient, }, sequencerblock::v1::RollupData, }, @@ -18,6 +18,7 @@ use astria_core::{ use astria_eyre::eyre::{ self, ensure, + OptionExt as _, WrapErr as _, }; use bytes::Bytes; @@ -59,15 +60,18 @@ impl Client { }) } - /// Calls RPC astria.execution.v1.GetBlock + /// Calls RPC astria.execution.v2.GetExecutedBlockMetadata #[instrument(skip_all, fields(block_number, uri = %self.uri), err)] - pub(crate) async fn get_block_with_retry(&mut self, block_number: u32) -> eyre::Result { - let raw_block = tryhard::retry_fn(|| { + pub(crate) async fn get_block_with_retry( + &mut self, + block_number: u64, + ) -> eyre::Result { + let raw_block_metadata = tryhard::retry_fn(|| { let mut client = self.inner.clone(); - let request = raw::GetBlockRequest { + let request = raw::GetExecutedBlockMetadataRequest { identifier: Some(block_identifier(block_number)), }; - async move { client.get_block(request).await } + async move { client.get_executed_block_metadata(request).await } }) .with_config(retry_config()) .in_current_span() @@ -78,48 +82,53 @@ impl Client { )? .into_inner(); ensure!( - block_number == raw_block.number, + block_number == raw_block_metadata.number, "requested block at number `{block_number}`, but received block contained `{}`", - raw_block.number + raw_block_metadata.number ); - Block::try_from_raw(raw_block).wrap_err("failed validating received block") + ExecutedBlockMetadata::try_from_raw(raw_block_metadata) + .wrap_err("failed validating received block") } - /// Calls remote procedure `astria.execution.v1.GetGenesisInfo` + /// Calls remote procedure `astria.execution.v2.CreateExecutionSession` #[instrument(skip_all, fields(uri = %self.uri), err)] - pub(crate) async fn get_genesis_info_with_retry(&mut self) -> eyre::Result { + pub(crate) async fn create_execution_session_with_retry( + &mut self, + ) -> eyre::Result { let response = tryhard::retry_fn(|| { let mut client = self.inner.clone(); - let request = raw::GetGenesisInfoRequest {}; - async move { client.get_genesis_info(request).await } + let request = raw::CreateExecutionSessionRequest {}; + async move { client.create_execution_session(request).await } }) .with_config(retry_config()) .in_current_span() .await .wrap_err( - "failed to execute astria.execution.v1.GetGenesisInfo RPC because of gRPC status code \ - or because number of retries were exhausted", + "failed to execute astria.execution.v2.CreateExecutionSession RPC because of gRPC \ + status code or because number of retries were exhausted", )? .into_inner(); - let genesis_info = GenesisInfo::try_from_raw(response) - .wrap_err("failed converting raw response to validated genesis info")?; - Ok(genesis_info) + let execution_session = ExecutionSession::try_from_raw(response) + .wrap_err("failed converting raw response to validated execution session")?; + Ok(execution_session) } - /// Calls remote procedure `astria.execution.v1.ExecuteBlock` + /// Calls remote procedure `astria.execution.v2.ExecuteBlock`. /// /// # Arguments /// - /// * `prev_block_hash` - Block hash of the parent block + /// * `session_id` - ID of the current execution session + /// * `parent_hash` - Hash of the parent block /// * `transactions` - List of transactions extracted from the sequencer block /// * `timestamp` - Optional timestamp of the sequencer block #[instrument(skip_all, fields(uri = %self.uri), err)] pub(super) async fn execute_block_with_retry( &mut self, - prev_block_hash: Bytes, + session_id: String, + parent_hash: String, transactions: Vec, timestamp: Timestamp, - ) -> eyre::Result { + ) -> eyre::Result { use prost::Message; let transactions = transactions @@ -129,7 +138,8 @@ impl Client { .wrap_err("failed to decode tx bytes as RollupData")?; let request = raw::ExecuteBlockRequest { - prev_block_hash, + session_id, + parent_hash, transactions, timestamp: Some(timestamp), }; @@ -142,52 +152,32 @@ impl Client { .in_current_span() .await .wrap_err( - "failed to execute astria.execution.v1.ExecuteBlock RPC because of gRPC status code \ + "failed to execute astria.execution.v2.ExecuteBlock RPC because of gRPC status code \ or because number of retries were exhausted", )? .into_inner(); - let block = Block::try_from_raw(response) - .wrap_err("failed converting raw response to validated block")?; - Ok(block) - } - - /// Calls remote procedure `astria.execution.v1.GetCommitmentState` - #[instrument(skip_all, fields(uri = %self.uri), err)] - pub(crate) async fn get_commitment_state_with_retry( - &mut self, - ) -> eyre::Result { - let response = tryhard::retry_fn(|| { - let mut client = self.inner.clone(); - async move { - let request = raw::GetCommitmentStateRequest {}; - client.get_commitment_state(request).await - } - }) - .with_config(retry_config()) - .in_current_span() - .await - .wrap_err( - "failed to execute astria.execution.v1.GetCommitmentState RPC because of gRPC status \ - code or because number of retries were exhausted", - )? - .into_inner(); - let commitment_state = CommitmentState::try_from_raw(response) - .wrap_err("failed converting raw response to validated commitment state")?; - Ok(commitment_state) + let response_metadata = response + .executed_block_metadata + .ok_or_eyre("response is missing executed block metadata")?; + let block_metadata = ExecutedBlockMetadata::try_from_raw(response_metadata) + .wrap_err("failed converting raw response to validated block metadata")?; + Ok(block_metadata) } - /// Calls remote procedure `astria.execution.v1.UpdateCommitmentState` + /// Calls remote procedure `astria.execution.v2.UpdateCommitmentState` /// /// # Arguments /// - /// * `firm` - The firm block - /// * `soft` - The soft block + /// * `session_id` - ID of the current execution session + /// * `commitment_state` - New commitment state to be updated with #[instrument(skip_all, fields(uri = %self.uri), err)] pub(super) async fn update_commitment_state_with_retry( &mut self, + session_id: String, commitment_state: CommitmentState, ) -> eyre::Result { let request = raw::UpdateCommitmentStateRequest { + session_id, commitment_state: Some(commitment_state.into_raw()), }; let response = tryhard::retry_fn(|| { @@ -199,7 +189,7 @@ impl Client { .in_current_span() .await .wrap_err( - "failed to execute astria.execution.v1.UpdateCommitmentState RPC because of gRPC \ + "failed to execute astria.execution.v2.UpdateCommitmentState RPC because of gRPC \ status code or because number of retries were exhausted", )? .into_inner(); @@ -209,11 +199,11 @@ impl Client { } } -/// Utility function to construct a `astria.execution.v1.BlockIdentifier` from `number` +/// Utility function to construct a `astria.execution.v2.ExecutedBlockIdentifier` from `number` /// to use in RPC requests. -fn block_identifier(number: u32) -> raw::BlockIdentifier { - raw::BlockIdentifier { - identifier: Some(raw::block_identifier::Identifier::BlockNumber(number)), +fn block_identifier(number: u64) -> raw::ExecutedBlockIdentifier { + raw::ExecutedBlockIdentifier { + identifier: Some(raw::executed_block_identifier::Identifier::Number(number)), } } diff --git a/crates/astria-conductor/src/executor/mod.rs b/crates/astria-conductor/src/executor/mod.rs index ad475db85f..1a786f5200 100644 --- a/crates/astria-conductor/src/executor/mod.rs +++ b/crates/astria-conductor/src/executor/mod.rs @@ -4,9 +4,9 @@ use std::{ }; use astria_core::{ - execution::v1::{ - Block, + execution::v2::{ CommitmentState, + ExecutedBlockMetadata, }, primitive::v1::RollupId, sequencerblock::v1::block::{ @@ -44,6 +44,7 @@ use tracing::{ debug_span, error, info, + info_span, instrument, warn, }; @@ -62,10 +63,11 @@ mod client; mod state; #[cfg(test)] mod tests; - pub(super) use client::Client; -use state::State; -pub(crate) use state::StateReceiver; +pub(crate) use state::{ + State, + StateReceiver, +}; use self::state::StateSender; @@ -83,17 +85,32 @@ pub(crate) struct Executor { metrics: &'static Metrics, } -impl Executor { - const CELESTIA: &'static str = "celestia"; - const SEQUENCER: &'static str = "sequencer"; +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +enum ReaderKind { + Firm, + Soft, +} + +impl std::fmt::Display for ReaderKind { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let msg = match self { + ReaderKind::Firm => "firm celestia reader", + ReaderKind::Soft => "soft sequencer reader", + }; + f.write_str(msg) + } +} - pub(crate) async fn run_until_stopped(self) -> eyre::Result<()> { +impl Executor { + pub(crate) async fn run_until_stopped(self) -> eyre::Result> { let initialized = select!( + biased; + () = self.shutdown.clone().cancelled_owned() => { - return report_exit(Ok( - "received shutdown signal while initializing task; \ - aborting intialization and exiting" - ), ""); + info_span!("shutdown signal on init").in_scope(|| { + info!("received shutdown signal while initializing executor; cancelling initialization"); + }); + return Ok(None); } res = self.init() => { res.wrap_err("initialization failed")? @@ -117,8 +134,14 @@ impl Executor { let reader_cancellation_token = self.shutdown.child_token(); let (firm_blocks_tx, firm_blocks_rx) = tokio::sync::mpsc::channel(16); - let (soft_blocks_tx, soft_blocks_rx) = - tokio::sync::mpsc::channel(state.calculate_max_spread()); + let (soft_blocks_tx, soft_blocks_rx) = tokio::sync::mpsc::channel( + state + .celestia_search_height_max_look_ahead() + .try_into() + .expect( + "converting a u64 to usize should work on any architecture conductor runs on", + ), + ); let mut reader_tasks = JoinMap::new(); if self.config.is_with_firm() { @@ -136,14 +159,12 @@ impl Executor { rollup_state: state.subscribe(), sequencer_cometbft_client: sequencer_cometbft_client.clone(), sequencer_requests_per_second: self.config.sequencer_requests_per_second, - expected_celestia_chain_id: self.config.expected_celestia_chain_id.clone(), - expected_sequencer_chain_id: self.config.expected_sequencer_chain_id.clone(), shutdown: reader_cancellation_token.child_token(), metrics: self.metrics, } .build() .wrap_err("failed to build Celestia Reader")?; - reader_tasks.spawn(Self::CELESTIA, reader.run_until_stopped()); + reader_tasks.spawn(ReaderKind::Firm, reader.run_until_stopped()); } if self.config.is_with_soft() { @@ -155,13 +176,12 @@ impl Executor { sequencer_grpc_client, sequencer_cometbft_client: sequencer_cometbft_client.clone(), sequencer_block_time: Duration::from_millis(self.config.sequencer_block_time_ms), - expected_sequencer_chain_id: self.config.expected_sequencer_chain_id.clone(), shutdown: reader_cancellation_token.child_token(), soft_blocks: soft_blocks_tx, rollup_state: state.subscribe(), } .build(); - reader_tasks.spawn(Self::SEQUENCER, sequencer_reader.run_until_stopped()); + reader_tasks.spawn(ReaderKind::Soft, sequencer_reader.run_until_stopped()); }; Ok(Initialized { @@ -178,45 +198,30 @@ impl Executor { }) } - #[instrument(skip_all, err)] + #[instrument(skip_all, err, ret(Display))] async fn create_initial_node_state(&self) -> eyre::Result { - let genesis_info = { - async { - self.client - .clone() - .get_genesis_info_with_retry() - .await - .wrap_err("failed getting genesis info") - } - }; - let commitment_state = { - async { - self.client - .clone() - .get_commitment_state_with_retry() - .await - .wrap_err("failed getting commitment state") - } - }; - let (genesis_info, commitment_state) = tokio::try_join!(genesis_info, commitment_state)?; + let execution_session = self + .client + .clone() + .create_execution_session_with_retry() + .await + .wrap_err("failed creating execution session")?; let (state, _) = state::channel( - State::try_from_genesis_info_and_commitment_state(genesis_info, commitment_state) - .wrap_err( - "failed to construct initial state gensis and commitment info received from \ - rollup", - )?, + State::try_from_execution_session( + &execution_session, + self.config.execution_commit_level, + ) + .wrap_err( + "failed to construct initial state using execution session received from rollup", + )?, ); self.metrics .absolute_set_executed_firm_block_number(state.firm_number()); self.metrics .absolute_set_executed_soft_block_number(state.soft_number()); - info!( - initial_state = serde_json::to_string(&*state.get()) - .expect("writing json to a string should not fail"), - "received genesis info from rollup", - ); + Ok(state) } } @@ -245,19 +250,19 @@ struct Initialized { /// /// Required to mark firm blocks received from celestia as executed /// without re-executing on top of the rollup node. - blocks_pending_finalization: HashMap, + blocks_pending_finalization: HashMap, metrics: &'static Metrics, /// The tasks reading block data off Celestia or Sequencer. - reader_tasks: JoinMap<&'static str, eyre::Result<()>>, + reader_tasks: JoinMap>, /// The cancellation token specifically for signaling the `reader_tasks` to shut down. reader_cancellation_token: CancellationToken, } impl Initialized { - async fn run(mut self) -> eyre::Result<()> { + async fn run(mut self) -> eyre::Result> { let reason = select!( biased; @@ -285,9 +290,7 @@ impl Initialized { block.hash = %block.block_hash(), "received block from celestia reader", )); - if let Err(error) = self.execute_firm(block).await { - break Err(error).wrap_err("failed executing firm block"); - } + self.execute_firm(block).await.wrap_err("failed executing firm block")?; } Some(block) = self.soft_blocks.recv(), if !self.is_spread_too_large() => @@ -297,13 +300,11 @@ impl Initialized { block.hash = %block.block_hash(), "received block from sequencer reader", )); - if let Err(error) = self.execute_soft(block).await { - break Err(error).wrap_err("failed executing soft block"); - } + self.execute_soft(block).await.wrap_err("failed executing soft block")?; } Some((task, res)) = self.reader_tasks.join_next() => { - break handle_task_exit(task, res); + self.handle_task_exit(task, res)?; } else => break Ok("all channels are closed") @@ -329,9 +330,8 @@ impl Initialized { (next_firm, next_soft) }; - let is_too_far_ahead = usize::try_from(next_soft.saturating_sub(next_firm)) - .map(|spread| spread >= self.state.calculate_max_spread()) - .unwrap_or(false); + let is_too_far_ahead = next_soft.saturating_sub(next_firm) + >= self.state.celestia_search_height_max_look_ahead(); if is_too_far_ahead { debug!("soft blocks are too far ahead of firm; skipping soft blocks"); @@ -365,23 +365,29 @@ impl Initialized { std::cmp::Ordering::Equal => {} } - let genesis_height = self.state.sequencer_genesis_block_height(); - let block_height = executable_block.height; - let Some(block_number) = - state::map_sequencer_height_to_rollup_height(genesis_height, block_height) - else { + let sequencer_start_block_height = self.state.sequencer_start_block_height(); + let rollup_start_block_number = self.state.rollup_start_block_number(); + let current_block_height = executable_block.height; + let Some(block_number) = state::map_sequencer_height_to_rollup_height( + sequencer_start_block_height, + rollup_start_block_number, + current_block_height, + ) else { bail!( - "failed to map block height rollup number. This means the operation - `sequencer_height - sequencer_genesis_height` underflowed or was not a valid - cometbft height. Sequencer height: `{block_height}`, sequencer genesis height: \ - `{genesis_height}`", + "failed to map block height to rollup number. This means the operation + `sequencer_height - sequencer_start_block_height + rollup_start_block_number` \ + underflowed or was not a valid cometbft height. Sequencer height: \ + `{current_block_height}`, + sequencer start height: `{sequencer_start_block_height}`, + rollup start number: `{rollup_start_block_number}`" ) }; // The parent hash of the next block is the hash of the block at the current head. let parent_hash = self.state.soft_hash(); + let session_id = self.state.execution_session_id(); let executed_block = self - .execute_block(parent_hash, executable_block) + .execute_block(session_id, parent_hash, executable_block) .await .wrap_err("failed to execute block")?; @@ -418,22 +424,28 @@ impl Initialized { "expected block at sequencer height {expected_height}, but got {block_height}", ); - let genesis_height = self.state.sequencer_genesis_block_height(); - let Some(block_number) = - state::map_sequencer_height_to_rollup_height(genesis_height, block_height) - else { + let sequencer_start_block_height = self.state.sequencer_start_block_height(); + let rollup_start_block_number = self.state.rollup_start_block_number(); + let Some(block_number) = state::map_sequencer_height_to_rollup_height( + sequencer_start_block_height, + rollup_start_block_number, + block_height, + ) else { bail!( - "failed to map block height rollup number. This means the operation - `sequencer_height - sequencer_genesis_height` underflowed or was not a valid - cometbft height. Sequencer height: `{block_height}`, sequencer genesis height: \ - `{genesis_height}`", + "failed to map block height to rollup number. This means the operation + `sequencer_height - sequencer_start_block_height + rollup_start_block_number` \ + underflowed or was not a valid cometbft height. Sequencer block height: \ + `{block_height}`, + sequencer start height: `{sequencer_start_block_height}`, + rollup start number: `{rollup_start_block_number}`" ) }; let update = if self.should_execute_firm_block() { let parent_hash = self.state.firm_hash(); + let session_id = self.state.execution_session_id(); let executed_block = self - .execute_block(parent_hash, executable_block) + .execute_block(session_id, parent_hash, executable_block) .await .wrap_err("failed to execute block")?; self.does_block_response_fulfill_contract(ExecutionKind::Firm, &executed_block) @@ -492,9 +504,10 @@ impl Initialized { ))] async fn execute_block( &mut self, - parent_hash: Bytes, + session_id: String, + parent_hash: String, block: ExecutableBlock, - ) -> eyre::Result { + ) -> eyre::Result { let ExecutableBlock { transactions, timestamp, @@ -503,9 +516,9 @@ impl Initialized { let n_transactions = transactions.len(); - let executed_block = self + let executed_block_metadata = self .client - .execute_block_with_retry(parent_hash, transactions, timestamp) + .execute_block_with_retry(session_id, parent_hash, transactions, timestamp) .await .wrap_err("failed to run execute_block RPC")?; @@ -513,12 +526,12 @@ impl Initialized { .record_transactions_per_executed_block(n_transactions); info!( - executed_block.hash = %telemetry::display::base64(&executed_block.hash()), - executed_block.number = executed_block.number(), + executed_block_metadata.hash = %telemetry::display::base64(&executed_block_metadata.hash()), + executed_block_metadata.number = executed_block_metadata.number(), "executed block", ); - Ok(executed_block) + Ok(executed_block_metadata) } #[instrument(skip_all, err)] @@ -528,24 +541,38 @@ impl Initialized { OnlySoft, ToSame, }; - let (firm, soft, celestia_height) = match update { - OnlyFirm(firm, celestia_height) => (firm, self.state.soft(), celestia_height), + + use crate::config::CommitLevel; + let (firm, soft, celestia_height, commit_level) = match update { + OnlyFirm(firm, celestia_height) => ( + firm, + self.state.soft(), + celestia_height, + CommitLevel::FirmOnly, + ), OnlySoft(soft) => ( self.state.firm(), soft, - self.state.celestia_base_block_height(), + self.state.lowest_celestia_search_height(), + CommitLevel::SoftOnly, + ), + ToSame(block, celestia_height) => ( + block.clone(), + block, + celestia_height, + CommitLevel::SoftAndFirm, ), - ToSame(block, celestia_height) => (block.clone(), block, celestia_height), }; let commitment_state = CommitmentState::builder() - .firm(firm) - .soft(soft) - .base_celestia_height(celestia_height) + .firm_executed_block_metadata(firm) + .soft_executed_block_metadata(soft) + .lowest_celestia_search_height(celestia_height) .build() .wrap_err("failed constructing commitment state")?; + let session_id = self.state.execution_session_id(); let new_state = self .client - .update_commitment_state_with_retry(commitment_state) + .update_commitment_state_with_retry(session_id, commitment_state) .await .wrap_err("failed updating remote commitment state")?; info!( @@ -556,7 +583,7 @@ impl Initialized { "updated commitment state", ); self.state - .try_update_commitment_state(new_state) + .try_update_commitment_state(new_state, commit_level) .wrap_err("failed updating internal state tracking rollup state; invalid?")?; Ok(()) } @@ -564,9 +591,9 @@ impl Initialized { fn does_block_response_fulfill_contract( &mut self, kind: ExecutionKind, - block: &Block, + block_metadata: &ExecutedBlockMetadata, ) -> Result<(), ContractViolation> { - does_block_response_fulfill_contract(&mut self.state, kind, block) + does_block_response_fulfill_contract(&mut self.state, kind, block_metadata) } /// Returns whether a firm block should be executed. @@ -583,57 +610,113 @@ impl Initialized { } #[instrument(skip_all, err)] - async fn shutdown(mut self, reason: eyre::Result<&'static str>) -> eyre::Result<()> { - info!("signaling all reader tasks to exit"); - self.reader_cancellation_token.cancel(); - while let Some((task, exit_status)) = self.reader_tasks.join_next().await { - match crate::utils::flatten(exit_status) { - Ok(()) => info!(task, "task exited"), - Err(error) => warn!(task, %error, "task exited with error"), + fn handle_task_exit( + &mut self, + task: ReaderKind, + res: Result, JoinError>, + ) -> eyre::Result<()> { + match task { + ReaderKind::Firm if self.config.is_with_firm() => { + match ( + self.state.rollup_end_block_number().is_some(), + self.state.has_firm_number_reached_stop_height(), + ) { + (true, true) => { + info!( + "firm number has reached stop height; signalling all readers to stop \ + and closing channels" + ); + self.reader_cancellation_token.cancel(); + self.firm_blocks.close(); + self.soft_blocks.close(); + Ok(()) + } + + (true, false) => match res { + Ok(Ok(())) => Err(eyre!("task exited with sucess value")), + Ok(Err(err)) => Err(err).wrap_err("task exited with error"), + Err(err) => Err(err).wrap_err("task panicked"), + } + .wrap_err_with(|| { + format!("task `{task}` exited unexpectedly before stop height was reached") + }), + + // fall-through case, no stop height was configured + (false, _) => match res { + Ok(Ok(())) => Err(eyre!("task exited with sucess value")), + Ok(Err(err)) => Err(err).wrap_err("task exited with error"), + Err(err) => Err(err).wrap_err("task panicked"), + } + .wrap_err_with(|| format!("task `{task}` exited unexpectedly")), + } } - } - report_exit(reason, "shutting down") - } -} -/// Wraps a task result to explain why it exited. -/// -/// Right now only the err-branch is populated because tasks should -/// never exit. Still returns an `eyre::Result` to line up with the -/// return type of [`Executor::run_until_stopped`]. -/// -/// Executor should `break handle_task_exit` immediately after calling -/// this method. -fn handle_task_exit( - task: &'static str, - res: Result, JoinError>, -) -> eyre::Result<&'static str> { - match res { - Ok(Ok(())) => Err(eyre!("task `{task}` finished unexpectedly")), - Ok(Err(err)) => Err(err).wrap_err_with(|| format!("task `{task}` exited with error")), - Err(err) => Err(err).wrap_err_with(|| format!("task `{task}` panicked")), + ReaderKind::Soft if self.config.is_with_soft() => { + match ( + self.state.rollup_end_block_number().is_some(), + self.state.has_soft_number_reached_stop_height(), + ) { + (true, true) => { + info!("soft number has reached stop height"); + Ok(()) + } + (true, false) => match res { + Ok(Ok(())) => Err(eyre!("task exited with sucess value")), + Ok(Err(err)) => Err(err).wrap_err("task exited with error"), + Err(err) => Err(err).wrap_err("task panicked"), + } + .wrap_err_with(|| { + format!("task `{task}` exited unexpectedly before stop height was reached") + }), + + (false, _) => match res { + Ok(Ok(())) => Err(eyre!("task exited with sucess value")), + Ok(Err(err)) => Err(err).wrap_err("task exited with error"), + Err(err) => Err(err).wrap_err("task panicked"), + } + .wrap_err_with(|| format!("task `{task}` exited unexpectedly")), + } + } + + ReaderKind::Firm | ReaderKind::Soft => Err(eyre!( + "task `{task}` exited but it shouldn't have run in the first place because \ + because commit level is set to `{}`", + self.config.execution_commit_level + )), + } } -} -#[instrument(skip_all)] -fn report_exit(reason: eyre::Result<&str>, message: &str) -> eyre::Result<()> { - // XXX: explicitly setting the message (usually implicitly set by tracing) - match reason { - Ok(reason) => { - info!(%reason, message); - Ok(()) + #[instrument(skip_all, err)] + async fn shutdown(mut self, reason: eyre::Result<&'static str>) -> eyre::Result> { + let message = "shutting down"; + match &reason { + Ok(reason) => { + info!(%reason, message); + } + Err(reason) => { + error!(%reason, message); + } } - Err(error) => { - error!(%error, message); - Err(error) + + info!("signaling all reader tasks to exit, closing all channels"); + self.reader_cancellation_token.cancel(); + self.firm_blocks.close(); + self.soft_blocks.close(); + while let Some((task, exit_status)) = self.reader_tasks.join_next().await { + match crate::utils::flatten(exit_status) { + Ok(()) => info!(%task, "task exited"), + Err(error) => warn!(%task, %error, "task exited"), + } } + + reason.map(|_| (Some(self.state.get().clone()))) } } enum Update { - OnlyFirm(Block, CelestiaHeight), - OnlySoft(Block), - ToSame(Block, CelestiaHeight), + OnlyFirm(ExecutedBlockMetadata, CelestiaHeight), + OnlySoft(ExecutedBlockMetadata), + ToSame(ExecutedBlockMetadata, CelestiaHeight), } #[derive(Debug)] @@ -719,24 +802,24 @@ enum ContractViolation { )] WrongBlock { kind: ExecutionKind, - current: u32, - expected: u32, - actual: u32, + current: u64, + expected: u64, + actual: u64, }, #[error("contract violated: current height cannot be incremented")] - CurrentBlockNumberIsMax { kind: ExecutionKind, actual: u32 }, + CurrentBlockNumberIsMax { kind: ExecutionKind, actual: u64 }, } fn does_block_response_fulfill_contract( state: &mut StateSender, kind: ExecutionKind, - block: &Block, + block_metadata: &ExecutedBlockMetadata, ) -> Result<(), ContractViolation> { let current = match kind { ExecutionKind::Firm => state.firm_number(), ExecutionKind::Soft => state.soft_number(), }; - let actual = block.number(); + let actual = block_metadata.number(); let expected = current .checked_add(1) .ok_or(ContractViolation::CurrentBlockNumberIsMax { diff --git a/crates/astria-conductor/src/executor/state.rs b/crates/astria-conductor/src/executor/state.rs index 1f315b078b..d03b6706d4 100644 --- a/crates/astria-conductor/src/executor/state.rs +++ b/crates/astria-conductor/src/executor/state.rs @@ -2,15 +2,21 @@ //! the other methods can be used. Otherwise, they will panic. //! //! The inner state must not be unset after having been set. +use std::num::NonZeroU64; + use astria_core::{ - execution::v1::{ - Block, + execution::v2::{ CommitmentState, - GenesisInfo, + ExecutedBlockMetadata, + ExecutionSession, + ExecutionSessionParameters, }, primitive::v1::RollupId, }; -use bytes::Bytes; +use astria_eyre::eyre::{ + self, + eyre, +}; use sequencer_client::tendermint::block::Height as SequencerHeight; use tokio::sync::watch::{ self, @@ -31,13 +37,15 @@ pub(super) fn channel(state: State) -> (StateSender, StateReceiver) { #[derive(Debug, thiserror::Error)] #[error( - "adding sequencer genesis height `{sequencer_genesis_height}` and `{commitment_type}` rollup \ - number `{rollup_number}` overflowed unsigned u32::MAX, the maximum permissible cometbft \ - height" + "could not map rollup number to sequencer height for commitment type `{commitment_type}`: the \ + operation `{sequencer_start_height} + ({rollup_number} - {rollup_start_block_number})` \ + failed because `{issue}`" )] -pub(super) struct InvalidState { +pub(crate) struct InvalidState { commitment_type: &'static str, - sequencer_genesis_height: u64, + issue: &'static str, + sequencer_start_height: u64, + rollup_start_block_number: u64, rollup_number: u64, } @@ -74,44 +82,80 @@ impl StateReceiver { self.inner.changed().await?; Ok(self.next_expected_soft_sequencer_height()) } + + pub(crate) fn sequencer_stop_height(&self) -> eyre::Result> { + let Some(rollup_end_block_number) = self.inner.borrow().rollup_end_block_number() else { + return Ok(None); + }; + let sequencer_start_height = self.inner.borrow().sequencer_start_block_height(); + let rollup_start_block_number = self.inner.borrow().rollup_start_block_number(); + Ok(NonZeroU64::new( + map_rollup_number_to_sequencer_height( + sequencer_start_height, + rollup_start_block_number, + rollup_end_block_number.get(), + ) + .map_err(|e| { + eyre!(e).wrap_err("failed to map rollup stop block number to sequencer height") + })? + .into(), + )) + } } pub(super) struct StateSender { inner: watch::Sender, } -fn can_map_firm_to_sequencer_height( - genesis_info: &GenesisInfo, +impl std::fmt::Display for StateSender { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let s = serde_json::to_string(&*self.inner.borrow()).unwrap(); + f.write_str(&s) + } +} + +fn map_firm_to_sequencer_height( + execution_session_parameters: &ExecutionSessionParameters, commitment_state: &CommitmentState, -) -> Result<(), InvalidState> { - let sequencer_genesis_height = genesis_info.sequencer_genesis_block_height(); +) -> Result { + let sequencer_start_height = execution_session_parameters.sequencer_start_block_height(); + let rollup_start_block_number = execution_session_parameters.rollup_start_block_number(); let rollup_number = commitment_state.firm().number(); - if map_rollup_number_to_sequencer_height(sequencer_genesis_height, rollup_number).is_none() { - Err(InvalidState { - commitment_type: "firm", - sequencer_genesis_height: sequencer_genesis_height.value(), - rollup_number: rollup_number.into(), - }) - } else { - Ok(()) - } + + map_rollup_number_to_sequencer_height( + sequencer_start_height, + rollup_start_block_number, + rollup_number, + ) + .map_err(|issue| InvalidState { + commitment_type: "firm", + issue, + sequencer_start_height, + rollup_start_block_number, + rollup_number, + }) } -fn can_map_soft_to_sequencer_height( - genesis_info: &GenesisInfo, +fn map_soft_to_sequencer_height( + execution_session_parameters: &ExecutionSessionParameters, commitment_state: &CommitmentState, -) -> Result<(), InvalidState> { - let sequencer_genesis_height = genesis_info.sequencer_genesis_block_height(); +) -> Result { + let sequencer_start_height = execution_session_parameters.sequencer_start_block_height(); + let rollup_start_block_number = execution_session_parameters.rollup_start_block_number(); let rollup_number = commitment_state.soft().number(); - if map_rollup_number_to_sequencer_height(sequencer_genesis_height, rollup_number).is_none() { - Err(InvalidState { - commitment_type: "soft", - sequencer_genesis_height: sequencer_genesis_height.value(), - rollup_number: rollup_number.into(), - }) - } else { - Ok(()) - } + + map_rollup_number_to_sequencer_height( + sequencer_start_height, + rollup_start_block_number, + rollup_number, + ) + .map_err(|issue| InvalidState { + commitment_type: "soft", + issue, + sequencer_start_height, + rollup_start_block_number, + rollup_number, + }) } impl StateSender { @@ -121,32 +165,18 @@ impl StateSender { } } - /// Calculates the maximum allowed spread between firm and soft commitments heights. - /// - /// The maximum allowed spread is taken as `max_spread = variance * 6`, where `variance` - /// is the `celestia_block_variance` as defined in the rollup node's genesis that this - /// executor/conductor talks to. - /// - /// The heuristic 6 is the largest number of Sequencer heights that will be found at - /// one Celestia height. - /// - /// # Panics - /// Panics if the `u32` underlying the celestia block variance tracked in the state could - /// not be converted to a `usize`. This should never happen on any reasonable architecture - /// that Conductor will run on. - pub(super) fn calculate_max_spread(&self) -> usize { - usize::try_from(self.celestia_block_variance()) - .expect("converting a u32 to usize should work on any architecture conductor runs on") - .saturating_mul(6) - } - pub(super) fn try_update_commitment_state( &mut self, commitment_state: CommitmentState, + commit_level: crate::config::CommitLevel, ) -> Result<(), InvalidState> { - let genesis_info = self.genesis_info(); - can_map_firm_to_sequencer_height(&genesis_info, &commitment_state)?; - can_map_soft_to_sequencer_height(&genesis_info, &commitment_state)?; + let execution_session_parameters = self.execution_session_parameters(); + if commit_level.is_with_firm() { + let _ = map_firm_to_sequencer_height(&execution_session_parameters, &commitment_state)?; + } + if commit_level.is_with_soft() { + let _ = map_soft_to_sequencer_height(&execution_session_parameters, &commitment_state)?; + } self.inner.send_modify(move |state| { state.set_commitment_state(commitment_state); }); @@ -195,205 +225,244 @@ macro_rules! forward_impls { forward_impls!( StateSender: - [genesis_info -> GenesisInfo], - [firm -> Block], - [soft -> Block], - [firm_number -> u32], - [soft_number -> u32], - [firm_hash -> Bytes], - [soft_hash -> Bytes], - [celestia_block_variance -> u64], + [execution_session_parameters -> ExecutionSessionParameters], + [execution_session_id -> String], + [firm -> ExecutedBlockMetadata], + [soft -> ExecutedBlockMetadata], + [firm_number -> u64], + [soft_number -> u64], + [firm_hash -> String], + [soft_hash -> String], [rollup_id -> RollupId], - [sequencer_genesis_block_height -> SequencerHeight], - [celestia_base_block_height -> u64], + [sequencer_start_block_height -> u64], + [lowest_celestia_search_height -> u64], + [celestia_search_height_max_look_ahead -> u64], + [rollup_start_block_number -> u64], + [rollup_end_block_number -> Option], + [has_firm_number_reached_stop_height -> bool], + [has_soft_number_reached_stop_height -> bool], ); forward_impls!( StateReceiver: - [celestia_base_block_height -> u64], - [celestia_block_variance -> u64], + [lowest_celestia_search_height -> u64], + [celestia_search_height_max_look_ahead -> u64], [rollup_id -> RollupId], + [sequencer_chain_id -> String], + [celestia_chain_id -> String], ); /// `State` tracks the genesis info and commitment state of the remote rollup node. -#[derive(Debug, serde::Serialize)] -pub(super) struct State { +#[derive(Clone, Debug, serde::Serialize)] +#[expect( + clippy::struct_field_names, + reason = "`commitment_state` is the most accurate name" +)] +pub(crate) struct State { + execution_session_id: String, + execution_session_parameters: ExecutionSessionParameters, commitment_state: CommitmentState, - genesis_info: GenesisInfo, } impl State { - pub(super) fn try_from_genesis_info_and_commitment_state( - genesis_info: GenesisInfo, - commitment_state: CommitmentState, + pub(crate) fn try_from_execution_session( + execution_session: &ExecutionSession, + commit_level: crate::config::CommitLevel, ) -> Result { - can_map_firm_to_sequencer_height(&genesis_info, &commitment_state)?; - can_map_soft_to_sequencer_height(&genesis_info, &commitment_state)?; + let execution_session_parameters = execution_session.execution_session_parameters(); + let commitment_state = execution_session.commitment_state(); + if commit_level.is_with_firm() { + let _ = map_firm_to_sequencer_height(execution_session_parameters, commitment_state)?; + } + if commit_level.is_with_soft() { + let _ = map_soft_to_sequencer_height(execution_session_parameters, commitment_state)?; + } Ok(State { - commitment_state, - genesis_info, + execution_session_id: execution_session.session_id().clone(), + execution_session_parameters: execution_session_parameters.clone(), + commitment_state: commitment_state.clone(), }) } + /// Returns if the tracked firm state of the rollup has reached the rollup stop block number. + pub(crate) fn has_firm_number_reached_stop_height(&self) -> bool { + let Some(rollup_end_block_number) = self.rollup_end_block_number() else { + return false; + }; + self.commitment_state.firm().number() >= rollup_end_block_number.get() + } + + /// Returns if the tracked soft state of the rollup has reached the rollup stop block number. + pub(crate) fn has_soft_number_reached_stop_height(&self) -> bool { + let Some(rollup_end_block_number) = self.rollup_end_block_number() else { + return false; + }; + self.commitment_state.soft().number() >= rollup_end_block_number.get() + } + /// Sets the inner commitment state. fn set_commitment_state(&mut self, commitment_state: CommitmentState) { self.commitment_state = commitment_state; } - fn genesis_info(&self) -> GenesisInfo { - self.genesis_info + fn execution_session_parameters(&self) -> &ExecutionSessionParameters { + &self.execution_session_parameters + } + + fn execution_session_id(&self) -> String { + self.execution_session_id.clone() } - fn firm(&self) -> &Block { + fn firm(&self) -> &ExecutedBlockMetadata { self.commitment_state.firm() } - fn soft(&self) -> &Block { + fn soft(&self) -> &ExecutedBlockMetadata { self.commitment_state.soft() } - fn firm_number(&self) -> u32 { + pub(crate) fn firm_number(&self) -> u64 { self.commitment_state.firm().number() } - fn soft_number(&self) -> u32 { + pub(crate) fn soft_number(&self) -> u64 { self.commitment_state.soft().number() } - fn firm_hash(&self) -> Bytes { - self.firm().hash().clone() + fn firm_hash(&self) -> String { + self.firm().hash().to_string() + } + + fn soft_hash(&self) -> String { + self.soft().hash().to_string() } - fn soft_hash(&self) -> Bytes { - self.soft().hash().clone() + fn lowest_celestia_search_height(&self) -> u64 { + self.commitment_state.lowest_celestia_search_height() } - fn celestia_base_block_height(&self) -> u64 { - self.commitment_state.base_celestia_height() + fn celestia_search_height_max_look_ahead(&self) -> u64 { + self.execution_session_parameters + .celestia_search_height_max_look_ahead() } - fn celestia_block_variance(&self) -> u64 { - self.genesis_info.celestia_block_variance() + pub(crate) fn sequencer_start_block_height(&self) -> u64 { + self.execution_session_parameters + .sequencer_start_block_height() } - fn sequencer_genesis_block_height(&self) -> SequencerHeight { - self.genesis_info.sequencer_genesis_block_height() + fn sequencer_chain_id(&self) -> String { + self.execution_session_parameters + .sequencer_chain_id() + .to_string() + } + + fn celestia_chain_id(&self) -> String { + self.execution_session_parameters + .celestia_chain_id() + .to_string() } fn rollup_id(&self) -> RollupId { - self.genesis_info.rollup_id() + self.execution_session_parameters.rollup_id() } - fn next_expected_firm_sequencer_height(&self) -> Option { - map_rollup_number_to_sequencer_height( - self.sequencer_genesis_block_height(), - self.firm_number().saturating_add(1), - ) + pub(crate) fn rollup_start_block_number(&self) -> u64 { + self.execution_session_parameters + .rollup_start_block_number() } - fn next_expected_soft_sequencer_height(&self) -> Option { - map_rollup_number_to_sequencer_height( - self.sequencer_genesis_block_height(), - self.soft_number().saturating_add(1), - ) + pub(crate) fn rollup_end_block_number(&self) -> Option { + self.execution_session_parameters.rollup_end_block_number() + } + + pub(crate) fn firm_block_number_as_sequencer_height(&self) -> SequencerHeight { + map_firm_to_sequencer_height(&self.execution_session_parameters, &self.commitment_state) + .expect( + "state must only contain numbers that can be mapped to sequencer heights; this is \ + enforced by its constructor and/or setter", + ) + } + + pub(crate) fn soft_block_number_as_sequencer_height(&self) -> SequencerHeight { + map_soft_to_sequencer_height(&self.execution_session_parameters, &self.commitment_state) + .expect( + "state must only contain numbers that can be mapped to sequencer heights; this is \ + enforced by its constructor and/or setter", + ) + } + + fn next_expected_firm_sequencer_height(&self) -> Result { + map_firm_to_sequencer_height(&self.execution_session_parameters, &self.commitment_state) + .map(SequencerHeight::increment) + } + + fn next_expected_soft_sequencer_height(&self) -> Result { + map_soft_to_sequencer_height(&self.execution_session_parameters, &self.commitment_state) + .map(SequencerHeight::increment) } } /// Maps a rollup height to a sequencer height. /// -/// Returns `None` if `sequencer_genesis_height + rollup_number` overflows -/// `u32::MAX`. +/// Returns error if `sequencer_start_height + (rollup_number - rollup_start_block_number)` +/// is out of range of `u64` or if `rollup_start_block_number` is more than 1 greater than +/// `rollup_number`. fn map_rollup_number_to_sequencer_height( - sequencer_genesis_height: SequencerHeight, - rollup_number: u32, -) -> Option { - let sequencer_genesis_height = sequencer_genesis_height.value(); - let rollup_number: u64 = rollup_number.into(); - let sequencer_height = sequencer_genesis_height.checked_add(rollup_number)?; - sequencer_height.try_into().ok() + sequencer_start_height: u64, + rollup_start_block_number: u64, + rollup_number: u64, +) -> Result { + if rollup_start_block_number > (rollup_number.checked_add(1).ok_or("overflows u64::MAX")?) { + return Err("rollup start height exceeds rollup number + 1"); + } + let sequencer_height = sequencer_start_height + .checked_add(rollup_number) + .ok_or("overflows u64::MAX")? + .checked_sub(rollup_start_block_number) + .ok_or("(sequencer height + rollup number - rollup start height) is negative")?; + sequencer_height + .try_into() + .map_err(|_| "overflows u64::MAX") } /// Maps a sequencer height to a rollup height. /// -/// Returns `None` if `sequencer_height - sequencer_genesis_height` underflows or if -/// the result does not fit in `u32`. +/// Returns `None` if `sequencer_height - sequencer_start_height + rollup_start_block_number` +/// underflows or if the result does not fit in `u64`. pub(super) fn map_sequencer_height_to_rollup_height( - sequencer_genesis_height: SequencerHeight, + sequencer_start_height: u64, + rollup_start_block_number: u64, sequencer_height: SequencerHeight, -) -> Option { +) -> Option { sequencer_height .value() - .checked_sub(sequencer_genesis_height.value())? - .try_into() - .ok() + .checked_sub(sequencer_start_height)? + .checked_add(rollup_start_block_number) } #[cfg(test)] mod tests { - use astria_core::{ - generated::astria::execution::v1 as raw, - Protobuf as _, - }; - use pbjson_types::Timestamp; - use super::*; - - fn make_commitment_state() -> CommitmentState { - let firm = Block::try_from_raw(raw::Block { - number: 1, - hash: vec![42u8; 32].into(), - parent_block_hash: vec![41u8; 32].into(), - timestamp: Some(Timestamp { - seconds: 123_456, - nanos: 789, - }), - }) - .unwrap(); - let soft = Block::try_from_raw(raw::Block { - number: 2, - hash: vec![43u8; 32].into(), - parent_block_hash: vec![42u8; 32].into(), - timestamp: Some(Timestamp { - seconds: 123_456, - nanos: 789, - }), - }) - .unwrap(); - CommitmentState::builder() - .firm(firm) - .soft(soft) - .base_celestia_height(1u64) - .build() - .unwrap() - } - - fn make_genesis_info() -> GenesisInfo { - let rollup_id = RollupId::new([24; 32]); - GenesisInfo::try_from_raw(raw::GenesisInfo { - rollup_id: Some(rollup_id.to_raw()), - sequencer_genesis_block_height: 10, - celestia_block_variance: 0, - }) - .unwrap() - } - - fn make_state() -> State { - State::try_from_genesis_info_and_commitment_state( - make_genesis_info(), - make_commitment_state(), - ) - .unwrap() - } + use crate::test_utils::{ + make_commitment_state, + make_execution_session_parameters, + make_rollup_state, + }; fn make_channel() -> (StateSender, StateReceiver) { - super::channel(make_state()) + super::channel(make_rollup_state( + "test_session".to_string(), + make_execution_session_parameters(), + make_commitment_state(), + )) } #[test] fn next_firm_sequencer_height_is_correct() { let (_, rx) = make_channel(); assert_eq!( - SequencerHeight::from(12u32), + SequencerHeight::from(11u32), rx.next_expected_firm_sequencer_height(), ); } @@ -402,24 +471,39 @@ mod tests { fn next_soft_sequencer_height_is_correct() { let (_, rx) = make_channel(); assert_eq!( - SequencerHeight::from(13u32), + SequencerHeight::from(12u32), rx.next_expected_soft_sequencer_height(), ); } #[track_caller] - fn assert_height_is_correct(left: u32, right: u32, expected: u32) { + fn assert_height_is_correct( + sequencer_start_height: u64, + rollup_start_number: u64, + rollup_number: u64, + expected_sequencer_height: u32, + ) { assert_eq!( - SequencerHeight::from(expected), - map_rollup_number_to_sequencer_height(SequencerHeight::from(left), right) - .expect("left + right is so small, they should never overflow"), + SequencerHeight::from(expected_sequencer_height), + map_rollup_number_to_sequencer_height( + sequencer_start_height, + rollup_start_number, + rollup_number, + ) + .unwrap() ); } + #[should_panic = "rollup start height exceeds rollup number"] + #[test] + fn is_error_if_rollup_start_exceeds_current_number_plus_one() { + map_rollup_number_to_sequencer_height(10, 11, 9).unwrap(); + } + #[test] fn mapping_rollup_height_to_sequencer_height_works() { - assert_height_is_correct(0, 0, 0); - assert_height_is_correct(0, 1, 1); - assert_height_is_correct(1, 0, 1); + assert_height_is_correct(0, 0, 0, 0); + assert_height_is_correct(0, 1, 1, 0); + assert_height_is_correct(1, 0, 1, 2); } } diff --git a/crates/astria-conductor/src/executor/tests.rs b/crates/astria-conductor/src/executor/tests.rs index a5206cb141..12e9a6e3ef 100644 --- a/crates/astria-conductor/src/executor/tests.rs +++ b/crates/astria-conductor/src/executor/tests.rs @@ -1,14 +1,12 @@ use astria_core::{ self, - execution::v1::{ - Block, - CommitmentState, - GenesisInfo, + execution::v2::{ + ExecutedBlockMetadata, + ExecutionSession, }, - generated::astria::execution::v1 as raw, + generated::astria::execution::v2 as raw, Protobuf as _, }; -use bytes::Bytes; use super::{ should_execute_firm_block, @@ -17,17 +15,17 @@ use super::{ StateReceiver, StateSender, }, - RollupId, }; -use crate::config::CommitLevel; - -const ROLLUP_ID: RollupId = RollupId::new([42u8; 32]); +use crate::{ + config::CommitLevel, + test_utils::make_execution_session_parameters, +}; -fn make_block(number: u32) -> raw::Block { - raw::Block { +fn make_block_metadata(number: u64) -> raw::ExecutedBlockMetadata { + raw::ExecutedBlockMetadata { number, - hash: Bytes::from_static(&[0u8; 32]), - parent_block_hash: Bytes::from_static(&[0u8; 32]), + hash: hex::encode([0u8; 32]).to_string(), + parent_hash: hex::encode([0u8; 32]).to_string(), timestamp: Some(pbjson_types::Timestamp { seconds: 0, nanos: 0, @@ -36,8 +34,8 @@ fn make_block(number: u32) -> raw::Block { } struct MakeState { - firm: u32, - soft: u32, + firm: u64, + soft: u64, } fn make_state( @@ -46,36 +44,38 @@ fn make_state( soft, }: MakeState, ) -> (StateSender, StateReceiver) { - let genesis_info = GenesisInfo::try_from_raw(raw::GenesisInfo { - rollup_id: Some(ROLLUP_ID.to_raw()), - sequencer_genesis_block_height: 1, - celestia_block_variance: 1, + let commitment_state = raw::CommitmentState { + firm_executed_block_metadata: Some(make_block_metadata(firm)), + soft_executed_block_metadata: Some(make_block_metadata(soft)), + lowest_celestia_search_height: 1, + }; + let execution_session = ExecutionSession::try_from_raw(raw::ExecutionSession { + session_id: "test_execution_session".to_string(), + execution_session_parameters: Some(make_execution_session_parameters()), + commitment_state: Some(commitment_state), }) .unwrap(); - let commitment_state = CommitmentState::try_from_raw(raw::CommitmentState { - firm: Some(make_block(firm)), - soft: Some(make_block(soft)), - base_celestia_height: 1, - }) + let state = State::try_from_execution_session( + &execution_session, + crate::config::CommitLevel::SoftAndFirm, + ) .unwrap(); - let state = - State::try_from_genesis_info_and_commitment_state(genesis_info, commitment_state).unwrap(); super::state::channel(state) } #[track_caller] -fn assert_contract_fulfilled(kind: super::ExecutionKind, state: MakeState, number: u32) { - let block = Block::try_from_raw(make_block(number)).unwrap(); +fn assert_contract_fulfilled(kind: super::ExecutionKind, state: MakeState, number: u64) { + let block_metadata = ExecutedBlockMetadata::try_from_raw(make_block_metadata(number)).unwrap(); let mut state = make_state(state); - super::does_block_response_fulfill_contract(&mut state.0, kind, &block) + super::does_block_response_fulfill_contract(&mut state.0, kind, &block_metadata) .expect("number stored in response block must be one more than in tracked state"); } #[track_caller] -fn assert_contract_violated(kind: super::ExecutionKind, state: MakeState, number: u32) { - let block = Block::try_from_raw(make_block(number)).unwrap(); +fn assert_contract_violated(kind: super::ExecutionKind, state: MakeState, number: u64) { + let block_metadata = ExecutedBlockMetadata::try_from_raw(make_block_metadata(number)).unwrap(); let mut state = make_state(state); - super::does_block_response_fulfill_contract(&mut state.0, kind, &block).expect_err( + super::does_block_response_fulfill_contract(&mut state.0, kind, &block_metadata).expect_err( "number stored in response block must not be one more than in tracked state", ); diff --git a/crates/astria-conductor/src/lib.rs b/crates/astria-conductor/src/lib.rs index ee26570938..228a374f40 100644 --- a/crates/astria-conductor/src/lib.rs +++ b/crates/astria-conductor/src/lib.rs @@ -20,6 +20,8 @@ pub mod config; pub(crate) mod executor; pub(crate) mod metrics; pub(crate) mod sequencer; +#[cfg(test)] +pub(crate) mod test_utils; mod utils; pub use build_info::BUILD_INFO; diff --git a/crates/astria-conductor/src/metrics.rs b/crates/astria-conductor/src/metrics.rs index 208bc2bbc4..a132228e78 100644 --- a/crates/astria-conductor/src/metrics.rs +++ b/crates/astria-conductor/src/metrics.rs @@ -61,14 +61,12 @@ impl Metrics { .record(block_count); } - pub(crate) fn absolute_set_executed_firm_block_number(&self, block_number: u32) { - self.executed_firm_block_number - .absolute(u64::from(block_number)); + pub(crate) fn absolute_set_executed_firm_block_number(&self, block_number: u64) { + self.executed_firm_block_number.absolute(block_number); } - pub(crate) fn absolute_set_executed_soft_block_number(&self, block_number: u32) { - self.executed_soft_block_number - .absolute(u64::from(block_number)); + pub(crate) fn absolute_set_executed_soft_block_number(&self, block_number: u64) { + self.executed_soft_block_number.absolute(block_number); } pub(crate) fn record_transactions_per_executed_block(&self, tx_count: usize) { diff --git a/crates/astria-conductor/src/sequencer/block_stream.rs b/crates/astria-conductor/src/sequencer/block_stream.rs index 490ae8f633..0d9f673370 100644 --- a/crates/astria-conductor/src/sequencer/block_stream.rs +++ b/crates/astria-conductor/src/sequencer/block_stream.rs @@ -1,5 +1,6 @@ use std::{ error::Error as StdError, + num::NonZeroU64, pin::Pin, task::Poll, }; @@ -35,6 +36,7 @@ struct Heights { rollup_expects: u64, greatest_requested_height: Option, latest_observed_sequencer_height: Option, + stop_height: Option, max_ahead: u64, } @@ -49,7 +51,11 @@ impl Heights { let not_too_far_ahead = potential_height < (self.rollup_expects.saturating_add(self.max_ahead)); let height_exists_on_sequencer = potential_height <= latest_observed_sequencer_height; - if not_too_far_ahead && height_exists_on_sequencer { + let stop_height_reached = self + .stop_height + .map_or(false, |stop_height| potential_height > stop_height.into()); + + if not_too_far_ahead && height_exists_on_sequencer && !stop_height_reached { Some(potential_height) } else { None @@ -148,12 +154,14 @@ impl BlocksFromHeightStream { pub(super) fn new( rollup_id: RollupId, first_height: Height, + last_height: Option, client: SequencerGrpcClient, ) -> Self { let heights = Heights { rollup_expects: first_height.value(), latest_observed_sequencer_height: None, greatest_requested_height: None, + stop_height: last_height, max_ahead: 128, }; Self { @@ -273,6 +281,8 @@ async fn fetch_block( #[cfg(test)] mod tests { + use std::num::NonZeroU64; + use super::Heights; #[test] @@ -281,6 +291,7 @@ mod tests { rollup_expects: 5, greatest_requested_height: None, latest_observed_sequencer_height: Some(6), + stop_height: None, max_ahead: 3, }; let next = heights.next_height_to_fetch(); @@ -306,18 +317,33 @@ mod tests { rollup_expects: 4, greatest_requested_height: Some(5), latest_observed_sequencer_height: Some(6), + stop_height: None, max_ahead: 2, }; let next = heights.next_height_to_fetch(); assert_eq!(None, next); } + #[test] + fn next_height_is_none_if_last_height_reached() { + let heights = Heights { + rollup_expects: 4, + greatest_requested_height: Some(6), + latest_observed_sequencer_height: Some(6), + stop_height: Some(NonZeroU64::new(6).unwrap()), + max_ahead: 5, + }; + let next = heights.next_height_to_fetch(); + assert_eq!(None, next); + } + #[test] fn next_height_is_none_if_at_sequencer_head() { let heights = Heights { rollup_expects: 4, greatest_requested_height: Some(5), latest_observed_sequencer_height: Some(5), + stop_height: None, max_ahead: 2, }; let next = heights.next_height_to_fetch(); @@ -330,6 +356,7 @@ mod tests { rollup_expects: 5, greatest_requested_height: None, latest_observed_sequencer_height: None, + stop_height: None, max_ahead: 3, }; let next = heights.next_height_to_fetch(); diff --git a/crates/astria-conductor/src/sequencer/builder.rs b/crates/astria-conductor/src/sequencer/builder.rs index a95b98e13a..c215074bc0 100644 --- a/crates/astria-conductor/src/sequencer/builder.rs +++ b/crates/astria-conductor/src/sequencer/builder.rs @@ -11,7 +11,6 @@ pub(crate) struct Builder { pub(crate) sequencer_grpc_client: SequencerGrpcClient, pub(crate) sequencer_cometbft_client: sequencer_client::HttpClient, pub(crate) sequencer_block_time: Duration, - pub(crate) expected_sequencer_chain_id: String, pub(crate) shutdown: CancellationToken, pub(crate) rollup_state: StateReceiver, pub(crate) soft_blocks: mpsc::Sender, @@ -23,7 +22,6 @@ impl Builder { sequencer_grpc_client, sequencer_cometbft_client, sequencer_block_time, - expected_sequencer_chain_id, shutdown, rollup_state, soft_blocks, @@ -34,7 +32,6 @@ impl Builder { sequencer_grpc_client, sequencer_cometbft_client, sequencer_block_time, - expected_sequencer_chain_id, shutdown, } } diff --git a/crates/astria-conductor/src/sequencer/mod.rs b/crates/astria-conductor/src/sequencer/mod.rs index c6006ecc50..49ff4ca5f9 100644 --- a/crates/astria-conductor/src/sequencer/mod.rs +++ b/crates/astria-conductor/src/sequencer/mod.rs @@ -15,6 +15,7 @@ use futures::{ self, BoxFuture, Fuse, + FusedFuture as _, }, FutureExt as _, StreamExt as _, @@ -74,9 +75,6 @@ pub(crate) struct Reader { /// height. sequencer_block_time: Duration, - /// The chain ID of the sequencer network the reader should be communicating with. - expected_sequencer_chain_id: String, - /// Token to listen for Conductor being shut down. shutdown: CancellationToken, } @@ -99,13 +97,13 @@ impl Reader { #[instrument(skip_all, err)] async fn initialize(&mut self) -> eyre::Result<()> { + let expected_sequencer_chain_id = self.rollup_state.sequencer_chain_id(); let actual_sequencer_chain_id = get_sequencer_chain_id(self.sequencer_cometbft_client.clone()) .await .wrap_err("failed to get chain ID from Sequencer")?; - let expected_sequencer_chain_id = &self.expected_sequencer_chain_id; ensure!( - self.expected_sequencer_chain_id == actual_sequencer_chain_id.as_str(), + expected_sequencer_chain_id == actual_sequencer_chain_id.as_str(), "expected chain id `{expected_sequencer_chain_id}` does not match actual: \ `{actual_sequencer_chain_id}`" ); @@ -152,6 +150,9 @@ impl RunningReader { } = reader; let next_expected_height = rollup_state.next_expected_soft_sequencer_height(); + let sequencer_stop_height = rollup_state + .sequencer_stop_height() + .wrap_err("failed to obtain sequencer stop height")?; let latest_height_stream = sequencer_cometbft_client.stream_latest_height(sequencer_block_time); @@ -162,6 +163,7 @@ impl RunningReader { let blocks_from_heights = BlocksFromHeightStream::new( rollup_state.rollup_id(), next_expected_height, + sequencer_stop_height, sequencer_grpc_client, ); @@ -186,9 +188,11 @@ impl RunningReader { } async fn run_loop(&mut self) -> eyre::Result<&'static str> { - use futures::future::FusedFuture as _; - loop { + if self.has_reached_stop_height()? { + return Ok("stop height reached"); + } + select! { biased; @@ -287,6 +291,19 @@ impl RunningReader { .set_next_expected_height_if_greater(next_height); self.block_cache.drop_obsolete(next_height); } + + /// The stop height is reached if a) the next height to be forwarded would be greater + /// than the stop height, and b) there is no block currently in flight. + fn has_reached_stop_height(&self) -> eyre::Result { + Ok(self + .rollup_state + .sequencer_stop_height() + .wrap_err("failed to obtain sequencer stop height")? + .map_or(false, |height| { + self.block_cache.next_height_to_pop() > height.get() + && self.enqueued_block.is_terminated() + })) + } } #[instrument(skip_all)] diff --git a/crates/astria-conductor/src/test_utils.rs b/crates/astria-conductor/src/test_utils.rs new file mode 100644 index 0000000000..9e81d25c6a --- /dev/null +++ b/crates/astria-conductor/src/test_utils.rs @@ -0,0 +1,68 @@ +use astria_core::{ + execution::v2::ExecutionSession, + generated::astria::execution::v2::{ + CommitmentState, + ExecutionSessionParameters, + }, + primitive::v1::RollupId, + Protobuf as _, +}; + +use crate::executor::State; + +pub(crate) fn make_commitment_state() -> CommitmentState { + let firm = astria_core::generated::astria::execution::v2::ExecutedBlockMetadata { + number: 1, + hash: hex::encode([42u8; 32]).to_string(), + parent_hash: hex::encode([41u8; 32]).to_string(), + timestamp: Some(pbjson_types::Timestamp { + seconds: 123_456, + nanos: 789, + }), + }; + let soft = astria_core::generated::astria::execution::v2::ExecutedBlockMetadata { + number: 2, + hash: hex::encode([43u8; 32]).to_string(), + parent_hash: hex::encode([42u8; 32]).to_string(), + timestamp: Some(pbjson_types::Timestamp { + seconds: 123_456, + nanos: 789, + }), + }; + + CommitmentState { + soft_executed_block_metadata: Some(soft), + firm_executed_block_metadata: Some(firm), + lowest_celestia_search_height: 1, + } +} + +pub(crate) fn make_execution_session_parameters() -> ExecutionSessionParameters { + let rollup_id = RollupId::new([24; 32]); + ExecutionSessionParameters { + rollup_id: Some(rollup_id.into_raw()), + rollup_start_block_number: 1, + rollup_end_block_number: 10, + sequencer_chain_id: "test-sequencer-0".to_string(), + sequencer_start_block_height: 10, + celestia_chain_id: "test-celestia-0".to_string(), + celestia_search_height_max_look_ahead: 90, + } +} + +pub(crate) fn make_rollup_state( + execution_session_id: String, + execution_session_parameters: ExecutionSessionParameters, + commitment_state: CommitmentState, +) -> State { + let execution_session = ExecutionSession::try_from_raw( + astria_core::generated::astria::execution::v2::ExecutionSession { + session_id: execution_session_id, + execution_session_parameters: Some(execution_session_parameters), + commitment_state: Some(commitment_state), + }, + ) + .unwrap(); + State::try_from_execution_session(&execution_session, crate::config::CommitLevel::SoftAndFirm) + .unwrap() +} diff --git a/crates/astria-conductor/tests/blackbox/firm_only.rs b/crates/astria-conductor/tests/blackbox/firm_only.rs index f08271ce43..48478ef6c6 100644 --- a/crates/astria-conductor/tests/blackbox/firm_only.rs +++ b/crates/astria-conductor/tests/blackbox/firm_only.rs @@ -5,10 +5,7 @@ use astria_conductor::{ Conductor, Config, }; -use astria_core::generated::astria::execution::v1::{ - GetCommitmentStateRequest, - GetGenesisInfoRequest, -}; +use astria_core::generated::astria::execution::v2::CreateExecutionSessionRequest; use futures::future::{ join, join4, @@ -27,8 +24,7 @@ use wiremock::{ use crate::{ celestia_network_head, - commitment_state, - genesis_info, + execution_session, helpers::{ make_config, spawn_conductor, @@ -39,9 +35,8 @@ use crate::{ }, mount_celestia_blobs, mount_celestia_header_network_head, - mount_executed_block, - mount_get_commitment_state, - mount_get_genesis_info, + mount_create_execution_session, + mount_execute_block, mount_sequencer_commit, mount_sequencer_genesis, mount_sequencer_validator_set, @@ -52,25 +47,27 @@ use crate::{ async fn simple() { let test_conductor = spawn_conductor(CommitLevel::FirmOnly).await; - mount_get_genesis_info!( - test_conductor, - sequencer_genesis_block_height: 1, - celestia_block_variance: 10, - ); - - mount_get_commitment_state!( + mount_create_execution_session!( test_conductor, - firm: ( - number: 1, - hash: [1; 64], - parent: [0; 64], - ), - soft: ( - number: 1, - hash: [1; 64], - parent: [0; 64], + execution_session_parameters: ( + rollup_start_block_number: 2, + rollup_end_block_number: 9, + sequencer_start_block_height: 3, + celestia_max_look_ahead: 10, ), - base_celestia_height: 1, + commitment_state: ( + firm: ( + number: 1, + hash: "1", + parent: "0", + ), + soft: ( + number: 1, + hash: "1", + parent: "0", + ), + lowest_celestia_search_height: 1, + ) ); mount_sequencer_genesis!(test_conductor); @@ -93,26 +90,26 @@ async fn simple() { mount_sequencer_validator_set!(test_conductor, height: 2u32); - let execute_block = mount_executed_block!( + let execute_block = mount_execute_block!( test_conductor, number: 2, - hash: [2; 64], - parent: [1; 64], + hash: "2", + parent: "1", ); let update_commitment_state = mount_update_commitment_state!( test_conductor, firm: ( number: 2, - hash: [2; 64], - parent: [1; 64], + hash: "2", + parent: "1", ), soft: ( number: 2, - hash: [2; 64], - parent: [1; 64], + hash: "2", + parent: "1", ), - base_celestia_height: 1, + lowest_celestia_search_height: 1, ); timeout( @@ -125,7 +122,7 @@ async fn simple() { .await .expect( "conductor should have executed the firm block and updated the firm commitment state \ - within 2000ms", + within 1000ms", ); } @@ -133,25 +130,27 @@ async fn simple() { async fn submits_two_heights_in_succession() { let test_conductor = spawn_conductor(CommitLevel::FirmOnly).await; - mount_get_genesis_info!( - test_conductor, - sequencer_genesis_block_height: 1, - celestia_block_variance: 10, - ); - - mount_get_commitment_state!( + mount_create_execution_session!( test_conductor, - firm: ( - number: 1, - hash: [1; 64], - parent: [0; 64], + execution_session_parameters: ( + rollup_start_block_number: 2, + rollup_end_block_number: 9, + sequencer_start_block_height: 3, + celestia_max_look_ahead: 10, ), - soft: ( - number: 1, - hash: [1; 64], - parent: [0; 64], + commitment_state: ( + firm: ( + number: 1, + hash: "1", + parent: "0", + ), + soft: ( + number: 1, + hash: "1", + parent: "0", + ), + lowest_celestia_search_height: 1, ), - base_celestia_height: 1, ); mount_sequencer_genesis!(test_conductor); @@ -181,48 +180,48 @@ async fn submits_two_heights_in_succession() { mount_sequencer_validator_set!(test_conductor, height: 3u32); - let execute_block_number_2 = mount_executed_block!( + let execute_block_number_2 = mount_execute_block!( test_conductor, number: 2, - hash: [2; 64], - parent: [1; 64], + hash: "2", + parent: "1", ); let update_commitment_state_number_2 = mount_update_commitment_state!( test_conductor, firm: ( number: 2, - hash: [2; 64], - parent: [1; 64], + hash: "2", + parent: "1", ), soft: ( number: 2, - hash: [2; 64], - parent: [1; 64], + hash: "2", + parent: "1", ), - base_celestia_height: 1, + lowest_celestia_search_height: 1, ); - let execute_block_number_3 = mount_executed_block!( + let execute_block_number_3 = mount_execute_block!( test_conductor, number: 3, - hash: [3; 64], - parent: [2; 64], + hash: "3", + parent: "2", ); let update_commitment_state_number_3 = mount_update_commitment_state!( test_conductor, firm: ( number: 3, - hash: [3; 64], - parent: [2; 64], + hash: "3", + parent: "2", ), soft: ( number: 3, - hash: [3; 64], - parent: [2; 64], + hash: "3", + parent: "2", ), - base_celestia_height: 1, + lowest_celestia_search_height: 1, ); timeout( @@ -236,7 +235,7 @@ async fn submits_two_heights_in_succession() { ) .await .expect( - "conductor should have executed the soft block and updated the soft commitment state \ + "conductor should have executed the firm block and updated the firm commitment state \ within 2000ms", ); } @@ -245,26 +244,29 @@ async fn submits_two_heights_in_succession() { async fn skips_already_executed_heights() { let test_conductor = spawn_conductor(CommitLevel::FirmOnly).await; - mount_get_genesis_info!( + mount_create_execution_session!( test_conductor, - sequencer_genesis_block_height: 1, - celestia_block_variance: 10, - ); - - mount_get_commitment_state!( - test_conductor, - firm: ( - number: 5, - hash: [1; 64], - parent: [0; 64], + execution_session_parameters: ( + rollup_start_block_number: 2, + rollup_end_block_number: 9, + sequencer_start_block_height: 3, + celestia_max_look_ahead: 10, ), - soft: ( - number: 5, - hash: [1; 64], - parent: [0; 64], + commitment_state: ( + firm: ( + number: 5, + hash: "1", + parent: "0", + ), + soft: ( + number: 5, + hash: "1", + parent: "0", + ), + lowest_celestia_search_height: 1, ), - base_celestia_height: 1, ); + mount_sequencer_genesis!(test_conductor); mount_celestia_header_network_head!( @@ -289,26 +291,26 @@ async fn skips_already_executed_heights() { mount_sequencer_validator_set!(test_conductor, height: 6u32); - let execute_block = mount_executed_block!( + let execute_block = mount_execute_block!( test_conductor, number: 6, - hash: [2; 64], - parent: [1; 64], + hash: "2", + parent: "1", ); let update_commitment_state = mount_update_commitment_state!( test_conductor, firm: ( number: 6, - hash: [2; 64], - parent: [1; 64], + hash: "2", + parent: "1", ), soft: ( number: 6, - hash: [2; 64], - parent: [1; 64], + hash: "2", + parent: "1", ), - base_celestia_height: 1, + lowest_celestia_search_height: 1, ); timeout( @@ -320,7 +322,7 @@ async fn skips_already_executed_heights() { ) .await .expect( - "conductor should have executed the soft block and updated the soft commitment state \ + "conductor should have executed the firm block and updated the firm commitment state \ within 1000ms", ); } @@ -329,25 +331,27 @@ async fn skips_already_executed_heights() { async fn fetch_from_later_celestia_height() { let test_conductor = spawn_conductor(CommitLevel::FirmOnly).await; - mount_get_genesis_info!( + mount_create_execution_session!( test_conductor, - sequencer_genesis_block_height: 1, - celestia_block_variance: 10, - ); - - mount_get_commitment_state!( - test_conductor, - firm: ( - number: 1, - hash: [1; 64], - parent: [0; 64], + execution_session_parameters: ( + rollup_start_block_number: 2, + rollup_end_block_number: 9, + sequencer_start_block_height: 3, + celestia_max_look_ahead: 10, ), - soft: ( - number: 1, - hash: [1; 64], - parent: [0; 64], + commitment_state: ( + firm: ( + number: 1, + hash: "1", + parent: "0", + ), + soft: ( + number: 1, + hash: "1", + parent: "0", + ), + lowest_celestia_search_height: 4, ), - base_celestia_height: 4, ); mount_sequencer_genesis!(test_conductor); @@ -370,26 +374,26 @@ async fn fetch_from_later_celestia_height() { mount_sequencer_validator_set!(test_conductor, height: 2u32); - let execute_block = mount_executed_block!( + let execute_block = mount_execute_block!( test_conductor, number: 2, - hash: [2; 64], - parent: [1; 64], + hash: "2", + parent: "1", ); let update_commitment_state = mount_update_commitment_state!( test_conductor, firm: ( number: 2, - hash: [2; 64], - parent: [1; 64], + hash: "2", + parent: "1", ), soft: ( number: 2, - hash: [2; 64], - parent: [1; 64], + hash: "2", + parent: "1", ), - base_celestia_height: 4, + lowest_celestia_search_height: 4, ); timeout( @@ -443,32 +447,30 @@ async fn exits_on_celestia_chain_id_mismatch() { }; GrpcMock::for_rpc_given( - "get_genesis_info", - matcher::message_type::(), - ) - .respond_with(GrpcResponse::constant_response( - genesis_info!(sequencer_genesis_block_height: 1, - celestia_block_variance: 10,), - )) - .expect(0..) - .mount(&mock_grpc.mock_server) - .await; - - GrpcMock::for_rpc_given( - "get_commitment_state", - matcher::message_type::(), + "create_execution_session", + matcher::message_type::(), ) - .respond_with(GrpcResponse::constant_response(commitment_state!(firm: ( - number: 1, - hash: [1; 64], - parent: [0; 64], - ), - soft: ( - number: 1, - hash: [1; 64], - parent: [0; 64], + .respond_with(GrpcResponse::constant_response(execution_session!( + execution_session_parameters: ( + rollup_start_block_number: 2, + rollup_end_block_number: 9, + sequencer_start_block_height: 3, + celestia_max_look_ahead: 10, ), - base_celestia_height: 1,))) + commitment_state: ( + firm: ( + number: 1, + hash: "1", + parent: "0", + ), + soft: ( + number: 1, + hash: "1", + parent: "0", + ), + lowest_celestia_search_height: 1, + ) + ))) .expect(0..) .mount(&mock_grpc.mock_server) .await; @@ -513,3 +515,172 @@ async fn exits_on_celestia_chain_id_mismatch() { } } } + +/// Tests that the conductor correctly stops at the stop block height and executes the firm block +/// for that height before restarting and continuing after requesting new execution session. +/// +/// It consists of the following steps: +/// 1. Mount execution session with a stop number of 2 for the first height (sequencer height 3), +/// only responding up to 1 time so that the same info is not provided after conductor restart. +/// 2. Mount sequencer genesis and celestia header network head. +/// 3. Mount firm blocks for heights 3 and 4. +/// 4. Mount `execute_block` and `update_commitment_state` for firm block 3, expecting only one call +/// since they should not be called after restarting. +/// 5. Wait ample time for conductor to restart before performing the next set of mounts. +/// 6. Mount new execution session with rollup start block number of 3 to reflect that the first +/// block has already been executed. +/// 7. Mount `execute_block` and `update_commitment_state` for firm block 4, awaiting their +/// satisfaction. +#[expect(clippy::too_many_lines, reason = "All lines reasonably necessary")] +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +async fn restarts_after_reaching_stop_block_height() { + let test_conductor = spawn_conductor(CommitLevel::FirmOnly).await; + + mount_create_execution_session!( + test_conductor, + execution_session_parameters: ( + rollup_start_block_number: 2, + rollup_end_block_number: 2, + sequencer_start_block_height: 3, + celestia_max_look_ahead: 10, + ), + commitment_state: ( + firm: ( + number: 1, + hash: "1", + parent: "0", + ), + soft: ( + number: 1, + hash: "1", + parent: "0", + ), + lowest_celestia_search_height: 1, + ), + up_to_n_times: 1, // Only respond once, since a new execution session is needed after restart. + ); + + mount_sequencer_genesis!(test_conductor); + mount_celestia_header_network_head!( + test_conductor, + height: 2u32, + ); + + mount_celestia_blobs!( + test_conductor, + celestia_height: 1, + sequencer_heights: [3, 4], + ); + mount_sequencer_commit!( + test_conductor, + height: 3u32, + ); + mount_sequencer_commit!( + test_conductor, + height: 4u32, + ); + mount_sequencer_validator_set!(test_conductor, height: 2u32); + mount_sequencer_validator_set!(test_conductor, height: 3u32); + + let execute_block_1 = mount_execute_block!( + test_conductor, + mock_name: "execute_block_1", + number: 2, + hash: "2", + parent: "1", + expected_calls: 1, // should not be called again upon restart + ); + + let update_commitment_state_1 = mount_update_commitment_state!( + test_conductor, + mock_name: "update_commitment_state_1", + firm: ( + number: 2, + hash: "2", + parent: "1", + ), + soft: ( + number: 2, + hash: "2", + parent: "1", + ), + lowest_celestia_search_height: 1, + expected_calls: 1, // should not be called again upon restart + ); + + timeout( + Duration::from_millis(1000), + join( + execute_block_1.wait_until_satisfied(), + update_commitment_state_1.wait_until_satisfied(), + ), + ) + .await + .expect( + "conductor should have executed the first firm block and updated the first firm \ + commitment state twice within 1000ms", + ); + + // Mount new execution session with updated heights and commitment state. + mount_create_execution_session!( + test_conductor, + execution_session_parameters: ( + rollup_start_block_number: 3, + rollup_end_block_number: 9, + sequencer_start_block_height: 4, + celestia_max_look_ahead: 10, + ), + commitment_state: ( + firm: ( + number: 2, + hash: "2", + parent: "1", + ), + soft: ( + number: 2, + hash: "2", + parent: "1", + ), + lowest_celestia_search_height: 1, + ), + ); + + let execute_block_2 = mount_execute_block!( + test_conductor, + mock_name: "execute_block_2", + number: 3, + hash: "3", + parent: "2", + expected_calls: 1, + ); + + let update_commitment_state_2 = mount_update_commitment_state!( + test_conductor, + mock_name: "update_commitment_state_2", + firm: ( + number: 3, + hash: "3", + parent: "2", + ), + soft: ( + number: 3, + hash: "3", + parent: "2", + ), + lowest_celestia_search_height: 1, + expected_calls: 1, + ); + + timeout( + Duration::from_millis(2000), + join( + execute_block_2.wait_until_satisfied(), + update_commitment_state_2.wait_until_satisfied(), + ), + ) + .await + .expect( + "conductor should have executed the second firm block and updated the second firm \ + commitment state twice within 2000ms", + ); +} diff --git a/crates/astria-conductor/tests/blackbox/helpers/macros.rs b/crates/astria-conductor/tests/blackbox/helpers/macros.rs index 981f4c31dc..71cb6cf668 100644 --- a/crates/astria-conductor/tests/blackbox/helpers/macros.rs +++ b/crates/astria-conductor/tests/blackbox/helpers/macros.rs @@ -1,10 +1,10 @@ #[macro_export] -macro_rules! block { +macro_rules! block_metadata { (number: $number:expr,hash: $hash:expr,parent: $parent:expr $(,)?) => { - ::astria_core::generated::astria::execution::v1::Block { + ::astria_core::generated::astria::execution::v2::ExecutedBlockMetadata { number: $number, - hash: ::bytes::Bytes::from(Vec::from($hash)), - parent_block_hash: ::bytes::Bytes::from(Vec::from($parent)), + hash: $hash.to_string(), + parent_hash: $parent.to_string(), timestamp: Some(::pbjson_types::Timestamp { seconds: 1, nanos: 1, @@ -52,29 +52,6 @@ macro_rules! celestia_network_head { }; } -#[macro_export] -macro_rules! commitment_state { - ( - firm: (number: $firm_number:expr,hash: $firm_hash:expr,parent: $firm_parent:expr $(,)?), - soft: (number: $soft_number:expr,hash: $soft_hash:expr,parent: $soft_parent:expr $(,)?), - base_celestia_height: $base_celestia_height:expr $(,)? - ) => { - ::astria_core::generated::astria::execution::v1::CommitmentState { - firm: Some($crate::block!( - number: $firm_number, - hash: $firm_hash, - parent: $firm_parent, - )), - soft: Some($crate::block!( - number: $soft_number, - hash: $soft_hash, - parent: $soft_parent, - )), - base_celestia_height: $base_celestia_height, - } - }; -} - #[macro_export] macro_rules! filtered_sequencer_block { (sequencer_height: $height:expr) => {{ @@ -92,16 +69,46 @@ macro_rules! filtered_sequencer_block { // 1. applying #[rustfmt::skip] on the macro or on the containing module triggers issue 52234. // 2. applying #![rustfmt::skip] triggers issue 64266. #[macro_export] -macro_rules! genesis_info { +macro_rules! execution_session { ( - sequencer_genesis_block_height: - $sequencer_height:expr,celestia_block_variance: - $variance:expr $(,)? + execution_session_parameters: ( + rollup_start_block_number: $rollup_start_block_number:expr, + rollup_end_block_number: $rollup_end_block_number:expr, + sequencer_start_block_height: $start_height:expr, + celestia_max_look_ahead: $celestia_max_look_ahead:expr$(,)? + ), + commitment_state: ( + firm: ( number: $firm_number:expr, hash: $firm_hash:expr, parent: $firm_parent:expr$(,)? ), + soft: ( number: $soft_number:expr, hash: $soft_hash:expr, parent: $soft_parent:expr$(,)? ), + lowest_celestia_search_height: $lowest_celestia_search_height:expr$(,)? + )$(,)? ) => { - ::astria_core::generated::astria::execution::v1::GenesisInfo { - rollup_id: Some($crate::ROLLUP_ID.to_raw()), - sequencer_genesis_block_height: $sequencer_height, - celestia_block_variance: $variance, + ::astria_core::generated::astria::execution::v2::ExecutionSession { + session_id: $crate::helpers::EXECUTION_SESSION_ID.to_string(), + execution_session_parameters: Some( + ::astria_core::generated::astria::execution::v2::ExecutionSessionParameters { + rollup_id: Some($crate::ROLLUP_ID.to_raw()), + rollup_start_block_number: $rollup_start_block_number, + rollup_end_block_number: $rollup_end_block_number, + sequencer_start_block_height: $start_height, + sequencer_chain_id: $crate::SEQUENCER_CHAIN_ID.to_string(), + celestia_chain_id: $crate::helpers::CELESTIA_CHAIN_ID.to_string(), + celestia_search_height_max_look_ahead: $celestia_max_look_ahead, + } + ), + commitment_state: Some(::astria_core::generated::astria::execution::v2::CommitmentState { + firm_executed_block_metadata: Some($crate::block_metadata!( + number: $firm_number, + hash: $firm_hash, + parent: $firm_parent, + )), + soft_executed_block_metadata: Some($crate::block_metadata!( + number: $soft_number, + hash: $soft_hash, + parent: $soft_parent, + )), + lowest_celestia_search_height: $lowest_celestia_search_height, + }), } }; } @@ -167,40 +174,13 @@ macro_rules! mount_celestia_header_network_head { } } -#[macro_export] -macro_rules! mount_get_commitment_state { - ( - $test_env:ident, - firm: ( number: $firm_number:expr, hash: $firm_hash:expr, parent: $firm_parent:expr$(,)? ), - soft: ( number: $soft_number:expr, hash: $soft_hash:expr, parent: $soft_parent:expr$(,)? ), - base_celestia_height: $base_celestia_height:expr - $(,)? - ) => { - $test_env - .mount_get_commitment_state($crate::commitment_state!( - firm: ( - number: $firm_number, - hash: $firm_hash, - parent: $firm_parent, - ), - soft: ( - number: $soft_number, - hash: $soft_hash, - parent: $soft_parent, - ), - base_celestia_height: $base_celestia_height, - )) - .await - }; -} - #[macro_export] macro_rules! mount_update_commitment_state { ( $test_env:ident, firm: ( number: $firm_number:expr, hash: $firm_hash:expr, parent: $firm_parent:expr$(,)? ), soft: ( number: $soft_number:expr, hash: $soft_hash:expr, parent: $soft_parent:expr$(,)? ), - base_celestia_height: $base_celestia_height:expr + lowest_celestia_search_height: $lowest_celestia_search_height:expr $(,)? ) => { mount_update_commitment_state!( @@ -208,7 +188,7 @@ macro_rules! mount_update_commitment_state { mock_name: None, firm: ( number: $firm_number, hash: $firm_hash, parent: $firm_parent, ), soft: ( number: $soft_number, hash: $soft_hash, parent: $soft_parent, ), - base_celestia_height: $base_celestia_height, + lowest_celestia_search_height: $lowest_celestia_search_height, expected_calls: 1, ) }; @@ -217,7 +197,7 @@ macro_rules! mount_update_commitment_state { mock_name: $mock_name:expr, firm: ( number: $firm_number:expr, hash: $firm_hash:expr, parent: $firm_parent:expr$(,)? ), soft: ( number: $soft_number:expr, hash: $soft_hash:expr, parent: $soft_parent:expr$(,)? ), - base_celestia_height: $base_celestia_height:expr + lowest_celestia_search_height: $lowest_celestia_search_height:expr $(,)? ) => { mount_update_commitment_state!( @@ -225,7 +205,7 @@ macro_rules! mount_update_commitment_state { mock_name: $mock_name, firm: ( number: $firm_number, hash: $firm_hash, parent: $firm_parent, ), soft: ( number: $soft_number, hash: $soft_hash, parent: $soft_parent, ), - base_celestia_height: $base_celestia_height, + lowest_celestia_search_height: $lowest_celestia_search_height, expected_calls: 1, ) }; @@ -234,26 +214,26 @@ macro_rules! mount_update_commitment_state { mock_name: $mock_name:expr, firm: ( number: $firm_number:expr, hash: $firm_hash:expr, parent: $firm_parent:expr$(,)? ), soft: ( number: $soft_number:expr, hash: $soft_hash:expr, parent: $soft_parent:expr$(,)? ), - base_celestia_height: $base_celestia_height:expr, + lowest_celestia_search_height: $lowest_celestia_search_height:expr, expected_calls: $expected_calls:expr $(,)? ) => { $test_env .mount_update_commitment_state( $mock_name.into(), - $crate::commitment_state!( - firm: ( + ::astria_core::generated::astria::execution::v2::CommitmentState { + firm_executed_block_metadata: Some($crate::block_metadata!( number: $firm_number, hash: $firm_hash, parent: $firm_parent, - ), - soft: ( + )), + soft_executed_block_metadata: Some($crate::block_metadata!( number: $soft_number, hash: $soft_hash, parent: $soft_parent, - ), - base_celestia_height: $base_celestia_height, - ), + )), + lowest_celestia_search_height: $lowest_celestia_search_height, + }, $expected_calls, ) .await @@ -268,36 +248,55 @@ macro_rules! mount_abci_info { } #[macro_export] -macro_rules! mount_executed_block { +macro_rules! mount_execute_block { ( $test_env:ident, mock_name: $mock_name:expr, number: $number:expr, hash: $hash:expr, - parent: $parent:expr $(,)? + parent: $parent:expr, + expected_calls: $expected_calls:expr $(,)? ) => {{ use ::base64::prelude::*; $test_env.mount_execute_block( $mock_name.into(), ::serde_json::json!({ - "prevBlockHash": BASE64_STANDARD.encode($parent), + "sessionId": $crate::helpers::EXECUTION_SESSION_ID, + "parentHash": $parent, "transactions": [{"sequencedData": BASE64_STANDARD.encode($crate::helpers::data())}], }), - $crate::block!( + $crate::block_metadata!( number: $number, hash: $hash, parent: $parent, - ) + ), + $expected_calls, ) .await }}; + ( + $test_env:ident, + mock_name: $mock_name:expr, + number: $number:expr, + hash: $hash:expr, + parent: $parent:expr, + ) => { + mount_execute_block!( + $test_env, + mock_name: None, + number: $number, + hash: $hash, + parent: $parent, + expected_calls: 1, + ) + }; ( $test_env:ident, number: $number:expr, hash: $hash:expr, parent: $parent:expr $(,)? ) => { - mount_executed_block!( + mount_execute_block!( $test_env, mock_name: None, number: $number, @@ -331,18 +330,104 @@ macro_rules! mount_get_filtered_sequencer_block { } #[macro_export] -macro_rules! mount_get_genesis_info { +macro_rules! mount_create_execution_session { + ( + $test_env:ident, + execution_session_parameters: ( + rollup_start_block_number: $rollup_start_block_number:expr, + rollup_end_block_number: $rollup_end_block_number:expr, + sequencer_start_block_height: $start_height:expr, + celestia_max_look_ahead: $celestia_max_look_ahead:expr $(,)? + ), + commitment_state: ( + firm: ( number: $firm_number:expr, hash: $firm_hash:expr, parent: $firm_parent:expr$(,)? ), + soft: ( number: $soft_number:expr, hash: $soft_hash:expr, parent: $soft_parent:expr$(,)? ), + lowest_celestia_search_height: $lowest_celestia_search_height:expr$(,)? + ) + $(,)? + ) => { + mount_create_execution_session!( + $test_env, + execution_session_parameters: ( + rollup_start_block_number: $rollup_start_block_number, + rollup_end_block_number: $rollup_end_block_number, + sequencer_start_block_height: $start_height, + celestia_max_look_ahead: $celestia_max_look_ahead, + ), + commitment_state: ( + firm: ( number: $firm_number, hash: $firm_hash, parent: $firm_parent, ), + soft: ( number: $soft_number, hash: $soft_hash, parent: $soft_parent, ), + lowest_celestia_search_height: $lowest_celestia_search_height, + ), + expected_calls: 1, + up_to_n_times: 1, + ) + }; ( $test_env:ident, - sequencer_genesis_block_height: $sequencer_height:expr, - celestia_block_variance: $variance:expr + execution_session_parameters: ( + rollup_start_block_number: $rollup_start_block_number:expr, + rollup_end_block_number: $rollup_end_block_number:expr, + sequencer_start_block_height: $start_height:expr, + celestia_max_look_ahead: $celestia_max_look_ahead:expr $(,)? + ), + commitment_state: ( + firm: ( number: $firm_number:expr, hash: $firm_hash:expr, parent: $firm_parent:expr$(,)? ), + soft: ( number: $soft_number:expr, hash: $soft_hash:expr, parent: $soft_parent:expr$(,)? ), + lowest_celestia_search_height: $lowest_celestia_search_height:expr$(,)? + ), + up_to_n_times: $up_to_n_times:expr $(,)? ) => { - $test_env.mount_get_genesis_info( - $crate::genesis_info!( - sequencer_genesis_block_height: $sequencer_height, - celestia_block_variance: $variance, - ) + mount_create_execution_session!( + $test_env, + execution_session_parameters: ( + rollup_start_block_number: $rollup_start_block_number, + rollup_end_block_number: $rollup_end_block_number, + sequencer_start_block_height: $start_height, + celestia_max_look_ahead: $celestia_max_look_ahead, + ), + commitment_state: ( + firm: ( number: $firm_number, hash: $firm_hash, parent: $firm_parent, ), + soft: ( number: $soft_number, hash: $soft_hash, parent: $soft_parent, ), + lowest_celestia_search_height: $lowest_celestia_search_height, + ), + expected_calls: 1, + up_to_n_times: $up_to_n_times, + ) + }; + ( + $test_env:ident, + execution_session_parameters: ( + rollup_start_block_number: $rollup_start_block_number:expr, + rollup_end_block_number: $rollup_end_block_number:expr, + sequencer_start_block_height: $start_height:expr, + celestia_max_look_ahead: $celestia_max_look_ahead:expr $(,)? + ), + commitment_state: ( + firm: ( number: $firm_number:expr, hash: $firm_hash:expr, parent: $firm_parent:expr$(,)? ), + soft: ( number: $soft_number:expr, hash: $soft_hash:expr, parent: $soft_parent:expr$(,)? ), + lowest_celestia_search_height: $lowest_celestia_search_height:expr$(,)? + ), + expected_calls: $expected_calls:expr, + up_to_n_times: $up_to_n_times:expr $(,)? + ) => { + $test_env.mount_create_execution_session( + $crate::execution_session!( + execution_session_parameters: ( + rollup_start_block_number: $rollup_start_block_number, + rollup_end_block_number: $rollup_end_block_number, + sequencer_start_block_height: $start_height, + celestia_max_look_ahead: $celestia_max_look_ahead, + ), + commitment_state: ( + firm: ( number: $firm_number, hash: $firm_hash, parent: $firm_parent), + soft: ( number: $soft_number, hash: $soft_hash, parent: $soft_parent), + lowest_celestia_search_height: $lowest_celestia_search_height, + ), + ), + $up_to_n_times, + $expected_calls, ).await; }; } @@ -373,24 +458,24 @@ macro_rules! mount_sequencer_genesis { } #[macro_export] -macro_rules! mount_get_block { +macro_rules! mount_get_executed_block_metadata { ( $test_env:ident, number: $number:expr, hash: $hash:expr, parent: $parent:expr $(,)? ) => {{ - let block = $crate::block!( + let block = $crate::block_metadata!( number: $number, hash: $hash, parent: $parent, ); - let identifier = ::astria_core::generated::astria::execution::v1::BlockIdentifier { + let identifier = ::astria_core::generated::astria::execution::v2::ExecutedBlockIdentifier { identifier: Some( - ::astria_core::generated::astria::execution::v1::block_identifier::Identifier::BlockNumber(block.number) + ::astria_core::generated::astria::execution::v2::executed_block_identifier::Identifier::Number(block.number) )}; - $test_env.mount_get_block( - ::astria_core::generated::astria::execution::v1::GetBlockRequest { + $test_env.mount_get_executed_block_metadata( + ::astria_core::generated::astria::execution::v2::GetExecutedBlockMetadataRequest { identifier: Some(identifier), }, block, @@ -409,7 +494,8 @@ macro_rules! mount_execute_block_tonic_code { use ::base64::prelude::*; $test_env.mount_tonic_status_code( ::serde_json::json!({ - "prevBlockHash": BASE64_STANDARD.encode($parent), + "sessionId": $crate::helpers::EXECUTION_SESSION_ID, + "parentHash": $parent, "transactions": [{"sequencedData": BASE64_STANDARD.encode($crate::helpers::data())}], }), $status_code diff --git a/crates/astria-conductor/tests/blackbox/helpers/mock_grpc.rs b/crates/astria-conductor/tests/blackbox/helpers/mock_grpc.rs index 457ef04e3d..97298637f9 100644 --- a/crates/astria-conductor/tests/blackbox/helpers/mock_grpc.rs +++ b/crates/astria-conductor/tests/blackbox/helpers/mock_grpc.rs @@ -4,20 +4,18 @@ use std::{ }; use astria_core::generated::astria::{ - execution::v1::{ + execution::v2::{ execution_service_server::{ ExecutionService, ExecutionServiceServer, }, - BatchGetBlocksRequest, - BatchGetBlocksResponse, - Block, CommitmentState, + CreateExecutionSessionRequest, ExecuteBlockRequest, - GenesisInfo, - GetBlockRequest, - GetCommitmentStateRequest, - GetGenesisInfoRequest, + ExecuteBlockResponse, + ExecutedBlockMetadata, + ExecutionSession, + GetExecutedBlockMetadataRequest, UpdateCommitmentStateRequest, }, sequencerblock::v1::{ @@ -145,10 +143,8 @@ macro_rules! define_and_impl_service { } define_and_impl_service!(impl ExecutionService for ExecutionServiceImpl { - (get_block: GetBlockRequest => Block) - (get_genesis_info: GetGenesisInfoRequest => GenesisInfo) - (batch_get_blocks: BatchGetBlocksRequest => BatchGetBlocksResponse) - (execute_block: ExecuteBlockRequest => Block) - (get_commitment_state: GetCommitmentStateRequest => CommitmentState) + (get_executed_block_metadata: GetExecutedBlockMetadataRequest => ExecutedBlockMetadata) + (create_execution_session: CreateExecutionSessionRequest => ExecutionSession) + (execute_block: ExecuteBlockRequest => ExecuteBlockResponse) (update_commitment_state: UpdateCommitmentStateRequest => CommitmentState) }); diff --git a/crates/astria-conductor/tests/blackbox/helpers/mod.rs b/crates/astria-conductor/tests/blackbox/helpers/mod.rs index c14ffff018..31a90c2ecd 100644 --- a/crates/astria-conductor/tests/blackbox/helpers/mod.rs +++ b/crates/astria-conductor/tests/blackbox/helpers/mod.rs @@ -13,10 +13,10 @@ use astria_conductor::{ use astria_core::{ brotli::compress_bytes, generated::astria::{ - execution::v1::{ - Block, + execution::v2::{ CommitmentState, - GenesisInfo, + ExecutedBlockMetadata, + ExecutionSession, }, sequencerblock::v1::FilteredSequencerBlock, }, @@ -52,6 +52,7 @@ pub static ROLLUP_ID_BYTES: Bytes = Bytes::from_static(ROLLUP_ID.as_bytes()); pub const SEQUENCER_CHAIN_ID: &str = "test_sequencer-1000"; pub const CELESTIA_CHAIN_ID: &str = "test_celestia-1000"; +pub const EXECUTION_SESSION_ID: &str = "test_execution_session"; pub const INITIAL_SOFT_HASH: [u8; 64] = [1; 64]; pub const INITIAL_FIRM_HASH: [u8; 64] = [1; 64]; @@ -176,21 +177,24 @@ impl TestConductor { .await; } - pub async fn mount_get_block( + pub async fn mount_get_executed_block_metadata( &self, expected_pbjson: S, - block: astria_core::generated::astria::execution::v1::Block, + block: ExecutedBlockMetadata, ) { use astria_grpc_mock::{ matcher::message_partial_pbjson, response::constant_response, Mock, }; - Mock::for_rpc_given("get_block", message_partial_pbjson(&expected_pbjson)) - .respond_with(constant_response(block)) - .expect(1..) - .mount(&self.mock_grpc.mock_server) - .await; + Mock::for_rpc_given( + "get_executed_block_metadata", + message_partial_pbjson(&expected_pbjson), + ) + .respond_with(constant_response(block)) + .expect(1..) + .mount(&self.mock_grpc.mock_server) + .await; } pub async fn mount_celestia_blob_get_all( @@ -305,29 +309,22 @@ impl TestConductor { mount_genesis(&self.mock_http, chain_id).await; } - pub async fn mount_get_genesis_info(&self, genesis_info: GenesisInfo) { - use astria_core::generated::astria::execution::v1::GetGenesisInfoRequest; - astria_grpc_mock::Mock::for_rpc_given( - "get_genesis_info", - astria_grpc_mock::matcher::message_type::(), - ) - .respond_with(astria_grpc_mock::response::constant_response(genesis_info)) - .expect(1..) - .mount(&self.mock_grpc.mock_server) - .await; - } - - pub async fn mount_get_commitment_state(&self, commitment_state: CommitmentState) { - use astria_core::generated::astria::execution::v1::GetCommitmentStateRequest; - + pub async fn mount_create_execution_session( + &self, + execution_session: ExecutionSession, + up_to_n_times: u64, + expected_calls: u64, + ) { + use astria_core::generated::astria::execution::v2::CreateExecutionSessionRequest; astria_grpc_mock::Mock::for_rpc_given( - "get_commitment_state", - astria_grpc_mock::matcher::message_type::(), + "create_execution_session", + astria_grpc_mock::matcher::message_type::(), ) .respond_with(astria_grpc_mock::response::constant_response( - commitment_state, + execution_session, )) - .expect(1..) + .up_to_n_times(up_to_n_times) + .expect(expected_calls) .mount(&self.mock_grpc.mock_server) .await; } @@ -336,8 +333,10 @@ impl TestConductor { &self, mock_name: Option<&str>, expected_pbjson: S, - response: Block, + block_metadata: ExecutedBlockMetadata, + expected_calls: u64, ) -> astria_grpc_mock::MockGuard { + use astria_core::generated::astria::execution::v2::ExecuteBlockResponse; use astria_grpc_mock::{ matcher::message_partial_pbjson, response::constant_response, @@ -345,11 +344,13 @@ impl TestConductor { }; let mut mock = Mock::for_rpc_given("execute_block", message_partial_pbjson(&expected_pbjson)) - .respond_with(constant_response(response)); + .respond_with(constant_response(ExecuteBlockResponse { + executed_block_metadata: Some(block_metadata.clone()), + })); if let Some(name) = mock_name { mock = mock.with_name(name); } - mock.expect(1) + mock.expect(expected_calls) .mount_as_scoped(&self.mock_grpc.mock_server) .await } @@ -379,9 +380,9 @@ impl TestConductor { &self, mock_name: Option<&str>, commitment_state: CommitmentState, - expected_calls: u64, + expected_calls: impl Into, ) -> astria_grpc_mock::MockGuard { - use astria_core::generated::astria::execution::v1::UpdateCommitmentStateRequest; + use astria_core::generated::astria::execution::v2::UpdateCommitmentStateRequest; use astria_grpc_mock::{ matcher::message_partial_pbjson, response::constant_response, @@ -390,6 +391,7 @@ impl TestConductor { let mut mock = Mock::for_rpc_given( "update_commitment_state", message_partial_pbjson(&UpdateCommitmentStateRequest { + session_id: EXECUTION_SESSION_ID.to_string(), commitment_state: Some(commitment_state.clone()), }), ) @@ -522,8 +524,6 @@ pub(crate) fn make_config() -> Config { sequencer_cometbft_url: "http://127.0.0.1:26657".into(), sequencer_requests_per_second: 500, sequencer_block_time_ms: 2000, - expected_celestia_chain_id: CELESTIA_CHAIN_ID.into(), - expected_sequencer_chain_id: SEQUENCER_CHAIN_ID.into(), execution_rpc_url: "http://127.0.0.1:50051".into(), log: "info".into(), execution_commit_level: astria_conductor::config::CommitLevel::SoftAndFirm, diff --git a/crates/astria-conductor/tests/blackbox/soft_and_firm.rs b/crates/astria-conductor/tests/blackbox/soft_and_firm.rs index 1054c43f80..00d0a75ea9 100644 --- a/crates/astria-conductor/tests/blackbox/soft_and_firm.rs +++ b/crates/astria-conductor/tests/blackbox/soft_and_firm.rs @@ -12,12 +12,11 @@ use crate::{ mount_abci_info, mount_celestia_blobs, mount_celestia_header_network_head, + mount_create_execution_session, + mount_execute_block, mount_execute_block_tonic_code, - mount_executed_block, - mount_get_block, - mount_get_commitment_state, + mount_get_executed_block_metadata, mount_get_filtered_sequencer_block, - mount_get_genesis_info, mount_sequencer_commit, mount_sequencer_genesis, mount_sequencer_validator_set, @@ -38,25 +37,27 @@ use crate::{ async fn executes_soft_first_then_updates_firm() { let test_conductor = spawn_conductor(CommitLevel::SoftAndFirm).await; - mount_get_genesis_info!( + mount_create_execution_session!( test_conductor, - sequencer_genesis_block_height: 1, - celestia_block_variance: 10, - ); - - mount_get_commitment_state!( - test_conductor, - firm: ( - number: 1, - hash: [1; 64], - parent: [0; 64], - ), - soft: ( - number: 1, - hash: [1; 64], - parent: [0; 64], + execution_session_parameters: ( + rollup_start_block_number: 2, + rollup_end_block_number: 9, + sequencer_start_block_height: 3, + celestia_max_look_ahead: 10, ), - base_celestia_height: 1, + commitment_state: ( + firm: ( + number: 1, + hash: "1", + parent: "0", + ), + soft: ( + number: 1, + hash: "1", + parent: "0", + ), + lowest_celestia_search_height: 1, + ) ); mount_abci_info!( @@ -76,30 +77,30 @@ async fn executes_soft_first_then_updates_firm() { sequencer_height: 3, ); - let execute_block = mount_executed_block!( + let execute_block = mount_execute_block!( test_conductor, number: 2, - hash: [2; 64], - parent: [1; 64], + hash: "2", + parent: "1", ); let update_commitment_state_soft = mount_update_commitment_state!( test_conductor, firm: ( number: 1, - hash: [1; 64], - parent: [0; 64], + hash: "1", + parent: "0", ), soft: ( number: 2, - hash: [2; 64], - parent: [1; 64], + hash: "2", + parent: "1", ), - base_celestia_height: 1, + lowest_celestia_search_height: 1, ); timeout( - Duration::from_millis(500), + Duration::from_millis(1000), join( execute_block.wait_until_satisfied(), update_commitment_state_soft.wait_until_satisfied(), @@ -108,7 +109,7 @@ async fn executes_soft_first_then_updates_firm() { .await .expect( "Conductor should have executed the block and updated the soft commitment state within \ - 500ms", + 1000ms", ); mount_celestia_blobs!( @@ -129,15 +130,15 @@ async fn executes_soft_first_then_updates_firm() { test_conductor, firm: ( number: 2, - hash: [2; 64], - parent: [1; 64], + hash: "2", + parent: "1", ), soft: ( number: 2, - hash: [2; 64], - parent: [1; 64], + hash: "2", + parent: "1", ), - base_celestia_height: 1, + lowest_celestia_search_height: 1, ); timeout( @@ -172,25 +173,27 @@ async fn executes_soft_first_then_updates_firm() { async fn executes_firm_then_soft_at_next_height() { let test_conductor = spawn_conductor(CommitLevel::SoftAndFirm).await; - mount_get_genesis_info!( - test_conductor, - sequencer_genesis_block_height: 1, - celestia_block_variance: 10, - ); - - mount_get_commitment_state!( + mount_create_execution_session!( test_conductor, - firm: ( - number: 1, - hash: [1; 64], - parent: [0; 64], - ), - soft: ( - number: 1, - hash: [1; 64], - parent: [0; 64], + execution_session_parameters: ( + rollup_start_block_number: 2, + rollup_end_block_number: 9, + sequencer_start_block_height: 3, + celestia_max_look_ahead: 10, ), - base_celestia_height: 1, + commitment_state: ( + firm: ( + number: 1, + hash: "1", + parent: "0", + ), + soft: ( + number: 1, + hash: "1", + parent: "0", + ), + lowest_celestia_search_height: 1, + ) ); mount_abci_info!( @@ -218,11 +221,11 @@ async fn executes_firm_then_soft_at_next_height() { mount_sequencer_validator_set!(test_conductor, height: 2u32); - let execute_block = mount_executed_block!( + let execute_block = mount_execute_block!( test_conductor, number: 2, - hash: [2; 64], - parent: [1; 64], + hash: "2", + parent: "1", ); // Mount soft block at current height with a slight delay @@ -243,15 +246,15 @@ async fn executes_firm_then_soft_at_next_height() { test_conductor, firm: ( number: 2, - hash: [2; 64], - parent: [1; 64], + hash: "2", + parent: "1", ), soft: ( number: 2, - hash: [2; 64], - parent: [1; 64], + hash: "2", + parent: "1", ), - base_celestia_height: 1, + lowest_celestia_search_height: 1, ); // This guard's conditions will be checked when it is dropped, ensuring that there have been 0 @@ -265,15 +268,15 @@ async fn executes_firm_then_soft_at_next_height() { mock_name: "should_be_ignored_update_commitment_state_soft", firm: ( number: 1, - hash: [1; 64], - parent: [0; 64], + hash: "1", + parent: "0", ), soft: ( number: 2, - hash: [2; 64], - parent: [1; 64], + hash: "2", + parent: "1", ), - base_celestia_height: 1, + lowest_celestia_search_height: 1, expected_calls: 0, ); @@ -290,26 +293,26 @@ async fn executes_firm_then_soft_at_next_height() { 1000ms", ); - let execute_block = mount_executed_block!( + let execute_block = mount_execute_block!( test_conductor, number: 3, - hash: [3; 64], - parent: [2; 64], + hash: "3", + parent: "2", ); let update_commitment_state_soft = mount_update_commitment_state!( test_conductor, firm: ( number: 2, - hash: [2; 64], - parent: [1; 64], + hash: "2", + parent: "1", ), soft: ( number: 3, - hash: [3; 64], - parent: [2; 64], + hash: "3", + parent: "2", ), - base_celestia_height: 1, + lowest_celestia_search_height: 1, ); timeout( @@ -331,25 +334,27 @@ async fn executes_firm_then_soft_at_next_height() { async fn missing_block_is_fetched_for_updating_firm_commitment() { let test_conductor = spawn_conductor(CommitLevel::SoftAndFirm).await; - mount_get_genesis_info!( - test_conductor, - sequencer_genesis_block_height: 1, - celestia_block_variance: 10, - ); - - mount_get_commitment_state!( + mount_create_execution_session!( test_conductor, - firm: ( - number: 1, - hash: [1; 64], - parent: [0; 64], - ), - soft: ( - number: 2, - hash: [2; 64], - parent: [1; 64], + execution_session_parameters: ( + rollup_start_block_number: 2, + rollup_end_block_number: 9, + sequencer_start_block_height: 3, + celestia_max_look_ahead: 10, ), - base_celestia_height: 1, + commitment_state: ( + firm: ( + number: 1, + hash: "1", + parent: "0", + ), + soft: ( + number: 2, + hash: "2", + parent: "1", + ), + lowest_celestia_search_height: 1, + ) ); mount_abci_info!( @@ -359,11 +364,11 @@ async fn missing_block_is_fetched_for_updating_firm_commitment() { mount_sequencer_genesis!(test_conductor); - mount_get_block!( + mount_get_executed_block_metadata!( test_conductor, number: 2, - hash: [2; 64], - parent: [1; 64], + hash: "2", + parent: "1", ); mount_celestia_header_network_head!( @@ -388,15 +393,15 @@ async fn missing_block_is_fetched_for_updating_firm_commitment() { test_conductor, firm: ( number: 2, - hash: [2; 64], - parent: [1; 64], + hash: "2", + parent: "1", ), soft: ( number: 2, - hash: [2; 64], - parent: [1; 64], + hash: "2", + parent: "1", ), - base_celestia_height: 1, + lowest_celestia_search_height: 1, ); timeout( @@ -411,26 +416,26 @@ async fn missing_block_is_fetched_for_updating_firm_commitment() { sequencer_height: 4, ); - let execute_block_soft = mount_executed_block!( + let execute_block_soft = mount_execute_block!( test_conductor, number: 3, - hash: [3; 64], - parent: [2; 64], + hash: "3", + parent: "2", ); let update_commitment_state_soft = mount_update_commitment_state!( test_conductor, firm: ( number: 2, - hash: [2; 64], - parent: [1; 64], + hash: "2", + parent: "1", ), soft: ( number: 3, - hash: [3; 64], - parent: [2; 64], + hash: "3", + parent: "2", ), - base_celestia_height: 1, + lowest_celestia_search_height: 1, ); timeout( @@ -457,28 +462,32 @@ async fn missing_block_is_fetched_for_updating_firm_commitment() { reason = "all lines fairly necessary, and I don't think a test warrants a refactor" )] #[tokio::test(flavor = "multi_thread", worker_threads = 1)] -async fn conductor_restarts_on_permission_denied() { +async fn restarts_on_permission_denied() { let test_conductor = spawn_conductor(CommitLevel::SoftAndFirm).await; - mount_get_genesis_info!( - test_conductor, - sequencer_genesis_block_height: 1, - celestia_block_variance: 10, - ); - - mount_get_commitment_state!( + mount_create_execution_session!( test_conductor, - firm: ( - number: 1, - hash: [1; 64], - parent: [0; 64], + execution_session_parameters: ( + rollup_start_block_number: 2, + rollup_end_block_number: 9, + sequencer_start_block_height: 3, + celestia_max_look_ahead: 10, ), - soft: ( - number: 1, - hash: [1; 64], - parent: [0; 64], + commitment_state: ( + firm: ( + number: 1, + hash: "1", + parent: "0", + ), + soft: ( + number: 1, + hash: "1", + parent: "0", + ), + lowest_celestia_search_height: 1, ), - base_celestia_height: 1, + expected_calls: 2, + up_to_n_times: 2, ); mount_sequencer_genesis!(test_conductor); @@ -516,7 +525,7 @@ async fn conductor_restarts_on_permission_denied() { // This mock can only be called up to 1 time, allowing a normal `execute_block` call after. let execute_block_tonic_code = mount_execute_block_tonic_code!( test_conductor, - parent: [1; 64], + parent: "1", status_code: tonic::Code::PermissionDenied, ); @@ -527,41 +536,41 @@ async fn conductor_restarts_on_permission_denied() { .await .expect("conductor should have restarted after a permission denied error within 1000ms"); - let execute_block = mount_executed_block!( + let execute_block = mount_execute_block!( test_conductor, number: 2, - hash: [2; 64], - parent: [1; 64], + hash: "2", + parent: "1", ); let update_commitment_state_soft = mount_update_commitment_state!( test_conductor, firm: ( number: 1, - hash: [1; 64], - parent: [0; 64], + hash: "1", + parent: "0", ), soft: ( number: 2, - hash: [2; 64], - parent: [1; 64], + hash: "2", + parent: "1", ), - base_celestia_height: 1, + lowest_celestia_search_height: 1, ); let update_commitment_state_firm = mount_update_commitment_state!( test_conductor, firm: ( number: 2, - hash: [2; 64], - parent: [1; 64], + hash: "2", + parent: "1", ), soft: ( number: 2, - hash: [2; 64], - parent: [1; 64], + hash: "2", + parent: "1", ), - base_celestia_height: 1, + lowest_celestia_search_height: 1, ); timeout( @@ -578,3 +587,447 @@ async fn conductor_restarts_on_permission_denied() { within 1000ms", ); } + +/// Tests if the conductor correctly stops and procedes to restart after soft block height reaches +/// sequencer stop height (from genesis info provided by rollup). In `SoftAndFirm` mode executor +/// should execute both the soft and firm blocks at the stop height and then perform a restart. +/// +/// This test consists of the following steps: +/// 1. Mount execution session with a rollup stop number of 2 (sequencer height 3), only responding +/// up to 1 time so that Conductor will not receive the same response after restart. +/// 2. Mount Celestia network head and sequencer genesis. +/// 3. Mount ABCI info and sequencer blocks (soft blocks) for heights 3 and 4. +/// 4. Mount firm blocks at heights 3 and 4 with a slight delay to ensure that the soft blocks +/// arrive first. +/// 5. Mount `execute_block` and `update_commitment_state` for both soft and firm blocks at height 3 +/// 6. Await satisfaction of the `execute_block` and `update_commitment_state` for the soft and firm +/// blocks at height 3 with a timeout of 1000ms. +/// 7. Mount new execution session with a rollup stop number of 9 and a start block number of 2, +/// reflecting that block 1 has already been executed and the commitment state updated. +/// 8. Mount `execute_block` and `update_commitment_state` for both soft and firm blocks at height 4 +/// and await their satisfaction. +#[expect( + clippy::too_many_lines, + reason = "All lines reasonably necessary for the thoroughness of this test" +)] +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +async fn restarts_after_reaching_soft_stop_height_first() { + let test_conductor = spawn_conductor(CommitLevel::SoftAndFirm).await; + + mount_create_execution_session!( + test_conductor, + execution_session_parameters: ( + rollup_start_block_number: 2, + rollup_end_block_number: 2, + sequencer_start_block_height: 3, + celestia_max_look_ahead: 10, + ), + commitment_state: ( + firm: ( + number: 1, + hash: "1", + parent: "0", + ), + soft: ( + number: 1, + hash: "1", + parent: "0", + ), + lowest_celestia_search_height: 1, + ), + up_to_n_times: 1, // We only respond once since a new execution session is needed after restart + ); + + mount_sequencer_genesis!(test_conductor); + mount_celestia_header_network_head!( + test_conductor, + height: 1u32, + ); + mount_abci_info!( + test_conductor, + latest_sequencer_height: 4, + ); + + // Mount soft blocks for heights 3 and 4 + mount_get_filtered_sequencer_block!( + test_conductor, + sequencer_height: 3, + ); + mount_get_filtered_sequencer_block!( + test_conductor, + sequencer_height: 4, + ); + + // Mount firm blocks for heights 3 and 4 + mount_celestia_blobs!( + test_conductor, + celestia_height: 1, + sequencer_heights: [3, 4], + delay: Some(Duration::from_millis(200)) // short delay to ensure soft block at height 4 gets executed first after restart + ); + mount_sequencer_commit!( + test_conductor, + height: 3u32, + ); + mount_sequencer_commit!( + test_conductor, + height: 4u32, + ); + mount_sequencer_validator_set!(test_conductor, height: 2u32); + mount_sequencer_validator_set!(test_conductor, height: 3u32); + + let execute_block_1 = mount_execute_block!( + test_conductor, + mock_name: "execute_block_1", + number: 2, + hash: "2", + parent: "1", + expected_calls: 1, // This should not be called again after restart + ); + + let update_commitment_state_soft_1 = mount_update_commitment_state!( + test_conductor, + mock_name: "update_commitment_state_soft_1", + firm: ( + number: 1, + hash: "1", + parent: "0", + ), + soft: ( + number: 2, + hash: "2", + parent: "1", + ), + lowest_celestia_search_height: 1, + expected_calls: 1, + ); + + let update_commitment_state_firm_1 = mount_update_commitment_state!( + test_conductor, + mock_name: "update_commitment_state_firm_1", + firm: ( + number: 2, + hash: "2", + parent: "1", + ), + soft: ( + number: 2, + hash: "2", + parent: "1", + ), + lowest_celestia_search_height: 1, + expected_calls: 1, // Should not be called again after restart + ); + + timeout( + Duration::from_millis(1000), + join3( + execute_block_1.wait_until_satisfied(), + update_commitment_state_firm_1.wait_until_satisfied(), + update_commitment_state_soft_1.wait_until_satisfied(), + ), + ) + .await + .expect("conductor should have updated the firm commitment state within 1000ms"); + + mount_create_execution_session!( + test_conductor, + execution_session_parameters: ( + rollup_start_block_number: 3, + rollup_end_block_number: 9, + sequencer_start_block_height: 4, + celestia_max_look_ahead: 10, + ), + commitment_state: ( + firm: ( + number: 2, + hash: "2", + parent: "1", + ), + soft: ( + number: 2, + hash: "2", + parent: "1", + ), + lowest_celestia_search_height: 1, + ), + ); + + let execute_block_2 = mount_execute_block!( + test_conductor, + mock_name: "execute_block_2", + number: 3, + hash: "3", + parent: "2", + expected_calls: 1, + ); + + // This condition should be satisfied, since there is a delay on the firm block response + let update_commitment_state_soft_2 = mount_update_commitment_state!( + test_conductor, + mock_name: "update_commitment_state_soft_2", + firm: ( + number: 2, + hash: "2", + parent: "1", + ), + soft: ( + number: 3, + hash: "3", + parent: "2", + ), + lowest_celestia_search_height: 1, + expected_calls: 1, + ); + + let update_commitment_state_firm_2 = mount_update_commitment_state!( + test_conductor, + mock_name: "update_commitment_state_firm_2", + firm: ( + number: 3, + hash: "3", + parent: "2", + ), + soft: ( + number: 3, + hash: "3", + parent: "2", + ), + lowest_celestia_search_height: 1, + expected_calls: 1, + ); + + timeout( + Duration::from_millis(1000), + join3( + execute_block_2.wait_until_satisfied(), + update_commitment_state_firm_2.wait_until_satisfied(), + update_commitment_state_soft_2.wait_until_satisfied(), + ), + ) + .await + .expect("conductor should have updated the firm commitment state within 1000ms"); +} + +/// Tests if the conductor correctly stops and procedes to restart after firm height reaches +/// sequencer stop height, *without* updating soft commitment state, since the firm was received +/// first. +/// +/// This test consists of the following steps: +/// 1. Mount execution session with a rollup stop number of 2 (sequencer height 3), only responding +/// up to 1 time so that Conductor will not receive the same response after restart. +/// 2. Mount Celestia network head and sequencer genesis. +/// 3. Mount ABCI info and sequencer blocks (soft blocks) for heights 3 and 4 with a slight delay, +/// to ensure the firm blocks arrive first. +/// 4. Mount firm blocks at heights 3 and 4. +/// 5. Mount `update_commitment_state` for the soft block at height 3, expecting 0 calls since the +/// firm block will be received first. +/// 5. Mount `execute_block` and `update_commitment_state` for firm block at height 3. +/// 6. Await satisfaction of the `execute_block` and `update_commitment_state` for the firm block at +/// height 3 with a timeout of 1000ms. +/// 7. Mount new genesis info with a rollup stop number of 9 and a start block number of 2, +/// reflecting that block 1 has already been executed and the commitment state updated. +/// 8. Mount `execute_block` and `update_commitment_state` for both soft and firm blocks at height 4 +/// and await their satisfaction (the soft mount need not be satisfied in the case that the firm +/// block is received first; we are just looking to see that the conductor restarted properly). +#[expect( + clippy::too_many_lines, + reason = "All lines reasonably necessary for the thoroughness of this test" +)] +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +async fn restarts_after_reaching_firm_stop_height_first() { + let test_conductor = spawn_conductor(CommitLevel::SoftAndFirm).await; + + mount_create_execution_session!( + test_conductor, + execution_session_parameters: ( + rollup_start_block_number: 2, + rollup_end_block_number: 2, + sequencer_start_block_height: 3, + celestia_max_look_ahead: 10, + ), + commitment_state: ( + firm: ( + number: 1, + hash: "1", + parent: "0", + ), + soft: ( + number: 1, + hash: "1", + parent: "0", + ), + lowest_celestia_search_height: 1, + ), + up_to_n_times: 1, // We only respond once since a new execution session is needed after restart + ); + + mount_sequencer_genesis!(test_conductor); + mount_celestia_header_network_head!( + test_conductor, + height: 1u32, + ); + mount_abci_info!( + test_conductor, + latest_sequencer_height: 4, + ); + + // Mount soft blocks for heights 3 and 4 + mount_get_filtered_sequencer_block!( + test_conductor, + sequencer_height: 3, + delay: Duration::from_millis(200), + ); + mount_get_filtered_sequencer_block!( + test_conductor, + sequencer_height: 4, + ); + + // Mount firm blocks for heights 3 and 4 + mount_celestia_blobs!( + test_conductor, + celestia_height: 1, + sequencer_heights: [3, 4], + ); + mount_sequencer_commit!( + test_conductor, + height: 3u32, + ); + mount_sequencer_commit!( + test_conductor, + height: 4u32, + ); + mount_sequencer_validator_set!(test_conductor, height: 2u32); + mount_sequencer_validator_set!(test_conductor, height: 3u32); + + let execute_block_1 = mount_execute_block!( + test_conductor, + mock_name: "execute_block_1", + number: 2, + hash: "2", + parent: "1", + expected_calls: 1, // This should not be called again after restart + ); + + // Should not be called since the firm block will be received first + let _update_commitment_state_soft_1 = mount_update_commitment_state!( + test_conductor, + mock_name: "update_commitment_state_soft_1", + firm: ( + number: 1, + hash: "1", + parent: "0", + ), + soft: ( + number: 2, + hash: "2", + parent: "1", + ), + lowest_celestia_search_height: 1, + expected_calls: 0, + ); + + let update_commitment_state_firm_1 = mount_update_commitment_state!( + test_conductor, + mock_name: "update_commitment_state_firm_1", + firm: ( + number: 2, + hash: "2", + parent: "1", + ), + soft: ( + number: 2, + hash: "2", + parent: "1", + ), + lowest_celestia_search_height: 1, + expected_calls: 1, // Should not be called again after restart + ); + + timeout( + Duration::from_millis(1000), + join( + execute_block_1.wait_until_satisfied(), + update_commitment_state_firm_1.wait_until_satisfied(), + ), + ) + .await + .expect("conductor should have updated the firm commitment state within 1000ms"); + + mount_create_execution_session!( + test_conductor, + execution_session_parameters: ( + rollup_start_block_number: 3, + rollup_end_block_number: 9, + sequencer_start_block_height: 4, + celestia_max_look_ahead: 10, + ), + commitment_state: ( + firm: ( + number: 2, + hash: "2", + parent: "1", + ), + soft: ( + number: 2, + hash: "2", + parent: "1", + ), + lowest_celestia_search_height: 1, + ), + ); + + let execute_block_2 = mount_execute_block!( + test_conductor, + mock_name: "execute_block_2", + number: 3, + hash: "3", + parent: "2", + expected_calls: 1, + ); + + // This condition does not need to be satisfied, since firm block may fire first after restart + let _update_commitment_state_soft_2 = mount_update_commitment_state!( + test_conductor, + mock_name: "update_commitment_state_soft_2", + firm: ( + number: 2, + hash: "2", + parent: "1", + ), + soft: ( + number: 3, + hash: "3", + parent: "2", + ), + lowest_celestia_search_height: 1, + expected_calls: 0..=1, + ); + + let update_commitment_state_firm_2 = mount_update_commitment_state!( + test_conductor, + mock_name: "update_commitment_state_firm_2", + firm: ( + number: 3, + hash: "3", + parent: "2", + ), + soft: ( + number: 3, + hash: "3", + parent: "2", + ), + lowest_celestia_search_height: 1, + expected_calls: 1, + ); + + timeout( + Duration::from_millis(1000), + join( + execute_block_2.wait_until_satisfied(), + update_commitment_state_firm_2.wait_until_satisfied(), + ), + ) + .await + .expect("conductor should have updated the firm commitment state within 1000ms"); +} diff --git a/crates/astria-conductor/tests/blackbox/soft_only.rs b/crates/astria-conductor/tests/blackbox/soft_only.rs index 8fff4e8c1c..0bf44c11f7 100644 --- a/crates/astria-conductor/tests/blackbox/soft_only.rs +++ b/crates/astria-conductor/tests/blackbox/soft_only.rs @@ -5,10 +5,7 @@ use astria_conductor::{ Conductor, Config, }; -use astria_core::generated::astria::execution::v1::{ - GetCommitmentStateRequest, - GetGenesisInfoRequest, -}; +use astria_core::generated::astria::execution::v2::CreateExecutionSessionRequest; use futures::future::{ join, join4, @@ -17,8 +14,7 @@ use telemetry::metrics; use tokio::time::timeout; use crate::{ - commitment_state, - genesis_info, + execution_session, helpers::{ make_config, mount_genesis, @@ -26,10 +22,9 @@ use crate::{ MockGrpc, }, mount_abci_info, - mount_executed_block, - mount_get_commitment_state, + mount_create_execution_session, + mount_execute_block, mount_get_filtered_sequencer_block, - mount_get_genesis_info, mount_sequencer_genesis, mount_update_commitment_state, SEQUENCER_CHAIN_ID, @@ -39,25 +34,27 @@ use crate::{ async fn simple() { let test_conductor = spawn_conductor(CommitLevel::SoftOnly).await; - mount_get_genesis_info!( - test_conductor, - sequencer_genesis_block_height: 1, - celestia_block_variance: 10, - ); - - mount_get_commitment_state!( + mount_create_execution_session!( test_conductor, - firm: ( - number: 1, - hash: [1; 64], - parent: [0; 64], + execution_session_parameters: ( + rollup_start_block_number: 2, + rollup_end_block_number: 9, + sequencer_start_block_height: 3, + celestia_max_look_ahead: 10, ), - soft: ( - number: 1, - hash: [1; 64], - parent: [0; 64], - ), - base_celestia_height: 1, + commitment_state: ( + firm: ( + number: 1, + hash: "1", + parent: "0", + ), + soft: ( + number: 1, + hash: "1", + parent: "0", + ), + lowest_celestia_search_height: 1, + ) ); mount_sequencer_genesis!(test_conductor); @@ -72,26 +69,26 @@ async fn simple() { sequencer_height: 3, ); - let execute_block = mount_executed_block!( + let execute_block = mount_execute_block!( test_conductor, number: 2, - hash: [2; 64], - parent: [1; 64], + hash: "2", + parent: "1", ); let update_commitment_state = mount_update_commitment_state!( test_conductor, firm: ( number: 1, - hash: [1; 64], - parent: [0; 64], + hash: "1", + parent: "0", ), soft: ( number: 2, - hash: [2; 64], - parent: [1; 64], + hash: "2", + parent: "1", ), - base_celestia_height: 1, + lowest_celestia_search_height: 1, ); timeout( @@ -112,25 +109,27 @@ async fn simple() { async fn submits_two_heights_in_succession() { let test_conductor = spawn_conductor(CommitLevel::SoftOnly).await; - mount_get_genesis_info!( - test_conductor, - sequencer_genesis_block_height: 1, - celestia_block_variance: 10, - ); - - mount_get_commitment_state!( + mount_create_execution_session!( test_conductor, - firm: ( - number: 1, - hash: [1; 64], - parent: [0; 64], + execution_session_parameters: ( + rollup_start_block_number: 2, + rollup_end_block_number: 9, + sequencer_start_block_height: 3, + celestia_max_look_ahead: 10, ), - soft: ( - number: 1, - hash: [1; 64], - parent: [0; 64], - ), - base_celestia_height: 1, + commitment_state: ( + firm: ( + number: 1, + hash: "1", + parent: "0", + ), + soft: ( + number: 1, + hash: "1", + parent: "0", + ), + lowest_celestia_search_height: 1, + ) ); mount_sequencer_genesis!(test_conductor); @@ -150,12 +149,12 @@ async fn submits_two_heights_in_succession() { sequencer_height: 4, ); - let execute_block_number_2 = mount_executed_block!( + let execute_block_number_2 = mount_execute_block!( test_conductor, mock_name: "first_execute", number: 2, - hash: [2; 64], - parent: [1; 64], + hash: "2", + parent: "1", ); let update_commitment_state_number_2 = mount_update_commitment_state!( @@ -163,23 +162,23 @@ async fn submits_two_heights_in_succession() { mock_name: "first_update", firm: ( number: 1, - hash: [1; 64], - parent: [0; 64], + hash: "1", + parent: "0", ), soft: ( number: 2, - hash: [2; 64], - parent: [1; 64], + hash: "2", + parent: "1", ), - base_celestia_height: 1, + lowest_celestia_search_height: 1, ); - let execute_block_number_3 = mount_executed_block!( + let execute_block_number_3 = mount_execute_block!( test_conductor, mock_name: "second_execute", number: 3, - hash: [3; 64], - parent: [2; 64], + hash: "3", + parent: "2", ); let update_commitment_state_number_3 = mount_update_commitment_state!( @@ -187,15 +186,15 @@ async fn submits_two_heights_in_succession() { mock_name: "second_update", firm: ( number: 1, - hash: [1; 64], - parent: [0; 64], + hash: "1", + parent: "0", ), soft: ( number: 3, - hash: [3; 64], - parent: [2; 64], + hash: "3", + parent: "2", ), - base_celestia_height: 1, + lowest_celestia_search_height: 1, ); timeout( @@ -218,25 +217,27 @@ async fn submits_two_heights_in_succession() { async fn skips_already_executed_heights() { let test_conductor = spawn_conductor(CommitLevel::SoftOnly).await; - mount_get_genesis_info!( + mount_create_execution_session!( test_conductor, - sequencer_genesis_block_height: 1, - celestia_block_variance: 10, - ); - - mount_get_commitment_state!( - test_conductor, - firm: ( - number: 1, - hash: [1; 64], - parent: [0; 64], + execution_session_parameters: ( + rollup_start_block_number: 2, + rollup_end_block_number: 9, + sequencer_start_block_height: 3, + celestia_max_look_ahead: 10, ), - soft: ( - number: 5, - hash: [1; 64], - parent: [0; 64], - ), - base_celestia_height: 1, + commitment_state: ( + firm: ( + number: 1, + hash: "1", + parent: "0", + ), + soft: ( + number: 5, + hash: "1", + parent: "0", + ), + lowest_celestia_search_height: 1, + ) ); mount_sequencer_genesis!(test_conductor); @@ -251,26 +252,26 @@ async fn skips_already_executed_heights() { sequencer_height: 7, ); - let execute_block = mount_executed_block!( + let execute_block = mount_execute_block!( test_conductor, number: 6, - hash: [2; 64], - parent: [1; 64], + hash: "2", + parent: "1", ); let update_commitment_state = mount_update_commitment_state!( test_conductor, firm: ( number: 1, - hash: [1; 64], - parent: [0; 64], + hash: "1", + parent: "0", ), soft: ( number: 6, - hash: [2; 64], - parent: [1; 64], + hash: "2", + parent: "1", ), - base_celestia_height: 1, + lowest_celestia_search_height: 1, ); timeout( @@ -291,25 +292,27 @@ async fn skips_already_executed_heights() { async fn requests_from_later_genesis_height() { let test_conductor = spawn_conductor(CommitLevel::SoftOnly).await; - mount_get_genesis_info!( + mount_create_execution_session!( test_conductor, - sequencer_genesis_block_height: 10, - celestia_block_variance: 10, - ); - - mount_get_commitment_state!( - test_conductor, - firm: ( - number: 1, - hash: [1; 64], - parent: [0; 64], + execution_session_parameters: ( + rollup_start_block_number: 2, + rollup_end_block_number: 10, + sequencer_start_block_height: 12, + celestia_max_look_ahead: 10, ), - soft: ( - number: 1, - hash: [1; 64], - parent: [0; 64], - ), - base_celestia_height: 1, + commitment_state: ( + firm: ( + number: 1, + hash: "1", + parent: "0", + ), + soft: ( + number: 1, + hash: "1", + parent: "0", + ), + lowest_celestia_search_height: 1, + ) ); mount_sequencer_genesis!(test_conductor); @@ -324,26 +327,26 @@ async fn requests_from_later_genesis_height() { sequencer_height: 12, ); - let execute_block = mount_executed_block!( + let execute_block = mount_execute_block!( test_conductor, number: 2, - hash: [2; 64], - parent: [1; 64], + hash: "2", + parent: "1", ); let update_commitment_state = mount_update_commitment_state!( test_conductor, firm: ( number: 1, - hash: [1; 64], - parent: [0; 64], + hash: "1", + parent: "0", ), soft: ( number: 2, - hash: [2; 64], - parent: [1; 64], + hash: "2", + parent: "1", ), - base_celestia_height: 1 + lowest_celestia_search_height: 1 ); timeout( @@ -397,32 +400,30 @@ async fn exits_on_sequencer_chain_id_mismatch() { }; GrpcMock::for_rpc_given( - "get_genesis_info", - matcher::message_type::(), + "create_execution_session", + matcher::message_type::(), ) - .respond_with(GrpcResponse::constant_response( - genesis_info!(sequencer_genesis_block_height: 1, - celestia_block_variance: 10,), - )) - .expect(0..) - .mount(&mock_grpc.mock_server) - .await; - - GrpcMock::for_rpc_given( - "get_commitment_state", - matcher::message_type::(), - ) - .respond_with(GrpcResponse::constant_response(commitment_state!(firm: ( - number: 1, - hash: [1; 64], - parent: [0; 64], + .respond_with(GrpcResponse::constant_response(execution_session!( + execution_session_parameters: ( + rollup_start_block_number: 2, + rollup_end_block_number: 9, + sequencer_start_block_height: 3, + celestia_max_look_ahead: 10, ), - soft: ( - number: 1, - hash: [1; 64], - parent: [0; 64], + commitment_state: ( + firm: ( + number: 1, + hash: "1", + parent: "0", + ), + soft: ( + number: 1, + hash: "1", + parent: "0", + ), + lowest_celestia_search_height: 1, ), - base_celestia_height: 1,))) + ))) .expect(0..) .mount(&mock_grpc.mock_server) .await; @@ -452,3 +453,168 @@ async fn exits_on_sequencer_chain_id_mismatch() { } } } + +/// Tests that the conductor correctly stops at the sequencer stop block height in soft only mode, +/// executing the soft block at that height. Then, tests that the conductor correctly restarts +/// and continues executing soft blocks after receiving updated genesis info and commitment state. +/// +/// It consists of the following steps: +/// 1. Mount execution session with a rollup stop number of 2 (sequencer height 3), responding only +/// up to 1 time so that the same information is not retrieved after restarting. +/// 2. Mount sequencer genesis, ABCI info, and sequencer blocks for heights 3 and 4. +/// 3. Mount `execute_block` and `update_commitment_state` mocks for the soft block at height 3, +/// expecting only 1 call and timing out after 1000ms. +/// 4. Mount updated execution session with a rollup stop number of 9 (more than high enough) and a +/// start block number of 2, reflecting that the first block has already been executed. +/// 5. Mount `execute_block` and `update_commitment_state` mocks for the soft block at height 4, +/// awaiting their satisfaction. +#[expect( + clippy::too_many_lines, + reason = "All lines reasonably necessary for the thoroughness of this test" +)] +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +async fn restarts_after_reaching_stop_block_height() { + let test_conductor = spawn_conductor(CommitLevel::SoftOnly).await; + + mount_create_execution_session!( + test_conductor, + execution_session_parameters: ( + rollup_start_block_number: 2, + rollup_end_block_number: 2, + sequencer_start_block_height: 3, + celestia_max_look_ahead: 10, + ), + commitment_state: ( + firm: ( + number: 1, + hash: "1", + parent: "0", + ), + soft: ( + number: 1, + hash: "1", + parent: "0", + ), + lowest_celestia_search_height: 1, + ), + up_to_n_times: 1, // We need a new execution session after restart + ); + + mount_sequencer_genesis!(test_conductor); + + mount_abci_info!( + test_conductor, + latest_sequencer_height: 4, + ); + + mount_get_filtered_sequencer_block!( + test_conductor, + sequencer_height: 3, + ); + + mount_get_filtered_sequencer_block!( + test_conductor, + sequencer_height: 4, + ); + + let execute_block_1 = mount_execute_block!( + test_conductor, + mock_name: "execute_block_1", + number: 2, + hash: "2", + parent: "1", + expected_calls: 1, + ); + + let update_commitment_state_1 = mount_update_commitment_state!( + test_conductor, + mock_name: "update_commitment_state_1", + firm: ( + number: 1, + hash: "1", + parent: "0", + ), + soft: ( + number: 2, + hash: "2", + parent: "1", + ), + lowest_celestia_search_height: 1, + expected_calls: 1, + ); + + timeout( + Duration::from_millis(1000), + join( + execute_block_1.wait_until_satisfied(), + update_commitment_state_1.wait_until_satisfied(), + ), + ) + .await + .expect( + "conductor should have executed the first soft block and updated the first soft \ + commitment state within 1000ms", + ); + + mount_create_execution_session!( + test_conductor, + execution_session_parameters: ( + rollup_start_block_number: 3, + rollup_end_block_number: 9, + sequencer_start_block_height: 4, + celestia_max_look_ahead: 10, + ), + commitment_state: ( + firm: ( + number: 1, + hash: "1", + parent: "0", + ), + soft: ( + number: 2, + hash: "2", + parent: "1", + ), + lowest_celestia_search_height: 1, + ), + ); + + let execute_block_2 = mount_execute_block!( + test_conductor, + mock_name: "execute_block_2", + number: 3, + hash: "3", + parent: "2", + expected_calls: 1, + ); + + let update_commitment_state_2 = mount_update_commitment_state!( + test_conductor, + mock_name: "update_commitment_state_2", + firm: ( + number: 1, + hash: "1", + parent: "0", + ), + soft: ( + number: 3, + hash: "3", + parent: "2", + ), + lowest_celestia_search_height: 1, + expected_calls: 1, + ); + + timeout( + Duration::from_millis(1000), + join( + execute_block_2.wait_until_satisfied(), + update_commitment_state_2.wait_until_satisfied(), + ), + ) + .await + .expect( + "conductor should have executed the second soft block and updated the second soft \ + commitment state within 1000ms", + ); +} diff --git a/crates/astria-core/src/execution/mod.rs b/crates/astria-core/src/execution/mod.rs index a3a6d96c3f..7083bd82d0 100644 --- a/crates/astria-core/src/execution/mod.rs +++ b/crates/astria-core/src/execution/mod.rs @@ -1 +1 @@ -pub mod v1; +pub mod v2; diff --git a/crates/astria-core/src/execution/v1/mod.rs b/crates/astria-core/src/execution/v1/mod.rs deleted file mode 100644 index 3327cd0edc..0000000000 --- a/crates/astria-core/src/execution/v1/mod.rs +++ /dev/null @@ -1,468 +0,0 @@ -use bytes::Bytes; -use pbjson_types::Timestamp; - -use crate::{ - generated::astria::execution::v1 as raw, - primitive::v1::{ - IncorrectRollupIdLength, - RollupId, - }, - Protobuf, -}; - -// An error when transforming a [`raw::GenesisInfo`] into a [`GenesisInfo`]. -#[derive(Debug, thiserror::Error)] -#[error(transparent)] -pub struct GenesisInfoError(GenesisInfoErrorKind); - -impl GenesisInfoError { - fn incorrect_rollup_id_length(inner: IncorrectRollupIdLength) -> Self { - Self(GenesisInfoErrorKind::IncorrectRollupIdLength(inner)) - } - - fn no_rollup_id() -> Self { - Self(GenesisInfoErrorKind::NoRollupId) - } -} - -#[derive(Debug, thiserror::Error)] -enum GenesisInfoErrorKind { - #[error("`rollup_id` field contained an invalid rollup ID")] - IncorrectRollupIdLength(IncorrectRollupIdLength), - #[error("`rollup_id` was not set")] - NoRollupId, -} - -/// Genesis Info required from a rollup to start an execution client. -/// -/// Contains information about the rollup id, and base heights for both sequencer & celestia. -/// -/// Usually constructed its [`Protobuf`] implementation from a -/// [`raw::GenesisInfo`]. -#[derive(Clone, Copy, Debug)] -#[cfg_attr(feature = "serde", derive(serde::Serialize))] -#[cfg_attr( - feature = "serde", - serde(into = "crate::generated::astria::execution::v1::GenesisInfo") -)] -pub struct GenesisInfo { - /// The rollup id which is used to identify the rollup txs. - rollup_id: RollupId, - /// The Sequencer block height which contains the first block of the rollup. - sequencer_genesis_block_height: tendermint::block::Height, - /// The allowed variance in the block height of celestia when looking for sequencer blocks. - celestia_block_variance: u64, -} - -impl GenesisInfo { - #[must_use] - pub fn rollup_id(&self) -> RollupId { - self.rollup_id - } - - #[must_use] - pub fn sequencer_genesis_block_height(&self) -> tendermint::block::Height { - self.sequencer_genesis_block_height - } - - #[must_use] - pub fn celestia_block_variance(&self) -> u64 { - self.celestia_block_variance - } -} - -impl From for raw::GenesisInfo { - fn from(value: GenesisInfo) -> Self { - value.to_raw() - } -} - -impl Protobuf for GenesisInfo { - type Error = GenesisInfoError; - type Raw = raw::GenesisInfo; - - fn try_from_raw_ref(raw: &Self::Raw) -> Result { - let raw::GenesisInfo { - rollup_id, - sequencer_genesis_block_height, - celestia_block_variance, - } = raw; - let Some(rollup_id) = rollup_id else { - return Err(Self::Error::no_rollup_id()); - }; - let rollup_id = RollupId::try_from_raw_ref(rollup_id) - .map_err(Self::Error::incorrect_rollup_id_length)?; - - Ok(Self { - rollup_id, - sequencer_genesis_block_height: (*sequencer_genesis_block_height).into(), - celestia_block_variance: *celestia_block_variance, - }) - } - - fn to_raw(&self) -> Self::Raw { - let Self { - rollup_id, - sequencer_genesis_block_height, - celestia_block_variance, - } = self; - - let sequencer_genesis_block_height: u32 = - (*sequencer_genesis_block_height).value().try_into().expect( - "block height overflow, this should not happen since tendermint heights are i64 \ - under the hood", - ); - Self::Raw { - rollup_id: Some(rollup_id.to_raw()), - sequencer_genesis_block_height, - celestia_block_variance: *celestia_block_variance, - } - } -} - -/// An error when transforming a [`raw::Block`] into a [`Block`]. -#[derive(Debug, thiserror::Error)] -#[error(transparent)] -pub struct BlockError(BlockErrorKind); - -impl BlockError { - fn field_not_set(field: &'static str) -> Self { - Self(BlockErrorKind::FieldNotSet(field)) - } -} - -#[derive(Debug, thiserror::Error)] -enum BlockErrorKind { - #[error("{0} field not set")] - FieldNotSet(&'static str), -} - -/// An Astria execution block on a rollup. -/// -/// Contains information about the block number, its hash, -/// its parent block's hash, and timestamp. -/// -/// Usually constructed its [`Protobuf`] implementation from a -/// [`raw::Block`]. -#[derive(Clone, Debug, PartialEq)] -#[cfg_attr(feature = "serde", derive(serde::Serialize))] -#[cfg_attr( - feature = "serde", - serde(into = "crate::generated::astria::execution::v1::Block") -)] -pub struct Block { - /// The block number - number: u32, - /// The hash of the block - hash: Bytes, - /// The hash of the parent block - parent_block_hash: Bytes, - /// Timestamp on the block, standardized to google protobuf standard. - timestamp: Timestamp, -} - -impl Block { - #[must_use] - pub fn number(&self) -> u32 { - self.number - } - - #[must_use] - pub fn hash(&self) -> &Bytes { - &self.hash - } - - #[must_use] - pub fn parent_block_hash(&self) -> &Bytes { - &self.parent_block_hash - } - - #[must_use] - pub fn timestamp(&self) -> Timestamp { - // prost_types::Timestamp is a (i64, i32) tuple, so this is - // effectively just a copy - self.timestamp.clone() - } -} - -impl From for raw::Block { - fn from(value: Block) -> Self { - value.to_raw() - } -} - -impl Protobuf for Block { - type Error = BlockError; - type Raw = raw::Block; - - fn try_from_raw_ref(raw: &Self::Raw) -> Result { - let raw::Block { - number, - hash, - parent_block_hash, - timestamp, - } = raw; - // Cloning timestamp is effectively a copy because timestamp is just a (i32, i64) tuple - let timestamp = timestamp - .clone() - .ok_or(Self::Error::field_not_set(".timestamp"))?; - - Ok(Self { - number: *number, - hash: hash.clone(), - parent_block_hash: parent_block_hash.clone(), - timestamp, - }) - } - - fn to_raw(&self) -> Self::Raw { - let Self { - number, - hash, - parent_block_hash, - timestamp, - } = self; - Self::Raw { - number: *number, - hash: hash.clone(), - parent_block_hash: parent_block_hash.clone(), - // Cloning timestamp is effectively a copy because timestamp is just a (i32, i64) - // tuple - timestamp: Some(timestamp.clone()), - } - } -} - -#[derive(Debug, thiserror::Error)] -#[error(transparent)] -pub struct CommitmentStateError(CommitmentStateErrorKind); - -impl CommitmentStateError { - fn field_not_set(field: &'static str) -> Self { - Self(CommitmentStateErrorKind::FieldNotSet(field)) - } - - fn firm(source: BlockError) -> Self { - Self(CommitmentStateErrorKind::Firm(source)) - } - - fn soft(source: BlockError) -> Self { - Self(CommitmentStateErrorKind::Soft(source)) - } - - fn firm_exceeds_soft(source: FirmExceedsSoft) -> Self { - Self(CommitmentStateErrorKind::FirmExceedsSoft(source)) - } -} - -#[derive(Debug, thiserror::Error)] -enum CommitmentStateErrorKind { - #[error("{0} field not set")] - FieldNotSet(&'static str), - #[error(".firm field did not contain a valid block")] - Firm(#[source] BlockError), - #[error(".soft field did not contain a valid block")] - Soft(#[source] BlockError), - #[error(transparent)] - FirmExceedsSoft(FirmExceedsSoft), -} - -#[derive(Debug, thiserror::Error)] -#[error("firm commitment at `{firm} exceeds soft commitment at `{soft}")] -pub struct FirmExceedsSoft { - firm: u32, - soft: u32, -} - -pub struct NoFirm; -pub struct NoSoft; -pub struct NoBaseCelestiaHeight; -pub struct WithFirm(Block); -pub struct WithSoft(Block); -pub struct WithCelestiaBaseHeight(u64); -#[derive(Default)] -pub struct CommitmentStateBuilder< - TFirm = NoFirm, - TSoft = NoSoft, - TBaseCelestiaHeight = NoBaseCelestiaHeight, -> { - firm: TFirm, - soft: TSoft, - base_celestia_height: TBaseCelestiaHeight, -} - -impl CommitmentStateBuilder { - fn new() -> Self { - Self { - firm: NoFirm, - soft: NoSoft, - base_celestia_height: NoBaseCelestiaHeight, - } - } -} - -impl CommitmentStateBuilder { - pub fn firm(self, firm: Block) -> CommitmentStateBuilder { - let Self { - soft, - base_celestia_height, - .. - } = self; - CommitmentStateBuilder { - firm: WithFirm(firm), - soft, - base_celestia_height, - } - } - - pub fn soft(self, soft: Block) -> CommitmentStateBuilder { - let Self { - firm, - base_celestia_height, - .. - } = self; - CommitmentStateBuilder { - firm, - soft: WithSoft(soft), - base_celestia_height, - } - } - - pub fn base_celestia_height( - self, - base_celestia_height: u64, - ) -> CommitmentStateBuilder { - let Self { - firm, - soft, - .. - } = self; - CommitmentStateBuilder { - firm, - soft, - base_celestia_height: WithCelestiaBaseHeight(base_celestia_height), - } - } -} - -impl CommitmentStateBuilder { - /// Finalize the commitment state. - /// - /// # Errors - /// Returns an error if the firm block exceeds the soft one. - pub fn build(self) -> Result { - let Self { - firm: WithFirm(firm), - soft: WithSoft(soft), - base_celestia_height: WithCelestiaBaseHeight(base_celestia_height), - } = self; - if firm.number() > soft.number() { - return Err(FirmExceedsSoft { - firm: firm.number(), - soft: soft.number(), - }); - } - Ok(CommitmentState { - soft, - firm, - base_celestia_height, - }) - } -} - -/// Information about the [`Block`] at each sequencer commitment level. -/// -/// A commitment state is valid if: -/// - Block numbers are such that soft >= firm (upheld by this type). -/// - No blocks ever decrease in block number. -/// - The chain defined by soft is the head of the canonical chain the firm block must belong to. -#[derive(Clone, Debug, PartialEq)] -#[cfg_attr(feature = "serde", derive(serde::Serialize))] -#[cfg_attr( - feature = "serde", - serde(into = "crate::generated::astria::execution::v1::CommitmentState") -)] -pub struct CommitmentState { - /// Soft commitment is the rollup block matching latest sequencer block. - soft: Block, - /// Firm commitment is achieved when data has been seen in DA. - firm: Block, - /// The base height of celestia from which to search for blocks after this - /// commitment state. - base_celestia_height: u64, -} - -impl CommitmentState { - #[must_use = "a commitment state must be built to be useful"] - pub fn builder() -> CommitmentStateBuilder { - CommitmentStateBuilder::new() - } - - #[must_use] - pub fn firm(&self) -> &Block { - &self.firm - } - - #[must_use] - pub fn soft(&self) -> &Block { - &self.soft - } - - pub fn base_celestia_height(&self) -> u64 { - self.base_celestia_height - } -} - -impl From for raw::CommitmentState { - fn from(value: CommitmentState) -> Self { - value.to_raw() - } -} - -impl Protobuf for CommitmentState { - type Error = CommitmentStateError; - type Raw = raw::CommitmentState; - - fn try_from_raw_ref(raw: &Self::Raw) -> Result { - let Self::Raw { - soft, - firm, - base_celestia_height, - } = raw; - let soft = 'soft: { - let Some(soft) = soft else { - break 'soft Err(Self::Error::field_not_set(".soft")); - }; - Block::try_from_raw_ref(soft).map_err(Self::Error::soft) - }?; - let firm = 'firm: { - let Some(firm) = firm else { - break 'firm Err(Self::Error::field_not_set(".firm")); - }; - Block::try_from_raw_ref(firm).map_err(Self::Error::firm) - }?; - - Self::builder() - .firm(firm) - .soft(soft) - .base_celestia_height(*base_celestia_height) - .build() - .map_err(Self::Error::firm_exceeds_soft) - } - - fn to_raw(&self) -> Self::Raw { - let Self { - soft, - firm, - base_celestia_height, - } = self; - let soft = soft.to_raw(); - let firm = firm.to_raw(); - let base_celestia_height = *base_celestia_height; - Self::Raw { - soft: Some(soft), - firm: Some(firm), - base_celestia_height, - } - } -} diff --git a/crates/astria-core/src/execution/v2/mod.rs b/crates/astria-core/src/execution/v2/mod.rs new file mode 100644 index 0000000000..7172644862 --- /dev/null +++ b/crates/astria-core/src/execution/v2/mod.rs @@ -0,0 +1,688 @@ +use std::num::NonZeroU64; + +use pbjson_types::Timestamp; + +use crate::{ + generated::astria::execution::v2 as raw, + primitive::v1::{ + IncorrectRollupIdLength, + RollupId, + }, + Protobuf, +}; + +#[derive(Debug, thiserror::Error)] +#[error(transparent)] +pub struct ExecutionSessionError(ExecutionSessionErrorKind); + +impl ExecutionSessionError { + fn execution_session_parameters(inner: ExecutionSessionParametersError) -> Self { + Self(ExecutionSessionErrorKind::InvalidExecutionSessionParameters(inner)) + } + + fn commitment_state(inner: CommitmentStateError) -> Self { + Self(ExecutionSessionErrorKind::InvalidCommitmentState(inner)) + } + + fn missing_execution_session_parameters() -> Self { + Self(ExecutionSessionErrorKind::MissingExecutionSessionParameters) + } + + fn missing_commitment_state() -> Self { + Self(ExecutionSessionErrorKind::MissingCommitmentState) + } +} + +#[derive(Debug, thiserror::Error)] +enum ExecutionSessionErrorKind { + #[error("invalid execution session parameters")] + InvalidExecutionSessionParameters(#[from] ExecutionSessionParametersError), + #[error("invalid commitment state")] + InvalidCommitmentState(#[from] CommitmentStateError), + #[error("execution session parameters missing")] + MissingExecutionSessionParameters, + #[error("commitment state missing")] + MissingCommitmentState, +} + +/// `ExecutionSession` contains the information needed to drive the full execution +/// of a rollup chain in the rollup. +/// +/// The execution session is only valid for the execution config params with +/// which it was created. Once all blocks within the session have been executed, +/// the execution client must request a new session. The `session_id` is used to +/// to track which session is being used. +#[derive(Debug)] +pub struct ExecutionSession { + /// An ID for the session. + session_id: String, + /// The configuration for the execution session. + execution_session_parameters: ExecutionSessionParameters, + /// The commitment state for executing client to start from. + commitment_state: CommitmentState, +} + +impl ExecutionSession { + #[must_use] + pub fn session_id(&self) -> &String { + &self.session_id + } + + #[must_use] + pub fn execution_session_parameters(&self) -> &ExecutionSessionParameters { + &self.execution_session_parameters + } + + #[must_use] + pub fn commitment_state(&self) -> &CommitmentState { + &self.commitment_state + } +} + +impl Protobuf for ExecutionSession { + type Error = ExecutionSessionError; + type Raw = raw::ExecutionSession; + + fn try_from_raw_ref(raw: &Self::Raw) -> Result { + let raw::ExecutionSession { + session_id, + execution_session_parameters, + commitment_state, + } = raw; + let execution_session_parameters = execution_session_parameters + .as_ref() + .ok_or_else(Self::Error::missing_execution_session_parameters)?; + let execution_session_parameters = + ExecutionSessionParameters::try_from_raw_ref(execution_session_parameters) + .map_err(Self::Error::execution_session_parameters)?; + let commitment_state = commitment_state + .as_ref() + .ok_or_else(Self::Error::missing_commitment_state)?; + let commitment_state = CommitmentState::try_from_raw_ref(commitment_state) + .map_err(Self::Error::commitment_state)?; + Ok(Self { + session_id: session_id.clone(), + execution_session_parameters, + commitment_state, + }) + } + + fn to_raw(&self) -> Self::Raw { + let Self { + session_id, + execution_session_parameters, + commitment_state, + } = self; + let execution_session_parameters = execution_session_parameters.to_raw(); + let commitment_state = commitment_state.to_raw(); + Self::Raw { + session_id: session_id.clone(), + execution_session_parameters: Some(execution_session_parameters), + commitment_state: Some(commitment_state), + } + } +} + +// An error when transforming a [`raw::ExecutionSessionParameters`] into a +// [`ExecutionSessionParameters`]. +#[derive(Debug, thiserror::Error)] +#[error(transparent)] +pub struct ExecutionSessionParametersError(ExecutionSessionParametersErrorKind); + +impl ExecutionSessionParametersError { + fn incorrect_rollup_id_length(inner: IncorrectRollupIdLength) -> Self { + Self(ExecutionSessionParametersErrorKind::IncorrectRollupIdLength(inner)) + } + + fn no_rollup_id() -> Self { + Self(ExecutionSessionParametersErrorKind::NoRollupId) + } + + fn invalid_sequencer_start_block_height(inner: tendermint::Error) -> Self { + Self(ExecutionSessionParametersErrorKind::InvalidSequencerStartBlockHeight(inner)) + } +} + +#[derive(Debug, thiserror::Error)] +enum ExecutionSessionParametersErrorKind { + #[error("`rollup_id` field contained an invalid rollup ID")] + IncorrectRollupIdLength(IncorrectRollupIdLength), + #[error("`rollup_id` was not set")] + NoRollupId, + #[error( + "`tendermint::block::Height` could not be constructed from `sequencer_start_block_height`" + )] + InvalidSequencerStartBlockHeight(#[from] tendermint::Error), +} + +/// Genesis Info required from a rollup to start an execution client. +/// +/// Contains information about the rollup id, and base heights for both sequencer & celestia. +/// +/// Usually constructed its [`Protobuf`] implementation from a +/// [`raw::ExecutionSessionParameters`]. +#[derive(Clone, Debug)] +#[cfg_attr(feature = "serde", derive(serde::Serialize))] +#[cfg_attr( + feature = "serde", + serde(into = "crate::generated::astria::execution::v2::ExecutionSessionParameters") +)] +pub struct ExecutionSessionParameters { + // The rollup_id is the unique identifier for the rollup chain. + rollup_id: RollupId, + // The first rollup block number to be executed. This is mapped to + // `sequencer_first_block_height`. The minimum first block number is 1, since 0 represents + // the genesis block. Implementors should reject a value of 0. + // + // Servers implementing this API should reject execution of blocks below this + // value with an OUT_OF_RANGE error code. + rollup_start_block_number: u64, + // The final rollup block number to execute as part of a session. + // + // If not set or set to 0, the execution session does not have an upper bound. + // + // Servers implementing this API should reject execution of blocks past this + // value with an OUT_OF_RANGE error code. + rollup_end_block_number: Option, + // The ID of the Astria Sequencer network to retrieve Sequencer blocks from. + // Conductor implementations should verify that the Sequencer network they are + // connected to have this chain ID (if fetching soft Sequencer blocks), and verify + // that the Sequencer metadata blobs retrieved from Celestia contain this chain + // ID (if extracting firm Sequencer blocks from Celestia blobs). + sequencer_chain_id: String, + // The first block height on the sequencer chain to use for rollup transactions. + // This is mapped to `rollup_start_block_number`. + sequencer_start_block_height: tendermint::block::Height, + // The ID of the Celestia network to retrieve blobs from. + // Conductor implementations should verify that the Celestia network they are + // connected to have this chain ID (if extracting firm Sequencer blocks from + // Celestia blobs). + celestia_chain_id: String, + // The maximum number of Celestia blocks which can be read above + // `CommitmentState.lowest_celestia_search_height` in search of the next firm + // block. + celestia_search_height_max_look_ahead: u64, +} + +impl ExecutionSessionParameters { + #[must_use] + pub fn new( + rollup_id: RollupId, + rollup_start_block_number: u64, + rollup_end_block_number: u64, + sequencer_chain_id: String, + sequencer_start_block_height: tendermint::block::Height, + celestia_chain_id: String, + celestia_search_height_max_look_ahead: u64, + ) -> Self { + Self { + rollup_id, + rollup_start_block_number, + rollup_end_block_number: NonZeroU64::new(rollup_end_block_number), + sequencer_chain_id, + sequencer_start_block_height, + celestia_chain_id, + celestia_search_height_max_look_ahead, + } + } + + #[must_use] + pub fn rollup_id(&self) -> RollupId { + self.rollup_id + } + + #[must_use] + pub fn rollup_start_block_number(&self) -> u64 { + self.rollup_start_block_number + } + + #[must_use] + pub fn rollup_end_block_number(&self) -> Option { + self.rollup_end_block_number + } + + #[must_use] + pub fn sequencer_start_block_height(&self) -> u64 { + self.sequencer_start_block_height.into() + } + + #[must_use] + pub fn sequencer_chain_id(&self) -> &String { + &self.sequencer_chain_id + } + + #[must_use] + pub fn celestia_chain_id(&self) -> &String { + &self.celestia_chain_id + } + + #[must_use] + pub fn celestia_search_height_max_look_ahead(&self) -> u64 { + self.celestia_search_height_max_look_ahead + } +} + +impl From for raw::ExecutionSessionParameters { + fn from(value: ExecutionSessionParameters) -> Self { + value.to_raw() + } +} + +impl Protobuf for ExecutionSessionParameters { + type Error = ExecutionSessionParametersError; + type Raw = raw::ExecutionSessionParameters; + + fn try_from_raw_ref(raw: &Self::Raw) -> Result { + let raw::ExecutionSessionParameters { + rollup_id, + rollup_start_block_number, + rollup_end_block_number, + sequencer_chain_id, + sequencer_start_block_height, + celestia_chain_id, + celestia_search_height_max_look_ahead, + } = raw; + let Some(rollup_id) = rollup_id else { + return Err(Self::Error::no_rollup_id()); + }; + let rollup_id = RollupId::try_from_raw_ref(rollup_id) + .map_err(Self::Error::incorrect_rollup_id_length)?; + let sequencer_start_block_height = + tendermint::block::Height::try_from(*sequencer_start_block_height) + .map_err(Self::Error::invalid_sequencer_start_block_height)?; + + Ok(Self { + rollup_id, + rollup_start_block_number: *rollup_start_block_number, + rollup_end_block_number: NonZeroU64::new(*rollup_end_block_number), + sequencer_chain_id: sequencer_chain_id.clone(), + sequencer_start_block_height, + celestia_chain_id: celestia_chain_id.clone(), + celestia_search_height_max_look_ahead: *celestia_search_height_max_look_ahead, + }) + } + + fn to_raw(&self) -> Self::Raw { + let Self { + rollup_id, + rollup_start_block_number, + rollup_end_block_number, + sequencer_chain_id, + sequencer_start_block_height, + celestia_chain_id, + celestia_search_height_max_look_ahead, + } = self; + + Self::Raw { + rollup_id: Some(rollup_id.to_raw()), + rollup_start_block_number: *rollup_start_block_number, + rollup_end_block_number: rollup_end_block_number.map(NonZeroU64::get).unwrap_or(0), + sequencer_chain_id: sequencer_chain_id.clone(), + sequencer_start_block_height: sequencer_start_block_height.value(), + celestia_chain_id: celestia_chain_id.clone(), + celestia_search_height_max_look_ahead: *celestia_search_height_max_look_ahead, + } + } +} + +/// An error when transforming a [`raw::ExecutedBlockMetadata`] into a [`ExecutedBlockMetadata`]. +#[derive(Debug, thiserror::Error)] +#[error(transparent)] +pub struct ExecutedBlockMetadataError(ExecutedBlockMetadataErrorKind); + +impl ExecutedBlockMetadataError { + fn field_not_set(field: &'static str) -> Self { + Self(ExecutedBlockMetadataErrorKind::FieldNotSet(field)) + } +} + +#[derive(Debug, thiserror::Error)] +enum ExecutedBlockMetadataErrorKind { + #[error("{0} field not set")] + FieldNotSet(&'static str), +} + +/// An Astria execution block on a rollup. +/// +/// Contains information about the block number, its hash, +/// its parent block's hash, and timestamp. +/// +/// Usually constructed its [`Protobuf`] implementation from a +/// [`raw::ExecutedBlockMetadata`]. +#[derive(Clone, Debug, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize))] +#[cfg_attr( + feature = "serde", + serde(into = "crate::generated::astria::execution::v2::ExecutedBlockMetadata") +)] +pub struct ExecutedBlockMetadata { + /// The block number + number: u64, + /// The hash of the block + hash: String, + /// The hash of the parent block + parent_hash: String, + /// Timestamp of the block, taken from the sequencer block that this rollup block + /// was constructed from. + timestamp: Timestamp, +} + +impl ExecutedBlockMetadata { + #[must_use] + pub fn number(&self) -> u64 { + self.number + } + + #[must_use] + pub fn hash(&self) -> &str { + &self.hash + } + + #[must_use] + pub fn parent_hash(&self) -> &str { + &self.parent_hash + } + + #[must_use] + pub fn timestamp(&self) -> Timestamp { + // prost_types::Timestamp is a (i64, i32) tuple, so this is + // effectively just a copy + self.timestamp.clone() + } +} + +impl From for raw::ExecutedBlockMetadata { + fn from(value: ExecutedBlockMetadata) -> Self { + value.to_raw() + } +} + +impl Protobuf for ExecutedBlockMetadata { + type Error = ExecutedBlockMetadataError; + type Raw = raw::ExecutedBlockMetadata; + + fn try_from_raw_ref(raw: &Self::Raw) -> Result { + let raw::ExecutedBlockMetadata { + number, + hash, + parent_hash, + timestamp, + } = raw; + // Cloning timestamp is effectively a copy because timestamp is just a (i32, i64) tuple + let timestamp = timestamp + .clone() + .ok_or(Self::Error::field_not_set(".timestamp"))?; + + Ok(Self { + number: *number, + hash: hash.clone(), + parent_hash: parent_hash.clone(), + timestamp, + }) + } + + fn to_raw(&self) -> Self::Raw { + let Self { + number, + hash, + parent_hash, + timestamp, + } = self; + Self::Raw { + number: *number, + hash: hash.clone(), + parent_hash: parent_hash.clone(), + // Cloning timestamp is effectively a copy because timestamp is just a (i32, i64) + // tuple + timestamp: Some(timestamp.clone()), + } + } +} + +#[derive(Debug, thiserror::Error)] +#[error(transparent)] +pub struct CommitmentStateError(CommitmentStateErrorKind); + +impl CommitmentStateError { + fn field_not_set(field: &'static str) -> Self { + Self(CommitmentStateErrorKind::FieldNotSet(field)) + } + + fn firm(source: ExecutedBlockMetadataError) -> Self { + Self(CommitmentStateErrorKind::Firm(source)) + } + + fn soft(source: ExecutedBlockMetadataError) -> Self { + Self(CommitmentStateErrorKind::Soft(source)) + } + + fn firm_exceeds_soft(source: FirmExceedsSoft) -> Self { + Self(CommitmentStateErrorKind::FirmExceedsSoft(source)) + } +} + +#[derive(Debug, thiserror::Error)] +enum CommitmentStateErrorKind { + #[error("{0} field not set")] + FieldNotSet(&'static str), + #[error(".firm field did not contain valid executed block metadata")] + Firm(#[source] ExecutedBlockMetadataError), + #[error(".soft field did not contain valid executed block metadata")] + Soft(#[source] ExecutedBlockMetadataError), + #[error(transparent)] + FirmExceedsSoft(FirmExceedsSoft), +} + +#[derive(Debug, thiserror::Error)] +#[error("firm commitment at `{firm} exceeds soft commitment at `{soft}")] +pub struct FirmExceedsSoft { + firm: u64, + soft: u64, +} + +pub struct NoFirm; +pub struct NoSoft; +pub struct NoBaseCelestiaHeight; +pub struct WithFirm(ExecutedBlockMetadata); +pub struct WithSoft(ExecutedBlockMetadata); +pub struct WithLowestCelestiaSearchHeight(u64); +#[derive(Default)] +pub struct CommitmentStateBuilder< + TFirm = NoFirm, + TSoft = NoSoft, + TBaseCelestiaHeight = NoBaseCelestiaHeight, +> { + firm_executed_block_metadata: TFirm, + soft_executed_block_metadata: TSoft, + lowest_celestia_search_height: TBaseCelestiaHeight, +} + +impl CommitmentStateBuilder { + fn new() -> Self { + Self { + firm_executed_block_metadata: NoFirm, + soft_executed_block_metadata: NoSoft, + lowest_celestia_search_height: NoBaseCelestiaHeight, + } + } +} + +impl CommitmentStateBuilder { + pub fn firm_executed_block_metadata( + self, + firm_executed_block_metadata: ExecutedBlockMetadata, + ) -> CommitmentStateBuilder { + let Self { + soft_executed_block_metadata, + lowest_celestia_search_height, + .. + } = self; + CommitmentStateBuilder { + firm_executed_block_metadata: WithFirm(firm_executed_block_metadata), + soft_executed_block_metadata, + lowest_celestia_search_height, + } + } + + pub fn soft_executed_block_metadata( + self, + soft_executed_block_metadata: ExecutedBlockMetadata, + ) -> CommitmentStateBuilder { + let Self { + firm_executed_block_metadata, + lowest_celestia_search_height, + .. + } = self; + CommitmentStateBuilder { + firm_executed_block_metadata, + soft_executed_block_metadata: WithSoft(soft_executed_block_metadata), + lowest_celestia_search_height, + } + } + + pub fn lowest_celestia_search_height( + self, + lowest_celestia_search_height: u64, + ) -> CommitmentStateBuilder { + let Self { + firm_executed_block_metadata, + soft_executed_block_metadata, + .. + } = self; + CommitmentStateBuilder { + firm_executed_block_metadata, + soft_executed_block_metadata, + lowest_celestia_search_height: WithLowestCelestiaSearchHeight( + lowest_celestia_search_height, + ), + } + } +} + +impl CommitmentStateBuilder { + /// Finalize the commitment state. + /// + /// # Errors + /// Returns an error if the firm block exceeds the soft one. + pub fn build(self) -> Result { + let Self { + firm_executed_block_metadata: WithFirm(firm_executed_block_metadata), + soft_executed_block_metadata: WithSoft(soft_executed_block_metadata), + lowest_celestia_search_height: + WithLowestCelestiaSearchHeight(lowest_celestia_search_height), + } = self; + if firm_executed_block_metadata.number() > soft_executed_block_metadata.number() { + return Err(FirmExceedsSoft { + firm: firm_executed_block_metadata.number(), + soft: soft_executed_block_metadata.number(), + }); + } + Ok(CommitmentState { + soft_executed_block_metadata, + firm_executed_block_metadata, + lowest_celestia_search_height, + }) + } +} + +/// The `CommitmentState` holds the block at each stage of sequencer commitment +/// level +/// +/// A Valid `CommitmentState`: +/// - Block numbers are such that soft >= firm. +/// - No blocks ever decrease in block number. +/// - The chain defined by soft is the head of the canonical chain the firm block must belong to. +#[derive(Clone, Debug, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize))] +#[cfg_attr( + feature = "serde", + serde(into = "crate::generated::astria::execution::v2::CommitmentState") +)] +pub struct CommitmentState { + /// Soft committed block metadata derived directly from an Astria sequencer block. + soft_executed_block_metadata: ExecutedBlockMetadata, + /// Firm committed block metadata derived from a Sequencer block that has been + /// written to the data availability layer (Celestia). + firm_executed_block_metadata: ExecutedBlockMetadata, + /// The lowest Celestia height that will be searched for the next firm block. + /// This information is stored as part of `CommitmentState` so that it will be + /// routinely updated as new firm blocks are received, and so that the execution + /// client will not need to search from Celestia genesis. + lowest_celestia_search_height: u64, +} + +impl CommitmentState { + #[must_use = "a commitment state must be built to be useful"] + pub fn builder() -> CommitmentStateBuilder { + CommitmentStateBuilder::new() + } + + #[must_use] + pub fn firm(&self) -> &ExecutedBlockMetadata { + &self.firm_executed_block_metadata + } + + #[must_use] + pub fn soft(&self) -> &ExecutedBlockMetadata { + &self.soft_executed_block_metadata + } + + #[must_use] + pub fn lowest_celestia_search_height(&self) -> u64 { + self.lowest_celestia_search_height + } +} + +impl From for raw::CommitmentState { + fn from(value: CommitmentState) -> Self { + value.to_raw() + } +} + +impl Protobuf for CommitmentState { + type Error = CommitmentStateError; + type Raw = raw::CommitmentState; + + fn try_from_raw_ref(raw: &Self::Raw) -> Result { + let Self::Raw { + soft_executed_block_metadata, + firm_executed_block_metadata, + lowest_celestia_search_height, + } = raw; + let soft_executed_block_metadata = 'soft: { + let Some(soft) = soft_executed_block_metadata else { + break 'soft Err(Self::Error::field_not_set(".soft")); + }; + ExecutedBlockMetadata::try_from_raw_ref(soft).map_err(Self::Error::soft) + }?; + let firm_executed_block_metadata = 'firm: { + let Some(firm) = firm_executed_block_metadata else { + break 'firm Err(Self::Error::field_not_set(".firm")); + }; + ExecutedBlockMetadata::try_from_raw_ref(firm).map_err(Self::Error::firm) + }?; + + Self::builder() + .firm_executed_block_metadata(firm_executed_block_metadata) + .soft_executed_block_metadata(soft_executed_block_metadata) + .lowest_celestia_search_height(*lowest_celestia_search_height) + .build() + .map_err(Self::Error::firm_exceeds_soft) + } + + fn to_raw(&self) -> Self::Raw { + let Self { + soft_executed_block_metadata, + firm_executed_block_metadata, + lowest_celestia_search_height, + } = self; + let soft_executed_block_metadata = soft_executed_block_metadata.to_raw(); + let firm_executed_block_metadata = firm_executed_block_metadata.to_raw(); + let lowest_celestia_search_height = *lowest_celestia_search_height; + Self::Raw { + soft_executed_block_metadata: Some(soft_executed_block_metadata), + firm_executed_block_metadata: Some(firm_executed_block_metadata), + lowest_celestia_search_height, + } + } +} diff --git a/crates/astria-core/src/generated/astria.optimistic_execution.v1alpha1.rs b/crates/astria-core/src/generated/astria.optimistic_execution.v1alpha1.rs index 606000e263..10e31d0a86 100644 --- a/crates/astria-core/src/generated/astria.optimistic_execution.v1alpha1.rs +++ b/crates/astria-core/src/generated/astria.optimistic_execution.v1alpha1.rs @@ -41,7 +41,9 @@ pub struct ExecuteOptimisticBlockStreamResponse { /// Metadata identifying the block resulting from executing a block. Includes number, hash, /// parent hash and timestamp. #[prost(message, optional, tag = "1")] - pub block: ::core::option::Option, + pub block: ::core::option::Option< + super::super::execution::v2::ExecutedBlockMetadata, + >, /// The base_sequencer_block_hash is the hash from the base sequencer block this block /// is based on. This is used to associate an optimistic execution result with the hash /// received once a sequencer block is committed. diff --git a/crates/astria-core/src/generated/mod.rs b/crates/astria-core/src/generated/mod.rs index f9b4ffeece..79eb648f3c 100644 --- a/crates/astria-core/src/generated/mod.rs +++ b/crates/astria-core/src/generated/mod.rs @@ -55,15 +55,6 @@ pub mod astria { #[path = ""] pub mod execution { - pub mod v1 { - include!("astria.execution.v1.rs"); - - #[cfg(feature = "serde")] - mod _serde_impl { - use super::*; - include!("astria.execution.v1.serde.rs"); - } - } pub mod v2 { include!("astria.execution.v2.rs"); diff --git a/dev/values/rollup/dev.yaml b/dev/values/rollup/dev.yaml index 66eb3afb71..99ae70c1e7 100644 --- a/dev/values/rollup/dev.yaml +++ b/dev/values/rollup/dev.yaml @@ -10,18 +10,43 @@ global: evm-rollup: genesis: - ## These values are used to configure the genesis block of the rollup chain - ## no defaults as they are unique to each chain - - # Block height to start syncing rollup from, lowest possible is 2 - sequencerInitialHeight: 2 - # The first Celestia height to utilize when looking for rollup data - celestiaInitialHeight: 2 - # The variance in Celestia height to allow before halting the chain - celestiaHeightVariance: 10 - # Will fill the extra data in each block, can be left empty - # can also fill with something unique for your chain. - extraDataOverride: "" + # The name of the rollup chain, used to generate the Rollup ID + rollupName: "{{ .Values.global.rollupName }}" + + # The "forks" for upgrading the chain. Contains necessary information for starting + # and, if desired, restarting the chain at a given height. The necessary fields + # for the first fork are provided, and additional forks can be added as needed. + forks: + ## These values are used to configure the genesis block of the rollup chain + ## no defaults as they are unique to each chain + launch: + # The rollup number to start executing blocks at, lowest possible is 1 + height: 1 + # Configure the fee collector for the evm tx fees, activated at block heights. + # If not configured, all tx fees will be burned. + feeCollector: "0xaC21B97d35Bf75A7dAb16f35b111a50e78A72F30" + sequencer: + # The chain id of the sequencer chain + chainId: "sequencer-test-chain-0" + # The hrp for bech32m addresses, unlikely to be changed + addressPrefix: "astria" + # Block height to start syncing rollup from (inclusive), lowest possible is 2 + startHeight: 2 + celestia: + # The chain id of the celestia chain + chainId: "celestia-local-0" + # The first Celestia height to utilize when looking for rollup data + startHeight: 2 + # The maximum number of blocks ahead of the lowest Celestia search height + # to search for a firm commitment + searchHeightMaxLookAhead: 50 + # Configure the sequencer bridge addresses and allowed assets if using + # the astria canonical bridge. Recommend removing alloc values if so. + bridgeAddresses: + - bridgeAddress: "astria13ahqz4pjqfmynk9ylrqv4fwe4957x2p0h5782u" + senderAddress: "0x0000000000000000000000000000000000000000" + assetDenom: "nria" + assetPrecision: 9 ## These are general configuration values with some recommended defaults @@ -29,34 +54,6 @@ evm-rollup: gasLimit: "50000000" # If set to true the genesis block will contain extra data overrideGenesisExtraData: true - # The hrp for bech32m addresses, unlikely to be changed - sequencerAddressPrefix: "astria" - - ## These values are used to configure astria native bridging - ## Many of the fields have commented out example fields - - # Configure the sequencer bridge addresses and allowed assets if using - # the astria canonical bridge. Recommend removing alloc values if so. - bridgeAddresses: - - bridgeAddress: "astria13ahqz4pjqfmynk9ylrqv4fwe4957x2p0h5782u" - startHeight: 1 - senderAddress: "0x0000000000000000000000000000000000000000" - assetDenom: "nria" - assetPrecision: 9 - - - ## Fee configuration - - # Configure the fee collector for the evm tx fees, activated at block heights. - # If not configured, all tx fees will be burned. - feeCollectors: - 1: "0xaC21B97d35Bf75A7dAb16f35b111a50e78A72F30" - # Configure EIP-1559 params, activated at block heights - eip1559Params: {} - # 1: - # minBaseFee: 0 - # elasticityMultiplier: 2 - # baseFeeChangeDenominator: 8 ## Standard Eth Genesis config values # Configuration of Eth forks, setting to 0 will enable from height, diff --git a/dev/values/rollup/evm-restart-test.yaml b/dev/values/rollup/evm-restart-test.yaml new file mode 100644 index 0000000000..77535b16ea --- /dev/null +++ b/dev/values/rollup/evm-restart-test.yaml @@ -0,0 +1,251 @@ +global: + useTTY: true + dev: true + evmChainId: 1337 + rollupName: astria + sequencerRpc: http://node0-sequencer-rpc-service.astria-dev-cluster.svc.cluster.local:26657 + sequencerGrpc: http://node0-sequencer-grpc-service.astria-dev-cluster.svc.cluster.local:8080 + sequencerChainId: sequencer-test-chain-0 + celestiaChainId: celestia-local-0 + +evm-rollup: + genesis: + # The name of the rollup chain, used to generate the Rollup ID + rollupName: "{{ .Values.global.rollupName }}" + + # The "forks" for upgrading the chain. Contains necessary information for starting + # and, if desired, restarting the chain at a given height. The necessary fields + # for the genesis fork are provided, and additional forks can be added as needed. + forks: + ## These values are used to configure the genesis block of the rollup chain + ## no defaults as they are unique to each chain + launch: + # The rollup number to start executing blocks at, lowest possible is 1 + height: 1 + # Configure the fee collector for the evm tx fees, activated at block heights. + # If not configured, all tx fees will be burned. + feeCollector: "0xaC21B97d35Bf75A7dAb16f35b111a50e78A72F30" + sequencer: + # The chain id of the sequencer chain + chainId: "sequencer-test-chain-0" + # The hrp for bech32m addresses, unlikely to be changed + addressPrefix: "astria" + # Block height to start syncing rollup from (inclusive), lowest possible is 2 + startHeight: 2 + celestia: + # The chain id of the celestia chain + chainId: "celestia-local-0" + # The first Celestia height to utilize when looking for rollup data + startHeight: 2 + # The maximum number of blocks ahead of the lowest Celestia search height + # to search for a firm commitment + searchHeightMaxLookAhead: 50 + # Configure the sequencer bridge addresses and allowed assets if using + # the astria canonical bridge. Recommend removing alloc values if so. + bridgeAddresses: + - bridgeAddress: "astria13ahqz4pjqfmynk9ylrqv4fwe4957x2p0h5782u" + senderAddress: "0x0000000000000000000000000000000000000000" + assetDenom: "nria" + assetPrecision: 9 + restart: + height: 19 + + ## These are general configuration values with some recommended defaults + + # Configure the gas Limit + gasLimit: "50000000" + # If set to true the genesis block will contain extra data + overrideGenesisExtraData: true + + ## Standard Eth Genesis config values + # Configuration of Eth forks, setting to 0 will enable from height, + # left as is these forks will not activate. + cancunTime: "" + pragueTime: "" + verkleTime: "" + # Can configure the genesis allocs for the chain + alloc: + # Deploying the deterministic deploy proxy contract in genesis + # Forge and other tools use this for their CREATE2 usage, but + # can only be included through the genesis block after EIP-155 + # https://github.com/Arachnid/deterministic-deployment-proxy + - address: "0x4e59b44847b379578588920cA78FbF26c0B4956C" + value: + balance: "0" + code: "0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe03601600081602082378035828234f58015156039578182fd5b8082525050506014600cf3" + - address: "0xA58639fB5458e65E4fA917FF951C390292C24A15" + value: + balance: "0" + code: "0x6080604052600436106100f35760003560e01c8063b6476c7e1161008a578063e74b981b11610059578063e74b981b1461027b578063ebd090541461029b578063f2fde38b146102bb578063fc88d31b146102db57600080fd5b8063b6476c7e1461021c578063bab916d01461023e578063d294f09314610251578063db97dc981461026657600080fd5b80638da5cb5b116100c65780638da5cb5b146101a1578063a7eaa739146101d3578063a996e020146101f3578063ad2282471461020657600080fd5b80636f46384a146100f8578063715018a6146101215780637eb6dec7146101385780638897397914610181575b600080fd5b34801561010457600080fd5b5061010e60035481565b6040519081526020015b60405180910390f35b34801561012d57600080fd5b506101366102f1565b005b34801561014457600080fd5b5061016c7f000000000000000000000000000000000000000000000000000000000000000981565b60405163ffffffff9091168152602001610118565b34801561018d57600080fd5b5061013661019c3660046107a6565b610305565b3480156101ad57600080fd5b506000546001600160a01b03165b6040516001600160a01b039091168152602001610118565b3480156101df57600080fd5b506101366101ee3660046107a6565b610312565b610136610201366004610808565b61031f565b34801561021257600080fd5b5061010e60065481565b34801561022857600080fd5b50610231610414565b6040516101189190610874565b61013661024c3660046108c3565b6104a2565b34801561025d57600080fd5b50610136610588565b34801561027257600080fd5b506102316106b4565b34801561028757600080fd5b50610136610296366004610905565b6106c1565b3480156102a757600080fd5b506005546101bb906001600160a01b031681565b3480156102c757600080fd5b506101366102d6366004610905565b6106eb565b3480156102e757600080fd5b5061010e60045481565b6102f9610729565b6103036000610756565b565b61030d610729565b600455565b61031a610729565b600355565b3460045480821161034b5760405162461bcd60e51b815260040161034290610935565b60405180910390fd5b60007f000000000000000000000000000000000000000000000000000000003b9aca006103788385610998565b61038291906109b1565b1161039f5760405162461bcd60e51b8152600401610342906109d3565b600454600660008282546103b39190610a61565b90915550506004546103c59034610998565b336001600160a01b03167f0c64e29a5254a71c7f4e52b3d2d236348c80e00a00ba2e1961962bd2827c03fb888888886040516104049493929190610a9d565b60405180910390a3505050505050565b6002805461042190610acf565b80601f016020809104026020016040519081016040528092919081815260200182805461044d90610acf565b801561049a5780601f1061046f5761010080835404028352916020019161049a565b820191906000526020600020905b81548152906001019060200180831161047d57829003601f168201915b505050505081565b346003548082116104c55760405162461bcd60e51b815260040161034290610935565b60007f000000000000000000000000000000000000000000000000000000003b9aca006104f28385610998565b6104fc91906109b1565b116105195760405162461bcd60e51b8152600401610342906109d3565b6003546006600082825461052d9190610a61565b909155505060035461053f9034610998565b336001600160a01b03167f0f4961cab7530804898499aa89f5ec81d1a73102e2e4a1f30f88e5ae3513ba2a868660405161057a929190610b09565b60405180910390a350505050565b6005546001600160a01b031633146105f45760405162461bcd60e51b815260206004820152602960248201527f41737472696142726964676561626c6545524332303a206f6e6c7920666565206044820152681c9958da5c1a595b9d60ba1b6064820152608401610342565b6005546006546040516000926001600160a01b031691908381818185875af1925050503d8060008114610643576040519150601f19603f3d011682016040523d82523d6000602084013e610648565b606091505b50509050806106ac5760405162461bcd60e51b815260206004820152602a60248201527f41737472696142726964676561626c6545524332303a20666565207472616e7360448201526919995c8819985a5b195960b21b6064820152608401610342565b506000600655565b6001805461042190610acf565b6106c9610729565b600580546001600160a01b0319166001600160a01b0392909216919091179055565b6106f3610729565b6001600160a01b03811661071d57604051631e4fbdf760e01b815260006004820152602401610342565b61072681610756565b50565b6000546001600160a01b031633146103035760405163118cdaa760e01b8152336004820152602401610342565b600080546001600160a01b038381166001600160a01b0319831681178455604051919092169283917f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e09190a35050565b6000602082840312156107b857600080fd5b5035919050565b60008083601f8401126107d157600080fd5b50813567ffffffffffffffff8111156107e957600080fd5b60208301915083602082850101111561080157600080fd5b9250929050565b6000806000806040858703121561081e57600080fd5b843567ffffffffffffffff8082111561083657600080fd5b610842888389016107bf565b9096509450602087013591508082111561085b57600080fd5b50610868878288016107bf565b95989497509550505050565b60006020808352835180602085015260005b818110156108a257858101830151858201604001528201610886565b506000604082860101526040601f19601f8301168501019250505092915050565b600080602083850312156108d657600080fd5b823567ffffffffffffffff8111156108ed57600080fd5b6108f9858286016107bf565b90969095509350505050565b60006020828403121561091757600080fd5b81356001600160a01b038116811461092e57600080fd5b9392505050565b6020808252602d908201527f417374726961576974686472617765723a20696e73756666696369656e74207760408201526c69746864726177616c2066656560981b606082015260800190565b634e487b7160e01b600052601160045260246000fd5b818103818111156109ab576109ab610982565b92915050565b6000826109ce57634e487b7160e01b600052601260045260246000fd5b500490565b60208082526062908201527f417374726961576974686472617765723a20696e73756666696369656e74207660408201527f616c75652c206d7573742062652067726561746572207468616e203130202a2a60608201527f20283138202d20424153455f434841494e5f41535345545f505245434953494f6080820152614e2960f01b60a082015260c00190565b808201808211156109ab576109ab610982565b81835281816020850137506000828201602090810191909152601f909101601f19169091010190565b604081526000610ab1604083018688610a74565b8281036020840152610ac4818587610a74565b979650505050505050565b600181811c90821680610ae357607f821691505b602082108103610b0357634e487b7160e01b600052602260045260246000fd5b50919050565b602081526000610b1d602083018486610a74565b94935050505056fea2646970667358221220842bd8104ffc1c611919341f64a8277f2fc808138b97720a6dc1382e5670099064736f6c63430008190033" + + + config: + # The level at which core astria components will log out + # Options are: error, warn, info, and debug + logLevel: "debug" + + conductor: + # Determines what will drive block execution, options are: + # - "SoftOnly" -> blocks are only pulled from the sequencer + # - "FirmOnly" -> blocks are only pulled from DA + # - "SoftAndFirm" -> blocks are pulled from both the sequencer and DA + executionCommitLevel: 'SoftAndFirm' + # The expected fastest block time possible from sequencer, determines polling + # rate. + sequencerBlockTimeMs: 2000 + # The maximum number of requests to make to the sequencer per second + sequencerRequestsPerSecond: 500 + + celestia: + rpc: "http://celestia-service.astria-dev-cluster.svc.cluster.local:26658" + token: "" + + resources: + conductor: + requests: + cpu: 0.01 + memory: 1Mi + limits: + cpu: 0.1 + memory: 20Mi + geth: + requests: + cpu: 0.25 + memory: 256Mi + limits: + cpu: 2 + memory: 1Gi + + storage: + enabled: false + + ingress: + enabled: true + services: + rpc: + enabled: true + ws: + enabled: true + +celestia-node: + enabled: false + +composer: + enabled: true + config: + privateKey: + devContent: "2bd806c97f0e00af1a1fc3328fa763a9269723c8db8fac4f93af71db186d6e90" + +evm-bridge-withdrawer: + enabled: true + config: + minExpectedFeeAssetBalance: "0" + sequencerBridgeAddress: "astria13ahqz4pjqfmynk9ylrqv4fwe4957x2p0h5782u" + feeAssetDenom: "nria" + rollupAssetDenom: "nria" + evmContractAddress: "0xA58639fB5458e65E4fA917FF951C390292C24A15" + sequencerPrivateKey: + devContent: "dfa7108e38ab71f89f356c72afc38600d5758f11a8c337164713e4471411d2e0" + +evm-faucet: + enabled: true + ingress: + enabled: true + config: + privateKey: + devContent: "8b3a7999072c9c9314c084044fe705db11714c6c4ed7cddb64da18ea270dd203" + +postgresql: + enabled: true + nameOverride: blockscout-postegres + primary: + persistence: + enabled: false + resourcesPreset: "medium" + auth: + enablePostgresUser: true + postgresPassword: bigsecretpassword + username: blockscout + password: blockscout + database: blockscout + audit: + logHostname: true + logConnections: true + logDisconnections: true +blockscout-stack: + enabled: true + config: + network: + id: 1337 + name: Astria + shortname: Astria + currency: + name: RIA + symbol: RIA + decimals: 18 + testnet: true + prometheus: + enabled: false + blockscout: + extraEnv: + - name: ECTO_USE_SSL + value: "false" + - name: DATABASE_URL + value: "postgres://postgres:bigsecretpassword@astria-chain-chart-blockscout-postegres.astria-dev-cluster.svc.cluster.local:5432/blockscout" + - name: ETHEREUM_JSONRPC_VARIANT + value: "geth" + - name: ETHEREUM_JSONRPC_HTTP_URL + value: "http://astria-evm-service.astria-dev-cluster.svc.cluster.local:8545/" + - name: ETHEREUM_JSONRPC_INSECURE + value: "true" + - name: ETHEREUM_JSONRPC_WS_URL + value: "ws://astria-evm-service.astria-dev-cluster.svc.cluster.local:8546/" + - name: INDEXER_DISABLE_BEACON_BLOB_FETCHER + value: "true" + - name: NETWORK + value: "Astria" + - name: SUBNETWORK + value: "Local" + - name: CONTRACT_VERIFICATION_ALLOWED_SOLIDITY_EVM_VERSIONS + value: "homestead,tangerineWhistle,spuriousDragon,byzantium,constantinople,petersburg,istanbul,berlin,london,paris,shanghai,default" + - name: CONTRACT_VERIFICATION_ALLOWED_VYPER_EVM_VERSIONS + value: "byzantium,constantinople,petersburg,istanbul,berlin,paris,shanghai,default" + - name: DISABLE_EXCHANGE_RATES + value: "true" + + ingress: + enabled: true + hostname: explorer.astria.localdev.me + paths: + - path: /api + pathType: Prefix + - path: /socket + pathType: Prefix + - path: /sitemap.xml + pathType: ImplementationSpecific + - path: /public-metrics + pathType: Prefix + - path: /auth/auth0 + pathType: Exact + - path: /auth/auth0/callback + pathType: Exact + - path: /auth/logout + pathType: Exact + + frontend: + extraEnv: + - name: NEXT_PUBLIC_NETWORK_VERIFICATION_TYPE + value: "validation" + - name: NEXT_PUBLIC_AD_BANNER_PROVIDER + value: "none" + - name: NEXT_PUBLIC_API_PROTOCOL + value: "http" + - name: NEXT_PUBLIC_API_WEBSOCKET_PROTOCOL + value: "ws" + - name: NEXT_PUBLIC_NETWORK_CURRENCY_WEI_NAME + value: "aRia" + - name: NEXT_PUBLIC_AD_TEXT_PROVIDER + value: "none" + ingress: + enabled: true + hostname: explorer.astria.localdev.me diff --git a/dev/values/rollup/ibc-bridge-test.yaml b/dev/values/rollup/ibc-bridge-test.yaml index b5f198927c..d9bfb95479 100644 --- a/dev/values/rollup/ibc-bridge-test.yaml +++ b/dev/values/rollup/ibc-bridge-test.yaml @@ -1,12 +1,46 @@ # this file contains overrides that are used for the ibc bridge tests +global: + rollupName: astria + sequencerChainId: sequencer-test-chain-0 + celestiaChainId: celestia-local-0 evm-rollup: genesis: - bridgeAddresses: - - bridgeAddress: "astria1d7zjjljc0dsmxa545xkpwxym86g8uvvwhtezcr" - startHeight: 1 - assetDenom: "transfer/channel-0/utia" - assetPrecision: 6 + # The name of the rollup chain, used to generate the Rollup ID + rollupName: "{{ .Values.global.rollupName }}" + + # The "forks" for upgrading the chain. Contains necessary information for starting + # and, if desired, restarting the chain at a given height. The necessary fields + # for the genesis fork are provided, and additional forks can be added as needed. + forks: + ## These values are used to configure the genesis block of the rollup chain + ## no defaults as they are unique to each chain + launch: + # The rollup number to start executing blocks at, lowest possible is 1 + height: 1 + # Configure the fee collector for the evm tx fees, activated at block heights. + # If not configured, all tx fees will be burned. + feeCollector: "0xaC21B97d35Bf75A7dAb16f35b111a50e78A72F30" + sequencer: + # The chain id of the sequencer chain + chainId: "sequencer-test-chain-0" + # The hrp for bech32m addresses, unlikely to be changed + addressPrefix: "astria" + # Block height to start syncing rollup from (inclusive), lowest possible is 2 + startHeight: 2 + celestia: + # The chain id of the celestia chain + chainId: "celestia-local-0" + # The first Celestia height to utilize when looking for rollup data + startHeight: 2 + # The maximum number of blocks ahead of the lowest Celestia search height + # to search for a firm commitment + searchHeightMaxLookAhead: 100 + bridgeAddresses: + - bridgeAddress: "astria1d7zjjljc0dsmxa545xkpwxym86g8uvvwhtezcr" + assetDenom: "transfer/channel-0/utia" + assetPrecision: 6 + alloc: - address: "0x4e59b44847b379578588920cA78FbF26c0B4956C" value: diff --git a/proto/executionapis/astria/optimistic_execution/v1alpha1/execute_optimistic_block_stream_response.proto b/proto/executionapis/astria/optimistic_execution/v1alpha1/execute_optimistic_block_stream_response.proto index e3291478ff..57a03c9a01 100644 --- a/proto/executionapis/astria/optimistic_execution/v1alpha1/execute_optimistic_block_stream_response.proto +++ b/proto/executionapis/astria/optimistic_execution/v1alpha1/execute_optimistic_block_stream_response.proto @@ -2,12 +2,12 @@ syntax = "proto3"; package astria.optimistic_execution.v1alpha1; -import "astria/execution/v1/execution.proto"; +import "astria/execution/v2/executed_block_metadata.proto"; message ExecuteOptimisticBlockStreamResponse { // Metadata identifying the block resulting from executing a block. Includes number, hash, // parent hash and timestamp. - astria.execution.v1.Block block = 1; + astria.execution.v2.ExecutedBlockMetadata block = 1; // The base_sequencer_block_hash is the hash from the base sequencer block this block // is based on. This is used to associate an optimistic execution result with the hash // received once a sequencer block is committed. diff --git a/specs/conductor.md b/specs/conductor.md index a2075922d9..43438850f4 100644 --- a/specs/conductor.md +++ b/specs/conductor.md @@ -8,7 +8,7 @@ against a rollup (currently geth). It does this by: 1. reading data specific to the rollup from Sequencer or from a data availability provider (currently Celestia); 2. and then executing that data against the rollup implementing the - [`astria.execution.v1alpha2` API](./execution-api.md). + [`astria.execution.v2` API](./execution-api.md). Executed rollup data that is read directly from Sequencer is referred to *soft*-committed, while rollup data read from the data availability provder @@ -30,54 +30,50 @@ not the data availability provider. It connects to a At a high level, it followed the following steps (all remote procedure calls are gRPC): -1. Call `astria.execution.v1alpha2.GetGenesisInfo` to get the rollup's genesis - information (call this `G`). -2. Call `astria.execution.v1alpha2.GetCommitmentState` to get the rollup's most - recent commitment state (call this `C`). -3. Map the current rollup's soft number/height to the next expected Sequencer's - height using `S = G.sequencer_genesis_block_height + C.soft.number`. -4. Call `astria.sequencerblock.v1alpha1.GetFilteredSequencerBlock` with - arguments `S` and `G.rollup_id` to get Sequencer block metadata and data +1. Call `astria.execution.v2.CreateExecutionSession` to initiate a new execution + session (call this `E`). +2. Map the current rollup's soft number/height to the next expected Sequencer's + height using `S = E.sequencer_start_block_height + (C.soft.number - E.rollup_start_block_number`. +3. Call `astria.sequencerblock.v1.GetFilteredSequencerBlock` with + arguments `S` and `E.rollup_id` to get Sequencer block metadata and data specific to Conductor's rollup node. -5. Call `astria.execution.v1alpha2.ExecuteBlock` with the result of step 4. -6. Call `astria.execution.v1alpha2.UpdateCommitmentState` with the result of - step 5, specifically updating the tracked commitment state +4. Call `astria.execution.v2.ExecuteBlock` with the result of step 3. +5. Call `astria.execution.v2.UpdateCommitmentState` with the result of + step 4, specifically updating the tracked commitment state `C.soft.number += 1`. -7. Go to step 3. +6. Go to step 2. ### Firm-only mode In firm-only mode, Conductor only reads rollup information from Celestia but not from Sequencer. Because Sequencer blocks are both batched and split by namespaces (see the [Sequencer-Relayer spec](./sequencer-relayer.md)), -Conductor must read, verify and match Sequencer block metadata to rollup data +Conductor must read, verify, and match Sequencer block metadata to rollup data for a given Sequencer height. At a high level, it followed the following steps (all remote procedure calls are gRPC): -1. Call `astria.execution.v1alpha2.GetGenesisInfo` to get the rollup's genesis - information (call this `G`). -2. Call `astria.execution.v1alpha2.GetCommitmentState` to get the rollup's most - recent commitment state (call this `C`). -3. Call Sequencer's CometBFT JSONRPC endpoint with arguments +1. Call `astria.execution.v2.CreateExecutionSession` to initiate a new execution + session (call this `E`). +2. Call Sequencer's CometBFT JSONRPC endpoint with arguments `{ "method": "genesis", "params": null }` to get its genesis - state (call this `Gs`). -4. Determine the rollup's [Celestia v0 namespace] from the first 10 bytes of its + state (call this `G`). +3. Determine the rollup's [Celestia v0 namespace] from the first 10 bytes of its ID, `G.rollup_id[0..10]` (call this Celestia namespace `Nr`) -5. Determine the Sequencer's [Celestia v0 namespace] from the first 10 bytes of +4. Determine the Sequencer's [Celestia v0 namespace] from the first 10 bytes of the Sha256 hash of its chain ID, `Sha256(Gs.chain_id)[0..10]` (call this Celestia namespace `Ns`). -6. Map the current rollup's firm number/height to the Sequencer's height using - `F = G.sequencer_genesis_block_height + C.soft.number`. -7. Determine the permissible Celestia height window that Conductor is allowed - to read from `H_start = C.base_celestia_height` and - `H_end = H_start + G.celestia_block_variance * 6`[^1]. -8. For every height `H` in the range `[H_start, H_end]` (inclusive): +5. Map the current rollup's firm number/height to the Sequencer's height using + `F = E.sequencer_start_block_height + (C.firm.number - E.rollup_start_block_number`. +6. Determine the permissible Celestia height window that Conductor is allowed + to read from `H_start = C.lowest_celestia_search_height` and + `H_end = H_start + E.celestia_search_height_max_look_ahead * 6`[^1]. +7. For every height `H` in the range `[H_start, H_end]` (inclusive): 1. Call Celestia-Node JSONRPC with arguments to get Sequencer block metadata `{"method": "blob.GetAll", "params": [, []]}`. 2. Decompress the result of 1. as brotli, decode as protobuf - `astria.sequencerblock.v1alpha1.SubmittedMetadataList`. + `astria.sequencerblock.v1.SubmittedMetadataList`. 3. For each metadata element found in the previous step: 1. Call the Sequencer CometBFT JSONRPC with the following arguments to get the commitment at the metadata sequencer height `M` @@ -86,23 +82,24 @@ are gRPC): get the set of validators at the metadata sequencer height `M-1` (the validators for height `M` are found at height `M-1`): `{"method": "validators", "params": { "height": }}`. - 3. validate the metadata using the commitment and validators + 3. Validate the metadata using the commitment and validator's information. 4. Call Celestia-Node JSONRPC with arguments to get Rollup data `{"method": "blob.GetAll", "params": [, []]}`. - 5. Decompress the result of 6. as brotli, decode as protobuf - `astria.sequencerblock.v1alpha1.SubmittedRollupDataList`. - 6. Match pairs `P = (metadata, rollup data)` found in the previous steps - using `rollup.block_hash` and `metadata.block_hash`. -9. Get that pair `P` with metadata sequencer height matching the next expected - firm Sequencer height `M == F` (as determined in step 6). If it exists, go to - step 10. If no such pair exists, exit. -10. Call `astria.execution.v1alpha2.ExecuteBlock` with the result of step 9. -11. Call `astria.execution.v1alpha2.UpdateCommitmentState` with the result of - step 10, specifically updating the tracked commitment state - `C.firm.number == C.soft.number += 1`[^2] and `C.base_celestia_height = H`, + 5. Decompress the result of 4. as brotli, decode as protobuf + `astria.sequencerblock.v1.SubmittedRollupDataList`. + 6. Match pairs `P = (metadata, rollup_data)` found in the previous steps + using `rollup_data.block_hash` and `metadata.block_hash`. +8. Get that pair `P` with metadata sequencer height matching the next expected + firm Sequencer height `M == F` (as determined in step 5). If it exists, go to + step 9. If no such pair exists, exit. +9. Call `astria.execution.v2.ExecuteBlock` with the result of step 9. +10. Call `astria.execution.v2.UpdateCommitmentState` with the result of + step 9, specifically updating the tracked commitment state + `C.firm.number == C.soft.number += 1`[^2] and `C.lowest_celestia_search_height + = H`, with `H` the source Celestia height of the just executed pair `P`. -12. Go to step 6. +11. Go to step 5. [Celestia v0 namespace]: https://celestiaorg.github.io/celestia-app/specs/namespace.html#version-0 [^1]: It is assumed that on average 6 Sequencer heights will fit into 1 @@ -119,10 +116,15 @@ exception of the execution and update-commitment steps: If the soft commitment is ahead of firm, `CommitmentState.soft.number > CommitmentState.firm.number`, then step -`firm-only.10` is skipped (i.e. the data is not executed against the rollup), -but only step `firm-only.11` is ran *without updating the soft number (i.e. +`firm-only.9` is skipped (i.e. the data is not executed against the rollup), +but only step `firm-only.10` is ran *without updating the soft number (i.e. only `CommitmentState.firm.number += 1` is advanced). Soft being ahead of firm is the expected operation. In certain rare situations -the numbers can match exactly, and step `firm-only.10` and `firm-only.11` are +the numbers can match exactly, and step `firm-only.9` and `firm-only.10` are executed as written. + +## Startup, Restarts, Execution, and Commitments + +See [`astria.execution.v2` API documentation](./execution-api.md) for more +information on Conductor startup, restart, execution, and commitment logic.