diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml index 232ae7c07c..311c118620 100644 --- a/.github/workflows/docker-build.yml +++ b/.github/workflows/docker-build.yml @@ -165,6 +165,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] @@ -300,3 +331,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 301620431e..de83ed954a 100644 --- a/charts/deploy.just +++ b/charts/deploy.just @@ -191,6 +191,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 fd614d54d6..f449c028d3 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.0 +version: 1.1.1 # 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.0.2" maintainers: - name: wafflesvonmaple diff --git a/charts/evm-rollup/files/genesis/geth-genesis.json b/charts/evm-rollup/files/genesis/geth-genesis.json index 68027198d0..c77b1c3259 100644 --- a/charts/evm-rollup/files/genesis/geth-genesis.json +++ b/charts/evm-rollup/files/genesis/geth-genesis.json @@ -15,10 +15,10 @@ {{- if .Values.genesis.cancunTime }} "cancunTime": {{ toString .Values.genesis.cancunTime | replace "\"" "" }}, {{- end }} - {{- if .Values.genesis.cancunTime }} + {{- if .Values.genesis.pragueTime }} "pragueTime": {{ toString .Values.genesis.pragueTime | replace "\"" "" }}, {{- end }} - {{- if .Values.genesis.cancunTime }} + {{- if .Values.genesis.verkleTime }} "verkleTime": {{ toString .Values.genesis.verkleTime | replace "\"" "" }}, {{- end }} "terminalTotalDifficulty": 0, @@ -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.heightVariance }} + {{- $celestiaFields = append $celestiaFields (printf "\"heightVariance\": %s" (toString .celestia.heightVariance | 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 a2b3b96e0d..bb692110e2 100644 --- a/charts/evm-rollup/values.yaml +++ b/charts/evm-rollup/values.yaml @@ -11,12 +11,12 @@ images: repo: ghcr.io/astriaorg/astria-geth pullPolicy: IfNotPresent tag: 1.0.0 - devTag: latest + devTag: pr-59 overrideTag: "" conductor: repo: ghcr.io/astriaorg/conductor pullPolicy: IfNotPresent - tag: 1.0.0 + tag: 1.0.1 devTag: latest snapshot: repo: rclone/rclone @@ -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 variance in Celestia height to allow before halting the chain + heightVariance: "" + # 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 ## 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 297cbc4735..7dfae4369b 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.0 + version: 1.1.1 - name: composer repository: file://../composer version: 1.0.0 @@ -20,5 +20,5 @@ dependencies: - name: blockscout-stack repository: https://blockscout.github.io/helm-charts version: 1.6.8 -digest: sha256:c437d6967341b9bb6e10a809ce13e81130bfb95fb111c8712088ab443adea3f1 -generated: "2025-01-28T23:46:42.687706-05:00" +digest: sha256:ba7c5032ea66f755d20b779a4dc708f62445fc060e52f0fd33d5f635dc95968b +generated: "2025-02-04T10:29:15.608433-06:00" diff --git a/charts/evm-stack/Chart.yaml b/charts/evm-stack/Chart.yaml index 17bceddce5..f949764149 100644 --- a/charts/evm-stack/Chart.yaml +++ b/charts/evm-stack/Chart.yaml @@ -15,7 +15,7 @@ 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.8 +version: 1.0.9 dependencies: - name: celestia-node @@ -23,7 +23,7 @@ dependencies: repository: "file://../celestia-node" condition: celestia-node.enabled - name: evm-rollup - version: 1.1.0 + version: 1.1.1 repository: "file://../evm-rollup" - name: composer version: 1.0.0 diff --git a/charts/evm-stack/values.yaml b/charts/evm-stack/values.yaml index e18df9b10c..6729a570dd 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-conductor/CHANGELOG.md b/crates/astria-conductor/CHANGELOG.md index 2210b7b33e..6e9294a3b2 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). +- Add stop height logic, remove chain id env vars, accomodate new genesis info +shape [#1928](https://github.com/astriaorg/astria/pull/1928). ## [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..3b76c19c5b 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}`" ); @@ -384,6 +378,10 @@ impl RunningReader { }); let reason = loop { + if self.has_reached_stop_height()? { + break Ok("stop height reached"); + } + self.schedule_new_blobs(); select!( @@ -449,6 +447,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 { diff --git a/crates/astria-conductor/src/conductor/inner.rs b/crates/astria-conductor/src/conductor/inner.rs index 7f964a7bac..ba0cca42c8 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,25 +88,25 @@ 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. @@ -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,265 @@ 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_stop_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() { + let restart_or_shutdown = if status.halt_at_rollup_stop_number() { + RestartOrShutdown::Shutdown + } else { + RestartOrShutdown::Restart + }; + Ok(restart_or_shutdown) + } 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_height(), + rollup_stop_block_number, + )) + } + } + crate::config::CommitLevel::SoftOnly => { + if status.has_soft_number_reached_stop_height() { + let restart_or_shutdown = if status.halt_at_rollup_stop_number() { + RestartOrShutdown::Shutdown + } else { + RestartOrShutdown::Restart + }; + Ok(restart_or_shutdown) + } 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_height(), + rollup_stop_block_number, + )) + } + } + } +} + #[cfg(test)] mod tests { + use astria_core::generated::astria::execution::v2::{ + Block, + CommitmentState, + GenesisInfo, + }; use astria_eyre::eyre::WrapErr as _; + use pbjson_types::Timestamp; + + use super::{ + executor::State, + RestartOrShutdown, + }; + use crate::{ + config::CommitLevel, + test_utils::{ + make_commitment_state, + make_genesis_info, + 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, + } + } #[test] - fn check_for_restart_ok() { + fn should_restart_despite_error() { let tonic_error: Result<&str, tonic::Status> = Err(tonic::Status::new(tonic::Code::PermissionDenied, "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())); + } + + #[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( + GenesisInfo { + sequencer_start_height: 10, + rollup_start_block_number: 10, + rollup_stop_block_number: 99, + halt_at_rollup_stop_number: false, + ..make_genesis_info() + }, + CommitmentState { + firm: Some(Block { + number: 99, + hash: vec![0u8; 32].into(), + parent_block_hash: vec![].into(), + timestamp: Some(Timestamp::default()), + }), + soft: Some(Block { + number: 99, + hash: vec![0u8; 32].into(), + parent_block_hash: vec![].into(), + timestamp: Some(Timestamp::default()), + }), + ..make_commitment_state() + }, + ), + &RestartOrShutdown::Restart, + ); + + assert_restart_or_shutdown( + &Config { + execution_commit_level: CommitLevel::SoftAndFirm, + ..make_config() + }, + &make_rollup_state( + GenesisInfo { + sequencer_start_height: 10, + rollup_start_block_number: 10, + rollup_stop_block_number: 99, + halt_at_rollup_stop_number: true, + ..make_genesis_info() + }, + CommitmentState { + firm: Some(Block { + number: 99, + hash: vec![0u8; 32].into(), + parent_block_hash: vec![].into(), + timestamp: Some(Timestamp::default()), + }), + soft: Some(Block { + number: 99, + hash: vec![0u8; 32].into(), + parent_block_hash: vec![].into(), + timestamp: Some(Timestamp::default()), + }), + ..make_commitment_state() + }, + ), + &RestartOrShutdown::Shutdown, + ); + } + + #[test] + fn restart_or_shutdown_on_soft_height_reached() { + assert_restart_or_shutdown( + &Config { + execution_commit_level: CommitLevel::SoftOnly, + ..make_config() + }, + &make_rollup_state( + GenesisInfo { + sequencer_start_height: 10, + rollup_start_block_number: 10, + rollup_stop_block_number: 99, + halt_at_rollup_stop_number: false, + ..make_genesis_info() + }, + CommitmentState { + firm: Some(Block { + number: 99, + hash: vec![0u8; 32].into(), + parent_block_hash: vec![].into(), + timestamp: Some(Timestamp::default()), + }), + soft: Some(Block { + number: 99, + hash: vec![0u8; 32].into(), + parent_block_hash: vec![].into(), + timestamp: Some(Timestamp::default()), + }), + ..make_commitment_state() + }, + ), + &RestartOrShutdown::Restart, + ); + + assert_restart_or_shutdown( + &Config { + execution_commit_level: CommitLevel::SoftOnly, + ..make_config() + }, + &make_rollup_state( + GenesisInfo { + sequencer_start_height: 10, + rollup_start_block_number: 10, + rollup_stop_block_number: 99, + halt_at_rollup_stop_number: true, + ..make_genesis_info() + }, + CommitmentState { + firm: Some(Block { + number: 99, + hash: vec![0u8; 32].into(), + parent_block_hash: vec![].into(), + timestamp: Some(Timestamp::default()), + }), + soft: Some(Block { + number: 99, + hash: vec![0u8; 32].into(), + parent_block_hash: vec![].into(), + timestamp: Some(Timestamp::default()), + }), + ..make_commitment_state() + }, + ), + &RestartOrShutdown::Shutdown, + ); } } 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..fd0bf6d9e2 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::{ + execution::v2::{ Block, CommitmentState, GenesisInfo, }, generated::astria::{ execution::{ - v1 as raw, - v1::execution_service_client::ExecutionServiceClient, + v2 as raw, + v2::execution_service_client::ExecutionServiceClient, }, sequencerblock::v1::RollupData, }, diff --git a/crates/astria-conductor/src/executor/mod.rs b/crates/astria-conductor/src/executor/mod.rs index ad475db85f..bcfb6dd669 100644 --- a/crates/astria-conductor/src/executor/mod.rs +++ b/crates/astria-conductor/src/executor/mod.rs @@ -4,7 +4,7 @@ use std::{ }; use astria_core::{ - execution::v1::{ + execution::v2::{ Block, CommitmentState, }, @@ -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")? @@ -136,14 +153,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 +170,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,7 +192,7 @@ 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 { @@ -201,22 +215,21 @@ impl Executor { let (genesis_info, commitment_state) = tokio::try_join!(genesis_info, commitment_state)?; 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_genesis_info_and_commitment_state( + genesis_info, + commitment_state, + self.config.execution_commit_level, + ) + .wrap_err( + "failed to construct initial state gensis and commitment info 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) } } @@ -250,14 +263,14 @@ struct Initialized { 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 +298,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 +308,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") @@ -365,16 +374,21 @@ 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_height = self.state.sequencer_start_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_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}`", + `sequencer_height - sequencer_start_height + rollup_start_block_number` \ + underflowed or was not a valid cometbft height. Sequencer height: \ + `{current_block_height}`, + sequencer start height: `{sequencer_start_height}`, + rollup start height: `{rollup_start_block_number}`" ) }; @@ -418,15 +432,20 @@ 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_height = self.state.sequencer_start_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_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}`", + `sequencer_height - sequencer_start_height + rollup_start_block_number` \ + underflowed or was not a valid cometbft height. Sequencer block height: \ + `{block_height}`, + sequencer start height: `{sequencer_start_height}`, + rollup start height: `{rollup_start_block_number}`" ) }; @@ -528,14 +547,27 @@ 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(), + 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) @@ -556,7 +588,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(()) } @@ -583,50 +615,106 @@ 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_stop_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_stop_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()))) } } diff --git a/crates/astria-conductor/src/executor/state.rs b/crates/astria-conductor/src/executor/state.rs index 1f315b078b..37fc136122 100644 --- a/crates/astria-conductor/src/executor/state.rs +++ b/crates/astria-conductor/src/executor/state.rs @@ -2,14 +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::{ + execution::v2::{ Block, CommitmentState, GenesisInfo, }, primitive::v1::RollupId, }; +use astria_eyre::eyre::{ + self, + eyre, + WrapErr as _, +}; use bytes::Bytes; use sequencer_client::tendermint::block::Height as SequencerHeight; use tokio::sync::watch::{ @@ -31,13 +38,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 +83,83 @@ 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_stop_block_number) = self.inner.borrow().rollup_stop_block_number() else { + return Ok(None); + }; + let sequencer_start_height = self.inner.borrow().sequencer_start_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_stop_block_number + .get() + .try_into() + .wrap_err("rollup stop block number overflows u32::MAX")?, + ) + .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( +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( genesis_info: &GenesisInfo, commitment_state: &CommitmentState, -) -> Result<(), InvalidState> { - let sequencer_genesis_height = genesis_info.sequencer_genesis_block_height(); +) -> Result { + let sequencer_start_height = genesis_info.sequencer_start_height(); + let rollup_start_block_number = genesis_info.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: rollup_number.into(), + }) } -fn can_map_soft_to_sequencer_height( +fn map_soft_to_sequencer_height( genesis_info: &GenesisInfo, commitment_state: &CommitmentState, -) -> Result<(), InvalidState> { - let sequencer_genesis_height = genesis_info.sequencer_genesis_block_height(); +) -> Result { + let sequencer_start_height = genesis_info.sequencer_start_height(); + let rollup_start_block_number = genesis_info.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: rollup_number.into(), + }) } impl StateSender { @@ -143,10 +191,15 @@ impl StateSender { 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)?; + if commit_level.is_with_firm() { + let _ = map_firm_to_sequencer_height(&genesis_info, &commitment_state)?; + } + if commit_level.is_with_soft() { + let _ = map_soft_to_sequencer_height(&genesis_info, &commitment_state)?; + } self.inner.send_modify(move |state| { state.set_commitment_state(commitment_state); }); @@ -204,8 +257,12 @@ forward_impls!( [soft_hash -> Bytes], [celestia_block_variance -> u64], [rollup_id -> RollupId], - [sequencer_genesis_block_height -> SequencerHeight], + [sequencer_start_height -> u64], [celestia_base_block_height -> u64], + [rollup_start_block_number -> u64], + [rollup_stop_block_number -> Option], + [has_firm_number_reached_stop_height -> bool], + [has_soft_number_reached_stop_height -> bool], ); forward_impls!( @@ -213,35 +270,58 @@ forward_impls!( [celestia_base_block_height -> u64], [celestia_block_variance -> 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)] +pub(crate) struct State { commitment_state: CommitmentState, genesis_info: GenesisInfo, } impl State { - pub(super) fn try_from_genesis_info_and_commitment_state( + pub(crate) fn try_from_genesis_info_and_commitment_state( genesis_info: GenesisInfo, commitment_state: CommitmentState, + 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)?; + if commit_level.is_with_firm() { + let _ = map_firm_to_sequencer_height(&genesis_info, &commitment_state)?; + } + if commit_level.is_with_soft() { + let _ = map_soft_to_sequencer_height(&genesis_info, &commitment_state)?; + } Ok(State { commitment_state, genesis_info, }) } + /// 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_stop_block_number) = self.rollup_stop_block_number() else { + return false; + }; + u64::from(self.commitment_state.firm().number()) >= rollup_stop_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_stop_block_number) = self.rollup_stop_block_number() else { + return false; + }; + u64::from(self.commitment_state.soft().number()) >= rollup_stop_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 genesis_info(&self) -> &GenesisInfo { + &self.genesis_info } fn firm(&self) -> &Block { @@ -252,11 +332,11 @@ impl State { self.commitment_state.soft() } - fn firm_number(&self) -> u32 { + pub(crate) fn firm_number(&self) -> u32 { self.commitment_state.firm().number() } - fn soft_number(&self) -> u32 { + pub(crate) fn soft_number(&self) -> u32 { self.commitment_state.soft().number() } @@ -276,124 +356,121 @@ impl State { self.genesis_info.celestia_block_variance() } - fn sequencer_genesis_block_height(&self) -> SequencerHeight { - self.genesis_info.sequencer_genesis_block_height() + pub(crate) fn sequencer_start_height(&self) -> u64 { + self.genesis_info.sequencer_start_height() + } + + pub(crate) fn halt_at_rollup_stop_number(&self) -> bool { + self.genesis_info.halt_at_rollup_stop_number() + } + + fn sequencer_chain_id(&self) -> String { + self.genesis_info.sequencer_chain_id().to_string() + } + + fn celestia_chain_id(&self) -> String { + self.genesis_info.celestia_chain_id().to_string() } fn rollup_id(&self) -> RollupId { self.genesis_info.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.genesis_info.rollup_start_block_number() + } + + pub(crate) fn rollup_stop_block_number(&self) -> Option { + self.genesis_info.rollup_stop_block_number() + } + + pub(crate) fn firm_block_number_as_sequencer_height(&self) -> SequencerHeight { + map_firm_to_sequencer_height(&self.genesis_info, &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_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 soft_block_number_as_sequencer_height(&self) -> SequencerHeight { + map_soft_to_sequencer_height(&self.genesis_info, &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.genesis_info, &self.commitment_state) + .map(SequencerHeight::increment) + } + + fn next_expected_soft_sequencer_height(&self) -> Result { + map_soft_to_sequencer_height(&self.genesis_info, &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 `u32` 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, + sequencer_start_height: u64, + rollup_start_block_number: u64, 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() +) -> Result { + let rollup_number = u64::from(rollup_number); + 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 u32::MAX, the maximum cometbft height") } /// 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 `u32`. 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 { sequencer_height .value() - .checked_sub(sequencer_genesis_height.value())? + .checked_sub(sequencer_start_height)? + .checked_add(rollup_start_block_number)? .try_into() .ok() } #[cfg(test)] mod tests { - use astria_core::{ - generated::astria::execution::v1 as raw, - Protobuf as _, - }; - use pbjson_types::Timestamp; - use super::*; + use crate::test_utils::{ + make_commitment_state, + make_genesis_info, + make_rollup_state, + }; - 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( + fn make_channel() -> (StateSender, StateReceiver) { + super::channel(make_rollup_state( make_genesis_info(), make_commitment_state(), - ) - .unwrap() - } - - fn make_channel() -> (StateSender, StateReceiver) { - super::channel(make_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 +479,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: u32, + rollup_start_number: u32, + rollup_number: u32, + 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.into(), + rollup_start_number.into(), + 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..3c4db78be8 100644 --- a/crates/astria-conductor/src/executor/tests.rs +++ b/crates/astria-conductor/src/executor/tests.rs @@ -1,11 +1,11 @@ use astria_core::{ self, - execution::v1::{ + execution::v2::{ Block, CommitmentState, GenesisInfo, }, - generated::astria::execution::v1 as raw, + generated::astria::execution::v2 as raw, Protobuf as _, }; use bytes::Bytes; @@ -17,11 +17,11 @@ use super::{ StateReceiver, StateSender, }, - RollupId, }; -use crate::config::CommitLevel; - -const ROLLUP_ID: RollupId = RollupId::new([42u8; 32]); +use crate::{ + config::CommitLevel, + test_utils::make_genesis_info, +}; fn make_block(number: u32) -> raw::Block { raw::Block { @@ -46,20 +46,19 @@ 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, - }) - .unwrap(); + let genesis_info = GenesisInfo::try_from_raw(make_genesis_info()).unwrap(); let commitment_state = CommitmentState::try_from_raw(raw::CommitmentState { firm: Some(make_block(firm)), soft: Some(make_block(soft)), base_celestia_height: 1, }) .unwrap(); - let state = - State::try_from_genesis_info_and_commitment_state(genesis_info, commitment_state).unwrap(); + let state = State::try_from_genesis_info_and_commitment_state( + genesis_info, + commitment_state, + crate::config::CommitLevel::SoftAndFirm, + ) + .unwrap(); super::state::channel(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/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..a91efdd0c3 --- /dev/null +++ b/crates/astria-conductor/src/test_utils.rs @@ -0,0 +1,66 @@ +use astria_core::{ + generated::astria::execution::v2::{ + CommitmentState, + GenesisInfo, + }, + primitive::v1::RollupId, + Protobuf as _, +}; + +use crate::executor::State; + +pub(crate) fn make_commitment_state() -> CommitmentState { + let firm = astria_core::generated::astria::execution::v2::Block { + number: 1, + hash: vec![42u8; 32].into(), + parent_block_hash: vec![41u8; 32].into(), + timestamp: Some(pbjson_types::Timestamp { + seconds: 123_456, + nanos: 789, + }), + }; + let soft = astria_core::generated::astria::execution::v2::Block { + number: 2, + hash: vec![43u8; 32].into(), + parent_block_hash: vec![42u8; 32].into(), + timestamp: Some(pbjson_types::Timestamp { + seconds: 123_456, + nanos: 789, + }), + }; + + CommitmentState { + soft: Some(soft), + firm: Some(firm), + base_celestia_height: 1, + } +} + +pub(crate) fn make_genesis_info() -> GenesisInfo { + let rollup_id = RollupId::new([24; 32]); + GenesisInfo { + rollup_id: Some(rollup_id.to_raw()), + sequencer_start_height: 10, + celestia_block_variance: 0, + rollup_start_block_number: 1, + rollup_stop_block_number: 90, + sequencer_chain_id: "test-sequencer-0".to_string(), + celestia_chain_id: "test-celestia-0".to_string(), + halt_at_rollup_stop_number: false, + } +} + +pub(crate) fn make_rollup_state( + genesis_info: GenesisInfo, + commitment_state: CommitmentState, +) -> State { + let genesis_info = astria_core::execution::v2::GenesisInfo::try_from_raw(genesis_info).unwrap(); + let commitment_state = + astria_core::execution::v2::CommitmentState::try_from_raw(commitment_state).unwrap(); + State::try_from_genesis_info_and_commitment_state( + genesis_info, + commitment_state, + 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..a66060144e 100644 --- a/crates/astria-conductor/tests/blackbox/firm_only.rs +++ b/crates/astria-conductor/tests/blackbox/firm_only.rs @@ -5,7 +5,7 @@ use astria_conductor::{ Conductor, Config, }; -use astria_core::generated::astria::execution::v1::{ +use astria_core::generated::astria::execution::v2::{ GetCommitmentStateRequest, GetGenesisInfoRequest, }; @@ -54,8 +54,10 @@ async fn simple() { mount_get_genesis_info!( test_conductor, - sequencer_genesis_block_height: 1, + sequencer_start_height: 3, celestia_block_variance: 10, + rollup_start_block_number: 2, + rollup_stop_block_number: 9, ); mount_get_commitment_state!( @@ -125,7 +127,7 @@ async fn simple() { .await .expect( "conductor should have executed the firm block and updated the firm commitment state \ - within 2000ms", + within 1000ms", ); } @@ -135,8 +137,10 @@ async fn submits_two_heights_in_succession() { mount_get_genesis_info!( test_conductor, - sequencer_genesis_block_height: 1, + sequencer_start_height: 3, celestia_block_variance: 10, + rollup_start_block_number: 2, + rollup_stop_block_number: 9, ); mount_get_commitment_state!( @@ -236,7 +240,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", ); } @@ -247,8 +251,10 @@ async fn skips_already_executed_heights() { mount_get_genesis_info!( test_conductor, - sequencer_genesis_block_height: 1, + sequencer_start_height: 3, celestia_block_variance: 10, + rollup_start_block_number: 2, + rollup_stop_block_number: 9, ); mount_get_commitment_state!( @@ -320,7 +326,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", ); } @@ -331,8 +337,10 @@ async fn fetch_from_later_celestia_height() { mount_get_genesis_info!( test_conductor, - sequencer_genesis_block_height: 1, + sequencer_start_height: 3, celestia_block_variance: 10, + rollup_start_block_number: 2, + rollup_stop_block_number: 9, ); mount_get_commitment_state!( @@ -447,8 +455,11 @@ async fn exits_on_celestia_chain_id_mismatch() { matcher::message_type::(), ) .respond_with(GrpcResponse::constant_response( - genesis_info!(sequencer_genesis_block_height: 1, - celestia_block_variance: 10,), + genesis_info!(sequencer_start_height: 3, + celestia_block_variance: 10, + rollup_start_block_number: 2, + rollup_stop_block_number: 9 + ), )) .expect(0..) .mount(&mock_grpc.mock_server) @@ -513,3 +524,174 @@ 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 fetching new genesis info and commitment +/// state. +/// +/// It consists of the following steps: +/// 1. Mount commitment state and genesis info with a stop height of 3 for the first height, 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 genesis info and updated commitment state with rollup start block height of 2 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_get_genesis_info!( + test_conductor, + sequencer_start_height: 3, + celestia_block_variance: 10, + rollup_start_block_number: 2, + rollup_stop_block_number: 2, + up_to_n_times: 1, // Only respond once, since updated information is needed after restart. + ); + + mount_get_commitment_state!( + test_conductor, + firm: ( + number: 1, + hash: [1; 64], + parent: [0; 64], + ), + soft: ( + number: 1, + hash: [1; 64], + parent: [0; 64], + ), + base_celestia_height: 1, + up_to_n_times: 1, + ); + + 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_executed_block!( + test_conductor, + mock_name: "execute_block_1", + number: 2, + hash: [2; 64], + parent: [1; 64], + 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; 64], + parent: [1; 64], + ), + soft: ( + number: 2, + hash: [2; 64], + parent: [1; 64], + ), + base_celestia_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 genesis info and commitment state with updated heights + mount_get_genesis_info!( + test_conductor, + sequencer_start_height: 4, + celestia_block_variance: 10, + rollup_start_block_number: 3, + rollup_stop_block_number: 9, + ); + + mount_get_commitment_state!( + test_conductor, + firm: ( + number: 2, + hash: [2; 64], + parent: [1; 64], + ), + soft: ( + number: 2, + hash: [2; 64], + parent: [1; 64], + ), + base_celestia_height: 1, + ); + + let execute_block_2 = mount_executed_block!( + test_conductor, + mock_name: "execute_block_2", + number: 3, + hash: [3; 64], + parent: [2; 64], + 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; 64], + parent: [2; 64], + ), + soft: ( + number: 3, + hash: [3; 64], + parent: [2; 64], + ), + base_celestia_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..4b9fba3aca 100644 --- a/crates/astria-conductor/tests/blackbox/helpers/macros.rs +++ b/crates/astria-conductor/tests/blackbox/helpers/macros.rs @@ -1,7 +1,7 @@ #[macro_export] macro_rules! block { (number: $number:expr,hash: $hash:expr,parent: $parent:expr $(,)?) => { - ::astria_core::generated::astria::execution::v1::Block { + ::astria_core::generated::astria::execution::v2::Block { number: $number, hash: ::bytes::Bytes::from(Vec::from($hash)), parent_block_hash: ::bytes::Bytes::from(Vec::from($parent)), @@ -59,7 +59,7 @@ macro_rules! commitment_state { 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 { + ::astria_core::generated::astria::execution::v2::CommitmentState { firm: Some($crate::block!( number: $firm_number, hash: $firm_hash, @@ -94,14 +94,37 @@ macro_rules! filtered_sequencer_block { #[macro_export] macro_rules! genesis_info { ( - sequencer_genesis_block_height: - $sequencer_height:expr,celestia_block_variance: - $variance:expr $(,)? + sequencer_start_height: + $start_height:expr,celestia_block_variance: + $variance:expr,rollup_start_block_number: + $rollup_start_block_number:expr, rollup_stop_block_number: + $rollup_stop_block_number:expr $(,)? ) => { - ::astria_core::generated::astria::execution::v1::GenesisInfo { + genesis_info!( + sequencer_start_height: $start_height, + celestia_block_variance: $variance, + rollup_start_block_number: $rollup_start_block_number, + rollup_stop_block_number: $rollup_stop_block_number, + halt_at_rollup_stop_number: false, + ) + }; + ( + sequencer_start_height: + $start_height:expr,celestia_block_variance: + $variance:expr,rollup_start_block_number: + $rollup_start_block_number:expr, + rollup_stop_block_number: $rollup_stop_block_number:expr, + halt_at_rollup_stop_number: $halt_at_rollup_stop_number:expr $(,)? + ) => { + ::astria_core::generated::astria::execution::v2::GenesisInfo { rollup_id: Some($crate::ROLLUP_ID.to_raw()), - sequencer_genesis_block_height: $sequencer_height, + sequencer_start_height: $start_height, celestia_block_variance: $variance, + rollup_start_block_number: $rollup_start_block_number, + rollup_stop_block_number: $rollup_stop_block_number, + sequencer_chain_id: $crate::SEQUENCER_CHAIN_ID.to_string(), + celestia_chain_id: $crate::helpers::CELESTIA_CHAIN_ID.to_string(), + halt_at_rollup_stop_number: $halt_at_rollup_stop_number, } }; } @@ -175,6 +198,22 @@ macro_rules! mount_get_commitment_state { soft: ( number: $soft_number:expr, hash: $soft_hash:expr, parent: $soft_parent:expr$(,)? ), base_celestia_height: $base_celestia_height:expr $(,)? + ) => { + mount_get_commitment_state!( + $test_env, + 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, + up_to_n_times: 1, + ) + }; + ( + $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, + up_to_n_times: $up_to_n_times:expr + $(,)? ) => { $test_env .mount_get_commitment_state($crate::commitment_state!( @@ -189,7 +228,7 @@ macro_rules! mount_get_commitment_state { parent: $soft_parent, ), base_celestia_height: $base_celestia_height, - )) + ), $up_to_n_times) .await }; } @@ -274,7 +313,8 @@ macro_rules! mount_executed_block { 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( @@ -287,10 +327,27 @@ macro_rules! mount_executed_block { 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_executed_block!( + $test_env, + mock_name: None, + number: $number, + hash: $hash, + parent: $parent, + expected_calls: 1, + ) + }; ( $test_env:ident, number: $number:expr, @@ -334,15 +391,62 @@ macro_rules! mount_get_filtered_sequencer_block { macro_rules! mount_get_genesis_info { ( $test_env:ident, - sequencer_genesis_block_height: $sequencer_height:expr, - celestia_block_variance: $variance:expr + sequencer_start_height: $start_height:expr, + celestia_block_variance: $variance:expr, + rollup_start_block_number: $rollup_start_block_number:expr, + rollup_stop_block_number: $rollup_stop_block_number:expr + $(,)? + ) => { + mount_get_genesis_info!( + $test_env, + sequencer_start_height: $start_height, + celestia_block_variance: $variance, + rollup_start_block_number: $rollup_start_block_number, + rollup_stop_block_number: $rollup_stop_block_number, + up_to_n_times: 1, + ) + }; + ( + $test_env:ident, + sequencer_start_height: $start_height:expr, + celestia_block_variance: $variance:expr, + rollup_start_block_number: $rollup_start_block_number:expr, + rollup_stop_block_number: $rollup_stop_block_number:expr, + up_to_n_times: $up_to_n_times:expr + $(,)? + ) => { + mount_get_genesis_info!( + $test_env, + sequencer_start_height: $start_height, + celestia_block_variance: $variance, + rollup_start_block_number: $rollup_start_block_number, + rollup_stop_block_number: $rollup_stop_block_number, + up_to_n_times: $up_to_n_times, + halt_at_rollup_stop_number: false, + expected_calls: 1, + ) + }; + ( + $test_env:ident, + sequencer_start_height: $start_height:expr, + celestia_block_variance: $variance:expr, + rollup_start_block_number: $rollup_start_block_number:expr, + rollup_stop_block_number: $rollup_stop_block_number:expr, + up_to_n_times: $up_to_n_times:expr, + halt_at_rollup_stop_number: $halt_at_rollup_stop_number:expr, + expected_calls: $expected_calls:expr $(,)? ) => { $test_env.mount_get_genesis_info( $crate::genesis_info!( - sequencer_genesis_block_height: $sequencer_height, + sequencer_start_height: $start_height, celestia_block_variance: $variance, - ) + rollup_start_block_number: $rollup_start_block_number, + rollup_stop_block_number: $rollup_stop_block_number, + halt_at_rollup_stop_number: $halt_at_rollup_stop_number, + ), + $up_to_n_times, + $expected_calls, ).await; }; } @@ -385,12 +489,12 @@ macro_rules! mount_get_block { hash: $hash, parent: $parent, ); - let identifier = ::astria_core::generated::astria::execution::v1::BlockIdentifier { + let identifier = ::astria_core::generated::astria::execution::v2::BlockIdentifier { identifier: Some( - ::astria_core::generated::astria::execution::v1::block_identifier::Identifier::BlockNumber(block.number) + ::astria_core::generated::astria::execution::v2::block_identifier::Identifier::BlockNumber(block.number) )}; $test_env.mount_get_block( - ::astria_core::generated::astria::execution::v1::GetBlockRequest { + ::astria_core::generated::astria::execution::v2::GetBlockRequest { identifier: Some(identifier), }, block, diff --git a/crates/astria-conductor/tests/blackbox/helpers/mock_grpc.rs b/crates/astria-conductor/tests/blackbox/helpers/mock_grpc.rs index 457ef04e3d..fba86349c6 100644 --- a/crates/astria-conductor/tests/blackbox/helpers/mock_grpc.rs +++ b/crates/astria-conductor/tests/blackbox/helpers/mock_grpc.rs @@ -4,7 +4,7 @@ use std::{ }; use astria_core::generated::astria::{ - execution::v1::{ + execution::v2::{ execution_service_server::{ ExecutionService, ExecutionServiceServer, diff --git a/crates/astria-conductor/tests/blackbox/helpers/mod.rs b/crates/astria-conductor/tests/blackbox/helpers/mod.rs index c14ffff018..3933ddd418 100644 --- a/crates/astria-conductor/tests/blackbox/helpers/mod.rs +++ b/crates/astria-conductor/tests/blackbox/helpers/mod.rs @@ -13,7 +13,7 @@ use astria_conductor::{ use astria_core::{ brotli::compress_bytes, generated::astria::{ - execution::v1::{ + execution::v2::{ Block, CommitmentState, GenesisInfo, @@ -179,7 +179,7 @@ impl TestConductor { pub async fn mount_get_block( &self, expected_pbjson: S, - block: astria_core::generated::astria::execution::v1::Block, + block: astria_core::generated::astria::execution::v2::Block, ) { use astria_grpc_mock::{ matcher::message_partial_pbjson, @@ -305,20 +305,30 @@ 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; + pub async fn mount_get_genesis_info( + &self, + genesis_info: GenesisInfo, + up_to_n_times: u64, + expected_calls: u64, + ) { + use astria_core::generated::astria::execution::v2::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..) + .up_to_n_times(up_to_n_times) + .expect(expected_calls) .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_get_commitment_state( + &self, + commitment_state: CommitmentState, + up_to_n_times: u64, + ) { + use astria_core::generated::astria::execution::v2::GetCommitmentStateRequest; astria_grpc_mock::Mock::for_rpc_given( "get_commitment_state", @@ -327,6 +337,7 @@ impl TestConductor { .respond_with(astria_grpc_mock::response::constant_response( commitment_state, )) + .up_to_n_times(up_to_n_times) .expect(1..) .mount(&self.mock_grpc.mock_server) .await; @@ -337,6 +348,7 @@ impl TestConductor { mock_name: Option<&str>, expected_pbjson: S, response: Block, + expected_calls: u64, ) -> astria_grpc_mock::MockGuard { use astria_grpc_mock::{ matcher::message_partial_pbjson, @@ -349,7 +361,7 @@ impl TestConductor { 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 +391,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, @@ -522,8 +534,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..b172b7de0f 100644 --- a/crates/astria-conductor/tests/blackbox/soft_and_firm.rs +++ b/crates/astria-conductor/tests/blackbox/soft_and_firm.rs @@ -40,8 +40,10 @@ async fn executes_soft_first_then_updates_firm() { mount_get_genesis_info!( test_conductor, - sequencer_genesis_block_height: 1, + sequencer_start_height: 3, celestia_block_variance: 10, + rollup_start_block_number: 2, + rollup_stop_block_number: 9, ); mount_get_commitment_state!( @@ -99,7 +101,7 @@ async fn executes_soft_first_then_updates_firm() { ); timeout( - Duration::from_millis(500), + Duration::from_millis(1000), join( execute_block.wait_until_satisfied(), update_commitment_state_soft.wait_until_satisfied(), @@ -108,7 +110,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!( @@ -174,8 +176,10 @@ async fn executes_firm_then_soft_at_next_height() { mount_get_genesis_info!( test_conductor, - sequencer_genesis_block_height: 1, + sequencer_start_height: 3, celestia_block_variance: 10, + rollup_start_block_number: 2, + rollup_stop_block_number: 9, ); mount_get_commitment_state!( @@ -333,8 +337,10 @@ async fn missing_block_is_fetched_for_updating_firm_commitment() { mount_get_genesis_info!( test_conductor, - sequencer_genesis_block_height: 1, + sequencer_start_height: 3, celestia_block_variance: 10, + rollup_start_block_number: 2, + rollup_stop_block_number: 9, ); mount_get_commitment_state!( @@ -457,13 +463,18 @@ 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, + sequencer_start_height: 3, celestia_block_variance: 10, + rollup_start_block_number: 2, + rollup_stop_block_number: 9, + up_to_n_times: 2, + halt_at_rollup_stop_number: false, + expected_calls: 2, ); mount_get_commitment_state!( @@ -479,6 +490,7 @@ async fn conductor_restarts_on_permission_denied() { parent: [0; 64], ), base_celestia_height: 1, + up_to_n_times: 2, ); mount_sequencer_genesis!(test_conductor); @@ -578,3 +590,577 @@ 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 commitment state and genesis info with a sequencer stop height of 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 genesis info with a sequencer stop height of 10 and a rollup start block height of +/// 2, along with corresponding commitment state, 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_get_genesis_info!( + test_conductor, + sequencer_start_height: 3, + celestia_block_variance: 10, + rollup_start_block_number: 2, + rollup_stop_block_number: 2, + up_to_n_times: 1, // We only respond once since this needs to be updated after restart + ); + + mount_get_commitment_state!( + test_conductor, + firm: ( + number: 1, + hash: [1; 64], + parent: [0; 64], + ), + soft: ( + number: 1, + hash: [1; 64], + parent: [0; 64], + ), + base_celestia_height: 1, + up_to_n_times: 1, // We only respond once since this needs to be updated 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_executed_block!( + test_conductor, + mock_name: "execute_block_1", + number: 2, + hash: [2; 64], + parent: [1; 64], + 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; 64], + parent: [0; 64], + ), + soft: ( + number: 2, + hash: [2; 64], + parent: [1; 64], + ), + base_celestia_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; 64], + parent: [1; 64], + ), + soft: ( + number: 2, + hash: [2; 64], + parent: [1; 64], + ), + base_celestia_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_get_genesis_info!( + test_conductor, + sequencer_start_height: 4, + celestia_block_variance: 10, + rollup_start_block_number: 3, + rollup_stop_block_number: 9, + ); + + mount_get_commitment_state!( + test_conductor, + firm: ( + number: 2, + hash: [2; 64], + parent: [1; 64], + ), + soft: ( + number: 2, + hash: [2; 64], + parent: [1; 64], + ), + base_celestia_height: 1, + ); + + let execute_block_2 = mount_executed_block!( + test_conductor, + mock_name: "execute_block_2", + number: 3, + hash: [3; 64], + parent: [2; 64], + 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; 64], + parent: [1; 64], + ), + soft: ( + number: 3, + hash: [3; 64], + parent: [2; 64], + ), + base_celestia_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; 64], + parent: [2; 64], + ), + soft: ( + number: 3, + hash: [3; 64], + parent: [2; 64], + ), + base_celestia_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 commitment state and genesis info with a sequencer stop height of 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 sequencer stop height of 10 and a rollup start block height of +/// 2, along with corresponding commitment state, 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_get_genesis_info!( + test_conductor, + sequencer_start_height: 3, + celestia_block_variance: 10, + rollup_start_block_number: 2, + rollup_stop_block_number: 2, + up_to_n_times: 1, // We only respond once since this needs to be updated after restart + ); + + mount_get_commitment_state!( + test_conductor, + firm: ( + number: 1, + hash: [1; 64], + parent: [0; 64], + ), + soft: ( + number: 1, + hash: [1; 64], + parent: [0; 64], + ), + base_celestia_height: 1, + up_to_n_times: 1, // We only respond once since this needs to be updated 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_executed_block!( + test_conductor, + mock_name: "execute_block_1", + number: 2, + hash: [2; 64], + parent: [1; 64], + 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; 64], + parent: [0; 64], + ), + soft: ( + number: 2, + hash: [2; 64], + parent: [1; 64], + ), + base_celestia_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; 64], + parent: [1; 64], + ), + soft: ( + number: 2, + hash: [2; 64], + parent: [1; 64], + ), + base_celestia_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_get_genesis_info!( + test_conductor, + sequencer_start_height: 4, + celestia_block_variance: 10, + rollup_start_block_number: 3, + rollup_stop_block_number: 9, + ); + + mount_get_commitment_state!( + test_conductor, + firm: ( + number: 2, + hash: [2; 64], + parent: [1; 64], + ), + soft: ( + number: 2, + hash: [2; 64], + parent: [1; 64], + ), + base_celestia_height: 1, + ); + + let execute_block_2 = mount_executed_block!( + test_conductor, + mock_name: "execute_block_2", + number: 3, + hash: [3; 64], + parent: [2; 64], + 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; 64], + parent: [1; 64], + ), + soft: ( + number: 3, + hash: [3; 64], + parent: [2; 64], + ), + base_celestia_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; 64], + parent: [2; 64], + ), + soft: ( + number: 3, + hash: [3; 64], + parent: [2; 64], + ), + base_celestia_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"); +} + +/// Tests if the conductor correctly stops and does not restart after reaching the sequencer stop +/// height if genesis info's `halt_at_rollup_stop_number` is `true`. +/// +/// This test consists of the following steps: +/// 1. Mount commitment state and genesis info with a sequencer stop height of 3, expecting only 1 +/// response. +/// 2. Mount Celestia network head and sequencer genesis. +/// 3. Mount ABCI info and sequencer blocks (soft blocks) for height 3. +/// 4. Mount firm blocks at height 3. +/// 5. Mount `execute_block` and `update_commitment_state` for soft and firm blocks at height 3. +/// 6. Await satisfaction of the `execute_block` and `update_commitment_state` for the firm block +/// height 3 with a timeout of 1000ms. The soft mount need not be satisfied in the case that the +/// firm block is received first. The test case of ensuring the soft commitment state is updated +/// correctly in the case of receiving a soft block first is covered in +/// `conductor_restarts_after_reaching_soft_stop_height_first`. +/// 7. Allow ample time for the conductor to potentially restart erroneously. +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +async fn stops_at_stop_height() { + let test_conductor = spawn_conductor(CommitLevel::SoftAndFirm).await; + + mount_get_genesis_info!( + test_conductor, + sequencer_start_height: 3, + celestia_block_variance: 10, + rollup_start_block_number: 2, + rollup_stop_block_number: 2, + up_to_n_times: 2, // allow for calls after an potential erroneous restart + halt_at_rollup_stop_number: true, + expected_calls: 1, + ); + + mount_get_commitment_state!( + test_conductor, + firm: ( + number: 1, + hash: [1; 64], + parent: [0; 64], + ), + soft: ( + number: 1, + hash: [1; 64], + parent: [0; 64], + ), + base_celestia_height: 1, + ); + + mount_sequencer_genesis!(test_conductor); + mount_celestia_header_network_head!( + test_conductor, + height: 1u32, + ); + mount_abci_info!( + test_conductor, + latest_sequencer_height: 3, + ); + + // Mount soft blocks for height 3 + mount_get_filtered_sequencer_block!( + test_conductor, + sequencer_height: 3, + ); + + // Mount firm blocks for height 3 + mount_celestia_blobs!( + test_conductor, + celestia_height: 1, + sequencer_heights: [3], + ); + mount_sequencer_commit!( + test_conductor, + height: 3u32, + ); + mount_sequencer_validator_set!(test_conductor, height: 2u32); + + let execute_block_1 = mount_executed_block!( + test_conductor, + mock_name: "execute_block_1", + number: 2, + hash: [2; 64], + parent: [1; 64], + ); + + let _update_commitment_state_soft_1 = mount_update_commitment_state!( + test_conductor, + mock_name: "update_commitment_state_soft_1", + firm: ( + number: 1, + hash: [1; 64], + parent: [0; 64], + ), + soft: ( + number: 2, + hash: [2; 64], + parent: [1; 64], + ), + base_celestia_height: 1, + expected_calls: 0..=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; 64], + parent: [1; 64], + ), + soft: ( + number: 2, + hash: [2; 64], + parent: [1; 64], + ), + base_celestia_height: 1, + ); + + 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"); +} diff --git a/crates/astria-conductor/tests/blackbox/soft_only.rs b/crates/astria-conductor/tests/blackbox/soft_only.rs index 8fff4e8c1c..1b51c6e4d9 100644 --- a/crates/astria-conductor/tests/blackbox/soft_only.rs +++ b/crates/astria-conductor/tests/blackbox/soft_only.rs @@ -5,7 +5,7 @@ use astria_conductor::{ Conductor, Config, }; -use astria_core::generated::astria::execution::v1::{ +use astria_core::generated::astria::execution::v2::{ GetCommitmentStateRequest, GetGenesisInfoRequest, }; @@ -41,8 +41,10 @@ async fn simple() { mount_get_genesis_info!( test_conductor, - sequencer_genesis_block_height: 1, + sequencer_start_height: 3, celestia_block_variance: 10, + rollup_start_block_number: 2, + rollup_stop_block_number: 9, ); mount_get_commitment_state!( @@ -114,8 +116,10 @@ async fn submits_two_heights_in_succession() { mount_get_genesis_info!( test_conductor, - sequencer_genesis_block_height: 1, + sequencer_start_height: 3, celestia_block_variance: 10, + rollup_start_block_number: 2, + rollup_stop_block_number: 9, ); mount_get_commitment_state!( @@ -220,8 +224,10 @@ async fn skips_already_executed_heights() { mount_get_genesis_info!( test_conductor, - sequencer_genesis_block_height: 1, + sequencer_start_height: 3, celestia_block_variance: 10, + rollup_start_block_number: 2, + rollup_stop_block_number: 9, ); mount_get_commitment_state!( @@ -293,8 +299,10 @@ async fn requests_from_later_genesis_height() { mount_get_genesis_info!( test_conductor, - sequencer_genesis_block_height: 10, + sequencer_start_height: 12, celestia_block_variance: 10, + rollup_start_block_number: 2, + rollup_stop_block_number: 10, ); mount_get_commitment_state!( @@ -401,8 +409,10 @@ async fn exits_on_sequencer_chain_id_mismatch() { matcher::message_type::(), ) .respond_with(GrpcResponse::constant_response( - genesis_info!(sequencer_genesis_block_height: 1, - celestia_block_variance: 10,), + genesis_info!(sequencer_start_height: 3, + celestia_block_variance: 10, + rollup_start_block_number: 2, + rollup_stop_block_number: 9), )) .expect(0..) .mount(&mock_grpc.mock_server) @@ -452,3 +462,170 @@ 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 commitment state and genesis info with a stop height of 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 commitment state and genesis info with a stop height of 10 (more than high +/// enough) and a rollup start block height 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_get_genesis_info!( + test_conductor, + sequencer_start_height: 3, + celestia_block_variance: 10, + rollup_start_block_number: 2, + rollup_stop_block_number: 2, + up_to_n_times: 1, // We need to mount a new genesis info after restart + ); + + mount_get_commitment_state!( + test_conductor, + firm: ( + number: 1, + hash: [1; 64], + parent: [0; 64], + ), + soft: ( + number: 1, + hash: [1; 64], + parent: [0; 64], + ), + base_celestia_height: 1, + up_to_n_times: 1, // We need to mount a new commitment state 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_executed_block!( + test_conductor, + mock_name: "execute_block_1", + number: 2, + hash: [2; 64], + parent: [1; 64], + 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; 64], + parent: [0; 64], + ), + soft: ( + number: 2, + hash: [2; 64], + parent: [1; 64], + ), + base_celestia_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_get_genesis_info!( + test_conductor, + sequencer_start_height: 4, + celestia_block_variance: 10, + rollup_start_block_number: 3, + rollup_stop_block_number: 9, + ); + + mount_get_commitment_state!( + test_conductor, + firm: ( + number: 1, + hash: [1; 64], + parent: [0; 64], + ), + soft: ( + number: 2, + hash: [2; 64], + parent: [1; 64], + ), + base_celestia_height: 1, + ); + + let execute_block_2 = mount_executed_block!( + test_conductor, + mock_name: "execute_block_2", + number: 3, + hash: [3; 64], + parent: [2; 64], + 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; 64], + parent: [0; 64], + ), + soft: ( + number: 3, + hash: [3; 64], + parent: [2; 64], + ), + base_celestia_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/CHANGELOG.md b/crates/astria-core/CHANGELOG.md index c14c392d3b..5dee89fd2e 100644 --- a/crates/astria-core/CHANGELOG.md +++ b/crates/astria-core/CHANGELOG.md @@ -15,6 +15,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Add method `TracePrefixed::leading_channel` to read the left-most channel of a trace prefixed ICS20 asset [#1768](https://github.com/astriaorg/astria/pull/1768). - Add `impl Protobuf for Address` [#1802](https://github.com/astriaorg/astria/pull/1802). +- Add v2 execution API, with notable changes to `astria_core::execution::v1::GenesisInfo` +to accomodate conductor stop height restart logic [#1928](https://github.com/astriaorg/astria/pull/1928). ### Changed diff --git a/crates/astria-core/src/execution/mod.rs b/crates/astria-core/src/execution/mod.rs index a3a6d96c3f..ae6adc7cf4 100644 --- a/crates/astria-core/src/execution/mod.rs +++ b/crates/astria-core/src/execution/mod.rs @@ -1 +1,2 @@ pub mod v1; +pub mod v2; 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..baa694b834 --- /dev/null +++ b/crates/astria-core/src/execution/v2/mod.rs @@ -0,0 +1,545 @@ +use std::num::NonZeroU64; + +use bytes::Bytes; +use pbjson_types::Timestamp; + +use crate::{ + generated::astria::execution::v2 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) + } + + fn invalid_sequencer_id(source: tendermint::Error) -> Self { + Self(GenesisInfoErrorKind::InvalidSequencerId(source)) + } + + fn invalid_celestia_id(source: celestia_tendermint::Error) -> Self { + Self(GenesisInfoErrorKind::InvalidCelestiaId(source)) + } +} + +#[derive(Debug, thiserror::Error)] +enum GenesisInfoErrorKind { + #[error("`rollup_id` field contained an invalid rollup ID")] + IncorrectRollupIdLength(IncorrectRollupIdLength), + #[error("`rollup_id` was not set")] + NoRollupId, + #[error("field `sequencer_chain_id` was invalid")] + InvalidSequencerId(#[source] tendermint::Error), + #[error("field `celestia_chain_id` was invalid")] + InvalidCelestiaId(#[source] celestia_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::GenesisInfo`]. +#[derive(Clone, Debug)] +#[cfg_attr(feature = "serde", derive(serde::Serialize))] +#[cfg_attr( + feature = "serde", + serde(into = "crate::generated::astria::execution::v2::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_start_height: tendermint::block::Height, + /// The allowed variance in the block height of celestia when looking for sequencer blocks. + celestia_block_variance: u64, + /// The rollup block number to map to the sequencer start block height. + rollup_start_block_number: u64, + /// The rollup block number to restart/halt at after executing. + rollup_stop_block_number: Option, + /// The chain ID of the sequencer network. + sequencer_chain_id: tendermint::chain::Id, + /// The chain ID of the celestia network. + celestia_chain_id: celestia_tendermint::chain::Id, + /// Whether the conductor should halt at the stop height instead of restarting. + halt_at_rollup_stop_number: bool, +} + +impl GenesisInfo { + #[must_use] + pub fn rollup_id(&self) -> RollupId { + self.rollup_id + } + + #[must_use] + pub fn sequencer_start_height(&self) -> u64 { + self.sequencer_start_height.into() + } + + #[must_use] + pub fn celestia_block_variance(&self) -> u64 { + self.celestia_block_variance + } + + #[must_use] + pub fn sequencer_chain_id(&self) -> &str { + self.sequencer_chain_id.as_str() + } + + #[must_use] + pub fn celestia_chain_id(&self) -> &str { + self.celestia_chain_id.as_str() + } + + #[must_use] + pub fn rollup_start_block_number(&self) -> u64 { + self.rollup_start_block_number + } + + #[must_use] + pub fn rollup_stop_block_number(&self) -> Option { + self.rollup_stop_block_number + } + + #[must_use] + pub fn halt_at_rollup_stop_number(&self) -> bool { + self.halt_at_rollup_stop_number + } +} + +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_start_height, + celestia_block_variance, + rollup_start_block_number, + rollup_stop_block_number, + sequencer_chain_id, + celestia_chain_id, + halt_at_rollup_stop_number, + } = 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_chain_id = sequencer_chain_id + .clone() + .try_into() + .map_err(Self::Error::invalid_sequencer_id)?; + let celestia_chain_id = celestia_chain_id + .clone() + .try_into() + .map_err(Self::Error::invalid_celestia_id)?; + + Ok(Self { + rollup_id, + sequencer_start_height: (*sequencer_start_height).into(), + celestia_block_variance: *celestia_block_variance, + rollup_start_block_number: *rollup_start_block_number, + rollup_stop_block_number: NonZeroU64::new(*rollup_stop_block_number), + sequencer_chain_id, + celestia_chain_id, + halt_at_rollup_stop_number: *halt_at_rollup_stop_number, + }) + } + + fn to_raw(&self) -> Self::Raw { + let Self { + rollup_id, + sequencer_start_height, + celestia_block_variance, + rollup_start_block_number, + rollup_stop_block_number, + sequencer_chain_id, + celestia_chain_id, + halt_at_rollup_stop_number, + } = self; + + let sequencer_start_height: u32 = (*sequencer_start_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_start_height, + celestia_block_variance: *celestia_block_variance, + rollup_start_block_number: *rollup_start_block_number, + rollup_stop_block_number: rollup_stop_block_number.map(NonZeroU64::get).unwrap_or(0), + sequencer_chain_id: sequencer_chain_id.to_string(), + celestia_chain_id: celestia_chain_id.to_string(), + halt_at_rollup_stop_number: *halt_at_rollup_stop_number, + } + } +} + +/// 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::v2::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::v2::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/generated/astria.bundle.v1alpha1.rs b/crates/astria-core/src/generated/astria.bundle.v1alpha1.rs index 44265dd03d..684ba5f9fa 100644 --- a/crates/astria-core/src/generated/astria.bundle.v1alpha1.rs +++ b/crates/astria-core/src/generated/astria.bundle.v1alpha1.rs @@ -411,7 +411,7 @@ 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, /// 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/astria.execution.v2.rs b/crates/astria-core/src/generated/astria.execution.v2.rs new file mode 100644 index 0000000000..bcc7c99b2f --- /dev/null +++ b/crates/astria-core/src/generated/astria.execution.v2.rs @@ -0,0 +1,954 @@ +/// GenesisInfo contains the information needed to start a rollup chain. +/// +/// This information is used to determine which sequencer & celestia data to +/// use from the Astria & Celestia networks. +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct GenesisInfo { + /// The rollup_id is the unique identifier for the rollup chain. + #[prost(message, optional, tag = "1")] + pub rollup_id: ::core::option::Option, + /// The first block height of sequencer chain to use for rollup transactions. + #[prost(uint32, tag = "2")] + pub sequencer_start_height: u32, + /// The allowed variance in celestia for sequencer blocks to have been posted. + #[prost(uint64, tag = "4")] + pub celestia_block_variance: u64, + /// The rollup block number to map to the sequencer start block height. + #[prost(uint64, tag = "5")] + pub rollup_start_block_number: u64, + /// The rollup block number to re-fetch the genesis info and continue executing with new data. + #[prost(uint64, tag = "6")] + pub rollup_stop_block_number: u64, + /// 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). + #[prost(string, tag = "7")] + pub sequencer_chain_id: ::prost::alloc::string::String, + /// 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). + #[prost(string, tag = "8")] + pub celestia_chain_id: ::prost::alloc::string::String, + /// Requests Conductor to halt at the stop number instead of re-fetching the genesis and continuing execution. + #[prost(bool, tag = "9")] + pub halt_at_rollup_stop_number: bool, +} +impl ::prost::Name for GenesisInfo { + const NAME: &'static str = "GenesisInfo"; + const PACKAGE: &'static str = "astria.execution.v2"; + fn full_name() -> ::prost::alloc::string::String { + ::prost::alloc::format!("astria.execution.v2.{}", Self::NAME) + } +} +/// The set of information which deterministic driver of block production +/// must know about a given rollup Block +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct Block { + /// The block number + #[prost(uint32, tag = "1")] + pub number: u32, + /// The hash of the block + #[prost(bytes = "bytes", tag = "2")] + pub hash: ::prost::bytes::Bytes, + /// The hash from the parent block + #[prost(bytes = "bytes", tag = "3")] + pub parent_block_hash: ::prost::bytes::Bytes, + /// Timestamp on the block, standardized to google protobuf standard. + #[prost(message, optional, tag = "4")] + pub timestamp: ::core::option::Option<::pbjson_types::Timestamp>, +} +impl ::prost::Name for Block { + const NAME: &'static str = "Block"; + const PACKAGE: &'static str = "astria.execution.v2"; + fn full_name() -> ::prost::alloc::string::String { + ::prost::alloc::format!("astria.execution.v2.{}", Self::NAME) + } +} +/// Fields which are indexed for finding blocks on a blockchain. +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct BlockIdentifier { + #[prost(oneof = "block_identifier::Identifier", tags = "1, 2")] + pub identifier: ::core::option::Option, +} +/// Nested message and enum types in `BlockIdentifier`. +pub mod block_identifier { + #[allow(clippy::derive_partial_eq_without_eq)] + #[derive(Clone, PartialEq, ::prost::Oneof)] + pub enum Identifier { + #[prost(uint32, tag = "1")] + BlockNumber(u32), + #[prost(bytes, tag = "2")] + BlockHash(::prost::bytes::Bytes), + } +} +impl ::prost::Name for BlockIdentifier { + const NAME: &'static str = "BlockIdentifier"; + const PACKAGE: &'static str = "astria.execution.v2"; + fn full_name() -> ::prost::alloc::string::String { + ::prost::alloc::format!("astria.execution.v2.{}", Self::NAME) + } +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct GetGenesisInfoRequest {} +impl ::prost::Name for GetGenesisInfoRequest { + const NAME: &'static str = "GetGenesisInfoRequest"; + const PACKAGE: &'static str = "astria.execution.v2"; + fn full_name() -> ::prost::alloc::string::String { + ::prost::alloc::format!("astria.execution.v2.{}", Self::NAME) + } +} +/// Used in GetBlock to find a single block. +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct GetBlockRequest { + #[prost(message, optional, tag = "1")] + pub identifier: ::core::option::Option, +} +impl ::prost::Name for GetBlockRequest { + const NAME: &'static str = "GetBlockRequest"; + const PACKAGE: &'static str = "astria.execution.v2"; + fn full_name() -> ::prost::alloc::string::String { + ::prost::alloc::format!("astria.execution.v2.{}", Self::NAME) + } +} +/// Used in BatchGetBlocks, will find all or none based on the list of +/// identifiers. +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct BatchGetBlocksRequest { + #[prost(message, repeated, tag = "1")] + pub identifiers: ::prost::alloc::vec::Vec, +} +impl ::prost::Name for BatchGetBlocksRequest { + const NAME: &'static str = "BatchGetBlocksRequest"; + const PACKAGE: &'static str = "astria.execution.v2"; + fn full_name() -> ::prost::alloc::string::String { + ::prost::alloc::format!("astria.execution.v2.{}", Self::NAME) + } +} +/// The list of blocks in response to BatchGetBlocks. +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct BatchGetBlocksResponse { + #[prost(message, repeated, tag = "1")] + pub blocks: ::prost::alloc::vec::Vec, +} +impl ::prost::Name for BatchGetBlocksResponse { + const NAME: &'static str = "BatchGetBlocksResponse"; + const PACKAGE: &'static str = "astria.execution.v2"; + fn full_name() -> ::prost::alloc::string::String { + ::prost::alloc::format!("astria.execution.v2.{}", Self::NAME) + } +} +/// ExecuteBlockRequest contains all the information needed to create a new rollup +/// block. +/// +/// This information comes from previous rollup blocks, as well as from sequencer +/// blocks. +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct ExecuteBlockRequest { + /// The hash of previous block, which new block will be created on top of. + #[prost(bytes = "bytes", tag = "1")] + pub prev_block_hash: ::prost::bytes::Bytes, + /// List of transactions to include in the new block. + #[prost(message, repeated, tag = "2")] + pub transactions: ::prost::alloc::vec::Vec< + super::super::sequencerblock::v1::RollupData, + >, + /// Timestamp to be used for new block. + #[prost(message, optional, tag = "3")] + pub timestamp: ::core::option::Option<::pbjson_types::Timestamp>, +} +impl ::prost::Name for ExecuteBlockRequest { + const NAME: &'static str = "ExecuteBlockRequest"; + const PACKAGE: &'static str = "astria.execution.v2"; + fn full_name() -> ::prost::alloc::string::String { + ::prost::alloc::format!("astria.execution.v2.{}", Self::NAME) + } +} +/// 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. +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct CommitmentState { + /// Soft commitment is the rollup block matching latest sequencer block. + #[prost(message, optional, tag = "1")] + pub soft: ::core::option::Option, + /// Firm commitment is achieved when data has been seen in DA. + #[prost(message, optional, tag = "2")] + pub firm: ::core::option::Option, + /// The lowest block number of celestia chain to be searched for rollup blocks given current state + #[prost(uint64, tag = "3")] + pub base_celestia_height: u64, +} +impl ::prost::Name for CommitmentState { + const NAME: &'static str = "CommitmentState"; + const PACKAGE: &'static str = "astria.execution.v2"; + fn full_name() -> ::prost::alloc::string::String { + ::prost::alloc::format!("astria.execution.v2.{}", Self::NAME) + } +} +/// There is only one CommitmentState object, so the request is empty. +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct GetCommitmentStateRequest {} +impl ::prost::Name for GetCommitmentStateRequest { + const NAME: &'static str = "GetCommitmentStateRequest"; + const PACKAGE: &'static str = "astria.execution.v2"; + fn full_name() -> ::prost::alloc::string::String { + ::prost::alloc::format!("astria.execution.v2.{}", Self::NAME) + } +} +/// The CommitmentState to set, must include complete state. +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct UpdateCommitmentStateRequest { + #[prost(message, optional, tag = "1")] + pub commitment_state: ::core::option::Option, +} +impl ::prost::Name for UpdateCommitmentStateRequest { + const NAME: &'static str = "UpdateCommitmentStateRequest"; + const PACKAGE: &'static str = "astria.execution.v2"; + fn full_name() -> ::prost::alloc::string::String { + ::prost::alloc::format!("astria.execution.v2.{}", Self::NAME) + } +} +/// Generated client implementations. +#[cfg(feature = "client")] +pub mod execution_service_client { + #![allow(unused_variables, dead_code, missing_docs, clippy::let_unit_value)] + use tonic::codegen::*; + use tonic::codegen::http::Uri; + /// ExecutionService is used to drive deterministic production of blocks. + /// + /// The service can be implemented by any blockchain which wants to utilize the + /// Astria Shared Sequencer, and will have block production driven via the Astria + /// "Conductor". + #[derive(Debug, Clone)] + pub struct ExecutionServiceClient { + inner: tonic::client::Grpc, + } + impl ExecutionServiceClient { + /// Attempt to create a new client by connecting to a given endpoint. + pub async fn connect(dst: D) -> Result + where + D: TryInto, + D::Error: Into, + { + let conn = tonic::transport::Endpoint::new(dst)?.connect().await?; + Ok(Self::new(conn)) + } + } + impl ExecutionServiceClient + where + T: tonic::client::GrpcService, + T::Error: Into, + T::ResponseBody: Body + Send + 'static, + ::Error: Into + Send, + { + pub fn new(inner: T) -> Self { + let inner = tonic::client::Grpc::new(inner); + Self { inner } + } + pub fn with_origin(inner: T, origin: Uri) -> Self { + let inner = tonic::client::Grpc::with_origin(inner, origin); + Self { inner } + } + pub fn with_interceptor( + inner: T, + interceptor: F, + ) -> ExecutionServiceClient> + where + F: tonic::service::Interceptor, + T::ResponseBody: Default, + T: tonic::codegen::Service< + http::Request, + Response = http::Response< + >::ResponseBody, + >, + >, + , + >>::Error: Into + Send + Sync, + { + ExecutionServiceClient::new(InterceptedService::new(inner, interceptor)) + } + /// Compress requests with the given encoding. + /// + /// This requires the server to support it otherwise it might respond with an + /// error. + #[must_use] + pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self { + self.inner = self.inner.send_compressed(encoding); + self + } + /// Enable decompressing responses. + #[must_use] + pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self { + self.inner = self.inner.accept_compressed(encoding); + self + } + /// Limits the maximum size of a decoded message. + /// + /// Default: `4MB` + #[must_use] + pub fn max_decoding_message_size(mut self, limit: usize) -> Self { + self.inner = self.inner.max_decoding_message_size(limit); + self + } + /// Limits the maximum size of an encoded message. + /// + /// Default: `usize::MAX` + #[must_use] + pub fn max_encoding_message_size(mut self, limit: usize) -> Self { + self.inner = self.inner.max_encoding_message_size(limit); + self + } + /// GetGenesisInfo returns the necessary genesis information for rollup chain. + pub async fn get_genesis_info( + &mut self, + request: impl tonic::IntoRequest, + ) -> std::result::Result, tonic::Status> { + self.inner + .ready() + .await + .map_err(|e| { + tonic::Status::new( + tonic::Code::Unknown, + format!("Service was not ready: {}", e.into()), + ) + })?; + let codec = tonic::codec::ProstCodec::default(); + let path = http::uri::PathAndQuery::from_static( + "/astria.execution.v2.ExecutionService/GetGenesisInfo", + ); + let mut req = request.into_request(); + req.extensions_mut() + .insert( + GrpcMethod::new( + "astria.execution.v2.ExecutionService", + "GetGenesisInfo", + ), + ); + self.inner.unary(req, path, codec).await + } + /// GetBlock will return a block given an identifier. + pub async fn get_block( + &mut self, + request: impl tonic::IntoRequest, + ) -> std::result::Result, tonic::Status> { + self.inner + .ready() + .await + .map_err(|e| { + tonic::Status::new( + tonic::Code::Unknown, + format!("Service was not ready: {}", e.into()), + ) + })?; + let codec = tonic::codec::ProstCodec::default(); + let path = http::uri::PathAndQuery::from_static( + "/astria.execution.v2.ExecutionService/GetBlock", + ); + let mut req = request.into_request(); + req.extensions_mut() + .insert( + GrpcMethod::new("astria.execution.v2.ExecutionService", "GetBlock"), + ); + self.inner.unary(req, path, codec).await + } + /// BatchGetBlocks will return an array of Blocks given an array of block + /// identifiers. + pub async fn batch_get_blocks( + &mut self, + request: impl tonic::IntoRequest, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + > { + self.inner + .ready() + .await + .map_err(|e| { + tonic::Status::new( + tonic::Code::Unknown, + format!("Service was not ready: {}", e.into()), + ) + })?; + let codec = tonic::codec::ProstCodec::default(); + let path = http::uri::PathAndQuery::from_static( + "/astria.execution.v2.ExecutionService/BatchGetBlocks", + ); + let mut req = request.into_request(); + req.extensions_mut() + .insert( + GrpcMethod::new( + "astria.execution.v2.ExecutionService", + "BatchGetBlocks", + ), + ); + self.inner.unary(req, path, codec).await + } + /// ExecuteBlock is called to deterministically derive a rollup block from + /// filtered sequencer block information. + pub async fn execute_block( + &mut self, + request: impl tonic::IntoRequest, + ) -> std::result::Result, tonic::Status> { + self.inner + .ready() + .await + .map_err(|e| { + tonic::Status::new( + tonic::Code::Unknown, + format!("Service was not ready: {}", e.into()), + ) + })?; + let codec = tonic::codec::ProstCodec::default(); + let path = http::uri::PathAndQuery::from_static( + "/astria.execution.v2.ExecutionService/ExecuteBlock", + ); + let mut req = request.into_request(); + req.extensions_mut() + .insert( + GrpcMethod::new( + "astria.execution.v2.ExecutionService", + "ExecuteBlock", + ), + ); + self.inner.unary(req, path, codec).await + } + /// GetCommitmentState fetches the current CommitmentState of the chain. + pub async fn get_commitment_state( + &mut self, + request: impl tonic::IntoRequest, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + > { + self.inner + .ready() + .await + .map_err(|e| { + tonic::Status::new( + tonic::Code::Unknown, + format!("Service was not ready: {}", e.into()), + ) + })?; + let codec = tonic::codec::ProstCodec::default(); + let path = http::uri::PathAndQuery::from_static( + "/astria.execution.v2.ExecutionService/GetCommitmentState", + ); + let mut req = request.into_request(); + req.extensions_mut() + .insert( + GrpcMethod::new( + "astria.execution.v2.ExecutionService", + "GetCommitmentState", + ), + ); + self.inner.unary(req, path, codec).await + } + /// UpdateCommitmentState replaces the whole CommitmentState with a new + /// CommitmentState. + pub async fn update_commitment_state( + &mut self, + request: impl tonic::IntoRequest, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + > { + self.inner + .ready() + .await + .map_err(|e| { + tonic::Status::new( + tonic::Code::Unknown, + format!("Service was not ready: {}", e.into()), + ) + })?; + let codec = tonic::codec::ProstCodec::default(); + let path = http::uri::PathAndQuery::from_static( + "/astria.execution.v2.ExecutionService/UpdateCommitmentState", + ); + let mut req = request.into_request(); + req.extensions_mut() + .insert( + GrpcMethod::new( + "astria.execution.v2.ExecutionService", + "UpdateCommitmentState", + ), + ); + self.inner.unary(req, path, codec).await + } + } +} +/// Generated server implementations. +#[cfg(feature = "server")] +pub mod execution_service_server { + #![allow(unused_variables, dead_code, missing_docs, clippy::let_unit_value)] + use tonic::codegen::*; + /// Generated trait containing gRPC methods that should be implemented for use with ExecutionServiceServer. + #[async_trait] + pub trait ExecutionService: Send + Sync + 'static { + /// GetGenesisInfo returns the necessary genesis information for rollup chain. + async fn get_genesis_info( + self: std::sync::Arc, + request: tonic::Request, + ) -> std::result::Result, tonic::Status>; + /// GetBlock will return a block given an identifier. + async fn get_block( + self: std::sync::Arc, + request: tonic::Request, + ) -> std::result::Result, tonic::Status>; + /// BatchGetBlocks will return an array of Blocks given an array of block + /// identifiers. + async fn batch_get_blocks( + self: std::sync::Arc, + request: tonic::Request, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + >; + /// ExecuteBlock is called to deterministically derive a rollup block from + /// filtered sequencer block information. + async fn execute_block( + self: std::sync::Arc, + request: tonic::Request, + ) -> std::result::Result, tonic::Status>; + /// GetCommitmentState fetches the current CommitmentState of the chain. + async fn get_commitment_state( + self: std::sync::Arc, + request: tonic::Request, + ) -> std::result::Result, tonic::Status>; + /// UpdateCommitmentState replaces the whole CommitmentState with a new + /// CommitmentState. + async fn update_commitment_state( + self: std::sync::Arc, + request: tonic::Request, + ) -> std::result::Result, tonic::Status>; + } + /// ExecutionService is used to drive deterministic production of blocks. + /// + /// The service can be implemented by any blockchain which wants to utilize the + /// Astria Shared Sequencer, and will have block production driven via the Astria + /// "Conductor". + #[derive(Debug)] + pub struct ExecutionServiceServer { + inner: _Inner, + accept_compression_encodings: EnabledCompressionEncodings, + send_compression_encodings: EnabledCompressionEncodings, + max_decoding_message_size: Option, + max_encoding_message_size: Option, + } + struct _Inner(Arc); + impl ExecutionServiceServer { + pub fn new(inner: T) -> Self { + Self::from_arc(Arc::new(inner)) + } + pub fn from_arc(inner: Arc) -> Self { + let inner = _Inner(inner); + Self { + inner, + accept_compression_encodings: Default::default(), + send_compression_encodings: Default::default(), + max_decoding_message_size: None, + max_encoding_message_size: None, + } + } + pub fn with_interceptor( + inner: T, + interceptor: F, + ) -> InterceptedService + where + F: tonic::service::Interceptor, + { + InterceptedService::new(Self::new(inner), interceptor) + } + /// Enable decompressing requests with the given encoding. + #[must_use] + pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self { + self.accept_compression_encodings.enable(encoding); + self + } + /// Compress responses with the given encoding, if the client supports it. + #[must_use] + pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self { + self.send_compression_encodings.enable(encoding); + self + } + /// Limits the maximum size of a decoded message. + /// + /// Default: `4MB` + #[must_use] + pub fn max_decoding_message_size(mut self, limit: usize) -> Self { + self.max_decoding_message_size = Some(limit); + self + } + /// Limits the maximum size of an encoded message. + /// + /// Default: `usize::MAX` + #[must_use] + pub fn max_encoding_message_size(mut self, limit: usize) -> Self { + self.max_encoding_message_size = Some(limit); + self + } + } + impl tonic::codegen::Service> for ExecutionServiceServer + where + T: ExecutionService, + B: Body + Send + 'static, + B::Error: Into + Send + 'static, + { + type Response = http::Response; + type Error = std::convert::Infallible; + type Future = BoxFuture; + fn poll_ready( + &mut self, + _cx: &mut Context<'_>, + ) -> Poll> { + Poll::Ready(Ok(())) + } + fn call(&mut self, req: http::Request) -> Self::Future { + let inner = self.inner.clone(); + match req.uri().path() { + "/astria.execution.v2.ExecutionService/GetGenesisInfo" => { + #[allow(non_camel_case_types)] + struct GetGenesisInfoSvc(pub Arc); + impl< + T: ExecutionService, + > tonic::server::UnaryService + for GetGenesisInfoSvc { + type Response = super::GenesisInfo; + type Future = BoxFuture< + tonic::Response, + tonic::Status, + >; + fn call( + &mut self, + request: tonic::Request, + ) -> Self::Future { + let inner = Arc::clone(&self.0); + let fut = async move { + ::get_genesis_info(inner, request) + .await + }; + Box::pin(fut) + } + } + let accept_compression_encodings = self.accept_compression_encodings; + let send_compression_encodings = self.send_compression_encodings; + let max_decoding_message_size = self.max_decoding_message_size; + let max_encoding_message_size = self.max_encoding_message_size; + let inner = self.inner.clone(); + let fut = async move { + let inner = inner.0; + let method = GetGenesisInfoSvc(inner); + let codec = tonic::codec::ProstCodec::default(); + let mut grpc = tonic::server::Grpc::new(codec) + .apply_compression_config( + accept_compression_encodings, + send_compression_encodings, + ) + .apply_max_message_size_config( + max_decoding_message_size, + max_encoding_message_size, + ); + let res = grpc.unary(method, req).await; + Ok(res) + }; + Box::pin(fut) + } + "/astria.execution.v2.ExecutionService/GetBlock" => { + #[allow(non_camel_case_types)] + struct GetBlockSvc(pub Arc); + impl< + T: ExecutionService, + > tonic::server::UnaryService + for GetBlockSvc { + type Response = super::Block; + type Future = BoxFuture< + tonic::Response, + tonic::Status, + >; + fn call( + &mut self, + request: tonic::Request, + ) -> Self::Future { + let inner = Arc::clone(&self.0); + let fut = async move { + ::get_block(inner, request).await + }; + Box::pin(fut) + } + } + let accept_compression_encodings = self.accept_compression_encodings; + let send_compression_encodings = self.send_compression_encodings; + let max_decoding_message_size = self.max_decoding_message_size; + let max_encoding_message_size = self.max_encoding_message_size; + let inner = self.inner.clone(); + let fut = async move { + let inner = inner.0; + let method = GetBlockSvc(inner); + let codec = tonic::codec::ProstCodec::default(); + let mut grpc = tonic::server::Grpc::new(codec) + .apply_compression_config( + accept_compression_encodings, + send_compression_encodings, + ) + .apply_max_message_size_config( + max_decoding_message_size, + max_encoding_message_size, + ); + let res = grpc.unary(method, req).await; + Ok(res) + }; + Box::pin(fut) + } + "/astria.execution.v2.ExecutionService/BatchGetBlocks" => { + #[allow(non_camel_case_types)] + struct BatchGetBlocksSvc(pub Arc); + impl< + T: ExecutionService, + > tonic::server::UnaryService + for BatchGetBlocksSvc { + type Response = super::BatchGetBlocksResponse; + type Future = BoxFuture< + tonic::Response, + tonic::Status, + >; + fn call( + &mut self, + request: tonic::Request, + ) -> Self::Future { + let inner = Arc::clone(&self.0); + let fut = async move { + ::batch_get_blocks(inner, request) + .await + }; + Box::pin(fut) + } + } + let accept_compression_encodings = self.accept_compression_encodings; + let send_compression_encodings = self.send_compression_encodings; + let max_decoding_message_size = self.max_decoding_message_size; + let max_encoding_message_size = self.max_encoding_message_size; + let inner = self.inner.clone(); + let fut = async move { + let inner = inner.0; + let method = BatchGetBlocksSvc(inner); + let codec = tonic::codec::ProstCodec::default(); + let mut grpc = tonic::server::Grpc::new(codec) + .apply_compression_config( + accept_compression_encodings, + send_compression_encodings, + ) + .apply_max_message_size_config( + max_decoding_message_size, + max_encoding_message_size, + ); + let res = grpc.unary(method, req).await; + Ok(res) + }; + Box::pin(fut) + } + "/astria.execution.v2.ExecutionService/ExecuteBlock" => { + #[allow(non_camel_case_types)] + struct ExecuteBlockSvc(pub Arc); + impl< + T: ExecutionService, + > tonic::server::UnaryService + for ExecuteBlockSvc { + type Response = super::Block; + type Future = BoxFuture< + tonic::Response, + tonic::Status, + >; + fn call( + &mut self, + request: tonic::Request, + ) -> Self::Future { + let inner = Arc::clone(&self.0); + let fut = async move { + ::execute_block(inner, request).await + }; + Box::pin(fut) + } + } + let accept_compression_encodings = self.accept_compression_encodings; + let send_compression_encodings = self.send_compression_encodings; + let max_decoding_message_size = self.max_decoding_message_size; + let max_encoding_message_size = self.max_encoding_message_size; + let inner = self.inner.clone(); + let fut = async move { + let inner = inner.0; + let method = ExecuteBlockSvc(inner); + let codec = tonic::codec::ProstCodec::default(); + let mut grpc = tonic::server::Grpc::new(codec) + .apply_compression_config( + accept_compression_encodings, + send_compression_encodings, + ) + .apply_max_message_size_config( + max_decoding_message_size, + max_encoding_message_size, + ); + let res = grpc.unary(method, req).await; + Ok(res) + }; + Box::pin(fut) + } + "/astria.execution.v2.ExecutionService/GetCommitmentState" => { + #[allow(non_camel_case_types)] + struct GetCommitmentStateSvc(pub Arc); + impl< + T: ExecutionService, + > tonic::server::UnaryService + for GetCommitmentStateSvc { + type Response = super::CommitmentState; + type Future = BoxFuture< + tonic::Response, + tonic::Status, + >; + fn call( + &mut self, + request: tonic::Request, + ) -> Self::Future { + let inner = Arc::clone(&self.0); + let fut = async move { + ::get_commitment_state( + inner, + request, + ) + .await + }; + Box::pin(fut) + } + } + let accept_compression_encodings = self.accept_compression_encodings; + let send_compression_encodings = self.send_compression_encodings; + let max_decoding_message_size = self.max_decoding_message_size; + let max_encoding_message_size = self.max_encoding_message_size; + let inner = self.inner.clone(); + let fut = async move { + let inner = inner.0; + let method = GetCommitmentStateSvc(inner); + let codec = tonic::codec::ProstCodec::default(); + let mut grpc = tonic::server::Grpc::new(codec) + .apply_compression_config( + accept_compression_encodings, + send_compression_encodings, + ) + .apply_max_message_size_config( + max_decoding_message_size, + max_encoding_message_size, + ); + let res = grpc.unary(method, req).await; + Ok(res) + }; + Box::pin(fut) + } + "/astria.execution.v2.ExecutionService/UpdateCommitmentState" => { + #[allow(non_camel_case_types)] + struct UpdateCommitmentStateSvc(pub Arc); + impl< + T: ExecutionService, + > tonic::server::UnaryService + for UpdateCommitmentStateSvc { + type Response = super::CommitmentState; + type Future = BoxFuture< + tonic::Response, + tonic::Status, + >; + fn call( + &mut self, + request: tonic::Request, + ) -> Self::Future { + let inner = Arc::clone(&self.0); + let fut = async move { + ::update_commitment_state( + inner, + request, + ) + .await + }; + Box::pin(fut) + } + } + let accept_compression_encodings = self.accept_compression_encodings; + let send_compression_encodings = self.send_compression_encodings; + let max_decoding_message_size = self.max_decoding_message_size; + let max_encoding_message_size = self.max_encoding_message_size; + let inner = self.inner.clone(); + let fut = async move { + let inner = inner.0; + let method = UpdateCommitmentStateSvc(inner); + let codec = tonic::codec::ProstCodec::default(); + let mut grpc = tonic::server::Grpc::new(codec) + .apply_compression_config( + accept_compression_encodings, + send_compression_encodings, + ) + .apply_max_message_size_config( + max_decoding_message_size, + max_encoding_message_size, + ); + let res = grpc.unary(method, req).await; + Ok(res) + }; + Box::pin(fut) + } + _ => { + Box::pin(async move { + Ok( + http::Response::builder() + .status(200) + .header("grpc-status", "12") + .header("content-type", "application/grpc") + .body(empty_body()) + .unwrap(), + ) + }) + } + } + } + } + impl Clone for ExecutionServiceServer { + fn clone(&self) -> Self { + let inner = self.inner.clone(); + Self { + inner, + accept_compression_encodings: self.accept_compression_encodings, + send_compression_encodings: self.send_compression_encodings, + max_decoding_message_size: self.max_decoding_message_size, + max_encoding_message_size: self.max_encoding_message_size, + } + } + } + impl Clone for _Inner { + fn clone(&self) -> Self { + Self(Arc::clone(&self.0)) + } + } + impl std::fmt::Debug for _Inner { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{:?}", self.0) + } + } + impl tonic::server::NamedService for ExecutionServiceServer { + const NAME: &'static str = "astria.execution.v2.ExecutionService"; + } +} diff --git a/crates/astria-core/src/generated/astria.execution.v2.serde.rs b/crates/astria-core/src/generated/astria.execution.v2.serde.rs new file mode 100644 index 0000000000..5f965c1353 --- /dev/null +++ b/crates/astria-core/src/generated/astria.execution.v2.serde.rs @@ -0,0 +1,1255 @@ +impl serde::Serialize for BatchGetBlocksRequest { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let mut len = 0; + if !self.identifiers.is_empty() { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("astria.execution.v2.BatchGetBlocksRequest", len)?; + if !self.identifiers.is_empty() { + struct_ser.serialize_field("identifiers", &self.identifiers)?; + } + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for BatchGetBlocksRequest { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "identifiers", + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + Identifiers, + } + impl<'de> serde::Deserialize<'de> for GeneratedField { + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + struct GeneratedVisitor; + + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = GeneratedField; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(formatter, "expected one of: {:?}", &FIELDS) + } + + #[allow(unused_variables)] + fn visit_str(self, value: &str) -> std::result::Result + where + E: serde::de::Error, + { + match value { + "identifiers" => Ok(GeneratedField::Identifiers), + _ => Err(serde::de::Error::unknown_field(value, FIELDS)), + } + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = BatchGetBlocksRequest; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct astria.execution.v2.BatchGetBlocksRequest") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + let mut identifiers__ = None; + while let Some(k) = map_.next_key()? { + match k { + GeneratedField::Identifiers => { + if identifiers__.is_some() { + return Err(serde::de::Error::duplicate_field("identifiers")); + } + identifiers__ = Some(map_.next_value()?); + } + } + } + Ok(BatchGetBlocksRequest { + identifiers: identifiers__.unwrap_or_default(), + }) + } + } + deserializer.deserialize_struct("astria.execution.v2.BatchGetBlocksRequest", FIELDS, GeneratedVisitor) + } +} +impl serde::Serialize for BatchGetBlocksResponse { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let mut len = 0; + if !self.blocks.is_empty() { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("astria.execution.v2.BatchGetBlocksResponse", len)?; + if !self.blocks.is_empty() { + struct_ser.serialize_field("blocks", &self.blocks)?; + } + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for BatchGetBlocksResponse { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "blocks", + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + Blocks, + } + impl<'de> serde::Deserialize<'de> for GeneratedField { + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + struct GeneratedVisitor; + + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = GeneratedField; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(formatter, "expected one of: {:?}", &FIELDS) + } + + #[allow(unused_variables)] + fn visit_str(self, value: &str) -> std::result::Result + where + E: serde::de::Error, + { + match value { + "blocks" => Ok(GeneratedField::Blocks), + _ => Err(serde::de::Error::unknown_field(value, FIELDS)), + } + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = BatchGetBlocksResponse; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct astria.execution.v2.BatchGetBlocksResponse") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + let mut blocks__ = None; + while let Some(k) = map_.next_key()? { + match k { + GeneratedField::Blocks => { + if blocks__.is_some() { + return Err(serde::de::Error::duplicate_field("blocks")); + } + blocks__ = Some(map_.next_value()?); + } + } + } + Ok(BatchGetBlocksResponse { + blocks: blocks__.unwrap_or_default(), + }) + } + } + deserializer.deserialize_struct("astria.execution.v2.BatchGetBlocksResponse", FIELDS, GeneratedVisitor) + } +} +impl serde::Serialize for Block { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let mut len = 0; + if self.number != 0 { + len += 1; + } + if !self.hash.is_empty() { + len += 1; + } + if !self.parent_block_hash.is_empty() { + len += 1; + } + if self.timestamp.is_some() { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("astria.execution.v2.Block", len)?; + if self.number != 0 { + struct_ser.serialize_field("number", &self.number)?; + } + if !self.hash.is_empty() { + #[allow(clippy::needless_borrow)] + struct_ser.serialize_field("hash", pbjson::private::base64::encode(&self.hash).as_str())?; + } + if !self.parent_block_hash.is_empty() { + #[allow(clippy::needless_borrow)] + struct_ser.serialize_field("parentBlockHash", pbjson::private::base64::encode(&self.parent_block_hash).as_str())?; + } + if let Some(v) = self.timestamp.as_ref() { + struct_ser.serialize_field("timestamp", v)?; + } + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for Block { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "number", + "hash", + "parent_block_hash", + "parentBlockHash", + "timestamp", + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + Number, + Hash, + ParentBlockHash, + Timestamp, + } + impl<'de> serde::Deserialize<'de> for GeneratedField { + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + struct GeneratedVisitor; + + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = GeneratedField; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(formatter, "expected one of: {:?}", &FIELDS) + } + + #[allow(unused_variables)] + fn visit_str(self, value: &str) -> std::result::Result + where + E: serde::de::Error, + { + match value { + "number" => Ok(GeneratedField::Number), + "hash" => Ok(GeneratedField::Hash), + "parentBlockHash" | "parent_block_hash" => Ok(GeneratedField::ParentBlockHash), + "timestamp" => Ok(GeneratedField::Timestamp), + _ => Err(serde::de::Error::unknown_field(value, FIELDS)), + } + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = Block; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct astria.execution.v2.Block") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + let mut number__ = None; + let mut hash__ = None; + let mut parent_block_hash__ = None; + let mut timestamp__ = None; + while let Some(k) = map_.next_key()? { + match k { + GeneratedField::Number => { + if number__.is_some() { + return Err(serde::de::Error::duplicate_field("number")); + } + number__ = + Some(map_.next_value::<::pbjson::private::NumberDeserialize<_>>()?.0) + ; + } + GeneratedField::Hash => { + if hash__.is_some() { + return Err(serde::de::Error::duplicate_field("hash")); + } + hash__ = + Some(map_.next_value::<::pbjson::private::BytesDeserialize<_>>()?.0) + ; + } + GeneratedField::ParentBlockHash => { + if parent_block_hash__.is_some() { + return Err(serde::de::Error::duplicate_field("parentBlockHash")); + } + parent_block_hash__ = + Some(map_.next_value::<::pbjson::private::BytesDeserialize<_>>()?.0) + ; + } + GeneratedField::Timestamp => { + if timestamp__.is_some() { + return Err(serde::de::Error::duplicate_field("timestamp")); + } + timestamp__ = map_.next_value()?; + } + } + } + Ok(Block { + number: number__.unwrap_or_default(), + hash: hash__.unwrap_or_default(), + parent_block_hash: parent_block_hash__.unwrap_or_default(), + timestamp: timestamp__, + }) + } + } + deserializer.deserialize_struct("astria.execution.v2.Block", FIELDS, GeneratedVisitor) + } +} +impl serde::Serialize for BlockIdentifier { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let mut len = 0; + if self.identifier.is_some() { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("astria.execution.v2.BlockIdentifier", len)?; + if let Some(v) = self.identifier.as_ref() { + match v { + block_identifier::Identifier::BlockNumber(v) => { + struct_ser.serialize_field("blockNumber", v)?; + } + block_identifier::Identifier::BlockHash(v) => { + #[allow(clippy::needless_borrow)] + struct_ser.serialize_field("blockHash", pbjson::private::base64::encode(&v).as_str())?; + } + } + } + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for BlockIdentifier { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "block_number", + "blockNumber", + "block_hash", + "blockHash", + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + BlockNumber, + BlockHash, + } + impl<'de> serde::Deserialize<'de> for GeneratedField { + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + struct GeneratedVisitor; + + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = GeneratedField; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(formatter, "expected one of: {:?}", &FIELDS) + } + + #[allow(unused_variables)] + fn visit_str(self, value: &str) -> std::result::Result + where + E: serde::de::Error, + { + match value { + "blockNumber" | "block_number" => Ok(GeneratedField::BlockNumber), + "blockHash" | "block_hash" => Ok(GeneratedField::BlockHash), + _ => Err(serde::de::Error::unknown_field(value, FIELDS)), + } + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = BlockIdentifier; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct astria.execution.v2.BlockIdentifier") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + let mut identifier__ = None; + while let Some(k) = map_.next_key()? { + match k { + GeneratedField::BlockNumber => { + if identifier__.is_some() { + return Err(serde::de::Error::duplicate_field("blockNumber")); + } + identifier__ = map_.next_value::<::std::option::Option<::pbjson::private::NumberDeserialize<_>>>()?.map(|x| block_identifier::Identifier::BlockNumber(x.0)); + } + GeneratedField::BlockHash => { + if identifier__.is_some() { + return Err(serde::de::Error::duplicate_field("blockHash")); + } + identifier__ = map_.next_value::<::std::option::Option<::pbjson::private::BytesDeserialize<_>>>()?.map(|x| block_identifier::Identifier::BlockHash(x.0)); + } + } + } + Ok(BlockIdentifier { + identifier: identifier__, + }) + } + } + deserializer.deserialize_struct("astria.execution.v2.BlockIdentifier", FIELDS, GeneratedVisitor) + } +} +impl serde::Serialize for CommitmentState { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let mut len = 0; + if self.soft.is_some() { + len += 1; + } + if self.firm.is_some() { + len += 1; + } + if self.base_celestia_height != 0 { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("astria.execution.v2.CommitmentState", len)?; + if let Some(v) = self.soft.as_ref() { + struct_ser.serialize_field("soft", v)?; + } + if let Some(v) = self.firm.as_ref() { + struct_ser.serialize_field("firm", v)?; + } + if self.base_celestia_height != 0 { + #[allow(clippy::needless_borrow)] + struct_ser.serialize_field("baseCelestiaHeight", ToString::to_string(&self.base_celestia_height).as_str())?; + } + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for CommitmentState { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "soft", + "firm", + "base_celestia_height", + "baseCelestiaHeight", + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + Soft, + Firm, + BaseCelestiaHeight, + } + impl<'de> serde::Deserialize<'de> for GeneratedField { + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + struct GeneratedVisitor; + + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = GeneratedField; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(formatter, "expected one of: {:?}", &FIELDS) + } + + #[allow(unused_variables)] + fn visit_str(self, value: &str) -> std::result::Result + where + E: serde::de::Error, + { + match value { + "soft" => Ok(GeneratedField::Soft), + "firm" => Ok(GeneratedField::Firm), + "baseCelestiaHeight" | "base_celestia_height" => Ok(GeneratedField::BaseCelestiaHeight), + _ => Err(serde::de::Error::unknown_field(value, FIELDS)), + } + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = CommitmentState; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct astria.execution.v2.CommitmentState") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + let mut soft__ = None; + let mut firm__ = None; + let mut base_celestia_height__ = None; + while let Some(k) = map_.next_key()? { + match k { + GeneratedField::Soft => { + if soft__.is_some() { + return Err(serde::de::Error::duplicate_field("soft")); + } + soft__ = map_.next_value()?; + } + GeneratedField::Firm => { + if firm__.is_some() { + return Err(serde::de::Error::duplicate_field("firm")); + } + firm__ = map_.next_value()?; + } + GeneratedField::BaseCelestiaHeight => { + if base_celestia_height__.is_some() { + return Err(serde::de::Error::duplicate_field("baseCelestiaHeight")); + } + base_celestia_height__ = + Some(map_.next_value::<::pbjson::private::NumberDeserialize<_>>()?.0) + ; + } + } + } + Ok(CommitmentState { + soft: soft__, + firm: firm__, + base_celestia_height: base_celestia_height__.unwrap_or_default(), + }) + } + } + deserializer.deserialize_struct("astria.execution.v2.CommitmentState", FIELDS, GeneratedVisitor) + } +} +impl serde::Serialize for ExecuteBlockRequest { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let mut len = 0; + if !self.prev_block_hash.is_empty() { + len += 1; + } + if !self.transactions.is_empty() { + len += 1; + } + if self.timestamp.is_some() { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("astria.execution.v2.ExecuteBlockRequest", len)?; + if !self.prev_block_hash.is_empty() { + #[allow(clippy::needless_borrow)] + struct_ser.serialize_field("prevBlockHash", pbjson::private::base64::encode(&self.prev_block_hash).as_str())?; + } + if !self.transactions.is_empty() { + struct_ser.serialize_field("transactions", &self.transactions)?; + } + if let Some(v) = self.timestamp.as_ref() { + struct_ser.serialize_field("timestamp", v)?; + } + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for ExecuteBlockRequest { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "prev_block_hash", + "prevBlockHash", + "transactions", + "timestamp", + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + PrevBlockHash, + Transactions, + Timestamp, + } + impl<'de> serde::Deserialize<'de> for GeneratedField { + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + struct GeneratedVisitor; + + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = GeneratedField; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(formatter, "expected one of: {:?}", &FIELDS) + } + + #[allow(unused_variables)] + fn visit_str(self, value: &str) -> std::result::Result + where + E: serde::de::Error, + { + match value { + "prevBlockHash" | "prev_block_hash" => Ok(GeneratedField::PrevBlockHash), + "transactions" => Ok(GeneratedField::Transactions), + "timestamp" => Ok(GeneratedField::Timestamp), + _ => Err(serde::de::Error::unknown_field(value, FIELDS)), + } + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = ExecuteBlockRequest; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct astria.execution.v2.ExecuteBlockRequest") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + let mut prev_block_hash__ = None; + let mut transactions__ = None; + let mut timestamp__ = None; + while let Some(k) = map_.next_key()? { + match k { + GeneratedField::PrevBlockHash => { + if prev_block_hash__.is_some() { + return Err(serde::de::Error::duplicate_field("prevBlockHash")); + } + prev_block_hash__ = + Some(map_.next_value::<::pbjson::private::BytesDeserialize<_>>()?.0) + ; + } + GeneratedField::Transactions => { + if transactions__.is_some() { + return Err(serde::de::Error::duplicate_field("transactions")); + } + transactions__ = Some(map_.next_value()?); + } + GeneratedField::Timestamp => { + if timestamp__.is_some() { + return Err(serde::de::Error::duplicate_field("timestamp")); + } + timestamp__ = map_.next_value()?; + } + } + } + Ok(ExecuteBlockRequest { + prev_block_hash: prev_block_hash__.unwrap_or_default(), + transactions: transactions__.unwrap_or_default(), + timestamp: timestamp__, + }) + } + } + deserializer.deserialize_struct("astria.execution.v2.ExecuteBlockRequest", FIELDS, GeneratedVisitor) + } +} +impl serde::Serialize for GenesisInfo { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let mut len = 0; + if self.rollup_id.is_some() { + len += 1; + } + if self.sequencer_start_height != 0 { + len += 1; + } + if self.celestia_block_variance != 0 { + len += 1; + } + if self.rollup_start_block_number != 0 { + len += 1; + } + if self.rollup_stop_block_number != 0 { + len += 1; + } + if !self.sequencer_chain_id.is_empty() { + len += 1; + } + if !self.celestia_chain_id.is_empty() { + len += 1; + } + if self.halt_at_rollup_stop_number { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("astria.execution.v2.GenesisInfo", len)?; + if let Some(v) = self.rollup_id.as_ref() { + struct_ser.serialize_field("rollupId", v)?; + } + if self.sequencer_start_height != 0 { + struct_ser.serialize_field("sequencerStartHeight", &self.sequencer_start_height)?; + } + if self.celestia_block_variance != 0 { + #[allow(clippy::needless_borrow)] + struct_ser.serialize_field("celestiaBlockVariance", ToString::to_string(&self.celestia_block_variance).as_str())?; + } + if self.rollup_start_block_number != 0 { + #[allow(clippy::needless_borrow)] + struct_ser.serialize_field("rollupStartBlockNumber", ToString::to_string(&self.rollup_start_block_number).as_str())?; + } + if self.rollup_stop_block_number != 0 { + #[allow(clippy::needless_borrow)] + struct_ser.serialize_field("rollupStopBlockNumber", ToString::to_string(&self.rollup_stop_block_number).as_str())?; + } + if !self.sequencer_chain_id.is_empty() { + struct_ser.serialize_field("sequencerChainId", &self.sequencer_chain_id)?; + } + if !self.celestia_chain_id.is_empty() { + struct_ser.serialize_field("celestiaChainId", &self.celestia_chain_id)?; + } + if self.halt_at_rollup_stop_number { + struct_ser.serialize_field("haltAtRollupStopNumber", &self.halt_at_rollup_stop_number)?; + } + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for GenesisInfo { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "rollup_id", + "rollupId", + "sequencer_start_height", + "sequencerStartHeight", + "celestia_block_variance", + "celestiaBlockVariance", + "rollup_start_block_number", + "rollupStartBlockNumber", + "rollup_stop_block_number", + "rollupStopBlockNumber", + "sequencer_chain_id", + "sequencerChainId", + "celestia_chain_id", + "celestiaChainId", + "halt_at_rollup_stop_number", + "haltAtRollupStopNumber", + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + RollupId, + SequencerStartHeight, + CelestiaBlockVariance, + RollupStartBlockNumber, + RollupStopBlockNumber, + SequencerChainId, + CelestiaChainId, + HaltAtRollupStopNumber, + } + impl<'de> serde::Deserialize<'de> for GeneratedField { + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + struct GeneratedVisitor; + + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = GeneratedField; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(formatter, "expected one of: {:?}", &FIELDS) + } + + #[allow(unused_variables)] + fn visit_str(self, value: &str) -> std::result::Result + where + E: serde::de::Error, + { + match value { + "rollupId" | "rollup_id" => Ok(GeneratedField::RollupId), + "sequencerStartHeight" | "sequencer_start_height" => Ok(GeneratedField::SequencerStartHeight), + "celestiaBlockVariance" | "celestia_block_variance" => Ok(GeneratedField::CelestiaBlockVariance), + "rollupStartBlockNumber" | "rollup_start_block_number" => Ok(GeneratedField::RollupStartBlockNumber), + "rollupStopBlockNumber" | "rollup_stop_block_number" => Ok(GeneratedField::RollupStopBlockNumber), + "sequencerChainId" | "sequencer_chain_id" => Ok(GeneratedField::SequencerChainId), + "celestiaChainId" | "celestia_chain_id" => Ok(GeneratedField::CelestiaChainId), + "haltAtRollupStopNumber" | "halt_at_rollup_stop_number" => Ok(GeneratedField::HaltAtRollupStopNumber), + _ => Err(serde::de::Error::unknown_field(value, FIELDS)), + } + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = GenesisInfo; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct astria.execution.v2.GenesisInfo") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + let mut rollup_id__ = None; + let mut sequencer_start_height__ = None; + let mut celestia_block_variance__ = None; + let mut rollup_start_block_number__ = None; + let mut rollup_stop_block_number__ = None; + let mut sequencer_chain_id__ = None; + let mut celestia_chain_id__ = None; + let mut halt_at_rollup_stop_number__ = None; + while let Some(k) = map_.next_key()? { + match k { + GeneratedField::RollupId => { + if rollup_id__.is_some() { + return Err(serde::de::Error::duplicate_field("rollupId")); + } + rollup_id__ = map_.next_value()?; + } + GeneratedField::SequencerStartHeight => { + if sequencer_start_height__.is_some() { + return Err(serde::de::Error::duplicate_field("sequencerStartHeight")); + } + sequencer_start_height__ = + Some(map_.next_value::<::pbjson::private::NumberDeserialize<_>>()?.0) + ; + } + GeneratedField::CelestiaBlockVariance => { + if celestia_block_variance__.is_some() { + return Err(serde::de::Error::duplicate_field("celestiaBlockVariance")); + } + celestia_block_variance__ = + Some(map_.next_value::<::pbjson::private::NumberDeserialize<_>>()?.0) + ; + } + GeneratedField::RollupStartBlockNumber => { + if rollup_start_block_number__.is_some() { + return Err(serde::de::Error::duplicate_field("rollupStartBlockNumber")); + } + rollup_start_block_number__ = + Some(map_.next_value::<::pbjson::private::NumberDeserialize<_>>()?.0) + ; + } + GeneratedField::RollupStopBlockNumber => { + if rollup_stop_block_number__.is_some() { + return Err(serde::de::Error::duplicate_field("rollupStopBlockNumber")); + } + rollup_stop_block_number__ = + Some(map_.next_value::<::pbjson::private::NumberDeserialize<_>>()?.0) + ; + } + GeneratedField::SequencerChainId => { + if sequencer_chain_id__.is_some() { + return Err(serde::de::Error::duplicate_field("sequencerChainId")); + } + sequencer_chain_id__ = Some(map_.next_value()?); + } + GeneratedField::CelestiaChainId => { + if celestia_chain_id__.is_some() { + return Err(serde::de::Error::duplicate_field("celestiaChainId")); + } + celestia_chain_id__ = Some(map_.next_value()?); + } + GeneratedField::HaltAtRollupStopNumber => { + if halt_at_rollup_stop_number__.is_some() { + return Err(serde::de::Error::duplicate_field("haltAtRollupStopNumber")); + } + halt_at_rollup_stop_number__ = Some(map_.next_value()?); + } + } + } + Ok(GenesisInfo { + rollup_id: rollup_id__, + sequencer_start_height: sequencer_start_height__.unwrap_or_default(), + celestia_block_variance: celestia_block_variance__.unwrap_or_default(), + rollup_start_block_number: rollup_start_block_number__.unwrap_or_default(), + rollup_stop_block_number: rollup_stop_block_number__.unwrap_or_default(), + sequencer_chain_id: sequencer_chain_id__.unwrap_or_default(), + celestia_chain_id: celestia_chain_id__.unwrap_or_default(), + halt_at_rollup_stop_number: halt_at_rollup_stop_number__.unwrap_or_default(), + }) + } + } + deserializer.deserialize_struct("astria.execution.v2.GenesisInfo", FIELDS, GeneratedVisitor) + } +} +impl serde::Serialize for GetBlockRequest { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let mut len = 0; + if self.identifier.is_some() { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("astria.execution.v2.GetBlockRequest", len)?; + if let Some(v) = self.identifier.as_ref() { + struct_ser.serialize_field("identifier", v)?; + } + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for GetBlockRequest { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "identifier", + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + Identifier, + } + impl<'de> serde::Deserialize<'de> for GeneratedField { + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + struct GeneratedVisitor; + + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = GeneratedField; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(formatter, "expected one of: {:?}", &FIELDS) + } + + #[allow(unused_variables)] + fn visit_str(self, value: &str) -> std::result::Result + where + E: serde::de::Error, + { + match value { + "identifier" => Ok(GeneratedField::Identifier), + _ => Err(serde::de::Error::unknown_field(value, FIELDS)), + } + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = GetBlockRequest; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct astria.execution.v2.GetBlockRequest") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + let mut identifier__ = None; + while let Some(k) = map_.next_key()? { + match k { + GeneratedField::Identifier => { + if identifier__.is_some() { + return Err(serde::de::Error::duplicate_field("identifier")); + } + identifier__ = map_.next_value()?; + } + } + } + Ok(GetBlockRequest { + identifier: identifier__, + }) + } + } + deserializer.deserialize_struct("astria.execution.v2.GetBlockRequest", FIELDS, GeneratedVisitor) + } +} +impl serde::Serialize for GetCommitmentStateRequest { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let len = 0; + let struct_ser = serializer.serialize_struct("astria.execution.v2.GetCommitmentStateRequest", len)?; + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for GetCommitmentStateRequest { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + } + impl<'de> serde::Deserialize<'de> for GeneratedField { + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + struct GeneratedVisitor; + + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = GeneratedField; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(formatter, "expected one of: {:?}", &FIELDS) + } + + #[allow(unused_variables)] + fn visit_str(self, value: &str) -> std::result::Result + where + E: serde::de::Error, + { + Err(serde::de::Error::unknown_field(value, FIELDS)) + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = GetCommitmentStateRequest; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct astria.execution.v2.GetCommitmentStateRequest") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + while map_.next_key::()?.is_some() { + let _ = map_.next_value::()?; + } + Ok(GetCommitmentStateRequest { + }) + } + } + deserializer.deserialize_struct("astria.execution.v2.GetCommitmentStateRequest", FIELDS, GeneratedVisitor) + } +} +impl serde::Serialize for GetGenesisInfoRequest { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let len = 0; + let struct_ser = serializer.serialize_struct("astria.execution.v2.GetGenesisInfoRequest", len)?; + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for GetGenesisInfoRequest { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + } + impl<'de> serde::Deserialize<'de> for GeneratedField { + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + struct GeneratedVisitor; + + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = GeneratedField; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(formatter, "expected one of: {:?}", &FIELDS) + } + + #[allow(unused_variables)] + fn visit_str(self, value: &str) -> std::result::Result + where + E: serde::de::Error, + { + Err(serde::de::Error::unknown_field(value, FIELDS)) + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = GetGenesisInfoRequest; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct astria.execution.v2.GetGenesisInfoRequest") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + while map_.next_key::()?.is_some() { + let _ = map_.next_value::()?; + } + Ok(GetGenesisInfoRequest { + }) + } + } + deserializer.deserialize_struct("astria.execution.v2.GetGenesisInfoRequest", FIELDS, GeneratedVisitor) + } +} +impl serde::Serialize for UpdateCommitmentStateRequest { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let mut len = 0; + if self.commitment_state.is_some() { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("astria.execution.v2.UpdateCommitmentStateRequest", len)?; + if let Some(v) = self.commitment_state.as_ref() { + struct_ser.serialize_field("commitmentState", v)?; + } + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for UpdateCommitmentStateRequest { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "commitment_state", + "commitmentState", + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + CommitmentState, + } + impl<'de> serde::Deserialize<'de> for GeneratedField { + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + struct GeneratedVisitor; + + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = GeneratedField; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(formatter, "expected one of: {:?}", &FIELDS) + } + + #[allow(unused_variables)] + fn visit_str(self, value: &str) -> std::result::Result + where + E: serde::de::Error, + { + match value { + "commitmentState" | "commitment_state" => Ok(GeneratedField::CommitmentState), + _ => Err(serde::de::Error::unknown_field(value, FIELDS)), + } + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = UpdateCommitmentStateRequest; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct astria.execution.v2.UpdateCommitmentStateRequest") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + let mut commitment_state__ = None; + while let Some(k) = map_.next_key()? { + match k { + GeneratedField::CommitmentState => { + if commitment_state__.is_some() { + return Err(serde::de::Error::duplicate_field("commitmentState")); + } + commitment_state__ = map_.next_value()?; + } + } + } + Ok(UpdateCommitmentStateRequest { + commitment_state: commitment_state__, + }) + } + } + deserializer.deserialize_struct("astria.execution.v2.UpdateCommitmentStateRequest", FIELDS, GeneratedVisitor) + } +} diff --git a/crates/astria-core/src/generated/mod.rs b/crates/astria-core/src/generated/mod.rs index 1fef28c5e7..908176bdc8 100644 --- a/crates/astria-core/src/generated/mod.rs +++ b/crates/astria-core/src/generated/mod.rs @@ -64,6 +64,15 @@ pub mod astria { include!("astria.execution.v1.serde.rs"); } } + pub mod v2 { + include!("astria.execution.v2.rs"); + + #[cfg(feature = "serde")] + mod _serde_impl { + use super::*; + include!("astria.execution.v2.serde.rs"); + } + } } #[path = ""] diff --git a/dev/values/rollup/dev.yaml b/dev/values/rollup/dev.yaml index 0d9be85422..4e4d2e1ce6 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 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 variance in Celestia height to allow before halting the chain + heightVariance: 10 + # 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 ## 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..e78414938f --- /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 variance in Celestia height to allow before halting the chain + heightVariance: 10 + # 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 + 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..83f8e17577 100644 --- a/dev/values/rollup/ibc-bridge-test.yaml +++ b/dev/values/rollup/ibc-bridge-test.yaml @@ -1,12 +1,48 @@ # 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 + # Block height (on sequencer) to stop syncing rollup at, continuing to next configuration fork + stopHeight: 0 + 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 variance in Celestia height to allow before halting the chain + heightVariance: 10 + bridgeAddresses: + - bridgeAddress: "astria1d7zjjljc0dsmxa545xkpwxym86g8uvvwhtezcr" + startHeight: 1 + assetDenom: "transfer/channel-0/utia" + assetPrecision: 6 + alloc: - address: "0x4e59b44847b379578588920cA78FbF26c0B4956C" value: diff --git a/proto/executionapis/astria/bundle/v1alpha1/optimistic_execution.proto b/proto/executionapis/astria/bundle/v1alpha1/optimistic_execution.proto index 426b72798d..c8fa7af943 100644 --- a/proto/executionapis/astria/bundle/v1alpha1/optimistic_execution.proto +++ b/proto/executionapis/astria/bundle/v1alpha1/optimistic_execution.proto @@ -2,7 +2,7 @@ syntax = "proto3"; package astria.bundle.v1alpha1; -import "astria/execution/v1/execution.proto"; +import "astria/execution/v2/execution.proto"; import "astria/sequencerblock/v1/block.proto"; import "google/protobuf/timestamp.proto"; @@ -24,7 +24,7 @@ message ExecuteOptimisticBlockStreamRequest { 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.Block 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/proto/executionapis/astria/execution/v2/execution.proto b/proto/executionapis/astria/execution/v2/execution.proto new file mode 100644 index 0000000000..fddded00eb --- /dev/null +++ b/proto/executionapis/astria/execution/v2/execution.proto @@ -0,0 +1,142 @@ +syntax = 'proto3'; + +package astria.execution.v2; + +import "astria/primitive/v1/types.proto"; +import "astria/sequencerblock/v1/block.proto"; +import "google/protobuf/timestamp.proto"; + +// GenesisInfo contains the information needed to start a rollup chain. +// +// This information is used to determine which sequencer & celestia data to +// use from the Astria & Celestia networks. +message GenesisInfo { + // The rollup_id is the unique identifier for the rollup chain. + astria.primitive.v1.RollupId rollup_id = 1; + // The first block height of sequencer chain to use for rollup transactions. + uint32 sequencer_start_height = 2; + // The allowed variance in celestia for sequencer blocks to have been posted. + uint64 celestia_block_variance = 4; + // The rollup block number to map to the sequencer start block height. + uint64 rollup_start_block_number = 5; + // The rollup block number to re-fetch the genesis info and continue executing with new data. + uint64 rollup_stop_block_number = 6; + // 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). + string sequencer_chain_id = 7; + // 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). + string celestia_chain_id = 8; + // Requests Conductor to halt at the stop number instead of re-fetching the genesis and continuing execution. + bool halt_at_rollup_stop_number = 9; +} + +// The set of information which deterministic driver of block production +// must know about a given rollup Block +message Block { + // The block number + uint32 number = 1; + // The hash of the block + bytes hash = 2; + // The hash from the parent block + bytes parent_block_hash = 3; + // Timestamp on the block, standardized to google protobuf standard. + google.protobuf.Timestamp timestamp = 4; +} + +// Fields which are indexed for finding blocks on a blockchain. +message BlockIdentifier { + oneof identifier { + uint32 block_number = 1; + bytes block_hash = 2; + } +} + +message GetGenesisInfoRequest {} + +// Used in GetBlock to find a single block. +message GetBlockRequest { + BlockIdentifier identifier = 1; +} + +// Used in BatchGetBlocks, will find all or none based on the list of +// identifiers. +message BatchGetBlocksRequest { + repeated BlockIdentifier identifiers = 1; +} + +// The list of blocks in response to BatchGetBlocks. +message BatchGetBlocksResponse { + repeated Block blocks = 1; +} + +// ExecuteBlockRequest contains all the information needed to create a new rollup +// block. +// +// This information comes from previous rollup blocks, as well as from sequencer +// blocks. +message ExecuteBlockRequest { + // The hash of previous block, which new block will be created on top of. + bytes prev_block_hash = 1; + // List of transactions to include in the new block. + repeated astria.sequencerblock.v1.RollupData transactions = 2; + // Timestamp to be used for new block. + google.protobuf.Timestamp timestamp = 3; +} + +// 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. +message CommitmentState { + // Soft commitment is the rollup block matching latest sequencer block. + Block soft = 1; + // Firm commitment is achieved when data has been seen in DA. + Block firm = 2; + // The lowest block number of celestia chain to be searched for rollup blocks given current state + uint64 base_celestia_height = 3; +} + +// There is only one CommitmentState object, so the request is empty. +message GetCommitmentStateRequest {} + +// The CommitmentState to set, must include complete state. +message UpdateCommitmentStateRequest { + CommitmentState commitment_state = 1; +} + +// ExecutionService is used to drive deterministic production of blocks. +// +// The service can be implemented by any blockchain which wants to utilize the +// Astria Shared Sequencer, and will have block production driven via the Astria +// "Conductor". +service ExecutionService { + // GetGenesisInfo returns the necessary genesis information for rollup chain. + rpc GetGenesisInfo(GetGenesisInfoRequest) returns (GenesisInfo); + + // GetBlock will return a block given an identifier. + rpc GetBlock(GetBlockRequest) returns (Block); + + // BatchGetBlocks will return an array of Blocks given an array of block + // identifiers. + rpc BatchGetBlocks(BatchGetBlocksRequest) returns (BatchGetBlocksResponse); + + // ExecuteBlock is called to deterministically derive a rollup block from + // filtered sequencer block information. + rpc ExecuteBlock(ExecuteBlockRequest) returns (Block); + + // GetCommitmentState fetches the current CommitmentState of the chain. + rpc GetCommitmentState(GetCommitmentStateRequest) returns (CommitmentState); + + // UpdateCommitmentState replaces the whole CommitmentState with a new + // CommitmentState. + rpc UpdateCommitmentState(UpdateCommitmentStateRequest) returns (CommitmentState); +} diff --git a/specs/conductor.md b/specs/conductor.md index a2075922d9..6a6a1b364d 100644 --- a/specs/conductor.md +++ b/specs/conductor.md @@ -126,3 +126,8 @@ 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 executed as written. + +## Startup, Restarts, Execution, and Commitments + +See [`astria.execution.v1alpha2` API documentation](./execution-api.md) for more +information on Conductor startup, restart, execution, and commitment logic. diff --git a/specs/execution-api.md b/specs/execution-api.md index c765229007..752134bb75 100644 --- a/specs/execution-api.md +++ b/specs/execution-api.md @@ -29,6 +29,32 @@ previous block data, Conductor must also track the block hash of any blocks between commitments, it will call `BatchGetBlocks` to get block information between commitments. +### Restart + +The conductor is able to gracefully restart under two scenarios: + +1. Conductor recieves a `PermissionDenied` status from the execution layer when +calling `ExecuteBlock`. This is meant to function seamlessly with [`astria-geth`](https://github.com/astriaorg/astria-geth)'s +behavior upon an unexpected restart. If `geth` receives a `ExecuteBlock` request +before receving *both* `GetGenesisInfo` and `GetCommitmentState` (which will be +the case if the execution layer has restarted), it responds with a `PremissionDenied` +status, prompting the conductor to restart. +2. `GenesisInfo` contains a `rollup_stop_block_number` and has a `halt_at_rollup_stop_number` +value of `false`, indicating a planned restart after execution of the given block. +Once the conductor reaches the stop height, it will perform a restart in one of the +following ways corresponding to its mode: + + - **Firm Only Mode**: Once the stop height is reached for the firm block stream, + the firm block at this height is executed (and commitment state updated) before + restarting the conductor, prompting the rollup for a new `GenesisInfo` with + new start/stop heights (and potentially chain IDs). + - **Soft and Firm Mode**: Once the stop height is reached for the soft block + stream, the block at this height will be executed. The conductor will then wait + for the firm block at the stop height, execute it, and restart. If the firm + block is executed first, conductor will restart immediately. + - **Soft Only Mode**: Once the stop height is reached for the soft block stream, + the block at this height is executed and Conductor restarts. + ### Execution & Commitments From the perspective of the sequencer: @@ -74,9 +100,18 @@ Note: For our EVM rollup, we map the `CommitmentState` to the `ForkchoiceRule`: ### GetGenesisInfo `GetGenesisInfo` returns information which is definitional to the rollup with -regards to how it serves data from the sequencer & celestia networks. This RPC -should ALWAYS succeed. The API is agnostic as to how the information is defined -in a rollups genesis, and used by the conductor as configuration on startup. +regards to how it serves data from the sequencer & celestia networks, along with +optional block heights for initiating a [conductor restart](#restart). This RPC +should ***always*** succeed. The API is agnostic as to how the information is defined +in a rollup's genesis, and used by the conductor as configuration on startup *or* +upon a restart. + +If the `GenesisInfo` provided by this RPC contains a `rollup_stop_block_number`, +the rollup should be prepared to provide an updated response when the conductor +restarts, including, at minimum, a new `rollup_start_block_number` and `sequencer_start_height`. +The updated response can also contain an updated `rollup_stop_block_number` (if +another restart is desired), `celestia_chain_id`, and/or `sequencer_chain_id` +(to facilitate network migration). ### ExecuteBlock