diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml index 2f6a00a918..8c89df0385 100644 --- a/.github/workflows/docker-build.yml +++ b/.github/workflows/docker-build.yml @@ -191,6 +191,10 @@ jobs: uses: helm/kind-action@v1 with: install_only: true + - name: Install astria cli (rust) + run: just install-cli + - name: Fetch and install celestia-appd + run: just get-celestia-appd v1.9.0 Linux x86_64 - name: Log in to GHCR uses: docker/login-action@v2 with: @@ -204,27 +208,10 @@ jobs: 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 ibc-test-infra $TAG - just build-and-load-bridge-tester-image $TAG - - name: Run IBC Bridge Tester - timeout-minutes: 2 - run: | - TAG=sha-$(git rev-parse --short HEAD) - printlogs() { - echo "IBC Transfer Test failed, printing logs..." - kubectl describe job bridge-tester-chart -n astria-dev-cluster - kubectl logs job/bridge-tester-chart --all-containers -n astria-dev-cluster - exit 1 - } - just deploy bridge-tester $TAG - # timeout before the gh job times out so we can print logs - kubectl wait --for=condition=complete --timeout=90s job/bridge-tester-chart -n astria-dev-cluster || printlogs - JOB_STATUS=$(kubectl get job bridge-tester-chart -n astria-dev-cluster -o jsonpath='{.status.succeeded}') - if [ "$JOB_STATUS" != "1" ]; then - printlogs - else - echo "IBC Transfer Test passed" - fi + just ibc-test deploy $TAG + - name: Run IBC ICS20 Transfer test + timeout-minutes: 3 + run: just ibc-test run ./celestia ./celestia-appd docker: if: ${{ always() && !cancelled() }} diff --git a/Cargo.lock b/Cargo.lock index 2816c4fc7f..2a839425cf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -492,6 +492,8 @@ dependencies = [ "ethers", "futures", "ibc-types", + "prost", + "serde", "serde_json", "tendermint", "thiserror", diff --git a/charts/bridge-test/.helmignore b/charts/bridge-test/.helmignore deleted file mode 100644 index 0e8a0eb36f..0000000000 --- a/charts/bridge-test/.helmignore +++ /dev/null @@ -1,23 +0,0 @@ -# Patterns to ignore when building packages. -# This supports shell glob matching, relative path matching, and -# negation (prefixed with !). Only one pattern per line. -.DS_Store -# Common VCS dirs -.git/ -.gitignore -.bzr/ -.bzrignore -.hg/ -.hgignore -.svn/ -# Common backup files -*.swp -*.bak -*.tmp -*.orig -*~ -# Various IDEs -.project -.idea/ -*.tmproj -.vscode/ diff --git a/charts/bridge-test/Chart.yaml b/charts/bridge-test/Chart.yaml deleted file mode 100644 index 7beb394e73..0000000000 --- a/charts/bridge-test/Chart.yaml +++ /dev/null @@ -1,28 +0,0 @@ -apiVersion: v2 -name: bridge-test -description: A Helm chart for Kubernetes - -# A chart can be either an 'application' or a 'library' chart. -# -# Application charts are a collection of templates that can be packaged into versioned archives -# to be deployed. -# -# Library charts provide useful utilities or functions for the chart developer. They're included as -# a dependency of application charts to inject those utilities and functions into the rendering -# pipeline. Library charts do not define any templates and therefore cannot be deployed. -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: 0.1.0 - -# 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: "0.1.0" - -maintainers: - - name: steezeburger - url: astria.org diff --git a/charts/bridge-test/files/scripts/test-ibc-transfer.sh b/charts/bridge-test/files/scripts/test-ibc-transfer.sh deleted file mode 100644 index d3e836ba95..0000000000 --- a/charts/bridge-test/files/scripts/test-ibc-transfer.sh +++ /dev/null @@ -1,58 +0,0 @@ -#!/bin/sh - -get_evm_balance() { - HEX_NUM=$(curl -X POST "$evm_url" -s -d "{\"jsonrpc\":\"2.0\",\"method\":\"eth_getBalance\",\"params\":[\"$evm_to_address\", \"latest\"],\"id\":1}" -H 'Content-Type: application/json' | jq -r '.result') - # strip 0x - HEX_NUM=$(echo "$HEX_NUM" | sed 's/^0x//') - # capitalize all lowercase letters - HEX_NUM=$(echo "$HEX_NUM" | tr '[:lower:]' '[:upper:]') - # print as integer - echo "ibase=16; $HEX_NUM" | bc -} - -addKeyForCelestiaAccount() { - # add key for the celestia dev account using the mnemonic - echo "Adding key for the celestia dev account..." - echo "$celestia_dev_account_mnemonic" | celestia-appd keys add \ - "$celestia_dev_account_key_name" \ - --home "$home_dir" \ - --keyring-backend="$keyring_backend" \ - --recover -} - -performIBCTransfer() { - # perform ibc transfer - echo "Performing IBC transfer..." - celestia-appd tx ibc-transfer transfer \ - transfer \ - channel-0 \ - "$bridge_account_address_bech32" \ - 53000utia \ - --memo="{\"rollupAddress\":\"$evm_to_address\"}" \ - --chain-id="$celestia_chain_id" \ - --node="$celestia_node_url" \ - --from="$celestia_dev_account_address" \ - --fees=26000utia \ - --yes \ - --log_level=debug \ - --home "$home_dir" \ - --keyring-backend="$keyring_backend" -} - -initial_balance=$(get_evm_balance) - -addKeyForCelestiaAccount -performIBCTransfer - -# FIXME - should probably poll w/ timeout instead of sleeping? -sleep 30 - -final_balance=$(get_evm_balance) -expected_balance=$(echo "$initial_balance + 53000000000000000" | bc) -if [ "$(echo "$final_balance == $expected_balance" | bc)" -eq 0 ]; then - echo "IBC Transfer failed!" - echo "Expected balance $expected_balance, got $final_balance" - exit 1 -else - echo "IBC Transfer successful!" -fi diff --git a/charts/bridge-test/templates/_helpers.tpl b/charts/bridge-test/templates/_helpers.tpl deleted file mode 100644 index ad997565bb..0000000000 --- a/charts/bridge-test/templates/_helpers.tpl +++ /dev/null @@ -1,23 +0,0 @@ -{{/* -Namespace to deploy elements into. -*/}} -{{- define "bridge-test.namespace" -}} -{{- default .Release.Namespace .Values.namespaceOverride | trunc 63 | trimSuffix "-" -}} -{{- end }} - -{{/* -Create a default fully qualified app name. -We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). -*/}} -{{- define "bridge-test.fullname" -}} -{{- if .Values.fullnameOverride -}} -{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}} -{{- else -}} -{{- $name := default .Chart.Name .Values.nameOverride -}} -{{- if contains $name .Release.Name -}} -{{- .Release.Name | trunc 63 | trimSuffix "-" -}} -{{- else -}} -{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}} -{{- end -}} -{{- end -}} -{{- end -}} diff --git a/charts/bridge-test/templates/configmap.yaml b/charts/bridge-test/templates/configmap.yaml deleted file mode 100644 index c31803c370..0000000000 --- a/charts/bridge-test/templates/configmap.yaml +++ /dev/null @@ -1,26 +0,0 @@ -apiVersion: v1 -kind: ConfigMap -metadata: - name: bridge-test-env - namespace: {{ include "bridge-test.namespace" . }} -data: - home_dir: "/home/celestia" - keyring_backend: "test" - bridge_account_address: "{{ .Values.bridgeAccount.address }}" - bridge_account_address_bech32: "{{ .Values.bridgeAccount.bech32 }}" - evm_to_address: "{{ .Values.evmToAddress }}" - evm_url: "{{ .Values.evmURL }}" - celestia_chain_id: "{{ .Values.celestiaChainID }}" - celestia_node_url: "{{ .Values.celestiaNodeURL }}" - celestia_dev_account_address: "{{ .Values.celestiaDevAccount.address }}" - celestia_dev_account_mnemonic: "{{ .Values.celestiaDevAccount.mnemonic }}" - celestia_dev_account_key_name: "{{ .Values.celestiaDevAccount.name }}" ---- -apiVersion: v1 -kind: ConfigMap -metadata: - name: bridge-test-scripts - namespace: {{ include "bridge-test.namespace" . }} -data: - test-ibc-transfer.sh: | - {{- .Files.Get "files/scripts/test-ibc-transfer.sh" | nindent 4 }} diff --git a/charts/bridge-test/templates/job.yaml b/charts/bridge-test/templates/job.yaml deleted file mode 100644 index 391e47288d..0000000000 --- a/charts/bridge-test/templates/job.yaml +++ /dev/null @@ -1,41 +0,0 @@ -apiVersion: batch/v1 -kind: Job -metadata: - name: {{ include "bridge-test.fullname" . }} - labels: - app: astria-dev-cluster - namespace: {{ include "bridge-test.namespace" . }} -spec: - template: - metadata: - name: {{ include "bridge-test.fullname" . }} - labels: - app: astria-dev-cluster - spec: - initContainers: - - name: init-bridge-account - image: {{ .Values.bridgeTesterUtilityImage }} - command: [ "astria-go", "sequencer", "bridge", "init", {{ .Values.evmRollupName }} ] - args: - - --privkey={{ .Values.bridgeAccount.privkey }} - - --sequencer-chain-id={{ .Values.sequencerChainId }} - - --sequencer-url={{ .Values.sequencerURL }} - - --asset={{ .Values.asset }} - - --fee-asset={{ .Values.feeAsset }} - - --log-level=debug - containers: - - name: test-ibc-transfer - image: {{ .Values.bridgeTesterUtilityImage }} - command: [ "/scripts/test-ibc-transfer.sh" ] - volumeMounts: - - mountPath: /scripts/ - name: bridge-test-scripts-volume - envFrom: - - configMapRef: - name: bridge-test-env - volumes: - - name: bridge-test-scripts-volume - configMap: - name: bridge-test-scripts - defaultMode: 0777 - restartPolicy: Never diff --git a/charts/bridge-test/values.yaml b/charts/bridge-test/values.yaml deleted file mode 100644 index 66b0dc9545..0000000000 --- a/charts/bridge-test/values.yaml +++ /dev/null @@ -1,38 +0,0 @@ -replicaCount: 1 - -# this image is overridden in ci/cd with the image built in ci/cd -bridgeTesterUtilityImage: ghcr.io/astriaorg/bridge-tester-utility:local - -imagePullSecrets: [] -nameOverride: "" -namespaceOverride: "" -fullnameOverride: "" - -# must match rollup name used in evm rollup chart values -evmRollupName: "astria" -# this is a shared dev address -evmToAddress: "0xaC21B97d35Bf75A7dAb16f35b111a50e78A72F30" -# evm execution api url -evmURL: "http://astria-evm-service.astria-dev-cluster.svc.cluster.local:8545" - -sequencerURL: "http://node0-sequencer-rpc-service.astria-dev-cluster.svc.cluster.local:26657" -# must match chain id used in sequencer chart values -sequencerChainId: "sequencer-test-chain-0" -asset: "transfer/channel-0/utia" -feeAsset: "nria" - -# sequencer bridge account. is funded during sequencer genesis. -bridgeAccount: - address: "6f85297e587b61b37695a1ac17189b3e907e318e" - bech32: "astria1d7zjjljc0dsmxa545xkpwxym86g8uvvwhtezcr" - privkey: "6015fbe1c365d3c5ef92dc891db8c5bb26ad454bec2db4762b96e9f8b2430285" - pubkey: "b78aa61c65f21e5fe0f31d221819053fa2286dd6eff83fc490e3ee746f144626" - -celestiaChainID: "celestia-local-0" -celestiaNodeURL: "http://celestia-app-service.astria-dev-cluster.svc.cluster.local:26657" - -# this account should be funded during celestia genesis -celestiaDevAccount: - name: "dev-account" - address: "celestia1m0ksdjl2p5nzhqy3p47fksv52at3ln885xvl96" - mnemonic: "enrich avocado local net will avoid dizzy truth column excuse ready lesson" diff --git a/charts/deploy.just b/charts/deploy.just index 6cdbfc97d2..ade1183c3b 100644 --- a/charts/deploy.just +++ b/charts/deploy.just @@ -1,3 +1,5 @@ +mod ibc-test + ############################################## ## Deploying and Running using Helm and K8s ## ############################################## @@ -225,6 +227,16 @@ init-rollup-bridge rollupName=defaultRollupName evmDestinationAddress=evm_destin --fee-asset=$FEE_ASSET --asset=$ASSET || exit 1 +init-ibc-bridge privateKey asset feeAsset rollupName=defaultRollupName: + astria-cli sequencer init-bridge-account \ + --rollup-name {{ rollupName }} \ + --private-key {{ privateKey }} \ + --sequencer.chain-id {{ sequencer_chain_id }} \ + --sequencer-url {{ sequencer_rpc_url }} \ + --fee-asset {{ feeAsset }} \ + --asset {{ asset }} + + eth_rpc_url := "http://executor.astria.localdev.me/" eth_ws_url := "ws://ws-executor.astria.localdev.me/" bridge_tx_bytes := "0xf8f280843c54e7f182898594a58639fb5458e65e4fa917ff951c390292c24a15880de0b6b3a7640000b884bab916d00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000002d617374726961313777306164656736346b7930646178776432756779756e65656c6c6d6a676e786c333935303400000000000000000000000000000000000000820a96a086b85348c9816f6d34533669db3d3626cf55eecea6a380d4d072efb1839df443a04b8b60c8b91dd30add1ca4a96097238d73bab29b0a958322d9a51755d5a5f287" diff --git a/charts/ibc-test.just b/charts/ibc-test.just new file mode 100644 index 0000000000..6561918fc5 --- /dev/null +++ b/charts/ibc-test.just @@ -0,0 +1,111 @@ +defaultTag := "" + +delete: + -just delete celestia-local + -just delete sequencer + -just delete hermes-local + -just delete rollup + +@deploy 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 ./sequencer > /dev/null + helm dependency update ./evm-stack > /dev/null + echo "Setting up single astria sequencer..." && helm install \ + -n astria-validator-single single-sequencer-chart ./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 ./evm-stack \ + -f ../dev/values/rollup/dev.yaml \ + -f ../dev/values/rollup/ibc-bridge-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 + echo "Deploying Hermes" + just deploy hermes-local > /dev/null + kubectl wait -n astria-dev-cluster deployment hermes-local-chart --for=condition=Available=True --timeout=300s + +[no-cd] +run celestiaHome pathToCelestiaAppd=default_celestia_appd : + #!/usr/bin/env bash + + initial_balance=$(just evm-get-balance {{evm_destination_address}}) + + # Create a bridge account on the sequencer + just init-ibc-bridge {{ sequencer_tia_bridge_pkey }} transfer/channel-0/utia nria + + # Load the private key of the Celestia dev account to issue transfers + just ibc-test _load-celestia-key "{{ celestiaHome}}" "{{ pathToCelestiaAppd }}" + + # Execute the transfer from Celestia to the Rollup + just ibc-test _do-ibc-transfer "{{ celestiaHome}}" "{{ pathToCelestiaAppd }}" + + # Multiplication factor is 10^-6 (utia to tia) * 10^18 (rollup factor) = 10^12 + let expected_balance="$initial_balance + {{ transfer_amount }} * 10**12" + + for i in {1..50} + do + current_balance=$(just evm-get-balance {{evm_destination_address}}) + echo "check $i, balance: $current_balance, expected: $expected_balance" + if (( expected_balance == $current_balance )); then + expected_balance_found="1" + break + else + sleep 1 + fi + done + if [[ -z $expected_balance_found ]]; then + echo "expected balance was not found; IBC transfer from Celestia to the Rollup failed" + exit 1 + fi + + +bridge_address := "astria1d7zjjljc0dsmxa545xkpwxym86g8uvvwhtezcr" +celestia_dev_account_address := "celestia1m0ksdjl2p5nzhqy3p47fksv52at3ln885xvl96" +celestia_dev_account_key_name := "dev" +celestia_dev_account_mnemonic := "enrich avocado local net will avoid dizzy truth column excuse ready lesson" +celestia_chain_id := "celestia-local-0" +celestia_node_url := "http://rpc.app.celestia.localdev.me:80" +sequencer_tia_bridge_pkey := "6015fbe1c365d3c5ef92dc891db8c5bb26ad454bec2db4762b96e9f8b2430285" +keyring_backend := "test" + +# This is the same address as used in deploy.just +evm_destination_address := "0xaC21B97d35Bf75A7dAb16f35b111a50e78A72F30" + +# all in units of utia +transfer_amount := "53000" +transfer_fees := "26000" + +default_celestia_appd := "celestia-appd" +[no-cd] +_load-celestia-key celestiaHome pathToCelestiaAppd=default_celestia_appd: + #!/usr/bin/env bash + pwd + "{{pathToCelestiaAppd}}" keys add {{ celestia_dev_account_key_name }} \ + --home "{{celestiaHome}}" \ + --keyring-backend="{{ keyring_backend }}" \ + --recover <<< "{{ celestia_dev_account_mnemonic }}" + +[no-cd] +_do-ibc-transfer celestiaHome pathToCelestiaAppd=default_celestia_appd: + echo "Performing IBC transfer..." + "{{pathToCelestiaAppd}}" tx ibc-transfer transfer \ + transfer \ + channel-0 \ + {{ bridge_address }} \ + "{{ transfer_amount }}utia" \ + --memo="{\"rollupDepositAddress\":\"{{ evm_destination_address }}\"}" \ + --chain-id="{{ celestia_chain_id }}" \ + --node="{{ celestia_node_url }}" \ + --from="{{ celestia_dev_account_address }}" \ + --fees="{{ transfer_fees }}utia" \ + --yes \ + --log_level=debug \ + --home "{{celestiaHome}}" \ + --keyring-backend="{{ keyring_backend }}" diff --git a/crates/astria-bridge-contracts/Cargo.toml b/crates/astria-bridge-contracts/Cargo.toml index a96bba7088..f417e32e7a 100644 --- a/crates/astria-bridge-contracts/Cargo.toml +++ b/crates/astria-bridge-contracts/Cargo.toml @@ -11,6 +11,8 @@ astria-core = { path = "../astria-core", features = ["serde"] } ethers = { workspace = true } futures = { workspace = true } ibc-types = { workspace = true } +prost = { workspace = true } +serde = { workspace = true } serde_json = { workspace = true } tendermint = { workspace = true } thiserror = { workspace = true } diff --git a/crates/astria-bridge-contracts/src/lib.rs b/crates/astria-bridge-contracts/src/lib.rs index 7266c93eb8..9587a81e23 100644 --- a/crates/astria-bridge-contracts/src/lib.rs +++ b/crates/astria-bridge-contracts/src/lib.rs @@ -12,9 +12,12 @@ use astria_core::{ Address, AddressError, }, - protocol::transaction::v1alpha1::{ - action::Ics20Withdrawal, - Action, + protocol::{ + memos, + transaction::v1alpha1::{ + action::Ics20Withdrawal, + Action, + }, }, }; use astria_withdrawer::{ @@ -370,15 +373,15 @@ where &self, log: Log, ) -> Result { - let block_number = log + let rollup_block_number = log .block_number .ok_or_else(|| GetWithdrawalActionsError::log_without_block_number(&log))? .as_u64(); - let transaction_hash = log + let rollup_transaction_hash = log .transaction_hash .ok_or_else(|| GetWithdrawalActionsError::log_without_transaction_hash(&log))? - .into(); + .to_string(); let event = decode_log::(log) .map_err(GetWithdrawalActionsError::decode_log)?; @@ -393,15 +396,13 @@ where .expect("must be set if this method is entered"), ); - let memo = serde_json::to_string(&astria_core::bridge::Ics20WithdrawalFromRollupMemo { + let memo = memo_to_json(&memos::v1alpha1::Ics20WithdrawalFromRollup { memo: event.memo.clone(), - block_number, + rollup_block_number, rollup_return_address: event.sender.to_string(), - transaction_hash, + rollup_transaction_hash, }) - .map_err(|source| { - GetWithdrawalActionsError::encode_memo("Ics20WithdrawalFromRollupMemo", source) - })?; + .map_err(GetWithdrawalActionsError::encode_memo)?; let amount = calculate_amount(&event, self.asset_withdrawal_divisor) .map_err(GetWithdrawalActionsError::calculate_withdrawal_amount)?; @@ -427,24 +428,24 @@ where &self, log: Log, ) -> Result { - let block_number = log + let rollup_block_number = log .block_number .ok_or_else(|| GetWithdrawalActionsError::log_without_block_number(&log))? .as_u64(); - let transaction_hash = log + let rollup_transaction_hash = log .transaction_hash .ok_or_else(|| GetWithdrawalActionsError::log_without_transaction_hash(&log))? - .into(); + .to_string(); let event = decode_log::(log) .map_err(GetWithdrawalActionsError::decode_log)?; - let memo = serde_json::to_string(&astria_core::bridge::UnlockMemo { - block_number, - transaction_hash, + let memo = memo_to_json(&memos::v1alpha1::BridgeUnlock { + rollup_block_number, + rollup_transaction_hash, }) - .map_err(|err| GetWithdrawalActionsError::encode_memo("bridge::UnlockMemo", err))?; + .map_err(GetWithdrawalActionsError::encode_memo)?; let amount = calculate_amount(&event, self.asset_withdrawal_divisor) .map_err(GetWithdrawalActionsError::calculate_withdrawal_amount)?; @@ -485,11 +486,8 @@ impl GetWithdrawalActionsError { )) } - fn encode_memo(which: &'static str, source: serde_json::Error) -> Self { - Self(GetWithdrawalActionsErrorKind::EncodeMemo { - which, - source, - }) + fn encode_memo(source: EncodeMemoError) -> Self { + Self(GetWithdrawalActionsErrorKind::EncodeMemo(source)) } fn get_logs(source: GetLogsError) -> Self { @@ -513,11 +511,8 @@ enum GetWithdrawalActionsErrorKind { DecodeLog(DecodeLogError), #[error(transparent)] DestinationChainAsAddress(DestinationChainAsAddressError), - #[error("failed encoding memo `{which}`")] - EncodeMemo { - which: &'static str, - source: serde_json::Error, - }, + #[error(transparent)] + EncodeMemo(EncodeMemoError), #[error(transparent)] GetLogs(GetLogsError), #[error("log did not contain a block number")] @@ -628,6 +623,20 @@ struct DestinationChainAsAddressError { source: AddressError, } +#[derive(Debug, thiserror::Error)] +#[error("failed encoding memo `{proto_message}` as JSON")] +struct EncodeMemoError { + proto_message: String, + source: serde_json::Error, +} + +fn memo_to_json(memo: &T) -> Result { + serde_json::to_string(memo).map_err(|source| EncodeMemoError { + proto_message: T::full_name(), + source, + }) +} + fn parse_destination_chain_as_address( event: &SequencerWithdrawalFilter, ) -> Result { diff --git a/crates/astria-bridge-withdrawer/src/bridge_withdrawer/startup.rs b/crates/astria-bridge-withdrawer/src/bridge_withdrawer/startup.rs index e3a2e8c5a0..8b6d4938d5 100644 --- a/crates/astria-bridge-withdrawer/src/bridge_withdrawer/startup.rs +++ b/crates/astria-bridge-withdrawer/src/bridge_withdrawer/startup.rs @@ -4,10 +4,6 @@ use std::{ }; use astria_core::{ - bridge::{ - self, - Ics20WithdrawalFromRollupMemo, - }, generated::sequencerblock::v1alpha1::{ sequencer_service_client::{ self, @@ -19,6 +15,7 @@ use astria_core::{ protocol::{ asset::v1alpha1::AllowedFeeAssetsResponse, bridge::v1alpha1::BridgeAccountLastTxHashResponse, + memos, transaction::v1alpha1::Action, }, }; @@ -442,14 +439,15 @@ fn rollup_height_from_signed_transaction( let last_batch_rollup_height = match withdrawal_action { Action::BridgeUnlock(action) => { - let memo: bridge::UnlockMemo = serde_json::from_str(&action.memo) + let memo: memos::v1alpha1::BridgeUnlock = serde_json::from_str(&action.memo) .wrap_err("failed to parse memo from last transaction by the bridge account")?; - Some(memo.block_number) + Some(memo.rollup_block_number) } Action::Ics20Withdrawal(action) => { - let memo: Ics20WithdrawalFromRollupMemo = serde_json::from_str(&action.memo) - .wrap_err("failed to parse memo from last transaction by the bridge account")?; - Some(memo.block_number) + let memo: memos::v1alpha1::Ics20WithdrawalFromRollup = + serde_json::from_str(&action.memo) + .wrap_err("failed to parse memo from last transaction by the bridge account")?; + Some(memo.rollup_block_number) } _ => None, } diff --git a/crates/astria-core/src/bridge.rs b/crates/astria-core/src/bridge.rs deleted file mode 100644 index c8cb1fb570..0000000000 --- a/crates/astria-core/src/bridge.rs +++ /dev/null @@ -1,91 +0,0 @@ -#[derive(Clone, Debug)] -#[cfg_attr( - feature = "serde", - derive(serde::Serialize), - derive(serde::Deserialize), - serde(rename_all = "camelCase", deny_unknown_fields) -)] -pub struct UnlockMemo { - pub block_number: u64, - #[cfg_attr( - feature = "serde", - serde( - serialize_with = "crate::serde::base64_serialize", - deserialize_with = "crate::serde::base64_deserialize_array" - ) - )] - pub transaction_hash: [u8; 32], -} - -/// Memo format for a ICS20 withdrawal from the rollup which is sent to -/// an external IBC-enabled chain. -#[derive(Debug, Clone)] -#[cfg_attr( - feature = "serde", - derive(serde::Serialize), - derive(serde::Deserialize), - serde(rename_all = "camelCase", deny_unknown_fields) -)] -pub struct Ics20WithdrawalFromRollupMemo { - pub memo: String, - pub block_number: u64, - pub rollup_return_address: String, - #[cfg_attr( - feature = "serde", - serde( - serialize_with = "crate::serde::base64_serialize", - deserialize_with = "crate::serde::base64_deserialize_array" - ) - )] - pub transaction_hash: [u8; 32], -} - -/// Memo format for a ICS20 transfer to Astria which is sent to a -/// bridge account, which will then be deposited into the rollup. -#[derive(Debug, Clone)] -#[cfg_attr( - feature = "serde", - derive(serde::Serialize), - derive(serde::Deserialize), - serde(rename_all = "camelCase", deny_unknown_fields) -)] -pub struct Ics20TransferDepositMemo { - /// the destination address for the deposit on the rollup - pub rollup_address: String, -} - -#[cfg(test)] -mod test { - use super::*; - - #[test] - fn bridge_unlock_memo_snapshot() { - let memo = UnlockMemo { - block_number: 42, - transaction_hash: [88; 32], - }; - - insta::assert_json_snapshot!(memo); - } - - #[test] - fn ics20_withdrawal_from_rollup_memo_snapshot() { - let memo = Ics20WithdrawalFromRollupMemo { - memo: "hello".to_string(), - block_number: 1, - rollup_return_address: "rollup-defined".to_string(), - transaction_hash: [88; 32], - }; - - insta::assert_json_snapshot!(memo); - } - - #[test] - fn ics20_transfer_deposit_memo_snapshot() { - let memo = Ics20TransferDepositMemo { - rollup_address: "some_rollup_address".to_string(), - }; - - insta::assert_json_snapshot!(memo); - } -} diff --git a/crates/astria-core/src/generated/astria.protocol.memos.v1alpha1.rs b/crates/astria-core/src/generated/astria.protocol.memos.v1alpha1.rs new file mode 100644 index 0000000000..bd066f2abf --- /dev/null +++ b/crates/astria-core/src/generated/astria.protocol.memos.v1alpha1.rs @@ -0,0 +1,77 @@ +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct BridgeUnlock { + /// The block number on the rollup that triggered the transaction underlying + /// this bridge unlock memo. + #[prost(uint64, tag = "1")] + pub rollup_block_number: u64, + /// The hash of the original rollup transaction that triggered a bridge unlock + /// and that is underlying this bridge unlock memo. + /// + /// This field is of type `string` so that it can be formatted in the preferred + /// format of the rollup when targeting plain text encoding. + #[prost(string, tag = "2")] + pub rollup_transaction_hash: ::prost::alloc::string::String, +} +impl ::prost::Name for BridgeUnlock { + const NAME: &'static str = "BridgeUnlock"; + const PACKAGE: &'static str = "astria.protocol.memos.v1alpha1"; + fn full_name() -> ::prost::alloc::string::String { + ::prost::alloc::format!("astria.protocol.memos.v1alpha1.{}", Self::NAME) + } +} +/// Memo for an ICS20 withdrawal from the rollup which is sent to +/// an external IBC-enabled chain. +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct Ics20WithdrawalFromRollup { + /// The block number on the rollup that triggered the transaction underlying + /// this ics20 withdrawal memo. + #[prost(uint64, tag = "1")] + pub rollup_block_number: u64, + /// The hash of the original rollup transaction that triggered this ics20 + /// withdrawal and that is underlying this bridge unlock memo. + /// + /// This field is of type `string` so that it can be formatted in the preferred + /// format of the rollup when targeting plain text encoding. + #[prost(string, tag = "2")] + pub rollup_transaction_hash: ::prost::alloc::string::String, + /// The return address on the rollup to which funds should returned in case of + /// failure. This field exists so that the rollup can identify which account + /// the returned funds originated from. + /// + /// This field is of type `string` so that it can be formatted in the preferred + /// format of the rollup when targeting plain text encoding. + #[prost(string, tag = "3")] + pub rollup_return_address: ::prost::alloc::string::String, + /// A field that can be populated by the rollup. It is assumed that this field + /// will be consumed by the downstream chain. + #[prost(string, tag = "4")] + pub memo: ::prost::alloc::string::String, +} +impl ::prost::Name for Ics20WithdrawalFromRollup { + const NAME: &'static str = "Ics20WithdrawalFromRollup"; + const PACKAGE: &'static str = "astria.protocol.memos.v1alpha1"; + fn full_name() -> ::prost::alloc::string::String { + ::prost::alloc::format!("astria.protocol.memos.v1alpha1.{}", Self::NAME) + } +} +/// Memo for an ICS20 transfer to Astria which is sent to a +/// bridge account, which will then be deposited into the rollup. +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct Ics20TransferDeposit { + /// The destination address for the deposit on the rollup. + /// + /// This field is of type `string` so that it can be formatted in the preferred + /// format of the rollup when targeting plain text encoding. + #[prost(string, tag = "1")] + pub rollup_deposit_address: ::prost::alloc::string::String, +} +impl ::prost::Name for Ics20TransferDeposit { + const NAME: &'static str = "Ics20TransferDeposit"; + const PACKAGE: &'static str = "astria.protocol.memos.v1alpha1"; + fn full_name() -> ::prost::alloc::string::String { + ::prost::alloc::format!("astria.protocol.memos.v1alpha1.{}", Self::NAME) + } +} diff --git a/crates/astria-core/src/generated/astria.protocol.memos.v1alpha1.serde.rs b/crates/astria-core/src/generated/astria.protocol.memos.v1alpha1.serde.rs new file mode 100644 index 0000000000..315161ec8d --- /dev/null +++ b/crates/astria-core/src/generated/astria.protocol.memos.v1alpha1.serde.rs @@ -0,0 +1,353 @@ +impl serde::Serialize for BridgeUnlock { + #[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_block_number != 0 { + len += 1; + } + if !self.rollup_transaction_hash.is_empty() { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("astria.protocol.memos.v1alpha1.BridgeUnlock", len)?; + if self.rollup_block_number != 0 { + #[allow(clippy::needless_borrow)] + struct_ser.serialize_field("rollupBlockNumber", ToString::to_string(&self.rollup_block_number).as_str())?; + } + if !self.rollup_transaction_hash.is_empty() { + struct_ser.serialize_field("rollupTransactionHash", &self.rollup_transaction_hash)?; + } + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for BridgeUnlock { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "rollup_block_number", + "rollupBlockNumber", + "rollup_transaction_hash", + "rollupTransactionHash", + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + RollupBlockNumber, + RollupTransactionHash, + } + 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 { + "rollupBlockNumber" | "rollup_block_number" => Ok(GeneratedField::RollupBlockNumber), + "rollupTransactionHash" | "rollup_transaction_hash" => Ok(GeneratedField::RollupTransactionHash), + _ => Err(serde::de::Error::unknown_field(value, FIELDS)), + } + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = BridgeUnlock; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct astria.protocol.memos.v1alpha1.BridgeUnlock") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + let mut rollup_block_number__ = None; + let mut rollup_transaction_hash__ = None; + while let Some(k) = map_.next_key()? { + match k { + GeneratedField::RollupBlockNumber => { + if rollup_block_number__.is_some() { + return Err(serde::de::Error::duplicate_field("rollupBlockNumber")); + } + rollup_block_number__ = + Some(map_.next_value::<::pbjson::private::NumberDeserialize<_>>()?.0) + ; + } + GeneratedField::RollupTransactionHash => { + if rollup_transaction_hash__.is_some() { + return Err(serde::de::Error::duplicate_field("rollupTransactionHash")); + } + rollup_transaction_hash__ = Some(map_.next_value()?); + } + } + } + Ok(BridgeUnlock { + rollup_block_number: rollup_block_number__.unwrap_or_default(), + rollup_transaction_hash: rollup_transaction_hash__.unwrap_or_default(), + }) + } + } + deserializer.deserialize_struct("astria.protocol.memos.v1alpha1.BridgeUnlock", FIELDS, GeneratedVisitor) + } +} +impl serde::Serialize for Ics20TransferDeposit { + #[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_deposit_address.is_empty() { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("astria.protocol.memos.v1alpha1.Ics20TransferDeposit", len)?; + if !self.rollup_deposit_address.is_empty() { + struct_ser.serialize_field("rollupDepositAddress", &self.rollup_deposit_address)?; + } + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for Ics20TransferDeposit { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "rollup_deposit_address", + "rollupDepositAddress", + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + RollupDepositAddress, + } + 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 { + "rollupDepositAddress" | "rollup_deposit_address" => Ok(GeneratedField::RollupDepositAddress), + _ => Err(serde::de::Error::unknown_field(value, FIELDS)), + } + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = Ics20TransferDeposit; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct astria.protocol.memos.v1alpha1.Ics20TransferDeposit") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + let mut rollup_deposit_address__ = None; + while let Some(k) = map_.next_key()? { + match k { + GeneratedField::RollupDepositAddress => { + if rollup_deposit_address__.is_some() { + return Err(serde::de::Error::duplicate_field("rollupDepositAddress")); + } + rollup_deposit_address__ = Some(map_.next_value()?); + } + } + } + Ok(Ics20TransferDeposit { + rollup_deposit_address: rollup_deposit_address__.unwrap_or_default(), + }) + } + } + deserializer.deserialize_struct("astria.protocol.memos.v1alpha1.Ics20TransferDeposit", FIELDS, GeneratedVisitor) + } +} +impl serde::Serialize for Ics20WithdrawalFromRollup { + #[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_block_number != 0 { + len += 1; + } + if !self.rollup_transaction_hash.is_empty() { + len += 1; + } + if !self.rollup_return_address.is_empty() { + len += 1; + } + if !self.memo.is_empty() { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("astria.protocol.memos.v1alpha1.Ics20WithdrawalFromRollup", len)?; + if self.rollup_block_number != 0 { + #[allow(clippy::needless_borrow)] + struct_ser.serialize_field("rollupBlockNumber", ToString::to_string(&self.rollup_block_number).as_str())?; + } + if !self.rollup_transaction_hash.is_empty() { + struct_ser.serialize_field("rollupTransactionHash", &self.rollup_transaction_hash)?; + } + if !self.rollup_return_address.is_empty() { + struct_ser.serialize_field("rollupReturnAddress", &self.rollup_return_address)?; + } + if !self.memo.is_empty() { + struct_ser.serialize_field("memo", &self.memo)?; + } + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for Ics20WithdrawalFromRollup { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "rollup_block_number", + "rollupBlockNumber", + "rollup_transaction_hash", + "rollupTransactionHash", + "rollup_return_address", + "rollupReturnAddress", + "memo", + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + RollupBlockNumber, + RollupTransactionHash, + RollupReturnAddress, + Memo, + } + 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 { + "rollupBlockNumber" | "rollup_block_number" => Ok(GeneratedField::RollupBlockNumber), + "rollupTransactionHash" | "rollup_transaction_hash" => Ok(GeneratedField::RollupTransactionHash), + "rollupReturnAddress" | "rollup_return_address" => Ok(GeneratedField::RollupReturnAddress), + "memo" => Ok(GeneratedField::Memo), + _ => Err(serde::de::Error::unknown_field(value, FIELDS)), + } + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = Ics20WithdrawalFromRollup; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct astria.protocol.memos.v1alpha1.Ics20WithdrawalFromRollup") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + let mut rollup_block_number__ = None; + let mut rollup_transaction_hash__ = None; + let mut rollup_return_address__ = None; + let mut memo__ = None; + while let Some(k) = map_.next_key()? { + match k { + GeneratedField::RollupBlockNumber => { + if rollup_block_number__.is_some() { + return Err(serde::de::Error::duplicate_field("rollupBlockNumber")); + } + rollup_block_number__ = + Some(map_.next_value::<::pbjson::private::NumberDeserialize<_>>()?.0) + ; + } + GeneratedField::RollupTransactionHash => { + if rollup_transaction_hash__.is_some() { + return Err(serde::de::Error::duplicate_field("rollupTransactionHash")); + } + rollup_transaction_hash__ = Some(map_.next_value()?); + } + GeneratedField::RollupReturnAddress => { + if rollup_return_address__.is_some() { + return Err(serde::de::Error::duplicate_field("rollupReturnAddress")); + } + rollup_return_address__ = Some(map_.next_value()?); + } + GeneratedField::Memo => { + if memo__.is_some() { + return Err(serde::de::Error::duplicate_field("memo")); + } + memo__ = Some(map_.next_value()?); + } + } + } + Ok(Ics20WithdrawalFromRollup { + rollup_block_number: rollup_block_number__.unwrap_or_default(), + rollup_transaction_hash: rollup_transaction_hash__.unwrap_or_default(), + rollup_return_address: rollup_return_address__.unwrap_or_default(), + memo: memo__.unwrap_or_default(), + }) + } + } + deserializer.deserialize_struct("astria.protocol.memos.v1alpha1.Ics20WithdrawalFromRollup", FIELDS, GeneratedVisitor) + } +} diff --git a/crates/astria-core/src/generated/mod.rs b/crates/astria-core/src/generated/mod.rs index 283c2a7f30..5762c6e3c5 100644 --- a/crates/astria-core/src/generated/mod.rs +++ b/crates/astria-core/src/generated/mod.rs @@ -84,6 +84,18 @@ pub mod protocol { pub mod v1alpha1; } #[path = ""] + pub mod memos { + pub mod v1alpha1 { + include!("astria.protocol.memos.v1alpha1.rs"); + + #[cfg(feature = "serde")] + mod _serde_impls { + use super::*; + include!("astria.protocol.memos.v1alpha1.serde.rs"); + } + } + } + #[path = ""] pub mod transaction { pub mod v1alpha1 { include!("astria.protocol.transactions.v1alpha1.rs"); diff --git a/crates/astria-core/src/lib.rs b/crates/astria-core/src/lib.rs index da0a2151ae..a229dbfeaa 100644 --- a/crates/astria-core/src/lib.rs +++ b/crates/astria-core/src/lib.rs @@ -6,7 +6,6 @@ compile_error!( #[rustfmt::skip] pub mod generated; -pub mod bridge; pub mod crypto; pub mod execution; pub mod primitive; diff --git a/crates/astria-core/src/protocol/memos/mod.rs b/crates/astria-core/src/protocol/memos/mod.rs new file mode 100644 index 0000000000..32a5a9d4fd --- /dev/null +++ b/crates/astria-core/src/protocol/memos/mod.rs @@ -0,0 +1 @@ +pub mod v1alpha1; diff --git a/crates/astria-core/src/protocol/memos/snapshots/astria_core__protocol__memos__v1alpha1__test__bridge_unlock_memo_snapshot.snap b/crates/astria-core/src/protocol/memos/snapshots/astria_core__protocol__memos__v1alpha1__test__bridge_unlock_memo_snapshot.snap new file mode 100644 index 0000000000..03fd3880e5 --- /dev/null +++ b/crates/astria-core/src/protocol/memos/snapshots/astria_core__protocol__memos__v1alpha1__test__bridge_unlock_memo_snapshot.snap @@ -0,0 +1,8 @@ +--- +source: crates/astria-core/src/protocol/memos/v1alpha1.rs +expression: memo +--- +{ + "rollupBlockNumber": "42", + "rollupTransactionHash": "a-rollup-defined-hash" +} diff --git a/crates/astria-core/src/protocol/memos/snapshots/astria_core__protocol__memos__v1alpha1__test__ics20_transfer_deposit_memo_snapshot.snap b/crates/astria-core/src/protocol/memos/snapshots/astria_core__protocol__memos__v1alpha1__test__ics20_transfer_deposit_memo_snapshot.snap new file mode 100644 index 0000000000..85d1a4c2b8 --- /dev/null +++ b/crates/astria-core/src/protocol/memos/snapshots/astria_core__protocol__memos__v1alpha1__test__ics20_transfer_deposit_memo_snapshot.snap @@ -0,0 +1,7 @@ +--- +source: crates/astria-core/src/protocol/memos/v1alpha1.rs +expression: memo +--- +{ + "rollupDepositAddress": "an-address-on-the-rollup" +} diff --git a/crates/astria-core/src/protocol/memos/snapshots/astria_core__protocol__memos__v1alpha1__test__ics20_withdrawal_from_rollup_memo_snapshot.snap b/crates/astria-core/src/protocol/memos/snapshots/astria_core__protocol__memos__v1alpha1__test__ics20_withdrawal_from_rollup_memo_snapshot.snap new file mode 100644 index 0000000000..8fdb7038ea --- /dev/null +++ b/crates/astria-core/src/protocol/memos/snapshots/astria_core__protocol__memos__v1alpha1__test__ics20_withdrawal_from_rollup_memo_snapshot.snap @@ -0,0 +1,10 @@ +--- +source: crates/astria-core/src/protocol/memos/v1alpha1.rs +expression: memo +--- +{ + "rollupBlockNumber": "1", + "rollupTransactionHash": "a-rollup-defined-hash", + "rollupReturnAddress": "a-rollup-defined-address", + "memo": "hello" +} diff --git a/crates/astria-core/src/protocol/memos/v1alpha1.rs b/crates/astria-core/src/protocol/memos/v1alpha1.rs new file mode 100644 index 0000000000..6de9472aaf --- /dev/null +++ b/crates/astria-core/src/protocol/memos/v1alpha1.rs @@ -0,0 +1,41 @@ +pub use crate::generated::protocol::memos::v1alpha1::{ + BridgeUnlock, + Ics20TransferDeposit, + Ics20WithdrawalFromRollup, +}; + +#[cfg(all(feature = "serde", test))] +mod test { + use super::*; + + #[test] + fn bridge_unlock_memo_snapshot() { + let memo = BridgeUnlock { + rollup_block_number: 42, + rollup_transaction_hash: "a-rollup-defined-hash".to_string(), + }; + + insta::assert_json_snapshot!(memo); + } + + #[test] + fn ics20_withdrawal_from_rollup_memo_snapshot() { + let memo = Ics20WithdrawalFromRollup { + rollup_block_number: 1, + rollup_return_address: "a-rollup-defined-address".to_string(), + rollup_transaction_hash: "a-rollup-defined-hash".to_string(), + memo: "hello".to_string(), + }; + + insta::assert_json_snapshot!(memo); + } + + #[test] + fn ics20_transfer_deposit_memo_snapshot() { + let memo = Ics20TransferDeposit { + rollup_deposit_address: "an-address-on-the-rollup".to_string(), + }; + + insta::assert_json_snapshot!(memo); + } +} diff --git a/crates/astria-core/src/protocol/mod.rs b/crates/astria-core/src/protocol/mod.rs index e9766358ba..e7fea6cbbd 100644 --- a/crates/astria-core/src/protocol/mod.rs +++ b/crates/astria-core/src/protocol/mod.rs @@ -7,6 +7,7 @@ pub mod abci; pub mod account; pub mod asset; pub mod bridge; +pub mod memos; pub mod transaction; #[cfg(any(feature = "test-utils", test))] diff --git a/crates/astria-core/src/serde.rs b/crates/astria-core/src/serde.rs index fae04f8e00..d9c9f17b10 100644 --- a/crates/astria-core/src/serde.rs +++ b/crates/astria-core/src/serde.rs @@ -1,8 +1,5 @@ use base64_serde::base64_serde_type; -use serde::{ - Deserializer, - Serializer, -}; +use serde::Serializer; base64_serde_type!(pub(crate) Base64Standard, base64::engine::general_purpose::STANDARD); pub(crate) fn base64_serialize(value: &T, serializer: S) -> Result @@ -12,12 +9,3 @@ where { Base64Standard::serialize(value, serializer) } - -pub(crate) fn base64_deserialize_array<'de, T, D>(deserializer: D) -> Result -where - T: TryFrom>, - D: Deserializer<'de>, -{ - let bytes = Base64Standard::deserialize(deserializer)?; - T::try_from(bytes).map_err(|_| serde::de::Error::custom("invalid array length")) -} diff --git a/crates/astria-core/src/snapshots/astria_core__bridge__test__bridge_unlock_memo_snapshot.snap b/crates/astria-core/src/snapshots/astria_core__bridge__test__bridge_unlock_memo_snapshot.snap deleted file mode 100644 index 611bd40d7d..0000000000 --- a/crates/astria-core/src/snapshots/astria_core__bridge__test__bridge_unlock_memo_snapshot.snap +++ /dev/null @@ -1,8 +0,0 @@ ---- -source: crates/astria-core/src/bridge.rs -expression: memo ---- -{ - "blockNumber": 42, - "transactionHash": "WFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFg=" -} diff --git a/crates/astria-core/src/snapshots/astria_core__bridge__test__ics20_transfer_deposit_memo_snapshot.snap b/crates/astria-core/src/snapshots/astria_core__bridge__test__ics20_transfer_deposit_memo_snapshot.snap deleted file mode 100644 index 5c363c836a..0000000000 --- a/crates/astria-core/src/snapshots/astria_core__bridge__test__ics20_transfer_deposit_memo_snapshot.snap +++ /dev/null @@ -1,7 +0,0 @@ ---- -source: crates/astria-core/src/bridge.rs -expression: memo ---- -{ - "rollupAddress": "some_rollup_address" -} diff --git a/crates/astria-core/src/snapshots/astria_core__bridge__test__ics20_withdrawal_from_rollup_memo_snapshot.snap b/crates/astria-core/src/snapshots/astria_core__bridge__test__ics20_withdrawal_from_rollup_memo_snapshot.snap deleted file mode 100644 index 614cb85239..0000000000 --- a/crates/astria-core/src/snapshots/astria_core__bridge__test__ics20_withdrawal_from_rollup_memo_snapshot.snap +++ /dev/null @@ -1,10 +0,0 @@ ---- -source: crates/astria-core/src/bridge.rs -expression: memo ---- -{ - "memo": "hello", - "blockNumber": 1, - "rollupReturnAddress": "rollup-defined", - "transactionHash": "WFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFg=" -} diff --git a/crates/astria-sequencer/src/ibc/ics20_transfer.rs b/crates/astria-sequencer/src/ibc/ics20_transfer.rs index 0d60274bde..bd1d8a44a0 100644 --- a/crates/astria-sequencer/src/ibc/ics20_transfer.rs +++ b/crates/astria-sequencer/src/ibc/ics20_transfer.rs @@ -18,10 +18,6 @@ use anyhow::{ Result, }; use astria_core::{ - bridge::{ - Ics20TransferDepositMemo, - Ics20WithdrawalFromRollupMemo, - }, primitive::v1::{ asset::{ denom, @@ -29,6 +25,7 @@ use astria_core::{ }, Address, }, + protocol::memos, sequencerblock::v1alpha1::block::Deposit, }; use cnidarium::{ @@ -412,7 +409,9 @@ async fn execute_ics20_transfer( // // in this case, we lock the tokens back in the bridge account and // emit a `Deposit` event to send the tokens back to the rollup. - if is_refund && serde_json::from_str::(&packet_data.memo).is_ok() + if is_refund + && serde_json::from_str::(&packet_data.memo) + .is_ok() { let bridge_account = packet_data.sender.parse().context( "sender not an Astria Address: for refunds of ics20 withdrawals that came from a \ @@ -581,20 +580,27 @@ async fn execute_ics20_transfer_bridge_lock( } // assert memo is valid - let deposit_memo: Ics20TransferDepositMemo = + let deposit_memo: memos::v1alpha1::Ics20TransferDeposit = serde_json::from_str(&memo).context("failed to parse memo as Ics20TransferDepositMemo")?; ensure!( - !deposit_memo.rollup_address.is_empty(), - "packet memo field must be set for bridge account recipient", + !deposit_memo.rollup_deposit_address.is_empty(), + "rollup deposit address must be set to bridge funds from sequencer to rollup", ); ensure!( - deposit_memo.rollup_address.len() <= MAX_ROLLUP_ADDRESS_BYTE_LENGTH, + deposit_memo.rollup_deposit_address.len() <= MAX_ROLLUP_ADDRESS_BYTE_LENGTH, "rollup address is too long: exceeds MAX_ROLLUP_ADDRESS_BYTE_LENGTH", ); - execute_deposit(state, recipient, denom, amount, deposit_memo.rollup_address).await + execute_deposit( + state, + recipient, + denom, + amount, + deposit_memo.rollup_deposit_address, + ) + .await } async fn execute_deposit( @@ -766,8 +772,8 @@ mod test { .put_bridge_account_ibc_asset(&bridge_address, &denom) .unwrap(); - let memo = Ics20TransferDepositMemo { - rollup_address: "rollupaddress".to_string(), + let memo = memos::v1alpha1::Ics20TransferDeposit { + rollup_deposit_address: "rollupaddress".to_string(), }; let packet = FungibleTokenPacketData { @@ -1047,11 +1053,11 @@ mod test { sender: bridge_address.to_string(), amount: "100".to_string(), receiver: "other-chain-address".to_string(), - memo: serde_json::to_string(&Ics20WithdrawalFromRollupMemo { + memo: serde_json::to_string(&memos::v1alpha1::Ics20WithdrawalFromRollup { memo: String::new(), - block_number: 1, + rollup_block_number: 1, rollup_return_address: "rollup-defined".to_string(), - transaction_hash: [1u8; 32], + rollup_transaction_hash: hex::encode([1u8; 32]), }) .unwrap(), }; diff --git a/dev/bridgetester.just b/dev/bridgetester.just deleted file mode 100644 index 0c6a03343b..0000000000 --- a/dev/bridgetester.just +++ /dev/null @@ -1,71 +0,0 @@ -# deploy infra for an ibc bridge smoke test -@deploy-ibc-test-infra 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/dev.yaml \ - -f ./dev/values/rollup/ibc-bridge-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 - echo "Deploying Hermes and creating IBC channel..." - just deploy hermes-local > /dev/null - kubectl wait -n astria-dev-cluster deployment hermes-local-chart --for=condition=Available=True --timeout=300s - -# delete infra used for the ibc bridge test -delete-ibc-test-infra: - -just delete celestia-local - -just delete sequencer - -just delete rollup - -just delete hermes-local - -just delete bridge-tester - -# deploy a bridge tester chart that runs a job to test the ibc bridge -deploy-bridge-tester tag='local' namespace=defaultNamespace: - helm install --debug bridge-tester-chart ./charts/bridge-test \ - --namespace {{namespace}} \ - --set bridgeTesterUtilityImage=ghcr.io/astriaorg/bridge-tester-utility:{{tag}} - -# delete the bridge tester release -delete-bridge-tester namespace=defaultNamespace: - helm uninstall bridge-tester-chart --namespace {{namespace}} - -########### -# helpers # -########### - -# NOTE - can't build for darwin because base images don't support it -default_target_platform := 'linux/amd64' - -# build bridge tester utility image, load into cluster -build-and-load-bridge-tester-image tag='local' target_platform=default_target_platform: - docker buildx build \ - --load \ - --platform {{target_platform}} \ - -f dev/containerfiles/bridgetesterutility.Dockerfile \ - -t ghcr.io/astriaorg/bridge-tester-utility:{{tag}} . - just load-image ghcr.io/astriaorg/bridge-tester-utility:{{tag}} - -# build the astria-geth image and load into cluster. NOTE - assumes astria-geth is sibling directory to monorepo -build-and-load-astria-geth: - @echo "building astria-geth local docker image..." - cd ../astria-geth && docker buildx build -f Dockerfile -t ghcr.io/astriaorg/astria-geth:local . - just load-image ghcr.io/astriaorg/astria-geth:local - -# load astria-go and astria-geth images into cluster -load-images: - #just load-image ghcr.io/astriaorg/astria-go:local - just load-image ghcr.io/astriaorg/bridge-tester-utility:local - just load-image ghcr.io/astriaorg/astria-geth:local diff --git a/dev/containerfiles/bridgetesterutility.Dockerfile b/dev/containerfiles/bridgetesterutility.Dockerfile deleted file mode 100644 index 911c187e8c..0000000000 --- a/dev/containerfiles/bridgetesterutility.Dockerfile +++ /dev/null @@ -1,44 +0,0 @@ -FROM ubuntu:22.04 - -# This is a utility image for testing the bridge. -# It contains the Celestia app and the Astria CLI, plus some bash utilities for testing. - -# dependencies needed for testing -RUN apt-get update && apt-get install -y --no-install-recommends \ - curl \ - bc \ - jq \ - sed \ - ca-certificates \ - coreutils \ - && rm -rf /var/lib/apt/lists/* - -WORKDIR /home - -ARG CELESTIA_VERSION=v1.9.0 -ARG ASTRIA_VERSION=v0.12.0 - -# download architecture-specific binaries -ARG TARGETPLATFORM -RUN echo "TARGETPLATFORM: $TARGETPLATFORM" -RUN if [ "$TARGETPLATFORM" = "darwin/arm64" ]; then \ - curl -L "https://github.com/celestiaorg/celestia-app/releases/download/$CELESTIA_VERSION/celestia-app_Darwin_arm64.tar.gz" -o celestia-appd.tar.gz; \ - curl -L "https://github.com/astriaorg/astria-cli-go/releases/download/$ASTRIA_VERSION/astria-go-$ASTRIA_VERSION-darwin-arm64.tar.gz" -o astria-go.tar.gz; \ - elif [ "$TARGETPLATFORM" = "darwin/amd64" ]; then \ - curl -L "https://github.com/celestiaorg/celestia-app/releases/download/$CELESTIA_VERSION/celestia-app_Darwin_x86_64.tar.gz" -o celestia-appd.tar.gz; \ - curl -L "https://github.com/astriaorg/astria-cli-go/releases/download/$ASTRIA_VERSION/astria-go-$ASTRIA_VERSION-darwin-amd64.tar.gz" -o astria-go.tar.gz; \ - elif [ "$TARGETPLATFORM" = "linux/amd64" ]; then \ - curl -L "https://github.com/celestiaorg/celestia-app/releases/download/$CELESTIA_VERSION/celestia-app_Linux_x86_64.tar.gz" -o celestia-appd.tar.gz; \ - curl -L "https://github.com/astriaorg/astria-cli-go/releases/download/$ASTRIA_VERSION/astria-go-$ASTRIA_VERSION-linux-amd64.tar.gz" -o astria-go.tar.gz; \ - else \ - echo "Unsupported architecture"; \ - echo "TARGETPLATFORM: $TARGETPLATFORM"; \ - exit 1; \ - fi - -# untar and move to bin -RUN tar -xzvf celestia-appd.tar.gz && mv celestia-appd /usr/local/bin/celestia-appd && \ - tar -xzvf astria-go.tar.gz && mv astria-go /usr/local/bin/astria-go && \ - chmod +x /usr/local/bin/celestia-appd /usr/local/bin/astria-go - -CMD ["echo", "This is the bridge tester utility image!"] diff --git a/justfile b/justfile index e6fc5e91f5..4941477b47 100644 --- a/justfile +++ b/justfile @@ -1,5 +1,4 @@ import 'charts/deploy.just' -import 'dev/bridgetester.just' default: @just --list @@ -14,6 +13,22 @@ docker-build crate tag=default_docker_tag: install-cli: cargo install --path ./crates/astria-cli --locked +# version is a celestia-app release. Example: v1.9.0 +# operatingSystem is Linux or Darwin +# machineHardwareName is arm64 or x86_64 +celestia_default_appd_dst := "." +get-celestia-appd version operatingSystem machineHardwareName dst=celestia_default_appd_dst: + #!/usr/bin/env bash + src="celestia-app_{{operatingSystem}}_{{machineHardwareName}}.tar.gz" + curl -LOsS -q --output-dir "{{dst}}" \ + https://github.com/celestiaorg/celestia-app/releases/download/{{version}}/"$src" + curl -LOsS -q --output-dir "{{dst}}" \ + https://github.com/celestiaorg/celestia-app/releases/download/{{version}}/checksums.txt + cd "{{dst}}" + sha256sum --ignore-missing -c checksums.txt + cd - + tar --directory "{{dst}}" -xvzf "{{dst}}"/"$src" celestia-appd + # Compiles the generated rust code from protos which are used in crates. compile-protos: cargo run --manifest-path tools/protobuf-compiler/Cargo.toml diff --git a/proto/protocolapis/astria/protocol/memos/v1alpha1/types.proto b/proto/protocolapis/astria/protocol/memos/v1alpha1/types.proto new file mode 100644 index 0000000000..4e8b2e4936 --- /dev/null +++ b/proto/protocolapis/astria/protocol/memos/v1alpha1/types.proto @@ -0,0 +1,56 @@ +// Memo types that are intended to only be serialized as plaintext JSON and encoded +// as binary/protobuf. The intent is to follow the IBC convention of keeping memos as +// JSON. The specific JSON formatting should follows the pbjson mapping. +// +// XXX: Different from protobuf any changes in the field names is protocol breaking, +// because these messages are serialized as plaintext JSON. + +syntax = "proto3"; + +package astria.protocol.memos.v1alpha1; + +message BridgeUnlock { + // The block number on the rollup that triggered the transaction underlying + // this bridge unlock memo. + uint64 rollup_block_number = 1; + // The hash of the original rollup transaction that triggered a bridge unlock + // and that is underlying this bridge unlock memo. + // + // This field is of type `string` so that it can be formatted in the preferred + // format of the rollup when targeting plain text encoding. + string rollup_transaction_hash = 2; +} + +// Memo for an ICS20 withdrawal from the rollup which is sent to +// an external IBC-enabled chain. +message Ics20WithdrawalFromRollup { + // The block number on the rollup that triggered the transaction underlying + // this ics20 withdrawal memo. + uint64 rollup_block_number = 1; + // The hash of the original rollup transaction that triggered this ics20 + // withdrawal and that is underlying this bridge unlock memo. + // + // This field is of type `string` so that it can be formatted in the preferred + // format of the rollup when targeting plain text encoding. + string rollup_transaction_hash = 2; + // The return address on the rollup to which funds should returned in case of + // failure. This field exists so that the rollup can identify which account + // the returned funds originated from. + // + // This field is of type `string` so that it can be formatted in the preferred + // format of the rollup when targeting plain text encoding. + string rollup_return_address = 3; + // A field that can be populated by the rollup. It is assumed that this field + // will be consumed by the downstream chain. + string memo = 4; +} + +// Memo for an ICS20 transfer to Astria which is sent to a +// bridge account, which will then be deposited into the rollup. +message Ics20TransferDeposit { + // The destination address for the deposit on the rollup. + // + // This field is of type `string` so that it can be formatted in the preferred + // format of the rollup when targeting plain text encoding. + string rollup_deposit_address = 1; +}