From 80b8cf9108d5ec772122474cfe342b16f1bd7169 Mon Sep 17 00:00:00 2001 From: quasystaty Date: Mon, 14 Oct 2024 15:24:13 +0200 Subject: [PATCH 1/4] fix(charts): rollup application monitoring (#1601) ## Summary Fixes prometheus rules and metrics templates for `geth`, `conductor`, `bridge-withdrawer` and `composer` charts ## Background ServiceMonitors for alerting and monitoring is missing on charts ## Changes - adds service monitor and prometheus rules templating for composer, conductor and bridge-withdrawer - fix service labels and selectors for all charts - adds default labels ## Testing against dusk-10 ## Metrics - enables metrics and alerts on rollup apllications ## Related Issues closes #1600 --- charts/composer/Chart.yaml | 2 +- charts/composer/templates/_helpers.tpl | 21 +++++++++++++++++ charts/composer/templates/deployment.yaml | 2 +- charts/composer/templates/service.yaml | 6 ++--- charts/composer/templates/servicemonitor.yaml | 2 +- charts/composer/values.yaml | 23 +++++++++++++++++++ charts/evm-bridge-withdrawer/Chart.yaml | 2 +- .../templates/_helpers.tpl | 21 +++++++++++++++++ .../templates/prometheusrule.yaml | 20 ++++++++++++++++ .../templates/service.yaml | 4 +++- .../templates/servicemonitor.yaml | 2 +- charts/evm-bridge-withdrawer/values.yaml | 23 +++++++++++++++++++ charts/evm-rollup/Chart.yaml | 2 +- .../evm-rollup/templates/prometheusrule.yaml | 2 +- charts/evm-stack/Chart.lock | 10 ++++---- charts/evm-stack/Chart.yaml | 8 +++---- 16 files changed, 130 insertions(+), 20 deletions(-) create mode 100644 charts/evm-bridge-withdrawer/templates/prometheusrule.yaml diff --git a/charts/composer/Chart.yaml b/charts/composer/Chart.yaml index a5e2ac72fe..26b0666774 100644 --- a/charts/composer/Chart.yaml +++ b/charts/composer/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: 0.1.4 +version: 0.1.5 # 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 diff --git a/charts/composer/templates/_helpers.tpl b/charts/composer/templates/_helpers.tpl index 5d11460167..6e45bc120a 100644 --- a/charts/composer/templates/_helpers.tpl +++ b/charts/composer/templates/_helpers.tpl @@ -5,6 +5,27 @@ Namepsace to deploy elements into. {{- default .Release.Namespace .Values.global.namespaceOverride | trunc 63 | trimSuffix "-" -}} {{- end }} +{{/* +application name to deploy elements into. +*/}} +{{- define "composer.appName" -}} +composer +{{- end }} + +{{/* +Common labels +*/}} +{{- define "composer.labels" -}} +{{ include "rollup.selectorLabels" . }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "composer.selectorLabels" -}} +app: {{ include "rollup.appName" . }} +{{- end }} + {{/* Single entry of rollup names */}} diff --git a/charts/composer/templates/deployment.yaml b/charts/composer/templates/deployment.yaml index 24a19a8759..118356dc84 100644 --- a/charts/composer/templates/deployment.yaml +++ b/charts/composer/templates/deployment.yaml @@ -39,7 +39,7 @@ spec: ports: {{- if .Values.metrics.enabled }} - containerPort: {{ .Values.ports.metrics }} - name: metrics + name: composer-metrics {{- end }} - containerPort: {{ .Values.ports.healthApi }} name: health-api diff --git a/charts/composer/templates/service.yaml b/charts/composer/templates/service.yaml index cf317f8330..b03e325407 100644 --- a/charts/composer/templates/service.yaml +++ b/charts/composer/templates/service.yaml @@ -15,7 +15,7 @@ spec: kind: Service apiVersion: v1 metadata: - name: metrics + name: composer-metrics namespace: {{ include "composer.namespace" . }} labels: app: composer @@ -23,7 +23,7 @@ spec: selector: app: composer ports: - - name: metrics + - name: composer-metrics port: {{ .Values.ports.metrics }} - targetPort: geth-metr + targetPort: composer-metrics {{- end }} diff --git a/charts/composer/templates/servicemonitor.yaml b/charts/composer/templates/servicemonitor.yaml index aa3e4c05b9..b88fc369c1 100644 --- a/charts/composer/templates/servicemonitor.yaml +++ b/charts/composer/templates/servicemonitor.yaml @@ -16,7 +16,7 @@ spec: matchLabels: app: composer endpoints: - - port: metrics + - port: composer-metrics path: / {{- with .Values.serviceMonitor.interval }} interval: {{ . }} diff --git a/charts/composer/values.yaml b/charts/composer/values.yaml index 1aec56c3e1..95e79f9185 100644 --- a/charts/composer/values.yaml +++ b/charts/composer/values.yaml @@ -50,6 +50,29 @@ serviceMonitor: additionalLabels: release: kube-prometheus-stack +alerting: + enabled: false + interval: "" + additionalLabels: + release: kube-prometheus-stack + annotations: {} + # scrapeTimeout: 10s + # path: /metrics + prometheusRule: + enabled: true + additionalLabels: + release: kube-prometheus-stack + namespace: monitoring + rules: + - alert: Composer_Node_Down + expr: up{container="composer"} == 0 # Insert your query Expression + for: 1m # Rough number but should be enough to init warn + labels: + severity: warning + annotations: + summary: Composer is Down (instance {{ $labels.instance }}) + description: "composer node '{{ $labels.namespace }}' has disappeared from Prometheus target discovery.\n VALUE = {{ $value }}\n LABELS = {{ $labels }}" + # When deploying in a production environment should use a secret provider # This is configured for use with GCP, need to set own resource names # and keys diff --git a/charts/evm-bridge-withdrawer/Chart.yaml b/charts/evm-bridge-withdrawer/Chart.yaml index 096054476b..667ef89c1a 100644 --- a/charts/evm-bridge-withdrawer/Chart.yaml +++ b/charts/evm-bridge-withdrawer/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: 0.3.0 +version: 0.3.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 diff --git a/charts/evm-bridge-withdrawer/templates/_helpers.tpl b/charts/evm-bridge-withdrawer/templates/_helpers.tpl index 84c9085ebd..b4d5894371 100644 --- a/charts/evm-bridge-withdrawer/templates/_helpers.tpl +++ b/charts/evm-bridge-withdrawer/templates/_helpers.tpl @@ -8,3 +8,24 @@ Namepsace to deploy elements into. {{- define "evm-bridge-withdrawer.image" -}} {{ .Values.images.evmBridgeWithdrawer.repo }}:{{ if .Values.global.dev }}{{ .Values.images.evmBridgeWithdrawer.devTag }}{{ else }}{{ .Values.images.evmBridgeWithdrawer.tag }}{{ end }} {{- end }} + +{{/* +application name to deploy elements into. +*/}} +{{- define "evm-bridge-withdrawer.appName" -}} +evm-bridge-withdrawer +{{- end }} + +{{/* +Common labels +*/}} +{{- define "evm-bridge-withdrawer.labels" -}} +{{ include "evm-bridge-withdrawer.selectorLabels" . }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "evm-bridge-withdrawer.selectorLabels" -}} +app: {{ include "evm-bridge-withdrawer.appName" . }} +{{- end }} diff --git a/charts/evm-bridge-withdrawer/templates/prometheusrule.yaml b/charts/evm-bridge-withdrawer/templates/prometheusrule.yaml new file mode 100644 index 0000000000..7d746c9074 --- /dev/null +++ b/charts/evm-bridge-withdrawer/templates/prometheusrule.yaml @@ -0,0 +1,20 @@ +{{- if .Values.alerting.enabled -}} +apiVersion: monitoring.coreos.com/v1 +kind: PrometheusRule +metadata: + name: evm-bridge-withdrawer-alerting +{{- if .Values.alerting.prometheusRule.namespace }} + namespace: {{ .Values.alerting.prometheusRule.namespace | quote }} +{{- end }} + labels: + {{- include "evm-bridge-withdrawer.labels" . | nindent 4 }} + {{- if .Values.alerting.prometheusRule.additionalLabels }} + {{- toYaml .Values.alerting.prometheusRule.additionalLabels | nindent 4 }} + {{- end }} +spec: +{{- if .Values.alerting.prometheusRule.rules }} + groups: + - name: {{ template "evm-bridge-withdrawer.appName" . }} + rules: {{- toYaml .Values.alerting.prometheusRule.rules | nindent 4 }} +{{- end }} +{{- end }} diff --git a/charts/evm-bridge-withdrawer/templates/service.yaml b/charts/evm-bridge-withdrawer/templates/service.yaml index 6f16cc4540..c66a097ba4 100644 --- a/charts/evm-bridge-withdrawer/templates/service.yaml +++ b/charts/evm-bridge-withdrawer/templates/service.yaml @@ -4,9 +4,11 @@ apiVersion: v1 metadata: name: evm-bridge-withdrawer-metrics namespace: {{ include "evm-bridge-withdrawer.namespace" . }} + labels: + {{- include "evm-bridge-withdrawer.labels" . | nindent 4 }} spec: selector: - app: evm-bridge-withdrawer + {{- include "evm-bridge-withdrawer.labels" . | nindent 4 }} ports: {{- if .Values.metrics.enabled }} - name: metrics diff --git a/charts/evm-bridge-withdrawer/templates/servicemonitor.yaml b/charts/evm-bridge-withdrawer/templates/servicemonitor.yaml index dace167a38..e196d5e545 100644 --- a/charts/evm-bridge-withdrawer/templates/servicemonitor.yaml +++ b/charts/evm-bridge-withdrawer/templates/servicemonitor.yaml @@ -17,7 +17,7 @@ spec: matchLabels: app: evm-bridge-withdrawer endpoints: - - port: metric + - port: metrics path: / {{- with .Values.serviceMonitor.interval }} interval: {{ . }} diff --git a/charts/evm-bridge-withdrawer/values.yaml b/charts/evm-bridge-withdrawer/values.yaml index a84d3e951a..62a723a77e 100644 --- a/charts/evm-bridge-withdrawer/values.yaml +++ b/charts/evm-bridge-withdrawer/values.yaml @@ -54,6 +54,29 @@ serviceMonitor: additionalLabels: release: kube-prometheus-stack +alerting: + enabled: false + interval: "" + additionalLabels: + release: kube-prometheus-stack + annotations: {} + # scrapeTimeout: 10s + # path: /metrics + prometheusRule: + enabled: true + additionalLabels: + release: kube-prometheus-stack + namespace: monitoring + rules: + - alert: Bridge_Withdrawer_Down + expr: up{container="evm-bridge-withdrawer"} == 0 # Insert your query Expression + for: 1m # Rough number but should be enough to init warn + labels: + severity: warning + annotations: + summary: Bridge Withdrawer is Down (instance {{ $labels.instance }}) + description: "bridge withdrawer node '{{ $labels.namespace }}' has disappeared from Prometheus target discovery.\n VALUE = {{ $value }}\n LABELS = {{ $labels }}" + # When deploying in a production environment should use a secret provider # This is configured for use with GCP, need to set own resource names # and keys diff --git a/charts/evm-rollup/Chart.yaml b/charts/evm-rollup/Chart.yaml index 9222e2df02..44489a1b38 100644 --- a/charts/evm-rollup/Chart.yaml +++ b/charts/evm-rollup/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: 0.27.5 +version: 0.27.6 # 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 diff --git a/charts/evm-rollup/templates/prometheusrule.yaml b/charts/evm-rollup/templates/prometheusrule.yaml index 128a52f883..225805dd51 100644 --- a/charts/evm-rollup/templates/prometheusrule.yaml +++ b/charts/evm-rollup/templates/prometheusrule.yaml @@ -12,7 +12,7 @@ metadata: {{- toYaml .Values.alerting.prometheusRule.additionalLabels | nindent 4 }} {{- end }} spec: -{{- if .Values.config.rollup.alerting.prometheusRule.rules }} +{{- if .Values.alerting.prometheusRule.rules }} groups: - name: {{ template "rollup.name" . }} rules: {{- toYaml .Values.alerting.prometheusRule.rules | nindent 4 }} diff --git a/charts/evm-stack/Chart.lock b/charts/evm-stack/Chart.lock index 7b103a22aa..59d9b0d782 100644 --- a/charts/evm-stack/Chart.lock +++ b/charts/evm-stack/Chart.lock @@ -4,21 +4,21 @@ dependencies: version: 0.3.6 - name: evm-rollup repository: file://../evm-rollup - version: 0.27.5 + version: 0.27.6 - name: composer repository: file://../composer - version: 0.1.4 + version: 0.1.5 - name: evm-faucet repository: file://../evm-faucet version: 0.1.2 - name: evm-bridge-withdrawer repository: file://../evm-bridge-withdrawer - version: 0.3.0 + version: 0.3.1 - name: postgresql repository: https://charts.bitnami.com/bitnami version: 15.2.4 - name: blockscout-stack repository: https://blockscout.github.io/helm-charts version: 1.6.2 -digest: sha256:2d7e2f23cd9bbdb43b7cf42112db9ede0a7d5eee9e426b0b2344e43fcf52e1b1 -generated: "2024-10-02T09:43:51.238571-04:00" +digest: sha256:0428a6d56fd86c170e322ad79c7b5f87628b6187a1df5ad47ae7c2281b7f12da +generated: "2024-10-14T15:11:45.153501+02:00" diff --git a/charts/evm-stack/Chart.yaml b/charts/evm-stack/Chart.yaml index e2cf556597..b208660420 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: 0.6.2 +version: 0.6.3 dependencies: - name: celestia-node @@ -23,10 +23,10 @@ dependencies: repository: "file://../celestia-node" condition: celestia-node.enabled - name: evm-rollup - version: 0.27.5 + version: 0.27.6 repository: "file://../evm-rollup" - name: composer - version: 0.1.4 + version: 0.1.5 repository: "file://../composer" condition: composer.enabled - name: evm-faucet @@ -34,7 +34,7 @@ dependencies: repository: "file://../evm-faucet" condition: evm-faucet.enabled - name: evm-bridge-withdrawer - version: 0.3.0 + version: 0.3.1 repository: "file://../evm-bridge-withdrawer" condition: evm-bridge-withdrawer.enabled - name: postgresql From acfd3703186efd3a345e3a10e9b8bc7af1becaf0 Mon Sep 17 00:00:00 2001 From: Lily Johnson <35852084+Lilyjjo@users.noreply.github.com> Date: Mon, 14 Oct 2024 16:49:27 +0200 Subject: [PATCH 2/4] fix(composer)!: update to work with appside mempool (#1643) ## Summary Updates composer to work with the new appside mempool. New ABCI error codes were added and now nonces can be stacked, so the submission logic should use the GRPC `pending_nonce()` instead of the RPC `get_latest_nonce()`. ## Background New ABCI error codes were added in PR https://github.com/astriaorg/astria/pull/1515 which need to be handled in the composer's submission logic. New Error codes: - `NONCE_TAKEN` -> try resubmitting with new top-of-pending. If this is happening because the composer is out of funds, that is okay as the composer has it's own fund monitoring logic - `PARKED_FULL` -> normal error case ok - `ACCOUNT_SIZE_LIMIT` -> normal error case ok - `ALREADY_PRESENT` -> normal error case ok, shouldn't be returned ## Background We changed the internals of the mempool which resulted in the users of the mempool to have changes as well. ## Testing Smoke tests ## Related Issues closes #1633 --------- Co-authored-by: quasystaty --- Cargo.lock | 1 + charts/composer/Chart.yaml | 2 +- charts/composer/templates/configmap.yaml | 4 +- charts/composer/values.yaml | 1 + charts/evm-stack/Chart.lock | 6 +- charts/evm-stack/Chart.yaml | 4 +- crates/astria-composer/Cargo.toml | 7 +- crates/astria-composer/local.env.example | 7 +- crates/astria-composer/src/composer.rs | 3 +- crates/astria-composer/src/config.rs | 7 +- .../astria-composer/src/executor/builder.rs | 31 +- crates/astria-composer/src/executor/mod.rs | 73 +- crates/astria-composer/src/executor/tests.rs | 647 ------------------ .../tests/blackbox/geth_collector.rs | 91 +-- .../tests/blackbox/grpc_collector.rs | 52 +- ...ck_sequencer.rs => mock_abci_sequencer.rs} | 53 +- .../blackbox/helper/mock_grpc_sequencer.rs | 122 ++++ .../tests/blackbox/helper/mod.rs | 51 +- 18 files changed, 326 insertions(+), 836 deletions(-) delete mode 100644 crates/astria-composer/src/executor/tests.rs rename crates/astria-composer/tests/blackbox/helper/{mock_sequencer.rs => mock_abci_sequencer.rs} (63%) create mode 100644 crates/astria-composer/tests/blackbox/helper/mock_grpc_sequencer.rs diff --git a/Cargo.lock b/Cargo.lock index 063e3a9d2b..74eec93164 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -593,6 +593,7 @@ dependencies = [ "astria-config", "astria-core", "astria-eyre", + "astria-grpc-mock", "astria-sequencer-client", "astria-telemetry", "astria-test-utils", diff --git a/charts/composer/Chart.yaml b/charts/composer/Chart.yaml index 26b0666774..dd68073389 100644 --- a/charts/composer/Chart.yaml +++ b/charts/composer/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: 0.1.5 +version: 0.1.6 # 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 diff --git a/charts/composer/templates/configmap.yaml b/charts/composer/templates/configmap.yaml index 3d9b13a739..d11e419eaf 100644 --- a/charts/composer/templates/configmap.yaml +++ b/charts/composer/templates/configmap.yaml @@ -8,7 +8,6 @@ data: ASTRIA_COMPOSER_API_LISTEN_ADDR: "0.0.0.0:{{ .Values.ports.healthApi }}" ASTRIA_COMPOSER_GRPC_ADDR: "0.0.0.0:{{ .Values.ports.grpc }}" ASTRIA_COMPOSER_SEQUENCER_CHAIN_ID: "{{ tpl .Values.config.sequencerChainId . }}" - ASTRIA_COMPOSER_SEQUENCER_URL: "{{ tpl .Values.config.sequencerRpc . }}" ASTRIA_COMPOSER_ROLLUPS: "{{ include "composer.rollups" . }}" ASTRIA_COMPOSER_PRIVATE_KEY_FILE: "/var/secrets/{{ .Values.config.privateKey.secret.filename }}" ASTRIA_COMPOSER_MAX_BYTES_PER_BUNDLE: "{{ .Values.config.maxBytesPerBundle }}" @@ -30,7 +29,10 @@ data: OTEL_EXPORTER_OTLP_TRACE_HEADERS: "{{ tpl .Values.otel.traceHeaders . }}" OTEL_SERVICE_NAME: "{{ tpl .Values.otel.serviceName . }}" {{- if not .Values.global.dev }} + ASTRIA_COMPOSER_SEQUENCER_URL: "{{ tpl .Values.config.sequencerRpc . }}" {{- else }} + ASTRIA_COMPOSER_SEQUENCER_ABCI_ENDPOINT: "{{ tpl .Values.config.sequencerRpc . }}" + ASTRIA_COMPOSER_SEQUENCER_GRPC_ENDPOINT: "{{ tpl .Values.config.sequencerGrpc . }}" {{- end }} --- {{- if not .Values.secretProvider.enabled }} diff --git a/charts/composer/values.yaml b/charts/composer/values.yaml index 95e79f9185..7dd3f3ec1a 100644 --- a/charts/composer/values.yaml +++ b/charts/composer/values.yaml @@ -20,6 +20,7 @@ config: sequencerAddressPrefix: astria sequencerNativeAssetBaseDenomination: "nria" sequencerRpc: "" + sequencerGrpc: "" sequencerChainId: "" privateKey: devContent: "" diff --git a/charts/evm-stack/Chart.lock b/charts/evm-stack/Chart.lock index 59d9b0d782..f9ed82cec8 100644 --- a/charts/evm-stack/Chart.lock +++ b/charts/evm-stack/Chart.lock @@ -7,7 +7,7 @@ dependencies: version: 0.27.6 - name: composer repository: file://../composer - version: 0.1.5 + version: 0.1.6 - name: evm-faucet repository: file://../evm-faucet version: 0.1.2 @@ -20,5 +20,5 @@ dependencies: - name: blockscout-stack repository: https://blockscout.github.io/helm-charts version: 1.6.2 -digest: sha256:0428a6d56fd86c170e322ad79c7b5f87628b6187a1df5ad47ae7c2281b7f12da -generated: "2024-10-14T15:11:45.153501+02:00" +digest: sha256:80a70740a70f834b6ff6cdcfbb5f4a3504d6963f784ff678d1d52a7284b1dc20 +generated: "2024-10-14T16:04:40.995885+02:00" diff --git a/charts/evm-stack/Chart.yaml b/charts/evm-stack/Chart.yaml index b208660420..24ce1d35c1 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: 0.6.3 +version: 0.6.4 dependencies: - name: celestia-node @@ -26,7 +26,7 @@ dependencies: version: 0.27.6 repository: "file://../evm-rollup" - name: composer - version: 0.1.5 + version: 0.1.6 repository: "file://../composer" condition: composer.enabled - name: evm-faucet diff --git a/crates/astria-composer/Cargo.toml b/crates/astria-composer/Cargo.toml index c17567aaac..7039b15af1 100644 --- a/crates/astria-composer/Cargo.toml +++ b/crates/astria-composer/Cargo.toml @@ -13,7 +13,11 @@ name = "astria-composer" [dependencies] astria-build-info = { path = "../astria-build-info", features = ["runtime"] } -astria-core = { path = "../astria-core", features = ["serde", "server"] } +astria-core = { path = "../astria-core", features = [ + "client", + "serde", + "server", +] } astria-eyre = { path = "../astria-eyre" } config = { package = "astria-config", path = "../astria-config" } telemetry = { package = "astria-telemetry", path = "../astria-telemetry", features = [ @@ -59,6 +63,7 @@ path = "../astria-sequencer-client" features = ["http"] [dev-dependencies] +astria-grpc-mock = { path = "../astria-grpc-mock" } config = { package = "astria-config", path = "../astria-config", features = [ "tests", ] } diff --git a/crates/astria-composer/local.env.example b/crates/astria-composer/local.env.example index 8936089de1..eb37561857 100644 --- a/crates/astria-composer/local.env.example +++ b/crates/astria-composer/local.env.example @@ -24,8 +24,11 @@ NO_COLOR= # Address of the API server ASTRIA_COMPOSER_API_LISTEN_ADDR="0.0.0.0:0" -# Address of the RPC server for the sequencer chain -ASTRIA_COMPOSER_SEQUENCER_URL="http://127.0.0.1:26657" +# Address of the ABCI server for the sequencer chain +ASTRIA_COMPOSER_SEQUENCER_ABCI_ENDPOINT="http://127.0.0.1:26657" + +# Address of the gRPC server for the sequencer chain +ASTRIA_COMPOSER_SEQUENCER_GRPC_ENDPOINT="http://127.0.0.1:8080" # Chain ID of the sequencer chain which transactions are submitted to. ASTRIA_COMPOSER_SEQUENCER_CHAIN_ID="astria-dev-1" diff --git a/crates/astria-composer/src/composer.rs b/crates/astria-composer/src/composer.rs index dc9ab4ce17..4a6e5b774d 100644 --- a/crates/astria-composer/src/composer.rs +++ b/crates/astria-composer/src/composer.rs @@ -124,7 +124,8 @@ impl Composer { let shutdown_token = CancellationToken::new(); let (executor, executor_handle) = executor::Builder { - sequencer_url: cfg.sequencer_url.clone(), + sequencer_abci_endpoint: cfg.sequencer_abci_endpoint.clone(), + sequencer_grpc_endpoint: cfg.sequencer_grpc_endpoint.clone(), sequencer_chain_id: cfg.sequencer_chain_id.clone(), private_key_file: cfg.private_key_file.clone(), sequencer_address_prefix: cfg.sequencer_address_prefix.clone(), diff --git a/crates/astria-composer/src/config.rs b/crates/astria-composer/src/config.rs index b3f127d9ab..227dd2c2a6 100644 --- a/crates/astria-composer/src/config.rs +++ b/crates/astria-composer/src/config.rs @@ -26,8 +26,11 @@ pub struct Config { /// Address of the API server pub api_listen_addr: SocketAddr, - /// Address of the RPC server for the sequencer chain - pub sequencer_url: String, + /// Address of the ABCI server for the sequencer chain + pub sequencer_abci_endpoint: String, + + /// Address of the GRPC server for the sequencer chain + pub sequencer_grpc_endpoint: String, /// The chain ID of the sequencer chain pub sequencer_chain_id: String, diff --git a/crates/astria-composer/src/executor/builder.rs b/crates/astria-composer/src/executor/builder.rs index b84ef794d6..e8b1e972b7 100644 --- a/crates/astria-composer/src/executor/builder.rs +++ b/crates/astria-composer/src/executor/builder.rs @@ -6,6 +6,7 @@ use std::{ use astria_core::{ crypto::SigningKey, + generated::sequencerblock::v1alpha1::sequencer_service_client::SequencerServiceClient, primitive::v1::Address, protocol::transaction::v1alpha1::action::Sequence, }; @@ -24,7 +25,8 @@ use crate::{ }; pub(crate) struct Builder { - pub(crate) sequencer_url: String, + pub(crate) sequencer_abci_endpoint: String, + pub(crate) sequencer_grpc_endpoint: String, pub(crate) sequencer_chain_id: String, pub(crate) private_key_file: String, pub(crate) sequencer_address_prefix: String, @@ -38,7 +40,8 @@ pub(crate) struct Builder { impl Builder { pub(crate) fn build(self) -> eyre::Result<(super::Executor, executor::Handle)> { let Self { - sequencer_url, + sequencer_abci_endpoint, + sequencer_grpc_endpoint, sequencer_chain_id, private_key_file, sequencer_address_prefix, @@ -48,8 +51,14 @@ impl Builder { shutdown_token, metrics, } = self; - let sequencer_client = sequencer_client::HttpClient::new(sequencer_url.as_str()) - .wrap_err("failed constructing sequencer client")?; + let abci_client = sequencer_client::HttpClient::new(sequencer_abci_endpoint.as_str()) + .wrap_err("failed constructing sequencer http client")?; + + let grpc_client = + connect_sequencer_grpc(sequencer_grpc_endpoint.as_str()).wrap_err_with(|| { + format!("failed to connect to sequencer over gRPC at `{sequencer_grpc_endpoint}`") + })?; + let (status, _) = watch::channel(Status::new()); let sequencer_key = read_signing_key_from_file(&private_key_file).wrap_err_with(|| { @@ -69,7 +78,8 @@ impl Builder { super::Executor { status, serialized_rollup_transactions: serialized_rollup_transaction_rx, - sequencer_client, + abci_client, + grpc_client, sequencer_chain_id, sequencer_key, address: sequencer_address, @@ -91,3 +101,14 @@ fn read_signing_key_from_file>(path: P) -> eyre::Result eyre::Result> { + let uri: tonic::transport::Uri = grpc_endpoint + .parse() + .wrap_err("failed to parse endpoint as URI")?; + Ok(SequencerServiceClient::new( + tonic::transport::Endpoint::from(uri).connect_lazy(), + )) +} diff --git a/crates/astria-composer/src/executor/mod.rs b/crates/astria-composer/src/executor/mod.rs index f1512d43b7..45b0efdc22 100644 --- a/crates/astria-composer/src/executor/mod.rs +++ b/crates/astria-composer/src/executor/mod.rs @@ -12,6 +12,13 @@ use std::{ use astria_core::{ crypto::SigningKey, + generated::sequencerblock::v1alpha1::{ + sequencer_service_client::{ + self, + SequencerServiceClient, + }, + GetPendingNonceRequest, + }, protocol::{ abci::AbciErrorCode, transaction::v1alpha1::{ @@ -64,6 +71,7 @@ use tokio::{ }, }; use tokio_util::sync::CancellationToken; +use tonic::transport::Channel; use tracing::{ debug, error, @@ -89,8 +97,6 @@ use crate::{ mod bundle_factory; pub(crate) mod builder; -#[cfg(test)] -mod tests; pub(crate) use builder::Builder; @@ -122,8 +128,10 @@ pub(super) struct Executor { // Channel for receiving `SequenceAction`s to be bundled. serialized_rollup_transactions: mpsc::Receiver, // The client for submitting wrapped and signed pending eth transactions to the astria - // sequencer. - sequencer_client: sequencer_client::HttpClient, + // sequencer via the ABCI client. + abci_client: sequencer_client::HttpClient, + // The grpc client for grabbing the latest nonce from. + grpc_client: sequencer_service_client::SequencerServiceClient, // The chain id used for submission of transactions to the sequencer. sequencer_chain_id: String, // Private key used to sign sequencer transactions @@ -197,7 +205,8 @@ impl Executor { metrics: &'static Metrics, ) -> Fuse> { SubmitFut { - client: self.sequencer_client.clone(), + abci_client: self.abci_client.clone(), + grpc_client: self.grpc_client.clone(), address: self.address, nonce, chain_id: self.sequencer_chain_id.clone(), @@ -332,7 +341,7 @@ impl Executor { self.ensure_chain_id_is_correct() .await .wrap_err("failed to validate chain id")?; - let nonce = get_latest_nonce(self.sequencer_client.clone(), self.address, self.metrics) + let nonce = get_pending_nonce(self.grpc_client.clone(), self.address, self.metrics) .await .wrap_err("failed getting initial nonce from sequencer")?; Ok(nonce) @@ -378,10 +387,9 @@ impl Executor { futures::future::ready(()) }, ); - let client_genesis: tendermint::Genesis = - tryhard::retry_fn(|| self.sequencer_client.genesis()) - .with_config(retry_config) - .await?; + let client_genesis: tendermint::Genesis = tryhard::retry_fn(|| self.abci_client.genesis()) + .with_config(retry_config) + .await?; Ok(client_genesis.chain_id) } @@ -460,23 +468,21 @@ impl Executor { } } -/// Queries the sequencer for the latest nonce with an exponential backoff -#[instrument(name = "get latest nonce", skip_all, fields(%address), err)] -async fn get_latest_nonce( - client: sequencer_client::HttpClient, +/// Queries the sequencer for the latest pending nonce with an exponential backoff +#[instrument(name = "get pending nonce", skip_all, fields(%address), err)] +async fn get_pending_nonce( + client: sequencer_service_client::SequencerServiceClient, address: Address, metrics: &Metrics, ) -> eyre::Result { - debug!("fetching latest nonce from sequencer"); + debug!("fetching pending nonce from sequencer"); let span = Span::current(); let start = Instant::now(); let retry_config = tryhard::RetryFutureConfig::new(1024) .exponential_backoff(Duration::from_millis(200)) .max_delay(Duration::from_secs(60)) .on_retry( - |attempt, - next_delay: Option, - err: &sequencer_client::extension_trait::Error| { + |attempt, next_delay: Option, err: &tonic::Status| { metrics.increment_nonce_fetch_failure_count(); let wait_duration = next_delay @@ -493,14 +499,22 @@ async fn get_latest_nonce( }, ); let res = tryhard::retry_fn(|| { - let client = client.clone(); - let span = info_span!(parent: span.clone(), "attempt get nonce"); + let mut client = client.clone(); + let span = info_span!(parent: span.clone(), "attempt get pending nonce"); metrics.increment_nonce_fetch_count(); - async move { client.get_latest_nonce(address).await.map(|rsp| rsp.nonce) }.instrument(span) + async move { + client + .get_pending_nonce(GetPendingNonceRequest { + address: Some(address.into_raw()), + }) + .await + .map(|rsp| rsp.into_inner().inner) + } + .instrument(span) }) .with_config(retry_config) .await - .wrap_err("failed getting latest nonce from sequencer after 1024 attempts"); + .wrap_err("failed getting pending nonce from sequencer after 1024 attempts"); metrics.record_nonce_fetch_latency(start.elapsed()); @@ -637,7 +651,8 @@ pin_project! { /// If the sequencer returned a non-zero abci code (albeit not `INVALID_NONCE`), this future will return with /// that nonce it used to submit the non-zero abci code request. struct SubmitFut { - client: sequencer_client::HttpClient, + abci_client: sequencer_client::HttpClient, + grpc_client: SequencerServiceClient, address: Address, chain_id: String, nonce: u32, @@ -670,6 +685,8 @@ impl Future for SubmitFut { // FIXME (https://github.com/astriaorg/astria/issues/1572): This function is too long and should be refactored. fn poll(mut self: Pin<&mut Self>, cx: &mut std::task::Context<'_>) -> Poll { const INVALID_NONCE: Code = Code::Err(AbciErrorCode::INVALID_NONCE.value()); + const NONCE_TAKEN: Code = Code::Err(AbciErrorCode::NONCE_TAKEN.value()); + loop { let this = self.as_mut().project(); @@ -686,7 +703,7 @@ impl Future for SubmitFut { "submitting transaction to sequencer", ); SubmitState::WaitingForSend { - fut: submit_tx(this.client.clone(), tx, self.metrics).boxed(), + fut: submit_tx(this.abci_client.clone(), tx, self.metrics).boxed(), } } @@ -708,14 +725,14 @@ impl Future for SubmitFut { .checked_add(1) .expect("nonce should not overflow"))); } - INVALID_NONCE => { + INVALID_NONCE | NONCE_TAKEN => { info!( "sequencer rejected transaction due to invalid nonce; fetching \ new nonce" ); SubmitState::WaitingForNonce { - fut: get_latest_nonce( - this.client.clone(), + fut: get_pending_nonce( + this.grpc_client.clone(), *this.address, self.metrics, ) @@ -759,7 +776,7 @@ impl Future for SubmitFut { "resubmitting transaction to sequencer with new nonce", ); SubmitState::WaitingForSend { - fut: submit_tx(this.client.clone(), tx, self.metrics).boxed(), + fut: submit_tx(this.abci_client.clone(), tx, self.metrics).boxed(), } } Err(error) => { diff --git a/crates/astria-composer/src/executor/tests.rs b/crates/astria-composer/src/executor/tests.rs deleted file mode 100644 index 5abb7c6ff7..0000000000 --- a/crates/astria-composer/src/executor/tests.rs +++ /dev/null @@ -1,647 +0,0 @@ -use std::{ - io::Write, - net::{ - IpAddr, - SocketAddr, - }, - sync::LazyLock, - time::Duration, -}; - -use astria_core::{ - generated::protocol::accounts::v1alpha1::NonceResponse, - primitive::v1::{ - asset::{ - Denom, - IbcPrefixed, - }, - RollupId, - ROLLUP_ID_LEN, - }, - protocol::transaction::v1alpha1::action::Sequence, -}; -use astria_eyre::eyre; -use prost::{ - bytes::Bytes, - Message as _, -}; -use sequencer_client::SignedTransaction; -use serde_json::json; -use telemetry::Metrics as _; -use tempfile::NamedTempFile; -use tendermint::{ - consensus::{ - params::{ - AbciParams, - ValidatorParams, - }, - Params, - }, - Genesis, - Time, -}; -use tendermint_rpc::{ - endpoint::broadcast::tx_sync, - request, - response, - Id, -}; -use tokio::{ - sync::watch, - time, -}; -use tokio_util::sync::CancellationToken; -use tracing::debug; -use wiremock::{ - matchers::{ - body_partial_json, - body_string_contains, - }, - Mock, - MockGuard, - MockServer, - Request, - ResponseTemplate, -}; - -use crate::{ - executor, - executor::EnsureChainIdError, - metrics::Metrics, - test_utils::sequence_action_of_max_size, - Config, -}; - -static TELEMETRY: LazyLock<()> = LazyLock::new(|| { - // This config can be meaningless - it's only used inside `try_init` to init the metrics, but we - // haven't configured telemetry to provide metrics here. - let config = Config { - log: String::new(), - api_listen_addr: SocketAddr::new(IpAddr::from([0, 0, 0, 0]), 0), - sequencer_url: String::new(), - sequencer_chain_id: String::new(), - rollups: String::new(), - private_key_file: String::new(), - sequencer_address_prefix: String::new(), - block_time_ms: 0, - max_bytes_per_bundle: 0, - bundle_queue_capacity: 0, - force_stdout: false, - no_otel: false, - no_metrics: false, - metrics_http_listener_addr: String::new(), - pretty_print: false, - grpc_addr: SocketAddr::new(IpAddr::from([0, 0, 0, 0]), 0), - fee_asset: Denom::IbcPrefixed(IbcPrefixed::new([0; 32])), - }; - if std::env::var_os("TEST_LOG").is_some() { - let filter_directives = std::env::var("RUST_LOG").unwrap_or_else(|_| "info".into()); - telemetry::configure() - .set_no_otel(true) - .set_stdout_writer(std::io::stdout) - .set_filter_directives(&filter_directives) - .try_init::(&config) - .unwrap(); - } else { - telemetry::configure() - .set_no_otel(true) - .set_stdout_writer(std::io::sink) - .try_init::(&config) - .unwrap(); - } -}); - -fn sequence_action() -> Sequence { - Sequence { - rollup_id: RollupId::new([0; ROLLUP_ID_LEN]), - data: Bytes::new(), - fee_asset: "nria".parse().unwrap(), - } -} - -/// Start a mock sequencer server and mount a mock for the `accounts/nonce` query. -async fn setup() -> (MockServer, Config, NamedTempFile) { - LazyLock::force(&TELEMETRY); - let server = MockServer::start().await; - - let keyfile = NamedTempFile::new().unwrap(); - (&keyfile) - .write_all("2bd806c97f0e00af1a1fc3328fa763a9269723c8db8fac4f93af71db186d6e90".as_bytes()) - .unwrap(); - - let cfg = Config { - log: String::new(), - api_listen_addr: "127.0.0.1:0".parse().unwrap(), - rollups: String::new(), - sequencer_url: server.uri(), - sequencer_chain_id: "test-chain-1".to_string(), - private_key_file: keyfile.path().to_string_lossy().to_string(), - sequencer_address_prefix: "astria".into(), - block_time_ms: 2000, - max_bytes_per_bundle: 1000, - bundle_queue_capacity: 10, - no_otel: false, - force_stdout: false, - no_metrics: false, - metrics_http_listener_addr: String::new(), - pretty_print: true, - grpc_addr: "127.0.0.1:0".parse().unwrap(), - fee_asset: "nria".parse().unwrap(), - }; - (server, cfg, keyfile) -} - -/// Assert that given error is of correct type and contains the expected chain IDs. -#[track_caller] -fn assert_chain_id_err( - err: &EnsureChainIdError, - configured_expected: &str, - configured_actual: &tendermint::chain::Id, -) { - match err { - EnsureChainIdError::WrongChainId { - expected, - actual, - } => { - assert_eq!(*expected, configured_expected); - assert_eq!(*actual, *configured_actual); - } - other @ EnsureChainIdError::GetChainId(_) => { - panic!("expected `EnsureChainIdError::WrongChainId`, but got '{other:?}'") - } - } -} - -/// Mount a mock for the `abci_query` endpoint. -async fn mount_default_nonce_query_mock(server: &MockServer) -> MockGuard { - let query_path = "accounts/nonce"; - let response = NonceResponse { - height: 0, - nonce: 0, - }; - let expected_body = json!({ - "method": "abci_query" - }); - let response = tendermint_rpc::endpoint::abci_query::Response { - response: tendermint_rpc::endpoint::abci_query::AbciQuery { - value: response.encode_to_vec(), - ..Default::default() - }, - }; - let wrapper = response::Wrapper::new_with_id(Id::Num(1), Some(response), None); - Mock::given(body_partial_json(&expected_body)) - .and(body_string_contains(query_path)) - .respond_with( - ResponseTemplate::new(200) - .set_body_json(&wrapper) - .append_header("Content-Type", "application/json"), - ) - .up_to_n_times(1) - .expect(1) - .mount_as_scoped(server) - .await -} - -/// Convert a `Request` object to a `SignedTransaction` -fn signed_tx_from_request(request: &Request) -> SignedTransaction { - use astria_core::generated::protocol::transactions::v1alpha1::SignedTransaction as RawSignedTransaction; - use prost::Message as _; - - let wrapped_tx_sync_req: request::Wrapper = - serde_json::from_slice(&request.body) - .expect("can't deserialize to JSONRPC wrapped tx_sync::Request"); - let raw_signed_tx = RawSignedTransaction::decode(&*wrapped_tx_sync_req.params().tx) - .expect("can't deserialize signed sequencer tx from broadcast jsonrpc request"); - let signed_tx = SignedTransaction::try_from_raw(raw_signed_tx) - .expect("can't convert raw signed tx to checked signed tx"); - debug!(?signed_tx, "sequencer mock received signed transaction"); - - signed_tx -} - -/// Deserializes the bytes contained in a `tx_sync::Request` to a signed sequencer transaction -/// and verifies that the contained sequence action is in the given `expected_rollup_ids` and -/// `expected_nonces`. -async fn mount_broadcast_tx_sync_seq_actions_mock(server: &MockServer) -> MockGuard { - let matcher = move |request: &Request| { - let signed_tx = signed_tx_from_request(request); - let actions = signed_tx.actions(); - - // verify all received actions are sequence actions - actions.iter().all(|action| action.as_sequence().is_some()) - }; - let jsonrpc_rsp = response::Wrapper::new_with_id( - Id::Num(1), - Some(tx_sync::Response { - code: 0.into(), - data: vec![].into(), - log: String::new(), - hash: tendermint::Hash::Sha256([0; 32]), - }), - None, - ); - - Mock::given(matcher) - .respond_with(ResponseTemplate::new(200).set_body_json(&jsonrpc_rsp)) - .up_to_n_times(1) - .expect(1) - .mount_as_scoped(server) - .await -} - -/// Mounts genesis file with specified sequencer chain ID -async fn mount_genesis(server: &MockServer, mock_sequencer_chain_id: &str) { - Mock::given(body_partial_json( - json!({"jsonrpc": "2.0", "method": "genesis", "params": null}), - )) - .respond_with(ResponseTemplate::new(200).set_body_json( - tendermint_rpc::response::Wrapper::new_with_id( - tendermint_rpc::Id::uuid_v4(), - Some( - tendermint_rpc::endpoint::genesis::Response:: { - genesis: Genesis { - genesis_time: Time::from_unix_timestamp(1, 1).unwrap(), - chain_id: mock_sequencer_chain_id.try_into().unwrap(), - initial_height: 1, - consensus_params: Params { - block: tendermint::block::Size { - max_bytes: 1024, - max_gas: 1024, - time_iota_ms: 1000, - }, - evidence: tendermint::evidence::Params { - max_age_num_blocks: 1000, - max_age_duration: tendermint::evidence::Duration( - Duration::from_secs(3600), - ), - max_bytes: 1_048_576, - }, - validator: ValidatorParams { - pub_key_types: vec![tendermint::public_key::Algorithm::Ed25519], - }, - version: None, - abci: AbciParams::default(), - }, - validators: vec![], - app_hash: tendermint::hash::AppHash::default(), - app_state: serde_json::Value::Null, - }, - }, - ), - None, - ), - )) - .expect(1..) - .mount(server) - .await; -} - -/// Helper to wait for the executor to connect to the mock sequencer -async fn wait_for_startup( - mut status: watch::Receiver, - nonce_guard: MockGuard, -) -> eyre::Result<()> { - // wait to receive executor status - status - .wait_for(executor::Status::is_connected) - .await - .unwrap(); - - tokio::time::timeout( - Duration::from_millis(100), - nonce_guard.wait_until_satisfied(), - ) - .await - .unwrap(); - - Ok(()) -} - -/// Test to check that the executor sends a signed transaction to the sequencer as soon as it -/// receives a `SequenceAction` that fills it beyond its `max_bundle_size`. -#[tokio::test] -async fn full_bundle() { - // set up the executor, channel for writing seq actions, and the sequencer mock - let (sequencer, cfg, _keyfile) = setup().await; - let shutdown_token = CancellationToken::new(); - let metrics = Box::leak(Box::new(Metrics::noop_metrics(&cfg).unwrap())); - mount_genesis(&sequencer, &cfg.sequencer_chain_id).await; - let (executor, executor_handle) = executor::Builder { - sequencer_url: cfg.sequencer_url.clone(), - sequencer_chain_id: cfg.sequencer_chain_id.clone(), - private_key_file: cfg.private_key_file.clone(), - sequencer_address_prefix: "astria".into(), - block_time_ms: cfg.block_time_ms, - max_bytes_per_bundle: cfg.max_bytes_per_bundle, - bundle_queue_capacity: cfg.bundle_queue_capacity, - shutdown_token: shutdown_token.clone(), - metrics, - } - .build() - .unwrap(); - - let nonce_guard = mount_default_nonce_query_mock(&sequencer).await; - let status = executor.subscribe(); - - let _executor_task = tokio::spawn(executor.run_until_stopped()); - // wait for sequencer to get the initial nonce request from sequencer - wait_for_startup(status, nonce_guard).await.unwrap(); - - let response_guard = mount_broadcast_tx_sync_seq_actions_mock(&sequencer).await; - - // send two sequence actions to the executor, the first of which is large enough to fill the - // bundle sending the second should cause the first to immediately be submitted in - // order to make space for the second - let seq0 = sequence_action_of_max_size(cfg.max_bytes_per_bundle); - - let seq1 = Sequence { - rollup_id: RollupId::new([1; ROLLUP_ID_LEN]), - ..sequence_action_of_max_size(cfg.max_bytes_per_bundle) - }; - - // push both sequence actions to the executor in order to force the full bundle to be sent - executor_handle - .send_timeout(seq0.clone(), Duration::from_millis(1000)) - .await - .unwrap(); - executor_handle - .send_timeout(seq1.clone(), Duration::from_millis(1000)) - .await - .unwrap(); - - // wait for the mock sequencer to receive the signed transaction - tokio::time::timeout( - Duration::from_millis(100), - response_guard.wait_until_satisfied(), - ) - .await - .unwrap(); - - // verify only one signed transaction was received by the mock sequencer - // i.e. only the full bundle was sent and not the second one due to the block timer - let expected_seq_actions = [seq0]; - let requests = response_guard.received_requests().await; - assert_eq!(requests.len(), 1); - - // verify the expected sequence actions were received - let signed_tx = signed_tx_from_request(&requests[0]); - let actions = signed_tx.actions(); - - assert_eq!( - actions.len(), - expected_seq_actions.len(), - "received more than one action, one was supposed to fill the bundle" - ); - - for (action, expected_seq_action) in actions.iter().zip(expected_seq_actions.iter()) { - let seq_action = action.as_sequence().unwrap(); - assert_eq!( - seq_action.rollup_id, expected_seq_action.rollup_id, - "chain id does not match. actual {:?} expected {:?}", - seq_action.rollup_id, expected_seq_action.rollup_id - ); - assert_eq!( - seq_action.data, expected_seq_action.data, - "data does not match expected data for action with rollup_id {:?}", - seq_action.rollup_id, - ); - } -} - -/// Test to check that the executor sends a signed transaction to the sequencer after its -/// `block_timer` has ticked -#[tokio::test] -async fn bundle_triggered_by_block_timer() { - // set up the executor, channel for writing seq actions, and the sequencer mock - let (sequencer, cfg, _keyfile) = setup().await; - let shutdown_token = CancellationToken::new(); - let metrics = Box::leak(Box::new(Metrics::noop_metrics(&cfg).unwrap())); - mount_genesis(&sequencer, &cfg.sequencer_chain_id).await; - let (executor, executor_handle) = executor::Builder { - sequencer_url: cfg.sequencer_url.clone(), - sequencer_chain_id: cfg.sequencer_chain_id.clone(), - private_key_file: cfg.private_key_file.clone(), - sequencer_address_prefix: "astria".into(), - block_time_ms: cfg.block_time_ms, - max_bytes_per_bundle: cfg.max_bytes_per_bundle, - bundle_queue_capacity: cfg.bundle_queue_capacity, - shutdown_token: shutdown_token.clone(), - metrics, - } - .build() - .unwrap(); - - let nonce_guard = mount_default_nonce_query_mock(&sequencer).await; - let status = executor.subscribe(); - - let _executor_task = tokio::spawn(executor.run_until_stopped()); - - // wait for sequencer to get the initial nonce request from sequencer - wait_for_startup(status, nonce_guard).await.unwrap(); - - let response_guard = mount_broadcast_tx_sync_seq_actions_mock(&sequencer).await; - - // send two sequence actions to the executor, both small enough to fit in a single bundle - // without filling it - let seq0 = Sequence { - data: vec![0u8; cfg.max_bytes_per_bundle / 4].into(), - ..sequence_action() - }; - - // make sure at least one block has passed so that the executor will submit the bundle - // despite it not being full - time::pause(); - executor_handle - .send_timeout(seq0.clone(), Duration::from_millis(1000)) - .await - .unwrap(); - time::advance(Duration::from_millis(cfg.block_time_ms)).await; - time::resume(); - - // wait for the mock sequencer to receive the signed transaction - tokio::time::timeout( - Duration::from_millis(100), - response_guard.wait_until_satisfied(), - ) - .await - .unwrap(); - - // verify only one signed transaction was received by the mock sequencer - let expected_seq_actions = [seq0]; - let requests = response_guard.received_requests().await; - assert_eq!(requests.len(), 1); - - // verify the expected sequence actions were received - let signed_tx = signed_tx_from_request(&requests[0]); - let actions = signed_tx.actions(); - - assert_eq!( - actions.len(), - expected_seq_actions.len(), - "received more than one action, one was supposed to fill the bundle" - ); - - for (action, expected_seq_action) in actions.iter().zip(expected_seq_actions.iter()) { - let seq_action = action.as_sequence().unwrap(); - assert_eq!( - seq_action.rollup_id, expected_seq_action.rollup_id, - "chain id does not match. actual {:?} expected {:?}", - seq_action.rollup_id, expected_seq_action.rollup_id - ); - assert_eq!( - seq_action.data, expected_seq_action.data, - "data does not match expected data for action with rollup_id {:?}", - seq_action.rollup_id, - ); - } -} - -/// Test to check that the executor sends a signed transaction with two sequence actions to the -/// sequencer. -#[tokio::test] -async fn two_seq_actions_single_bundle() { - // set up the executor, channel for writing seq actions, and the sequencer mock - let (sequencer, cfg, _keyfile) = setup().await; - let shutdown_token = CancellationToken::new(); - let metrics = Box::leak(Box::new(Metrics::noop_metrics(&cfg).unwrap())); - mount_genesis(&sequencer, &cfg.sequencer_chain_id).await; - let (executor, executor_handle) = executor::Builder { - sequencer_url: cfg.sequencer_url.clone(), - sequencer_chain_id: cfg.sequencer_chain_id.clone(), - private_key_file: cfg.private_key_file.clone(), - sequencer_address_prefix: "astria".into(), - block_time_ms: cfg.block_time_ms, - max_bytes_per_bundle: cfg.max_bytes_per_bundle, - bundle_queue_capacity: cfg.bundle_queue_capacity, - shutdown_token: shutdown_token.clone(), - metrics, - } - .build() - .unwrap(); - - let nonce_guard = mount_default_nonce_query_mock(&sequencer).await; - let status = executor.subscribe(); - let _executor_task = tokio::spawn(executor.run_until_stopped()); - - // wait for sequencer to get the initial nonce request from sequencer - wait_for_startup(status, nonce_guard).await.unwrap(); - - let response_guard = mount_broadcast_tx_sync_seq_actions_mock(&sequencer).await; - - // send two sequence actions to the executor, both small enough to fit in a single bundle - // without filling it - let seq0 = Sequence { - data: vec![0u8; cfg.max_bytes_per_bundle / 4].into(), - ..sequence_action() - }; - - let seq1 = Sequence { - rollup_id: RollupId::new([1; ROLLUP_ID_LEN]), - data: vec![1u8; cfg.max_bytes_per_bundle / 4].into(), - ..sequence_action() - }; - - // make sure at least one block has passed so that the executor will submit the bundle - // despite it not being full - time::pause(); - executor_handle - .send_timeout(seq0.clone(), Duration::from_millis(1000)) - .await - .unwrap(); - executor_handle - .send_timeout(seq1.clone(), Duration::from_millis(1000)) - .await - .unwrap(); - time::advance(Duration::from_millis(cfg.block_time_ms)).await; - time::resume(); - - // wait for the mock sequencer to receive the signed transaction - tokio::time::timeout( - Duration::from_millis(100), - response_guard.wait_until_satisfied(), - ) - .await - .unwrap(); - - // verify only one signed transaction was received by the mock sequencer - let expected_seq_actions = [seq0, seq1]; - let requests = response_guard.received_requests().await; - assert_eq!(requests.len(), 1); - - // verify the expected sequence actions were received - let signed_tx = signed_tx_from_request(&requests[0]); - let actions = signed_tx.actions(); - - assert_eq!( - actions.len(), - expected_seq_actions.len(), - "received more than one action, one was supposed to fill the bundle" - ); - - for (action, expected_seq_action) in actions.iter().zip(expected_seq_actions.iter()) { - let seq_action = action.as_sequence().unwrap(); - assert_eq!( - seq_action.rollup_id, expected_seq_action.rollup_id, - "chain id does not match. actual {:?} expected {:?}", - seq_action.rollup_id, expected_seq_action.rollup_id - ); - assert_eq!( - seq_action.data, expected_seq_action.data, - "data does not match expected data for action with rollup_id {:?}", - seq_action.rollup_id, - ); - } -} - -/// Test to check that executor's chain ID check is properly checked against the sequencer's chain -/// ID -#[tokio::test] -async fn chain_id_mismatch_returns_error() { - use tendermint::chain::Id; - - // set up sequencer mock - let (sequencer, cfg, _keyfile) = setup().await; - let shutdown_token = CancellationToken::new(); - let metrics = Box::leak(Box::new(Metrics::noop_metrics(&cfg).unwrap())); - - // mount a status response with an incorrect chain_id - mount_genesis(&sequencer, "bad-chain-id").await; - - // build the executor with the correct chain_id - let (executor, _executor_handle) = executor::Builder { - sequencer_url: cfg.sequencer_url.clone(), - sequencer_chain_id: cfg.sequencer_chain_id.clone(), - private_key_file: cfg.private_key_file.clone(), - sequencer_address_prefix: cfg.sequencer_address_prefix.clone(), - block_time_ms: cfg.block_time_ms, - max_bytes_per_bundle: cfg.max_bytes_per_bundle, - bundle_queue_capacity: cfg.bundle_queue_capacity, - shutdown_token: shutdown_token.clone(), - metrics, - } - .build() - .unwrap(); - - // ensure that run_until_stopped returns WrongChainId error - let err = executor.run_until_stopped().await.expect_err( - "should exit with an error when reading a bad chain ID, but exited with success", - ); - let mut found = false; - for cause in err.chain() { - if let Some(err) = cause.downcast_ref::() { - assert_chain_id_err( - err, - &cfg.sequencer_chain_id, - &Id::try_from("bad-chain-id".to_string()).unwrap(), - ); - found = true; - break; - } - } - - // ensure that the error chain contains the expected error - assert!( - found, - "expected `EnsureChainIdError::WrongChainId` in error chain, but it was not found" - ); -} diff --git a/crates/astria-composer/tests/blackbox/geth_collector.rs b/crates/astria-composer/tests/blackbox/geth_collector.rs index e6bce832e9..8acf6c8178 100644 --- a/crates/astria-composer/tests/blackbox/geth_collector.rs +++ b/crates/astria-composer/tests/blackbox/geth_collector.rs @@ -1,9 +1,6 @@ use std::time::Duration; -use astria_core::{ - generated::protocol::accounts::v1alpha1::NonceResponse, - primitive::v1::RollupId, -}; +use astria_core::primitive::v1::RollupId; use ethers::types::Transaction; use crate::helper::{ @@ -19,12 +16,6 @@ async fn tx_from_one_rollup_is_received_by_sequencer() { // Spawn a composer with a mock sequencer and a mock rollup node // Initial nonce is 0 let test_composer = spawn_composer(&["test1"]).await; - tokio::time::timeout( - Duration::from_millis(100), - test_composer.setup_guard.wait_until_satisfied(), - ) - .await - .expect("composer and sequencer should have been setup successfully"); let expected_rollup_ids = vec![RollupId::from_unhashed_bytes("test1")]; let mock_guard = @@ -47,12 +38,6 @@ async fn collector_restarts_after_exit() { // Spawn a composer with a mock sequencer and a mock rollup node // Initial nonce is 0 let test_composer = spawn_composer(&["test1"]).await; - tokio::time::timeout( - Duration::from_millis(100), - test_composer.setup_guard.wait_until_satisfied(), - ) - .await - .expect("composer and sequencer should have been setup successfully"); // get rollup node let rollup_node = test_composer.rollup_nodes.get("test1").unwrap(); @@ -84,17 +69,9 @@ async fn collector_restarts_after_exit() { #[tokio::test] async fn invalid_nonce_causes_resubmission_under_different_nonce() { - use crate::helper::mock_sequencer::mount_abci_query_mock; - // Spawn a composer with a mock sequencer and a mock rollup node // Initial nonce is 0 let test_composer = spawn_composer(&["test1"]).await; - tokio::time::timeout( - Duration::from_millis(100), - test_composer.setup_guard.wait_until_satisfied(), - ) - .await - .expect("composer and sequencer should have been setup successfully"); // Reject the first transaction for invalid nonce let invalid_nonce_guard = mount_broadcast_tx_sync_invalid_nonce_mock( @@ -103,22 +80,17 @@ async fn invalid_nonce_causes_resubmission_under_different_nonce() { ) .await; - // Mount a response of 0 to a nonce query - let nonce_refetch_guard = mount_abci_query_mock( - &test_composer.sequencer, - "accounts/nonce", - NonceResponse { - height: 0, - nonce: 1, - }, - ) - .await; - let expected_rollup_ids = vec![RollupId::from_unhashed_bytes("test1")]; // Expect nonce 1 again so that the resubmitted tx is accepted let valid_nonce_guard = mount_broadcast_tx_sync_mock(&test_composer.sequencer, expected_rollup_ids, vec![1]).await; + // Mount a response of 1 to a nonce query + test_composer + .sequencer_mock + .mount_pending_nonce_response(1, "setup correct nonce") + .await; + // Push a tx to the rollup node so that it is picked up by the composer and submitted with the // stored nonce of 0, triggering the nonce refetch process test_composer.rollup_nodes["test1"] @@ -135,10 +107,49 @@ async fn invalid_nonce_causes_resubmission_under_different_nonce() { tokio::time::timeout( Duration::from_millis(100), - nonce_refetch_guard.wait_until_satisfied(), + valid_nonce_guard.wait_until_satisfied(), ) .await - .expect("new nonce should have been fetched from the sequencer"); + .expect("sequencer tx should have been accepted after nonce refetch"); +} + +#[tokio::test] +async fn nonce_taken_causes_resubmission_under_different_nonce() { + // Spawn a composer with a mock sequencer and a mock rollup node + // Initial nonce is 0 + let test_composer = spawn_composer(&["test1"]).await; + + // Reject the first transaction for taken nonce + let invalid_nonce_guard = mount_broadcast_tx_sync_invalid_nonce_mock( + &test_composer.sequencer, + RollupId::from_unhashed_bytes("test1"), + ) + .await; + + let expected_rollup_ids = vec![RollupId::from_unhashed_bytes("test1")]; + // Expect nonce 1 again so that the resubmitted tx is accepted + let valid_nonce_guard = + mount_broadcast_tx_sync_mock(&test_composer.sequencer, expected_rollup_ids, vec![1]).await; + + // Mount a response of 1 to a nonce query + test_composer + .sequencer_mock + .mount_pending_nonce_response(1, "setup correct nonce") + .await; + + // Push a tx to the rollup node so that it is picked up by the composer and submitted with the + // stored nonce of 0, triggering the nonce refetch process + test_composer.rollup_nodes["test1"] + .push_tx(Transaction::default()) + .unwrap(); + + // wait for 1 sequencer block time to make sure the bundle is preempted + tokio::time::timeout( + Duration::from_millis(test_composer.cfg.block_time_ms + 1000), + invalid_nonce_guard.wait_until_satisfied(), + ) + .await + .expect("sequencer tx should have been rejected due to invalid nonce"); tokio::time::timeout( Duration::from_millis(100), @@ -153,12 +164,6 @@ async fn single_rollup_tx_payload_integrity() { // Spawn a composer with a mock sequencer and a mock rollup node // Initial nonce is 0 let test_composer = spawn_composer(&["test1"]).await; - tokio::time::timeout( - Duration::from_millis(100), - test_composer.setup_guard.wait_until_satisfied(), - ) - .await - .expect("composer and sequencer should have been setup successfully"); let tx: Transaction = serde_json::from_str(TEST_ETH_TX_JSON).unwrap(); let mock_guard = diff --git a/crates/astria-composer/tests/blackbox/grpc_collector.rs b/crates/astria-composer/tests/blackbox/grpc_collector.rs index 8a4a963acc..bc0586a073 100644 --- a/crates/astria-composer/tests/blackbox/grpc_collector.rs +++ b/crates/astria-composer/tests/blackbox/grpc_collector.rs @@ -1,12 +1,9 @@ use std::time::Duration; use astria_core::{ - generated::{ - composer::v1alpha1::{ - grpc_collector_service_client::GrpcCollectorServiceClient, - SubmitRollupTransactionRequest, - }, - protocol::accounts::v1alpha1::NonceResponse, + generated::composer::v1alpha1::{ + grpc_collector_service_client::GrpcCollectorServiceClient, + SubmitRollupTransactionRequest, }, primitive::v1::RollupId, }; @@ -24,13 +21,6 @@ use crate::helper::{ #[tokio::test] async fn tx_from_one_rollup_is_received_by_sequencer() { let test_composer = spawn_composer(&[]).await; - tokio::time::timeout( - Duration::from_millis(100), - test_composer.setup_guard.wait_until_satisfied(), - ) - .await - .expect("composer and sequencer were not setup successfully"); - let rollup_id = RollupId::from_unhashed_bytes("test1"); let expected_chain_ids = vec![rollup_id]; let mock_guard = @@ -63,33 +53,20 @@ async fn tx_from_one_rollup_is_received_by_sequencer() { #[tokio::test] async fn invalid_nonce_causes_resubmission_under_different_nonce() { - use crate::helper::mock_sequencer::mount_abci_query_mock; - // Spawn a composer with a mock sequencer and a mock rollup node // Initial nonce is 0 let rollup_id = RollupId::from_unhashed_bytes("test1"); let test_composer = spawn_composer(&[]).await; - tokio::time::timeout( - Duration::from_millis(100), - test_composer.setup_guard.wait_until_satisfied(), - ) - .await - .expect("composer and sequencer should have been setup successfully"); // Reject the first transaction for invalid nonce let invalid_nonce_guard = mount_broadcast_tx_sync_invalid_nonce_mock(&test_composer.sequencer, rollup_id).await; - // Mount a response of 0 to a nonce query - let nonce_refetch_guard = mount_abci_query_mock( - &test_composer.sequencer, - "accounts/nonce", - NonceResponse { - height: 0, - nonce: 1, - }, - ) - .await; + // Mount a response of 1 to a nonce query + test_composer + .sequencer_mock + .mount_pending_nonce_response(1, "setup correct nonce") + .await; let expected_chain_ids = vec![rollup_id]; // Expect nonce 1 again so that the resubmitted tx is accepted @@ -122,13 +99,6 @@ async fn invalid_nonce_causes_resubmission_under_different_nonce() { .await .expect("sequencer tx should have been rejected due to invalid nonce"); - tokio::time::timeout( - Duration::from_millis(100), - nonce_refetch_guard.wait_until_satisfied(), - ) - .await - .expect("new nonce should have been fetched from the sequencer"); - tokio::time::timeout( Duration::from_millis(100), valid_nonce_guard.wait_until_satisfied(), @@ -143,12 +113,6 @@ async fn single_rollup_tx_payload_integrity() { // Initial nonce is 0 let rollup_id = RollupId::from_unhashed_bytes("test1"); let test_composer = spawn_composer(&[]).await; - tokio::time::timeout( - Duration::from_millis(100), - test_composer.setup_guard.wait_until_satisfied(), - ) - .await - .expect("composer and sequencer should have been setup successfully"); let tx: Transaction = serde_json::from_str(TEST_ETH_TX_JSON).unwrap(); let mock_guard = diff --git a/crates/astria-composer/tests/blackbox/helper/mock_sequencer.rs b/crates/astria-composer/tests/blackbox/helper/mock_abci_sequencer.rs similarity index 63% rename from crates/astria-composer/tests/blackbox/helper/mock_sequencer.rs rename to crates/astria-composer/tests/blackbox/helper/mock_abci_sequencer.rs index da8dea87e0..28f5ca53a2 100644 --- a/crates/astria-composer/tests/blackbox/helper/mock_sequencer.rs +++ b/crates/astria-composer/tests/blackbox/helper/mock_abci_sequencer.rs @@ -1,6 +1,5 @@ use std::time::Duration; -use prost::Message; use serde_json::json; use tendermint::{ consensus::{ @@ -13,63 +12,17 @@ use tendermint::{ Genesis, Time, }; -use tendermint_rpc::{ - response, - Id, -}; use wiremock::{ - matchers::{ - body_partial_json, - body_string_contains, - }, + matchers::body_partial_json, Mock, - MockGuard, MockServer, ResponseTemplate, }; -pub async fn start() -> (MockServer, MockGuard) { - use astria_core::generated::protocol::accounts::v1alpha1::NonceResponse; +pub async fn start() -> MockServer { let server = MockServer::start().await; - let startup_guard = mount_abci_query_mock( - &server, - "accounts/nonce", - NonceResponse { - height: 0, - nonce: 0, - }, - ) - .await; mount_genesis(&server, "test-chain-1").await; - (server, startup_guard) -} - -pub async fn mount_abci_query_mock( - server: &MockServer, - query_path: &str, - response: impl Message, -) -> MockGuard { - let expected_body = json!({ - "method": "abci_query" - }); - let response = tendermint_rpc::endpoint::abci_query::Response { - response: tendermint_rpc::endpoint::abci_query::AbciQuery { - value: response.encode_to_vec(), - ..Default::default() - }, - }; - let wrapper = response::Wrapper::new_with_id(Id::Num(1), Some(response), None); - Mock::given(body_partial_json(&expected_body)) - .and(body_string_contains(query_path)) - .respond_with( - ResponseTemplate::new(200) - .set_body_json(&wrapper) - .append_header("Content-Type", "application/json"), - ) - .up_to_n_times(1) - .expect(1) - .mount_as_scoped(server) - .await + server } async fn mount_genesis(server: &MockServer, mock_sequencer_chain_id: &str) { diff --git a/crates/astria-composer/tests/blackbox/helper/mock_grpc_sequencer.rs b/crates/astria-composer/tests/blackbox/helper/mock_grpc_sequencer.rs new file mode 100644 index 0000000000..546e3b9ebc --- /dev/null +++ b/crates/astria-composer/tests/blackbox/helper/mock_grpc_sequencer.rs @@ -0,0 +1,122 @@ +use std::{ + net::SocketAddr, + sync::Arc, +}; + +use astria_core::{ + self, + generated::sequencerblock::v1alpha1::{ + sequencer_service_server::{ + SequencerService, + SequencerServiceServer, + }, + FilteredSequencerBlock as RawFilteredSequencerBlock, + GetFilteredSequencerBlockRequest, + GetPendingNonceRequest, + GetPendingNonceResponse, + GetSequencerBlockRequest, + SequencerBlock as RawSequencerBlock, + }, +}; +use astria_eyre::eyre::{ + self, + WrapErr as _, +}; +use astria_grpc_mock::{ + matcher::message_type, + response::constant_response, + Mock, + MockServer, +}; +use tokio::task::JoinHandle; +use tonic::{ + transport::Server, + Request, + Response, + Status, +}; + +// NOTE: the actual full path name is +// /astria.sequencerblock.v1alpha1.SequencerService/GetPendingNonce +const GET_PENDING_NONCE_GRPC_NAME: &str = "get_pending_nonce"; + +pub struct MockGrpcSequencer { + _server: JoinHandle>, + pub(crate) mock_server: MockServer, + pub(crate) local_addr: SocketAddr, +} + +impl MockGrpcSequencer { + pub(crate) async fn spawn() -> Self { + use tokio_stream::wrappers::TcpListenerStream; + + let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap(); + let local_addr = listener.local_addr().unwrap(); + + let mock_server = MockServer::new(); + + let server = { + let sequencer_service = SequencerServiceImpl(mock_server.clone()); + tokio::spawn(async move { + Server::builder() + .add_service(SequencerServiceServer::new(sequencer_service)) + .serve_with_incoming(TcpListenerStream::new(listener)) + .await + .wrap_err("gRPC sequencer server failed") + }) + }; + Self { + _server: server, + mock_server, + local_addr, + } + } + + pub(crate) async fn mount_pending_nonce_response( + &self, + nonce_to_mount: u32, + debug_name: impl Into, + ) { + let resp = GetPendingNonceResponse { + inner: nonce_to_mount, + }; + Mock::for_rpc_given( + GET_PENDING_NONCE_GRPC_NAME, + message_type::(), + ) + .respond_with(constant_response(resp)) + .up_to_n_times(1) + .expect(1) + .with_name(debug_name) + .mount(&self.mock_server) + .await; + } +} + +struct SequencerServiceImpl(MockServer); + +#[tonic::async_trait] +impl SequencerService for SequencerServiceImpl { + async fn get_sequencer_block( + self: Arc, + _request: Request, + ) -> Result, Status> { + unimplemented!() + } + + async fn get_filtered_sequencer_block( + self: Arc, + _request: Request, + ) -> Result, Status> { + unimplemented!() + } + + async fn get_pending_nonce( + self: Arc, + request: Request, + ) -> Result, Status> { + self.0 + .handle_request(GET_PENDING_NONCE_GRPC_NAME, request) + .await + } +} diff --git a/crates/astria-composer/tests/blackbox/helper/mod.rs b/crates/astria-composer/tests/blackbox/helper/mod.rs index ecd5090441..d9405fdf93 100644 --- a/crates/astria-composer/tests/blackbox/helper/mod.rs +++ b/crates/astria-composer/tests/blackbox/helper/mod.rs @@ -29,6 +29,7 @@ use astria_core::{ }; use astria_eyre::eyre; use ethers::prelude::Transaction; +use mock_grpc_sequencer::MockGrpcSequencer; use telemetry::metrics; use tempfile::NamedTempFile; use tendermint_rpc::{ @@ -48,7 +49,8 @@ use wiremock::{ ResponseTemplate, }; -pub mod mock_sequencer; +pub mod mock_abci_sequencer; +pub mod mock_grpc_sequencer; static TELEMETRY: LazyLock<()> = LazyLock::new(|| { // This config can be meaningless - it's only used inside `try_init` to init the metrics, but we @@ -56,7 +58,8 @@ static TELEMETRY: LazyLock<()> = LazyLock::new(|| { let config = Config { log: String::new(), api_listen_addr: SocketAddr::new(IpAddr::from([0, 0, 0, 0]), 0), - sequencer_url: String::new(), + sequencer_abci_endpoint: String::new(), + sequencer_grpc_endpoint: String::new(), sequencer_chain_id: String::new(), rollups: String::new(), private_key_file: String::new(), @@ -96,7 +99,7 @@ pub struct TestComposer { pub composer: JoinHandle>, pub rollup_nodes: HashMap, pub sequencer: wiremock::MockServer, - pub setup_guard: MockGuard, + pub sequencer_mock: MockGrpcSequencer, pub grpc_collector_addr: SocketAddr, pub metrics_handle: metrics::Handle, } @@ -117,7 +120,8 @@ pub async fn spawn_composer(rollup_ids: &[&str]) -> TestComposer { rollup_nodes.insert((*id).to_string(), geth); rollups.push_str(&format!("{id}::{execution_url},")); } - let (sequencer, sequencer_setup_guard) = mock_sequencer::start().await; + let sequencer = mock_abci_sequencer::start().await; + let grpc_server = MockGrpcSequencer::spawn().await; let sequencer_url = sequencer.uri(); let keyfile = NamedTempFile::new().unwrap(); (&keyfile) @@ -128,7 +132,8 @@ pub async fn spawn_composer(rollup_ids: &[&str]) -> TestComposer { api_listen_addr: "127.0.0.1:0".parse().unwrap(), sequencer_chain_id: "test-chain-1".to_string(), rollups, - sequencer_url, + sequencer_abci_endpoint: sequencer_url.to_string(), + sequencer_grpc_endpoint: format!("http://{}", grpc_server.local_addr), private_key_file: keyfile.path().to_string_lossy().to_string(), sequencer_address_prefix: "astria".into(), block_time_ms: 2000, @@ -149,6 +154,11 @@ pub async fn spawn_composer(rollup_ids: &[&str]) -> TestComposer { .unwrap(); let metrics = Box::leak(Box::new(metrics)); + // prepare get nonce response + grpc_server + .mount_pending_nonce_response(0, "startup::wait_for_mempool()") + .await; + let (composer_addr, grpc_collector_addr, composer_handle) = { let composer = Composer::from_config(&config, metrics).await.unwrap(); let composer_addr = composer.local_addr(); @@ -163,7 +173,7 @@ pub async fn spawn_composer(rollup_ids: &[&str]) -> TestComposer { composer: composer_handle, rollup_nodes, sequencer, - setup_guard: sequencer_setup_guard, + sequencer_mock: grpc_server, grpc_collector_addr, metrics_handle, } @@ -332,6 +342,35 @@ pub async fn mount_broadcast_tx_sync_invalid_nonce_mock( .await } +/// Deserializes the bytes contained in a `tx_sync::Request` to a signed sequencer transaction and +/// verifies that the contained sequence action is for the given `expected_rollup_id`. It then +/// rejects the transaction for a taken nonce. +pub async fn mount_broadcast_tx_sync_nonce_taken_mock( + server: &MockServer, + expected_rollup_id: RollupId, +) -> MockGuard { + let matcher = move |request: &Request| { + let (rollup_id, _) = rollup_id_nonce_from_request(request); + rollup_id == expected_rollup_id + }; + let jsonrpc_rsp = response::Wrapper::new_with_id( + Id::Num(1), + Some(tx_sync::Response { + code: tendermint::abci::Code::Err(AbciErrorCode::NONCE_TAKEN.value()), + data: vec![].into(), + log: String::new(), + hash: tendermint::Hash::Sha256([0; 32]), + }), + None, + ); + Mock::given(matcher) + .respond_with(ResponseTemplate::new(200).set_body_json(&jsonrpc_rsp)) + .up_to_n_times(1) + .expect(1) + .mount_as_scoped(server) + .await +} + // A Uniswap V2 DAI-ETH swap transaction from mainnet // Etherscan link: https://etherscan.io/tx/0x99850dd1cf325c8ede9ba62b9d8a11aa199794450b581ce3a7bb8c1e5bb7562f pub const TEST_ETH_TX_JSON: &str = r#"{"blockHash":"0xe365f2163edb844b617ebe3d2af183b31d6c7ffa794f21d0b2d111d63e979a02","blockNumber":"0x1157959","from":"0xdc975a9bb00f4c030e4eb3268f68e4b8d0fa0362","gas":"0xcdf49","gasPrice":"0x374128344","maxFeePerGas":"0x374128344","maxPriorityFeePerGas":"0x0","hash":"0x99850dd1cf325c8ede9ba62b9d8a11aa199794450b581ce3a7bb8c1e5bb7562f","input":"0x022c0d9f0000000000000000000000000000000000000000000000c88a1ad5e15105525500000000000000000000000000000000000000000000000000000000000000000000000000000000000000001a2d11cb90d1de13bb81ee7b772a08ac234a8058000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000001208974000000000000000000000000000000000000000000000000000000004de4000000000000000000000000000000000000000000000000017038152c223cb100000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000005200000000000000000000000000000000000000000000000000000000000000000000000000000000000000087870bca3f3fd6335c3f4ce8392d69350b4fa4e2000000000000000000000000ab12275f2d91f87b301a4f01c9af4e83b3f45baa0000000000000000000000006b175474e89094c44da98b954eedeac495271d0f000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2","nonce":"0x28","to":"0xa478c2975ab1ea89e8196811f51a7b7ade33eb11","transactionIndex":"0x2","value":"0x0","type":"0x2","accessList":[{"address":"0x5f4ec3df9cbd43714fe2740f5e3616155c5b8419","storageKeys":["0x0000000000000000000000000000000000000000000000000000000000000005","0x0000000000000000000000000000000000000000000000000000000000000002"]},{"address":"0x7effd7b47bfd17e52fb7559d3f924201b9dbff3d","storageKeys":[]},{"address":"0x018008bfb33d285247a21d44e50697654f754e63","storageKeys":["0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc"]},{"address":"0x1a2d11cb90d1de13bb81ee7b772a08ac234a8058","storageKeys":[]},{"address":"0xe62b71cf983019bff55bc83b48601ce8419650cc","storageKeys":["0x9a09f352b299559621084d9b8d2625e8d5a97f382735872dd3bb1bdbdccc3fee","0x000000000000000000000000000000000000000000000000000000000000002b","0xfee3a99380070b792e111dd9a6a15e929983e2d0b7e170a5520e51b99be0c359"]},{"address":"0x87870bca3f3fd6335c3f4ce8392d69350b4fa4e2","storageKeys":["0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc","0x070a95ec3546cae47592e0bcea195bf8f96287077fbb7a23785cc2887152941c","0x070a95ec3546cae47592e0bcea195bf8f96287077fbb7a23785cc28871529420","0xf81d8d79f42adb4c73cc3aa0c78e25d3343882d0313c0b80ece3d3a103ef1ec6","0x5e14560e314427eb9d0c466a6058089f672317c8e26719a770a709c3f2481e4b","0xf81d8d79f42adb4c73cc3aa0c78e25d3343882d0313c0b80ece3d3a103ef1ebf","0xf81d8d79f42adb4c73cc3aa0c78e25d3343882d0313c0b80ece3d3a103ef1ec0","0x4c0bd942d17410ca1f6d3278a62feef7078602605466e37de958808f1454efbd","0x5e14560e314427eb9d0c466a6058089f672317c8e26719a770a709c3f2481e48","0xf81d8d79f42adb4c73cc3aa0c78e25d3343882d0313c0b80ece3d3a103ef1ec3","0x5e14560e314427eb9d0c466a6058089f672317c8e26719a770a709c3f2481e4f","0x5e14560e314427eb9d0c466a6058089f672317c8e26719a770a709c3f2481e4a","0x5e14560e314427eb9d0c466a6058089f672317c8e26719a770a709c3f2481e50","0x5e14560e314427eb9d0c466a6058089f672317c8e26719a770a709c3f2481e4d","0x4cb2b152c1b54ce671907a93c300fd5aa72383a9d4ec19a81e3333632ae92e00","0xf81d8d79f42adb4c73cc3aa0c78e25d3343882d0313c0b80ece3d3a103ef1ec4","0xf81d8d79f42adb4c73cc3aa0c78e25d3343882d0313c0b80ece3d3a103ef1ec7","0x4bea7244bd9088ac961c659a818b4f060de9712d20dc006c24f0985f19cf62d1","0x5e14560e314427eb9d0c466a6058089f672317c8e26719a770a709c3f2481e49","0xf81d8d79f42adb4c73cc3aa0c78e25d3343882d0313c0b80ece3d3a103ef1ec2","0x070a95ec3546cae47592e0bcea195bf8f96287077fbb7a23785cc2887152941d","0x5e14560e314427eb9d0c466a6058089f672317c8e26719a770a709c3f2481e4c","0x5e14560e314427eb9d0c466a6058089f672317c8e26719a770a709c3f2481e4e","0x4480713a5820391a4815a640728dab70c3847e45854ef9e8117382da26ce9105","0x070a95ec3546cae47592e0bcea195bf8f96287077fbb7a23785cc2887152941f","0x000000000000000000000000000000000000000000000000000000000000003b","0x108718ddd11d4cf696a068770009c44aef387eb858097a37824291f99278d5e3","0xf81d8d79f42adb4c73cc3aa0c78e25d3343882d0313c0b80ece3d3a103ef1ec1","0xf81d8d79f42adb4c73cc3aa0c78e25d3343882d0313c0b80ece3d3a103ef1ec5"]},{"address":"0x2f39d218133afab8f2b819b1066c7e434ad94e9e","storageKeys":["0x740f710666bd7a12af42df98311e541e47f7fd33d382d11602457a6d540cbd63","0x0d2c1bcee56447b4f46248272f34207a580a5c40f666a31f4e2fbb470ea53ab8"]},{"address":"0xe7b67f44ea304dd7f6d215b13686637ff64cd2b2","storageKeys":[]},{"address":"0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2","storageKeys":["0x7f6377583d24615ddfe989626525aeed0d158f924ee8c91664ab0dffd7863d00","0x3afb575d989d656a39ee0690da12b019915f3bd8709cc522e681b8dd04237970","0xa535fbd0ab3e0ad4ee444570368f3d474545b71fcc49228fe96a6406676fc126","0xb064600732a82908427d092d333e607598a6238a59aeb45e1288cb0bac7161cf"]},{"address":"0x4d5f47fa6a74757f35c14fd3a6ef8e3c9bc514e8","storageKeys":["0x000000000000000000000000000000000000000000000000000000000000003c","0x14a553e31736f19e3e380cf55bfb2f82dfd6d880cd07235affb68d8d3e0cac4d","0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc","0x5e8cc6ee686108b7fd15638e2dbb32555b30d0bd1a191628bb70b5459b86cedc","0x000000000000000000000000000000000000000000000000000000000000003d","0x0000000000000000000000000000000000000000000000000000000000000036","0x0000000000000000000000000000000000000000000000000000000000000039"]},{"address":"0x6b175474e89094c44da98b954eedeac495271d0f","storageKeys":["0xd86cc1e239204d48eb0055f151744c4bb3d2337612287be803ae8247e95a67d2","0xe7ab5c3b3c86286a122f1937d4c70a3170dba7ef4f7603d830e8bcf7c9af583b","0x87c358b8e65d7446f52ffce25e44c9673d2bf461b3d3e4748afcf1238e9224a3","0xad740bfd58072c0bd719418966c52da18e837afec1b47e07bba370568cc87fbb"]},{"address":"0xe175de51f29d822b86e46a9a61246ec90631210d","storageKeys":[]},{"address":"0xcf8d0c70c850859266f5c338b38f9d663181c314","storageKeys":["0x0000000000000000000000000000000000000000000000000000000000000037","0x000000000000000000000000000000000000000000000000000000000000003d","0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc","0x000000000000000000000000000000000000000000000000000000000000003a","0x4bea7244bd9088ac961c659a818b4f060de9712d20dc006c24f0985f19cf62d1"]},{"address":"0x413adac9e2ef8683adf5ddaece8f19613d60d1bb","storageKeys":["0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc","0x000000000000000000000000000000000000000000000000000000000000003f","0x000000000000000000000000000000000000000000000000000000000000003a","0x4bea7244bd9088ac961c659a818b4f060de9712d20dc006c24f0985f19cf62d1"]},{"address":"0xaed0c38402a5d19df6e4c03f4e2dced6e29c1ee9","storageKeys":["0x0000000000000000000000000000000000000000000000000000000000000005","0x0000000000000000000000000000000000000000000000000000000000000002"]},{"address":"0xea51d7853eefb32b6ee06b1c12e6dcca88be0ffe","storageKeys":["0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc","0x000000000000000000000000000000000000000000000000000000000000003a"]},{"address":"0x54586be62e3c3580375ae3723c145253060ca0c2","storageKeys":["0x7145bb02480b505fc02ccfdba07d3ba3a9d821606f0688263abedd0ac6e5bec5","0x2a11cb67ca5c7e99dba99b50e02c11472d0f19c22ed5af42a1599a7f57e1c7a4","0x5306b8fbe80b30a74098357ee8e26fad8dc069da9011cca5f0870a0a5982e541"]},{"address":"0x478238a1c8b862498c74d0647329aef9ea6819ed","storageKeys":["0x9ef04667c5a1bd8192837ceac2ad5f2c41549d4db3406185e8c6aa95ea557bc5","0x000000000000000000000000000000000000000000000000000000000000002b","0x0020b304a2489d03d215fadd3bb6d3de2dda5a6a1235e76d693c30263e3cd054"]},{"address":"0xa700b4eb416be35b2911fd5dee80678ff64ff6c9","storageKeys":["0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc","0x5e8cc6ee686108b7fd15638e2dbb32555b30d0bd1a191628bb70b5459b86cedc"]},{"address":"0x8164cc65827dcfe994ab23944cbc90e0aa80bfcb","storageKeys":["0x76f8b43dabb591eb6681562420f7f6aa393e6903d4e02e6f59e2957d94ceab20","0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc","0x176062dac4e737f036c34baf4b07185f9c9fd3c1337ca36eb7c1f7a74aedb8ea"]},{"address":"0x9a158802cd924747ef336ca3f9de3bdb60cf43d3","storageKeys":[]},{"address":"0xac725cb59d16c81061bdea61041a8a5e73da9ec6","storageKeys":[]},{"address":"0x15c5620dffac7c7366eed66c20ad222ddbb1ed57","storageKeys":[]},{"address":"0x547a514d5e3769680ce22b2361c10ea13619e8a9","storageKeys":["0x0000000000000000000000000000000000000000000000000000000000000005","0x0000000000000000000000000000000000000000000000000000000000000002"]},{"address":"0x8116b273cd75d79c382afacc706659ded5e0a59d","storageKeys":["0x0fb35ae12d348b84dc0910bcce7d3b0a3f6d23a3e1d0b53bbe5f135078b97b13","0x000000000000000000000000000000000000000000000000000000000000002b","0x1d90d8e683e6736ac0564a19732a642e4be100e7ee8c225feba909bbdaf1522b"]},{"address":"0x9f8ccdafcc39f3c7d6ebf637c9151673cbc36b88","storageKeys":[]},{"address":"0xa478c2975ab1ea89e8196811f51a7b7ade33eb11","storageKeys":["0x0000000000000000000000000000000000000000000000000000000000000007","0x0000000000000000000000000000000000000000000000000000000000000009","0x000000000000000000000000000000000000000000000000000000000000000a","0x000000000000000000000000000000000000000000000000000000000000000c","0x0000000000000000000000000000000000000000000000000000000000000008","0x0000000000000000000000000000000000000000000000000000000000000006"]},{"address":"0xf1cd4193bbc1ad4a23e833170f49d60f3d35a621","storageKeys":[]},{"address":"0x102633152313c81cd80419b6ecf66d14ad68949a","storageKeys":["0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc","0x000000000000000000000000000000000000000000000000000000000000003f","0x000000000000000000000000000000000000000000000000000000000000003a"]},{"address":"0xb02381b1d27aa9845e5012083ca288c1818884f0","storageKeys":[]}],"chainId":"0x1","v":"0x0","r":"0xcb4eccf09e298388220c5560a6539322bde17581cee6908d56a92a19575e28e2","s":"0x2b4e34adad48aee14b6600c6366ad683c00c63c9da88fc2a232308421cf69a21"}"#; From 10441afacac97c80391c6846bca270040cad977a Mon Sep 17 00:00:00 2001 From: Jordan Oroshiba Date: Tue, 15 Oct 2024 18:25:34 +0200 Subject: [PATCH 3/4] fix(sequencer): fix app hash in horcrux sentries (#1646) ## Summary Changes the comparison when deciding if we need to re-execute a proposal to use the validator address and the timestamp as identifiers, to enable horcrux sentry nodes to not have app hash clashes. ## Background When running horcrux all sentry nodes act as the same validator and all nodes create a new proposal, but the signer only signs one of them. This creates issues since all sentry nodes except the signed node will not reexecute the proposal since it was created by their validator address even though the proposals are different. Timestamps are used to add additional fingerprinting to this without adding high computation cost. ## Changes - Utilize timestamp as part of identifying whether to clear and execute proposal in prepare proposal ## Testing A validator reported this issue when running testnet, and similar changes were tested and validated. --- crates/astria-sequencer/src/app/mod.rs | 59 +++++++++++++++---- .../astria-sequencer/src/app/tests_app/mod.rs | 10 +++- 2 files changed, 56 insertions(+), 13 deletions(-) diff --git a/crates/astria-sequencer/src/app/mod.rs b/crates/astria-sequencer/src/app/mod.rs index 935b11fabd..30a39b0031 100644 --- a/crates/astria-sequencer/src/app/mod.rs +++ b/crates/astria-sequencer/src/app/mod.rs @@ -141,6 +141,40 @@ const POST_TRANSACTION_EXECUTION_RESULT_KEY: &str = "post_transaction_execution_ /// The inter-block state being written to by the application. type InterBlockState = Arc>; +/// This is used to identify a proposal constructed by the app instance +/// in `prepare_proposal` during a `process_proposal` call. +/// +/// The fields are not exhaustive, in most instances just the validator address +/// is adequate. When running a third party signer such as horcrux however it is +/// possible that multiple nodes are preparing proposals as the same validator +/// address, in these instances the timestamp is used as a unique identifier for +/// the proposal from that node. This is not a perfect solution, but it only +/// impacts sentry nodes does not halt the network and is cheaper computationally +/// than an exhaustive comparison. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub(crate) struct ProposalFingerprint { + validator_address: account::Id, + timestamp: tendermint::Time, +} + +impl From for ProposalFingerprint { + fn from(proposal: abci::request::PrepareProposal) -> Self { + Self { + validator_address: proposal.proposer_address, + timestamp: proposal.time, + } + } +} + +impl From for ProposalFingerprint { + fn from(proposal: abci::request::ProcessProposal) -> Self { + Self { + validator_address: proposal.proposer_address, + timestamp: proposal.time, + } + } +} + /// The Sequencer application, written as a bundle of [`Component`]s. /// /// Note: this is called `App` because this is a Tendermint ABCI application, @@ -157,14 +191,18 @@ pub(crate) struct App { // Transactions are pulled from this mempool during `prepare_proposal`. mempool: Mempool, - // The validator address in cometbft being used to sign votes. + // TODO(https://github.com/astriaorg/astria/issues/1660): The executed_proposal_fingerprint and + // executed_proposal_hash fields should be stored in the ephemeral storage instead of on the + // app struct, to avoid any issues with forgetting to reset them. + + // An identifier for a given proposal constructed by this app. // // Used to avoid executing a block in both `prepare_proposal` and `process_proposal`. It // is set in `prepare_proposal` from information sent in from cometbft and can potentially // change round-to-round. In `process_proposal` we check if we prepared the proposal, and - // if so, we clear the value and we skip re-execution of the block's transactions to avoid + // if so, we clear the value, and we skip re-execution of the block's transactions to avoid // failures caused by re-execution. - validator_address: Option, + executed_proposal_fingerprint: Option, // This is set to the executed hash of the proposal during `process_proposal` // @@ -222,7 +260,7 @@ impl App { Ok(Self { state, mempool, - validator_address: None, + executed_proposal_fingerprint: None, executed_proposal_hash: Hash::default(), recost_mempool: false, write_batch: None, @@ -332,7 +370,7 @@ impl App { prepare_proposal: abci::request::PrepareProposal, storage: Storage, ) -> Result { - self.validator_address = Some(prepare_proposal.proposer_address); + self.executed_proposal_fingerprint = Some(prepare_proposal.clone().into()); self.update_state_for_new_round(&storage); let mut block_size_constraints = BlockSizeConstraints::new( @@ -386,11 +424,12 @@ impl App { // we skip execution for this `process_proposal` call. // // if we didn't propose this block, `self.validator_address` will be None or a different - // value, so we will execute the block as normal. - if let Some(id) = self.validator_address { - if id == process_proposal.proposer_address { + // value, so we will execute block as normal. + if let Some(constructed_id) = self.executed_proposal_fingerprint { + let proposal_id = process_proposal.clone().into(); + if constructed_id == proposal_id { debug!("skipping process_proposal as we are the proposer for this block"); - self.validator_address = None; + self.executed_proposal_fingerprint = None; self.executed_proposal_hash = process_proposal.hash; // if we're the proposer, we should have the execution results from @@ -420,7 +459,7 @@ impl App { "our validator address was set but we're not the proposer, so our previous \ proposal was skipped, executing block" ); - self.validator_address = None; + self.executed_proposal_fingerprint = None; } self.update_state_for_new_round(&storage); diff --git a/crates/astria-sequencer/src/app/tests_app/mod.rs b/crates/astria-sequencer/src/app/tests_app/mod.rs index e215d1cee1..fbc6a6b95d 100644 --- a/crates/astria-sequencer/src/app/tests_app/mod.rs +++ b/crates/astria-sequencer/src/app/tests_app/mod.rs @@ -501,6 +501,7 @@ async fn app_execution_results_match_proposal_vs_after_proposal() { local_last_commit: None, misbehavior: vec![], }; + let proposal_fingerprint = prepare_proposal.clone().into(); let prepare_proposal_result = app .prepare_proposal(prepare_proposal, storage.clone()) @@ -508,7 +509,10 @@ async fn app_execution_results_match_proposal_vs_after_proposal() { .unwrap(); assert_eq!(prepare_proposal_result.txs, finalize_block.txs); assert_eq!(app.executed_proposal_hash, Hash::default()); - assert_eq!(app.validator_address.unwrap(), proposer_address); + assert_eq!( + app.executed_proposal_fingerprint, + Some(proposal_fingerprint) + ); app.mempool.run_maintenance(&app.state, false).await; @@ -530,7 +534,7 @@ async fn app_execution_results_match_proposal_vs_after_proposal() { .await .unwrap(); assert_eq!(app.executed_proposal_hash, block_hash); - assert!(app.validator_address.is_none()); + assert!(app.executed_proposal_fingerprint.is_none()); let finalize_block_after_prepare_proposal_result = app .finalize_block(finalize_block.clone(), storage.clone()) @@ -549,7 +553,7 @@ async fn app_execution_results_match_proposal_vs_after_proposal() { .await .unwrap(); assert_eq!(app.executed_proposal_hash, block_hash); - assert!(app.validator_address.is_none()); + assert!(app.executed_proposal_fingerprint.is_none()); let finalize_block_after_process_proposal_result = app .finalize_block(finalize_block, storage.clone()) .await From b677ce978130c58562f9ba2167f62c0e4ddb1f7d Mon Sep 17 00:00:00 2001 From: Ethan Oroshiba Date: Tue, 15 Oct 2024 18:22:55 -0500 Subject: [PATCH 4/4] feat(sequencer)!: rework all fees (#1647) ## Summary Gutted all current fee handling and replaced all with single fee calculation method in new trait. ## Background This implementation will simplify not only our fee calculation, but the process for adding new actions in the future. ## Changes - Created `FeeComponents` types for all fee-bearing transactions which all have a base fee component and a computed cost multiplier component. - Moved all fee checks and payment to one single function within the new sequencer fees component. Fee calculation is now always the following formula: `base_fee + computed_cost_base * computed_cost_multiplier` - Moved all state reads/writes for fees to the new fees component. - Initialized all fees in the fee component's `init_chain()`. - Changed `FeeChange` to be an enum which takes any fee-bearing action's fee components. - Moved `FeeChange` and `FeeAssetChange`'s `ActionHandler` impls to the fees component. - Allowed fee assets now stored in verifiable storage instead of non-verifiable. ## Testing All previous tests passing, added new tests for all state fee reads/writes. ## Breaking Changelist - Changed shape of `FeeChange` action. - Added storage of all the action fees, breaking the app hash. - Changed app genesis. - Removed a bunch of storage keys, breaking these snapshot tests - Allowed fee assets moved from non-verifiable to verifiable storage. ## Related Issues closes https://github.com/astriaorg/astria/issues/1369 closes https://github.com/astriaorg/astria/issues/1145 --------- Co-authored-by: Richard Janis Goldschmidt Co-authored-by: Fraser Hutchison Co-authored-by: Jordan Oroshiba --- Cargo.lock | 2 +- charts/sequencer/Chart.yaml | 4 +- .../files/cometbft/config/genesis.json | 73 +- charts/sequencer/values.yaml | 51 +- .../astria.protocol.fees.v1alpha1.rs | 241 +++ .../astria.protocol.fees.v1alpha1.serde.rs | 1731 +++++++++++++++++ .../astria.protocol.genesis.v1alpha1.rs | 66 +- .../astria.protocol.genesis.v1alpha1.serde.rs | 524 +++-- .../astria.protocol.transactions.v1alpha1.rs | 97 +- ...ia.protocol.transactions.v1alpha1.serde.rs | 463 ++--- crates/astria-core/src/generated/mod.rs | 13 + crates/astria-core/src/protocol/fees/mod.rs | 1 + .../astria-core/src/protocol/fees/v1alpha1.rs | 247 +++ ...a1__tests__genesis_state_is_unchanged.snap | 81 +- .../src/protocol/genesis/v1alpha1.rs | 364 +++- crates/astria-core/src/protocol/mod.rs | 1 + .../transaction/v1alpha1/action/group/mod.rs | 6 +- .../v1alpha1/action/group/tests.rs | 10 +- .../transaction/v1alpha1/action/mod.rs | 207 +- .../src/protocol/transaction/v1alpha1/mod.rs | 83 +- .../src/extension_trait.rs | 8 +- .../astria-sequencer-client/src/tests/http.rs | 10 +- .../src/genesis_example.rs | 139 +- crates/astria-sequencer/Cargo.toml | 2 +- .../astria-sequencer/src/accounts/action.rs | 102 +- .../src/accounts/component.rs | 4 - .../src/accounts/state_ext.rs | 36 - .../src/accounts/storage/keys.rs | 3 - .../src/accounts/storage/mod.rs | 1 - ...keys__tests__keys_should_not_change-2.snap | 5 +- ...__keys__tests__keys_should_not_change.snap | 5 +- crates/astria-sequencer/src/app/mod.rs | 53 +- ...ransaction_with_every_action_snapshot.snap | 63 +- ..._changes__app_finalize_block_snapshot.snap | 61 +- ...reaking_changes__app_genesis_snapshot.snap | 63 +- crates/astria-sequencer/src/app/test_utils.rs | 219 ++- .../src/app/tests_app/mempool.rs | 26 +- .../astria-sequencer/src/app/tests_app/mod.rs | 7 +- .../src/app/tests_block_fees.rs | 336 ---- .../src/app/tests_execute_transaction.rs | 102 +- crates/astria-sequencer/src/assets/query.rs | 57 +- ...__tests__storage_keys_are_unchanged-2.snap | 6 + .../astria-sequencer/src/assets/state_ext.rs | 333 +--- .../src/assets/storage/keys.rs | 65 - .../src/assets/storage/mod.rs | 5 +- .../astria-sequencer/src/authority/action.rs | 168 -- .../src/bridge/bridge_lock_action.rs | 256 +-- .../src/bridge/bridge_sudo_change_action.rs | 44 +- .../src/bridge/bridge_unlock_action.rs | 14 +- .../astria-sequencer/src/bridge/component.rs | 52 - .../src/bridge/init_bridge_account_action.rs | 55 +- crates/astria-sequencer/src/bridge/mod.rs | 2 - .../astria-sequencer/src/bridge/state_ext.rs | 99 - .../src/bridge/storage/keys.rs | 11 - .../src/bridge/storage/mod.rs | 1 - ...keys__tests__keys_should_not_change-2.snap | 5 +- ...keys__tests__keys_should_not_change-3.snap | 5 +- ...keys__tests__keys_should_not_change-4.snap | 5 +- ...keys__tests__keys_should_not_change-5.snap | 5 +- ...keys__tests__keys_should_not_change-6.snap | 5 +- ...keys__tests__keys_should_not_change-7.snap | 5 +- ...keys__tests__keys_should_not_change-8.snap | 5 +- ...__keys__tests__keys_should_not_change.snap | 5 +- .../src/bridge/storage/values/fee.rs | 42 - .../src/bridge/storage/values/mod.rs | 3 - .../astria-sequencer/src/fee_asset_change.rs | 61 - crates/astria-sequencer/src/fees/action.rs | 257 +++ crates/astria-sequencer/src/fees/component.rs | 119 ++ crates/astria-sequencer/src/fees/mod.rs | 380 ++++ crates/astria-sequencer/src/fees/query.rs | 66 + crates/astria-sequencer/src/fees/state_ext.rs | 968 +++++++++ .../astria-sequencer/src/fees/storage/keys.rs | 120 ++ .../astria-sequencer/src/fees/storage/mod.rs | 20 + ...orage__keys__tests__allowed_asset_key.snap | 5 + ...ge__keys__tests__allowed_asset_prefix.snap | 5 + ...ge__keys__tests__bridge_lock_fees_key.snap | 5 + ...s__tests__bridge_sudo_change_fees_key.snap | 5 + ...__keys__tests__bridge_unlock_fees_key.snap | 5 + ...eys__tests__fee_asset_change_fees_key.snap | 5 + ...age__keys__tests__fee_change_fees_key.snap | 5 + ...rage__keys__tests__ibc_relay_fees_key.snap | 5 + ...s__tests__ibc_relayer_change_fees_key.snap | 5 + ...keys__tests__ibc_sudo_change_fees_key.snap | 5 + ...eys__tests__ics20_withdrawal_fees_key.snap | 5 + ...__tests__init_bridge_account_fees_key.snap | 5 + ...orage__keys__tests__sequence_fees_key.snap | 5 + ...__tests__sudo_address_change_fees_key.snap | 5 + ...torage__keys__tests__transer_fees_key.snap | 5 + ...eys__tests__validator_update_fees_key.snap | 5 + .../src/fees/storage/values.rs | 206 ++ crates/astria-sequencer/src/fees/tests.rs | 469 +++++ crates/astria-sequencer/src/ibc/component.rs | 3 - .../src/ibc/ics20_withdrawal.rs | 16 - crates/astria-sequencer/src/ibc/state_ext.rs | 35 - .../astria-sequencer/src/ibc/storage/keys.rs | 3 - .../astria-sequencer/src/ibc/storage/mod.rs | 1 - ...keys__tests__keys_should_not_change-2.snap | 5 +- ...keys__tests__keys_should_not_change-3.snap | 5 +- .../src/ibc/storage/values.rs | 33 - crates/astria-sequencer/src/lib.rs | 2 +- .../astria-sequencer/src/sequence/action.rs | 89 +- .../src/sequence/component.rs | 51 - crates/astria-sequencer/src/sequence/mod.rs | 9 - .../src/sequence/state_ext.rs | 112 -- .../src/sequence/storage/keys.rs | 22 - .../src/sequence/storage/mod.rs | 5 - ...keys__tests__keys_should_not_change-2.snap | 6 - ...__keys__tests__keys_should_not_change.snap | 6 - .../src/sequence/storage/values.rs | 45 - .../astria-sequencer/src/service/info/mod.rs | 5 +- .../src/storage/stored_value.rs | 2 +- crates/astria-sequencer/src/test_utils.rs | 24 + .../src/transaction/checks.rs | 424 ++-- .../astria-sequencer/src/transaction/mod.rs | 49 +- .../astria-sequencer/src/transaction/query.rs | 2 +- .../astria/protocol/fees/v1alpha1/types.proto | 86 + .../protocol/genesis/v1alpha1/types.proto | 26 +- .../transactions/v1alpha1/action.proto | 43 +- 118 files changed, 7209 insertions(+), 3409 deletions(-) create mode 100644 crates/astria-core/src/generated/astria.protocol.fees.v1alpha1.rs create mode 100644 crates/astria-core/src/generated/astria.protocol.fees.v1alpha1.serde.rs create mode 100644 crates/astria-core/src/protocol/fees/mod.rs create mode 100644 crates/astria-core/src/protocol/fees/v1alpha1.rs delete mode 100644 crates/astria-sequencer/src/app/tests_block_fees.rs create mode 100644 crates/astria-sequencer/src/assets/snapshots/astria_sequencer__assets__state_ext__tests__storage_keys_are_unchanged-2.snap delete mode 100644 crates/astria-sequencer/src/bridge/component.rs delete mode 100644 crates/astria-sequencer/src/bridge/storage/values/fee.rs delete mode 100644 crates/astria-sequencer/src/fee_asset_change.rs create mode 100644 crates/astria-sequencer/src/fees/action.rs create mode 100644 crates/astria-sequencer/src/fees/component.rs create mode 100644 crates/astria-sequencer/src/fees/mod.rs create mode 100644 crates/astria-sequencer/src/fees/query.rs create mode 100644 crates/astria-sequencer/src/fees/state_ext.rs create mode 100644 crates/astria-sequencer/src/fees/storage/keys.rs create mode 100644 crates/astria-sequencer/src/fees/storage/mod.rs create mode 100644 crates/astria-sequencer/src/fees/storage/snapshots/astria_sequencer__fees__storage__keys__tests__allowed_asset_key.snap create mode 100644 crates/astria-sequencer/src/fees/storage/snapshots/astria_sequencer__fees__storage__keys__tests__allowed_asset_prefix.snap create mode 100644 crates/astria-sequencer/src/fees/storage/snapshots/astria_sequencer__fees__storage__keys__tests__bridge_lock_fees_key.snap create mode 100644 crates/astria-sequencer/src/fees/storage/snapshots/astria_sequencer__fees__storage__keys__tests__bridge_sudo_change_fees_key.snap create mode 100644 crates/astria-sequencer/src/fees/storage/snapshots/astria_sequencer__fees__storage__keys__tests__bridge_unlock_fees_key.snap create mode 100644 crates/astria-sequencer/src/fees/storage/snapshots/astria_sequencer__fees__storage__keys__tests__fee_asset_change_fees_key.snap create mode 100644 crates/astria-sequencer/src/fees/storage/snapshots/astria_sequencer__fees__storage__keys__tests__fee_change_fees_key.snap create mode 100644 crates/astria-sequencer/src/fees/storage/snapshots/astria_sequencer__fees__storage__keys__tests__ibc_relay_fees_key.snap create mode 100644 crates/astria-sequencer/src/fees/storage/snapshots/astria_sequencer__fees__storage__keys__tests__ibc_relayer_change_fees_key.snap create mode 100644 crates/astria-sequencer/src/fees/storage/snapshots/astria_sequencer__fees__storage__keys__tests__ibc_sudo_change_fees_key.snap create mode 100644 crates/astria-sequencer/src/fees/storage/snapshots/astria_sequencer__fees__storage__keys__tests__ics20_withdrawal_fees_key.snap create mode 100644 crates/astria-sequencer/src/fees/storage/snapshots/astria_sequencer__fees__storage__keys__tests__init_bridge_account_fees_key.snap create mode 100644 crates/astria-sequencer/src/fees/storage/snapshots/astria_sequencer__fees__storage__keys__tests__sequence_fees_key.snap create mode 100644 crates/astria-sequencer/src/fees/storage/snapshots/astria_sequencer__fees__storage__keys__tests__sudo_address_change_fees_key.snap create mode 100644 crates/astria-sequencer/src/fees/storage/snapshots/astria_sequencer__fees__storage__keys__tests__transer_fees_key.snap create mode 100644 crates/astria-sequencer/src/fees/storage/snapshots/astria_sequencer__fees__storage__keys__tests__validator_update_fees_key.snap create mode 100644 crates/astria-sequencer/src/fees/storage/values.rs create mode 100644 crates/astria-sequencer/src/fees/tests.rs delete mode 100644 crates/astria-sequencer/src/sequence/component.rs delete mode 100644 crates/astria-sequencer/src/sequence/state_ext.rs delete mode 100644 crates/astria-sequencer/src/sequence/storage/keys.rs delete mode 100644 crates/astria-sequencer/src/sequence/storage/mod.rs delete mode 100644 crates/astria-sequencer/src/sequence/storage/snapshots/astria_sequencer__sequence__storage__keys__tests__keys_should_not_change-2.snap delete mode 100644 crates/astria-sequencer/src/sequence/storage/snapshots/astria_sequencer__sequence__storage__keys__tests__keys_should_not_change.snap delete mode 100644 crates/astria-sequencer/src/sequence/storage/values.rs create mode 100644 proto/protocolapis/astria/protocol/fees/v1alpha1/types.proto diff --git a/Cargo.lock b/Cargo.lock index 74eec93164..f5f4485d1a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -787,7 +787,7 @@ dependencies = [ [[package]] name = "astria-sequencer" -version = "0.17.0" +version = "0.18.0" dependencies = [ "astria-build-info", "astria-config", diff --git a/charts/sequencer/Chart.yaml b/charts/sequencer/Chart.yaml index a76eec5f5b..d558deb2da 100644 --- a/charts/sequencer/Chart.yaml +++ b/charts/sequencer/Chart.yaml @@ -15,12 +15,12 @@ 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.23.2 +version: 0.24.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.17.0" +appVersion: "0.18.0" dependencies: - name: sequencer-relayer diff --git a/charts/sequencer/files/cometbft/config/genesis.json b/charts/sequencer/files/cometbft/config/genesis.json index 8caf12aa32..750ce5c0e1 100644 --- a/charts/sequencer/files/cometbft/config/genesis.json +++ b/charts/sequencer/files/cometbft/config/genesis.json @@ -3,13 +3,72 @@ "app_state": { "native_asset_base_denomination": "{{ .Values.genesis.nativeAssetBaseDenomination }}", "fees": { - "transfer_base_fee": {{ include "sequencer.toUint128Proto" .Values.genesis.fees.transferBaseFee }}, - "sequence_base_fee": {{ include "sequencer.toUint128Proto" .Values.genesis.fees.sequenceBaseFee }}, - "sequence_byte_cost_multiplier": {{ include "sequencer.toUint128Proto" .Values.genesis.fees.sequenceByteCostMultiplier }}, - "init_bridge_account_base_fee": {{ include "sequencer.toUint128Proto" .Values.genesis.fees.initBridgeAccountBaseFee }}, - "bridge_lock_byte_cost_multiplier": {{ include "sequencer.toUint128Proto" .Values.genesis.fees.bridgeLockByteCostMultiplier }}, - "bridge_sudo_change_fee": {{ include "sequencer.toUint128Proto" .Values.genesis.fees.bridgeSudoChangeFee }}, - "ics20_withdrawal_base_fee": {{ include "sequencer.toUint128Proto" .Values.genesis.fees.ics20WithdrawalBaseFee }} + {{- if not .Values.global.dev }} + "transfer_base_fee": {{ include "sequencer.toUint128Proto" .Values.genesis.fees.transfer.base }}, + "sequence_base_fee": {{ include "sequencer.toUint128Proto" .Values.genesis.fees.sequence.base }}, + "sequence_byte_cost_multiplier": {{ include "sequencer.toUint128Proto" .Values.genesis.fees.sequence.multiplier }}, + "init_bridge_account_base_fee": {{ include "sequencer.toUint128Proto" .Values.genesis.fees.initBridgeAccount.base }}, + "bridge_lock_byte_cost_multiplier": {{ include "sequencer.toUint128Proto" .Values.genesis.fees.bridgeLock.multiplier }}, + "bridge_sudo_change_fee": {{ include "sequencer.toUint128Proto" .Values.genesis.fees.bridgeSudoChange.base }}, + "ics20_withdrawal_base_fee": {{ include "sequencer.toUint128Proto" .Values.genesis.fees.ics20Withdrawal.base }} + {{- else }} + "bridge_lock": { + "base": {{ include "sequencer.toUint128Proto" .Values.genesis.fees.bridgeLock.base }}, + "multiplier": {{ include "sequencer.toUint128Proto" .Values.genesis.fees.bridgeLock.multiplier }} + }, + "bridge_sudo_change": { + "base": {{ include "sequencer.toUint128Proto" .Values.genesis.fees.bridgeSudoChange.base }}, + "multiplier": {{ include "sequencer.toUint128Proto" .Values.genesis.fees.bridgeSudoChange.multiplier }} + }, + "bridge_unlock": { + "base": {{ include "sequencer.toUint128Proto" .Values.genesis.fees.bridgeUnlock.base }}, + "multiplier": {{ include "sequencer.toUint128Proto" .Values.genesis.fees.bridgeUnlock.multiplier }} + }, + "fee_asset_change": { + "base": {{ include "sequencer.toUint128Proto" .Values.genesis.fees.feeAssetChange.base }}, + "multiplier": {{ include "sequencer.toUint128Proto" .Values.genesis.fees.feeAssetChange.multiplier }} + }, + "fee_change": { + "base": {{ include "sequencer.toUint128Proto" .Values.genesis.fees.feeChange.base }}, + "multiplier": {{ include "sequencer.toUint128Proto" .Values.genesis.fees.feeChange.multiplier }} + }, + "ibc_relay": { + "base": {{ include "sequencer.toUint128Proto" .Values.genesis.fees.ibcRelay.base }}, + "multiplier": {{ include "sequencer.toUint128Proto" .Values.genesis.fees.ibcRelay.multiplier }} + }, + "ibc_relayer_change": { + "base": {{ include "sequencer.toUint128Proto" .Values.genesis.fees.ibcRelayerChange.base }}, + "multiplier": {{ include "sequencer.toUint128Proto" .Values.genesis.fees.ibcRelayerChange.multiplier }} + }, + "ibc_sudo_change": { + "base": {{ include "sequencer.toUint128Proto" .Values.genesis.fees.ibcSudoChange.base }}, + "multiplier": {{ include "sequencer.toUint128Proto" .Values.genesis.fees.ibcSudoChange.multiplier }} + }, + "ics20_withdrawal": { + "base": {{ include "sequencer.toUint128Proto" .Values.genesis.fees.ics20Withdrawal.base }}, + "multiplier": {{ include "sequencer.toUint128Proto" .Values.genesis.fees.ics20Withdrawal.multiplier }} + }, + "init_bridge_account": { + "base": {{ include "sequencer.toUint128Proto" .Values.genesis.fees.initBridgeAccount.base }}, + "multiplier": {{ include "sequencer.toUint128Proto" .Values.genesis.fees.initBridgeAccount.multiplier }} + }, + "sequence": { + "base": {{ include "sequencer.toUint128Proto" .Values.genesis.fees.sequence.base }}, + "multiplier": {{ include "sequencer.toUint128Proto" .Values.genesis.fees.sequence.multiplier }} + }, + "sudo_address_change": { + "base": {{ include "sequencer.toUint128Proto" .Values.genesis.fees.sudoAddressChange.base }}, + "multiplier": {{ include "sequencer.toUint128Proto" .Values.genesis.fees.sudoAddressChange.multiplier }} + }, + "transfer": { + "base": {{ include "sequencer.toUint128Proto" .Values.genesis.fees.transfer.base }}, + "multiplier": {{ include "sequencer.toUint128Proto" .Values.genesis.fees.transfer.multiplier }} + }, + "validator_update": { + "base": {{ include "sequencer.toUint128Proto" .Values.genesis.fees.validatorUpdate.base }}, + "multiplier": {{ include "sequencer.toUint128Proto" .Values.genesis.fees.validatorUpdate.multiplier }} + } + {{- end }} }, "allowed_fee_assets": [ {{- range $index, $value := .Values.genesis.allowedFeeAssets }} diff --git a/charts/sequencer/values.yaml b/charts/sequencer/values.yaml index 85faf008db..e46129428e 100644 --- a/charts/sequencer/values.yaml +++ b/charts/sequencer/values.yaml @@ -23,7 +23,7 @@ images: sequencer: repo: ghcr.io/astriaorg/sequencer pullPolicy: IfNotPresent - tag: "0.17.0" + tag: "0.18.0" devTag: latest moniker: "" @@ -56,13 +56,48 @@ genesis: maxBytes: "1048576" fees: - transferBaseFee: "12" - sequenceBaseFee: "32" - sequenceByteCostMultiplier: "1" - initBridgeAccountBaseFee: "48" - bridgeLockByteCostMultiplier: "1" - bridgeSudoChangeFee: "24" - ics20WithdrawalBaseFee: "24" + bridgeLock: + base: "0" + multiplier: "1" + bridgeSudoChange: + base: "24" + multiplier: "0" + bridgeUnlock: + base: "0" + multiplier: "0" + feeAssetChange: + base: "0" + multiplier: "0" + feeChange: + base: "0" + multiplier: "0" + ibcRelay: + base: "0" + multiplier: "0" + ibcRelayerChange: + base: "0" + multiplier: "0" + ibcSudoChange: + base: "0" + multiplier: "0" + ics20Withdrawal: + base: "24" + multiplier: "0" + initBridgeAccount: + base: "48" + multiplier: "0" + sequence: + base: "32" + multiplier: "1" + sudoAddressChange: + base: "0" + multiplier: "0" + transfer: + base: "12" + multiplier: "0" + validatorUpdate: + base: "0" + multiplier: "0" validators: [] # - name: core diff --git a/crates/astria-core/src/generated/astria.protocol.fees.v1alpha1.rs b/crates/astria-core/src/generated/astria.protocol.fees.v1alpha1.rs new file mode 100644 index 0000000000..03998e0122 --- /dev/null +++ b/crates/astria-core/src/generated/astria.protocol.fees.v1alpha1.rs @@ -0,0 +1,241 @@ +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct TransactionFee { + #[prost(string, tag = "1")] + pub asset: ::prost::alloc::string::String, + #[prost(message, optional, tag = "2")] + pub fee: ::core::option::Option, +} +impl ::prost::Name for TransactionFee { + const NAME: &'static str = "TransactionFee"; + const PACKAGE: &'static str = "astria.protocol.fees.v1alpha1"; + fn full_name() -> ::prost::alloc::string::String { + ::prost::alloc::format!("astria.protocol.fees.v1alpha1.{}", Self::NAME) + } +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct TransferFeeComponents { + #[prost(message, optional, tag = "1")] + pub base: ::core::option::Option, + #[prost(message, optional, tag = "2")] + pub multiplier: ::core::option::Option, +} +impl ::prost::Name for TransferFeeComponents { + const NAME: &'static str = "TransferFeeComponents"; + const PACKAGE: &'static str = "astria.protocol.fees.v1alpha1"; + fn full_name() -> ::prost::alloc::string::String { + ::prost::alloc::format!("astria.protocol.fees.v1alpha1.{}", Self::NAME) + } +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct SequenceFeeComponents { + #[prost(message, optional, tag = "1")] + pub base: ::core::option::Option, + #[prost(message, optional, tag = "2")] + pub multiplier: ::core::option::Option, +} +impl ::prost::Name for SequenceFeeComponents { + const NAME: &'static str = "SequenceFeeComponents"; + const PACKAGE: &'static str = "astria.protocol.fees.v1alpha1"; + fn full_name() -> ::prost::alloc::string::String { + ::prost::alloc::format!("astria.protocol.fees.v1alpha1.{}", Self::NAME) + } +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct InitBridgeAccountFeeComponents { + #[prost(message, optional, tag = "1")] + pub base: ::core::option::Option, + #[prost(message, optional, tag = "2")] + pub multiplier: ::core::option::Option, +} +impl ::prost::Name for InitBridgeAccountFeeComponents { + const NAME: &'static str = "InitBridgeAccountFeeComponents"; + const PACKAGE: &'static str = "astria.protocol.fees.v1alpha1"; + fn full_name() -> ::prost::alloc::string::String { + ::prost::alloc::format!("astria.protocol.fees.v1alpha1.{}", Self::NAME) + } +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct BridgeLockFeeComponents { + #[prost(message, optional, tag = "1")] + pub base: ::core::option::Option, + #[prost(message, optional, tag = "2")] + pub multiplier: ::core::option::Option, +} +impl ::prost::Name for BridgeLockFeeComponents { + const NAME: &'static str = "BridgeLockFeeComponents"; + const PACKAGE: &'static str = "astria.protocol.fees.v1alpha1"; + fn full_name() -> ::prost::alloc::string::String { + ::prost::alloc::format!("astria.protocol.fees.v1alpha1.{}", Self::NAME) + } +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct BridgeUnlockFeeComponents { + #[prost(message, optional, tag = "1")] + pub base: ::core::option::Option, + #[prost(message, optional, tag = "2")] + pub multiplier: ::core::option::Option, +} +impl ::prost::Name for BridgeUnlockFeeComponents { + const NAME: &'static str = "BridgeUnlockFeeComponents"; + const PACKAGE: &'static str = "astria.protocol.fees.v1alpha1"; + fn full_name() -> ::prost::alloc::string::String { + ::prost::alloc::format!("astria.protocol.fees.v1alpha1.{}", Self::NAME) + } +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct BridgeSudoChangeFeeComponents { + #[prost(message, optional, tag = "1")] + pub base: ::core::option::Option, + #[prost(message, optional, tag = "2")] + pub multiplier: ::core::option::Option, +} +impl ::prost::Name for BridgeSudoChangeFeeComponents { + const NAME: &'static str = "BridgeSudoChangeFeeComponents"; + const PACKAGE: &'static str = "astria.protocol.fees.v1alpha1"; + fn full_name() -> ::prost::alloc::string::String { + ::prost::alloc::format!("astria.protocol.fees.v1alpha1.{}", Self::NAME) + } +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct Ics20WithdrawalFeeComponents { + #[prost(message, optional, tag = "1")] + pub base: ::core::option::Option, + #[prost(message, optional, tag = "2")] + pub multiplier: ::core::option::Option, +} +impl ::prost::Name for Ics20WithdrawalFeeComponents { + const NAME: &'static str = "Ics20WithdrawalFeeComponents"; + const PACKAGE: &'static str = "astria.protocol.fees.v1alpha1"; + fn full_name() -> ::prost::alloc::string::String { + ::prost::alloc::format!("astria.protocol.fees.v1alpha1.{}", Self::NAME) + } +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct IbcRelayFeeComponents { + #[prost(message, optional, tag = "1")] + pub base: ::core::option::Option, + #[prost(message, optional, tag = "2")] + pub multiplier: ::core::option::Option, +} +impl ::prost::Name for IbcRelayFeeComponents { + const NAME: &'static str = "IbcRelayFeeComponents"; + const PACKAGE: &'static str = "astria.protocol.fees.v1alpha1"; + fn full_name() -> ::prost::alloc::string::String { + ::prost::alloc::format!("astria.protocol.fees.v1alpha1.{}", Self::NAME) + } +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct ValidatorUpdateFeeComponents { + #[prost(message, optional, tag = "1")] + pub base: ::core::option::Option, + #[prost(message, optional, tag = "2")] + pub multiplier: ::core::option::Option, +} +impl ::prost::Name for ValidatorUpdateFeeComponents { + const NAME: &'static str = "ValidatorUpdateFeeComponents"; + const PACKAGE: &'static str = "astria.protocol.fees.v1alpha1"; + fn full_name() -> ::prost::alloc::string::String { + ::prost::alloc::format!("astria.protocol.fees.v1alpha1.{}", Self::NAME) + } +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct FeeAssetChangeFeeComponents { + #[prost(message, optional, tag = "1")] + pub base: ::core::option::Option, + #[prost(message, optional, tag = "2")] + pub multiplier: ::core::option::Option, +} +impl ::prost::Name for FeeAssetChangeFeeComponents { + const NAME: &'static str = "FeeAssetChangeFeeComponents"; + const PACKAGE: &'static str = "astria.protocol.fees.v1alpha1"; + fn full_name() -> ::prost::alloc::string::String { + ::prost::alloc::format!("astria.protocol.fees.v1alpha1.{}", Self::NAME) + } +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct FeeChangeFeeComponents { + #[prost(message, optional, tag = "1")] + pub base: ::core::option::Option, + #[prost(message, optional, tag = "2")] + pub multiplier: ::core::option::Option, +} +impl ::prost::Name for FeeChangeFeeComponents { + const NAME: &'static str = "FeeChangeFeeComponents"; + const PACKAGE: &'static str = "astria.protocol.fees.v1alpha1"; + fn full_name() -> ::prost::alloc::string::String { + ::prost::alloc::format!("astria.protocol.fees.v1alpha1.{}", Self::NAME) + } +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct IbcRelayerChangeFeeComponents { + #[prost(message, optional, tag = "1")] + pub base: ::core::option::Option, + #[prost(message, optional, tag = "2")] + pub multiplier: ::core::option::Option, +} +impl ::prost::Name for IbcRelayerChangeFeeComponents { + const NAME: &'static str = "IbcRelayerChangeFeeComponents"; + const PACKAGE: &'static str = "astria.protocol.fees.v1alpha1"; + fn full_name() -> ::prost::alloc::string::String { + ::prost::alloc::format!("astria.protocol.fees.v1alpha1.{}", Self::NAME) + } +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct SudoAddressChangeFeeComponents { + #[prost(message, optional, tag = "1")] + pub base: ::core::option::Option, + #[prost(message, optional, tag = "2")] + pub multiplier: ::core::option::Option, +} +impl ::prost::Name for SudoAddressChangeFeeComponents { + const NAME: &'static str = "SudoAddressChangeFeeComponents"; + const PACKAGE: &'static str = "astria.protocol.fees.v1alpha1"; + fn full_name() -> ::prost::alloc::string::String { + ::prost::alloc::format!("astria.protocol.fees.v1alpha1.{}", Self::NAME) + } +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct IbcSudoChangeFeeComponents { + #[prost(message, optional, tag = "1")] + pub base: ::core::option::Option, + #[prost(message, optional, tag = "2")] + pub multiplier: ::core::option::Option, +} +impl ::prost::Name for IbcSudoChangeFeeComponents { + const NAME: &'static str = "IbcSudoChangeFeeComponents"; + const PACKAGE: &'static str = "astria.protocol.fees.v1alpha1"; + fn full_name() -> ::prost::alloc::string::String { + ::prost::alloc::format!("astria.protocol.fees.v1alpha1.{}", Self::NAME) + } +} +/// Response to a transaction fee ABCI query. +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct TransactionFeeResponse { + #[prost(uint64, tag = "2")] + pub height: u64, + #[prost(message, repeated, tag = "3")] + pub fees: ::prost::alloc::vec::Vec, +} +impl ::prost::Name for TransactionFeeResponse { + const NAME: &'static str = "TransactionFeeResponse"; + const PACKAGE: &'static str = "astria.protocol.fees.v1alpha1"; + fn full_name() -> ::prost::alloc::string::String { + ::prost::alloc::format!("astria.protocol.fees.v1alpha1.{}", Self::NAME) + } +} diff --git a/crates/astria-core/src/generated/astria.protocol.fees.v1alpha1.serde.rs b/crates/astria-core/src/generated/astria.protocol.fees.v1alpha1.serde.rs new file mode 100644 index 0000000000..ae72b18920 --- /dev/null +++ b/crates/astria-core/src/generated/astria.protocol.fees.v1alpha1.serde.rs @@ -0,0 +1,1731 @@ +impl serde::Serialize for BridgeLockFeeComponents { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let mut len = 0; + if self.base.is_some() { + len += 1; + } + if self.multiplier.is_some() { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("astria.protocol.fees.v1alpha1.BridgeLockFeeComponents", len)?; + if let Some(v) = self.base.as_ref() { + struct_ser.serialize_field("base", v)?; + } + if let Some(v) = self.multiplier.as_ref() { + struct_ser.serialize_field("multiplier", v)?; + } + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for BridgeLockFeeComponents { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "base", + "multiplier", + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + Base, + Multiplier, + } + 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 { + "base" => Ok(GeneratedField::Base), + "multiplier" => Ok(GeneratedField::Multiplier), + _ => Err(serde::de::Error::unknown_field(value, FIELDS)), + } + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = BridgeLockFeeComponents; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct astria.protocol.fees.v1alpha1.BridgeLockFeeComponents") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + let mut base__ = None; + let mut multiplier__ = None; + while let Some(k) = map_.next_key()? { + match k { + GeneratedField::Base => { + if base__.is_some() { + return Err(serde::de::Error::duplicate_field("base")); + } + base__ = map_.next_value()?; + } + GeneratedField::Multiplier => { + if multiplier__.is_some() { + return Err(serde::de::Error::duplicate_field("multiplier")); + } + multiplier__ = map_.next_value()?; + } + } + } + Ok(BridgeLockFeeComponents { + base: base__, + multiplier: multiplier__, + }) + } + } + deserializer.deserialize_struct("astria.protocol.fees.v1alpha1.BridgeLockFeeComponents", FIELDS, GeneratedVisitor) + } +} +impl serde::Serialize for BridgeSudoChangeFeeComponents { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let mut len = 0; + if self.base.is_some() { + len += 1; + } + if self.multiplier.is_some() { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("astria.protocol.fees.v1alpha1.BridgeSudoChangeFeeComponents", len)?; + if let Some(v) = self.base.as_ref() { + struct_ser.serialize_field("base", v)?; + } + if let Some(v) = self.multiplier.as_ref() { + struct_ser.serialize_field("multiplier", v)?; + } + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for BridgeSudoChangeFeeComponents { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "base", + "multiplier", + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + Base, + Multiplier, + } + 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 { + "base" => Ok(GeneratedField::Base), + "multiplier" => Ok(GeneratedField::Multiplier), + _ => Err(serde::de::Error::unknown_field(value, FIELDS)), + } + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = BridgeSudoChangeFeeComponents; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct astria.protocol.fees.v1alpha1.BridgeSudoChangeFeeComponents") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + let mut base__ = None; + let mut multiplier__ = None; + while let Some(k) = map_.next_key()? { + match k { + GeneratedField::Base => { + if base__.is_some() { + return Err(serde::de::Error::duplicate_field("base")); + } + base__ = map_.next_value()?; + } + GeneratedField::Multiplier => { + if multiplier__.is_some() { + return Err(serde::de::Error::duplicate_field("multiplier")); + } + multiplier__ = map_.next_value()?; + } + } + } + Ok(BridgeSudoChangeFeeComponents { + base: base__, + multiplier: multiplier__, + }) + } + } + deserializer.deserialize_struct("astria.protocol.fees.v1alpha1.BridgeSudoChangeFeeComponents", FIELDS, GeneratedVisitor) + } +} +impl serde::Serialize for BridgeUnlockFeeComponents { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let mut len = 0; + if self.base.is_some() { + len += 1; + } + if self.multiplier.is_some() { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("astria.protocol.fees.v1alpha1.BridgeUnlockFeeComponents", len)?; + if let Some(v) = self.base.as_ref() { + struct_ser.serialize_field("base", v)?; + } + if let Some(v) = self.multiplier.as_ref() { + struct_ser.serialize_field("multiplier", v)?; + } + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for BridgeUnlockFeeComponents { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "base", + "multiplier", + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + Base, + Multiplier, + } + 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 { + "base" => Ok(GeneratedField::Base), + "multiplier" => Ok(GeneratedField::Multiplier), + _ => Err(serde::de::Error::unknown_field(value, FIELDS)), + } + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = BridgeUnlockFeeComponents; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct astria.protocol.fees.v1alpha1.BridgeUnlockFeeComponents") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + let mut base__ = None; + let mut multiplier__ = None; + while let Some(k) = map_.next_key()? { + match k { + GeneratedField::Base => { + if base__.is_some() { + return Err(serde::de::Error::duplicate_field("base")); + } + base__ = map_.next_value()?; + } + GeneratedField::Multiplier => { + if multiplier__.is_some() { + return Err(serde::de::Error::duplicate_field("multiplier")); + } + multiplier__ = map_.next_value()?; + } + } + } + Ok(BridgeUnlockFeeComponents { + base: base__, + multiplier: multiplier__, + }) + } + } + deserializer.deserialize_struct("astria.protocol.fees.v1alpha1.BridgeUnlockFeeComponents", FIELDS, GeneratedVisitor) + } +} +impl serde::Serialize for FeeAssetChangeFeeComponents { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let mut len = 0; + if self.base.is_some() { + len += 1; + } + if self.multiplier.is_some() { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("astria.protocol.fees.v1alpha1.FeeAssetChangeFeeComponents", len)?; + if let Some(v) = self.base.as_ref() { + struct_ser.serialize_field("base", v)?; + } + if let Some(v) = self.multiplier.as_ref() { + struct_ser.serialize_field("multiplier", v)?; + } + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for FeeAssetChangeFeeComponents { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "base", + "multiplier", + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + Base, + Multiplier, + } + 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 { + "base" => Ok(GeneratedField::Base), + "multiplier" => Ok(GeneratedField::Multiplier), + _ => Err(serde::de::Error::unknown_field(value, FIELDS)), + } + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = FeeAssetChangeFeeComponents; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct astria.protocol.fees.v1alpha1.FeeAssetChangeFeeComponents") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + let mut base__ = None; + let mut multiplier__ = None; + while let Some(k) = map_.next_key()? { + match k { + GeneratedField::Base => { + if base__.is_some() { + return Err(serde::de::Error::duplicate_field("base")); + } + base__ = map_.next_value()?; + } + GeneratedField::Multiplier => { + if multiplier__.is_some() { + return Err(serde::de::Error::duplicate_field("multiplier")); + } + multiplier__ = map_.next_value()?; + } + } + } + Ok(FeeAssetChangeFeeComponents { + base: base__, + multiplier: multiplier__, + }) + } + } + deserializer.deserialize_struct("astria.protocol.fees.v1alpha1.FeeAssetChangeFeeComponents", FIELDS, GeneratedVisitor) + } +} +impl serde::Serialize for FeeChangeFeeComponents { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let mut len = 0; + if self.base.is_some() { + len += 1; + } + if self.multiplier.is_some() { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("astria.protocol.fees.v1alpha1.FeeChangeFeeComponents", len)?; + if let Some(v) = self.base.as_ref() { + struct_ser.serialize_field("base", v)?; + } + if let Some(v) = self.multiplier.as_ref() { + struct_ser.serialize_field("multiplier", v)?; + } + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for FeeChangeFeeComponents { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "base", + "multiplier", + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + Base, + Multiplier, + } + 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 { + "base" => Ok(GeneratedField::Base), + "multiplier" => Ok(GeneratedField::Multiplier), + _ => Err(serde::de::Error::unknown_field(value, FIELDS)), + } + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = FeeChangeFeeComponents; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct astria.protocol.fees.v1alpha1.FeeChangeFeeComponents") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + let mut base__ = None; + let mut multiplier__ = None; + while let Some(k) = map_.next_key()? { + match k { + GeneratedField::Base => { + if base__.is_some() { + return Err(serde::de::Error::duplicate_field("base")); + } + base__ = map_.next_value()?; + } + GeneratedField::Multiplier => { + if multiplier__.is_some() { + return Err(serde::de::Error::duplicate_field("multiplier")); + } + multiplier__ = map_.next_value()?; + } + } + } + Ok(FeeChangeFeeComponents { + base: base__, + multiplier: multiplier__, + }) + } + } + deserializer.deserialize_struct("astria.protocol.fees.v1alpha1.FeeChangeFeeComponents", FIELDS, GeneratedVisitor) + } +} +impl serde::Serialize for IbcRelayFeeComponents { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let mut len = 0; + if self.base.is_some() { + len += 1; + } + if self.multiplier.is_some() { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("astria.protocol.fees.v1alpha1.IbcRelayFeeComponents", len)?; + if let Some(v) = self.base.as_ref() { + struct_ser.serialize_field("base", v)?; + } + if let Some(v) = self.multiplier.as_ref() { + struct_ser.serialize_field("multiplier", v)?; + } + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for IbcRelayFeeComponents { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "base", + "multiplier", + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + Base, + Multiplier, + } + 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 { + "base" => Ok(GeneratedField::Base), + "multiplier" => Ok(GeneratedField::Multiplier), + _ => Err(serde::de::Error::unknown_field(value, FIELDS)), + } + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = IbcRelayFeeComponents; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct astria.protocol.fees.v1alpha1.IbcRelayFeeComponents") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + let mut base__ = None; + let mut multiplier__ = None; + while let Some(k) = map_.next_key()? { + match k { + GeneratedField::Base => { + if base__.is_some() { + return Err(serde::de::Error::duplicate_field("base")); + } + base__ = map_.next_value()?; + } + GeneratedField::Multiplier => { + if multiplier__.is_some() { + return Err(serde::de::Error::duplicate_field("multiplier")); + } + multiplier__ = map_.next_value()?; + } + } + } + Ok(IbcRelayFeeComponents { + base: base__, + multiplier: multiplier__, + }) + } + } + deserializer.deserialize_struct("astria.protocol.fees.v1alpha1.IbcRelayFeeComponents", FIELDS, GeneratedVisitor) + } +} +impl serde::Serialize for IbcRelayerChangeFeeComponents { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let mut len = 0; + if self.base.is_some() { + len += 1; + } + if self.multiplier.is_some() { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("astria.protocol.fees.v1alpha1.IbcRelayerChangeFeeComponents", len)?; + if let Some(v) = self.base.as_ref() { + struct_ser.serialize_field("base", v)?; + } + if let Some(v) = self.multiplier.as_ref() { + struct_ser.serialize_field("multiplier", v)?; + } + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for IbcRelayerChangeFeeComponents { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "base", + "multiplier", + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + Base, + Multiplier, + } + 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 { + "base" => Ok(GeneratedField::Base), + "multiplier" => Ok(GeneratedField::Multiplier), + _ => Err(serde::de::Error::unknown_field(value, FIELDS)), + } + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = IbcRelayerChangeFeeComponents; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct astria.protocol.fees.v1alpha1.IbcRelayerChangeFeeComponents") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + let mut base__ = None; + let mut multiplier__ = None; + while let Some(k) = map_.next_key()? { + match k { + GeneratedField::Base => { + if base__.is_some() { + return Err(serde::de::Error::duplicate_field("base")); + } + base__ = map_.next_value()?; + } + GeneratedField::Multiplier => { + if multiplier__.is_some() { + return Err(serde::de::Error::duplicate_field("multiplier")); + } + multiplier__ = map_.next_value()?; + } + } + } + Ok(IbcRelayerChangeFeeComponents { + base: base__, + multiplier: multiplier__, + }) + } + } + deserializer.deserialize_struct("astria.protocol.fees.v1alpha1.IbcRelayerChangeFeeComponents", FIELDS, GeneratedVisitor) + } +} +impl serde::Serialize for IbcSudoChangeFeeComponents { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let mut len = 0; + if self.base.is_some() { + len += 1; + } + if self.multiplier.is_some() { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("astria.protocol.fees.v1alpha1.IbcSudoChangeFeeComponents", len)?; + if let Some(v) = self.base.as_ref() { + struct_ser.serialize_field("base", v)?; + } + if let Some(v) = self.multiplier.as_ref() { + struct_ser.serialize_field("multiplier", v)?; + } + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for IbcSudoChangeFeeComponents { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "base", + "multiplier", + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + Base, + Multiplier, + } + 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 { + "base" => Ok(GeneratedField::Base), + "multiplier" => Ok(GeneratedField::Multiplier), + _ => Err(serde::de::Error::unknown_field(value, FIELDS)), + } + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = IbcSudoChangeFeeComponents; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct astria.protocol.fees.v1alpha1.IbcSudoChangeFeeComponents") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + let mut base__ = None; + let mut multiplier__ = None; + while let Some(k) = map_.next_key()? { + match k { + GeneratedField::Base => { + if base__.is_some() { + return Err(serde::de::Error::duplicate_field("base")); + } + base__ = map_.next_value()?; + } + GeneratedField::Multiplier => { + if multiplier__.is_some() { + return Err(serde::de::Error::duplicate_field("multiplier")); + } + multiplier__ = map_.next_value()?; + } + } + } + Ok(IbcSudoChangeFeeComponents { + base: base__, + multiplier: multiplier__, + }) + } + } + deserializer.deserialize_struct("astria.protocol.fees.v1alpha1.IbcSudoChangeFeeComponents", FIELDS, GeneratedVisitor) + } +} +impl serde::Serialize for Ics20WithdrawalFeeComponents { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let mut len = 0; + if self.base.is_some() { + len += 1; + } + if self.multiplier.is_some() { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("astria.protocol.fees.v1alpha1.Ics20WithdrawalFeeComponents", len)?; + if let Some(v) = self.base.as_ref() { + struct_ser.serialize_field("base", v)?; + } + if let Some(v) = self.multiplier.as_ref() { + struct_ser.serialize_field("multiplier", v)?; + } + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for Ics20WithdrawalFeeComponents { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "base", + "multiplier", + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + Base, + Multiplier, + } + 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 { + "base" => Ok(GeneratedField::Base), + "multiplier" => Ok(GeneratedField::Multiplier), + _ => Err(serde::de::Error::unknown_field(value, FIELDS)), + } + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = Ics20WithdrawalFeeComponents; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct astria.protocol.fees.v1alpha1.Ics20WithdrawalFeeComponents") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + let mut base__ = None; + let mut multiplier__ = None; + while let Some(k) = map_.next_key()? { + match k { + GeneratedField::Base => { + if base__.is_some() { + return Err(serde::de::Error::duplicate_field("base")); + } + base__ = map_.next_value()?; + } + GeneratedField::Multiplier => { + if multiplier__.is_some() { + return Err(serde::de::Error::duplicate_field("multiplier")); + } + multiplier__ = map_.next_value()?; + } + } + } + Ok(Ics20WithdrawalFeeComponents { + base: base__, + multiplier: multiplier__, + }) + } + } + deserializer.deserialize_struct("astria.protocol.fees.v1alpha1.Ics20WithdrawalFeeComponents", FIELDS, GeneratedVisitor) + } +} +impl serde::Serialize for InitBridgeAccountFeeComponents { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let mut len = 0; + if self.base.is_some() { + len += 1; + } + if self.multiplier.is_some() { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("astria.protocol.fees.v1alpha1.InitBridgeAccountFeeComponents", len)?; + if let Some(v) = self.base.as_ref() { + struct_ser.serialize_field("base", v)?; + } + if let Some(v) = self.multiplier.as_ref() { + struct_ser.serialize_field("multiplier", v)?; + } + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for InitBridgeAccountFeeComponents { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "base", + "multiplier", + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + Base, + Multiplier, + } + 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 { + "base" => Ok(GeneratedField::Base), + "multiplier" => Ok(GeneratedField::Multiplier), + _ => Err(serde::de::Error::unknown_field(value, FIELDS)), + } + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = InitBridgeAccountFeeComponents; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct astria.protocol.fees.v1alpha1.InitBridgeAccountFeeComponents") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + let mut base__ = None; + let mut multiplier__ = None; + while let Some(k) = map_.next_key()? { + match k { + GeneratedField::Base => { + if base__.is_some() { + return Err(serde::de::Error::duplicate_field("base")); + } + base__ = map_.next_value()?; + } + GeneratedField::Multiplier => { + if multiplier__.is_some() { + return Err(serde::de::Error::duplicate_field("multiplier")); + } + multiplier__ = map_.next_value()?; + } + } + } + Ok(InitBridgeAccountFeeComponents { + base: base__, + multiplier: multiplier__, + }) + } + } + deserializer.deserialize_struct("astria.protocol.fees.v1alpha1.InitBridgeAccountFeeComponents", FIELDS, GeneratedVisitor) + } +} +impl serde::Serialize for SequenceFeeComponents { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let mut len = 0; + if self.base.is_some() { + len += 1; + } + if self.multiplier.is_some() { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("astria.protocol.fees.v1alpha1.SequenceFeeComponents", len)?; + if let Some(v) = self.base.as_ref() { + struct_ser.serialize_field("base", v)?; + } + if let Some(v) = self.multiplier.as_ref() { + struct_ser.serialize_field("multiplier", v)?; + } + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for SequenceFeeComponents { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "base", + "multiplier", + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + Base, + Multiplier, + } + 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 { + "base" => Ok(GeneratedField::Base), + "multiplier" => Ok(GeneratedField::Multiplier), + _ => Err(serde::de::Error::unknown_field(value, FIELDS)), + } + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = SequenceFeeComponents; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct astria.protocol.fees.v1alpha1.SequenceFeeComponents") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + let mut base__ = None; + let mut multiplier__ = None; + while let Some(k) = map_.next_key()? { + match k { + GeneratedField::Base => { + if base__.is_some() { + return Err(serde::de::Error::duplicate_field("base")); + } + base__ = map_.next_value()?; + } + GeneratedField::Multiplier => { + if multiplier__.is_some() { + return Err(serde::de::Error::duplicate_field("multiplier")); + } + multiplier__ = map_.next_value()?; + } + } + } + Ok(SequenceFeeComponents { + base: base__, + multiplier: multiplier__, + }) + } + } + deserializer.deserialize_struct("astria.protocol.fees.v1alpha1.SequenceFeeComponents", FIELDS, GeneratedVisitor) + } +} +impl serde::Serialize for SudoAddressChangeFeeComponents { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let mut len = 0; + if self.base.is_some() { + len += 1; + } + if self.multiplier.is_some() { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("astria.protocol.fees.v1alpha1.SudoAddressChangeFeeComponents", len)?; + if let Some(v) = self.base.as_ref() { + struct_ser.serialize_field("base", v)?; + } + if let Some(v) = self.multiplier.as_ref() { + struct_ser.serialize_field("multiplier", v)?; + } + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for SudoAddressChangeFeeComponents { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "base", + "multiplier", + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + Base, + Multiplier, + } + 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 { + "base" => Ok(GeneratedField::Base), + "multiplier" => Ok(GeneratedField::Multiplier), + _ => Err(serde::de::Error::unknown_field(value, FIELDS)), + } + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = SudoAddressChangeFeeComponents; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct astria.protocol.fees.v1alpha1.SudoAddressChangeFeeComponents") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + let mut base__ = None; + let mut multiplier__ = None; + while let Some(k) = map_.next_key()? { + match k { + GeneratedField::Base => { + if base__.is_some() { + return Err(serde::de::Error::duplicate_field("base")); + } + base__ = map_.next_value()?; + } + GeneratedField::Multiplier => { + if multiplier__.is_some() { + return Err(serde::de::Error::duplicate_field("multiplier")); + } + multiplier__ = map_.next_value()?; + } + } + } + Ok(SudoAddressChangeFeeComponents { + base: base__, + multiplier: multiplier__, + }) + } + } + deserializer.deserialize_struct("astria.protocol.fees.v1alpha1.SudoAddressChangeFeeComponents", FIELDS, GeneratedVisitor) + } +} +impl serde::Serialize for TransactionFee { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let mut len = 0; + if !self.asset.is_empty() { + len += 1; + } + if self.fee.is_some() { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("astria.protocol.fees.v1alpha1.TransactionFee", len)?; + if !self.asset.is_empty() { + struct_ser.serialize_field("asset", &self.asset)?; + } + if let Some(v) = self.fee.as_ref() { + struct_ser.serialize_field("fee", v)?; + } + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for TransactionFee { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "asset", + "fee", + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + Asset, + Fee, + } + 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 { + "asset" => Ok(GeneratedField::Asset), + "fee" => Ok(GeneratedField::Fee), + _ => Err(serde::de::Error::unknown_field(value, FIELDS)), + } + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = TransactionFee; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct astria.protocol.fees.v1alpha1.TransactionFee") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + let mut asset__ = None; + let mut fee__ = None; + while let Some(k) = map_.next_key()? { + match k { + GeneratedField::Asset => { + if asset__.is_some() { + return Err(serde::de::Error::duplicate_field("asset")); + } + asset__ = Some(map_.next_value()?); + } + GeneratedField::Fee => { + if fee__.is_some() { + return Err(serde::de::Error::duplicate_field("fee")); + } + fee__ = map_.next_value()?; + } + } + } + Ok(TransactionFee { + asset: asset__.unwrap_or_default(), + fee: fee__, + }) + } + } + deserializer.deserialize_struct("astria.protocol.fees.v1alpha1.TransactionFee", FIELDS, GeneratedVisitor) + } +} +impl serde::Serialize for TransactionFeeResponse { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let mut len = 0; + if self.height != 0 { + len += 1; + } + if !self.fees.is_empty() { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("astria.protocol.fees.v1alpha1.TransactionFeeResponse", len)?; + if self.height != 0 { + #[allow(clippy::needless_borrow)] + struct_ser.serialize_field("height", ToString::to_string(&self.height).as_str())?; + } + if !self.fees.is_empty() { + struct_ser.serialize_field("fees", &self.fees)?; + } + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for TransactionFeeResponse { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "height", + "fees", + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + Height, + Fees, + } + 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 { + "height" => Ok(GeneratedField::Height), + "fees" => Ok(GeneratedField::Fees), + _ => Err(serde::de::Error::unknown_field(value, FIELDS)), + } + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = TransactionFeeResponse; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct astria.protocol.fees.v1alpha1.TransactionFeeResponse") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + let mut height__ = None; + let mut fees__ = None; + while let Some(k) = map_.next_key()? { + match k { + GeneratedField::Height => { + if height__.is_some() { + return Err(serde::de::Error::duplicate_field("height")); + } + height__ = + Some(map_.next_value::<::pbjson::private::NumberDeserialize<_>>()?.0) + ; + } + GeneratedField::Fees => { + if fees__.is_some() { + return Err(serde::de::Error::duplicate_field("fees")); + } + fees__ = Some(map_.next_value()?); + } + } + } + Ok(TransactionFeeResponse { + height: height__.unwrap_or_default(), + fees: fees__.unwrap_or_default(), + }) + } + } + deserializer.deserialize_struct("astria.protocol.fees.v1alpha1.TransactionFeeResponse", FIELDS, GeneratedVisitor) + } +} +impl serde::Serialize for TransferFeeComponents { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let mut len = 0; + if self.base.is_some() { + len += 1; + } + if self.multiplier.is_some() { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("astria.protocol.fees.v1alpha1.TransferFeeComponents", len)?; + if let Some(v) = self.base.as_ref() { + struct_ser.serialize_field("base", v)?; + } + if let Some(v) = self.multiplier.as_ref() { + struct_ser.serialize_field("multiplier", v)?; + } + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for TransferFeeComponents { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "base", + "multiplier", + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + Base, + Multiplier, + } + 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 { + "base" => Ok(GeneratedField::Base), + "multiplier" => Ok(GeneratedField::Multiplier), + _ => Err(serde::de::Error::unknown_field(value, FIELDS)), + } + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = TransferFeeComponents; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct astria.protocol.fees.v1alpha1.TransferFeeComponents") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + let mut base__ = None; + let mut multiplier__ = None; + while let Some(k) = map_.next_key()? { + match k { + GeneratedField::Base => { + if base__.is_some() { + return Err(serde::de::Error::duplicate_field("base")); + } + base__ = map_.next_value()?; + } + GeneratedField::Multiplier => { + if multiplier__.is_some() { + return Err(serde::de::Error::duplicate_field("multiplier")); + } + multiplier__ = map_.next_value()?; + } + } + } + Ok(TransferFeeComponents { + base: base__, + multiplier: multiplier__, + }) + } + } + deserializer.deserialize_struct("astria.protocol.fees.v1alpha1.TransferFeeComponents", FIELDS, GeneratedVisitor) + } +} +impl serde::Serialize for ValidatorUpdateFeeComponents { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let mut len = 0; + if self.base.is_some() { + len += 1; + } + if self.multiplier.is_some() { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("astria.protocol.fees.v1alpha1.ValidatorUpdateFeeComponents", len)?; + if let Some(v) = self.base.as_ref() { + struct_ser.serialize_field("base", v)?; + } + if let Some(v) = self.multiplier.as_ref() { + struct_ser.serialize_field("multiplier", v)?; + } + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for ValidatorUpdateFeeComponents { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "base", + "multiplier", + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + Base, + Multiplier, + } + 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 { + "base" => Ok(GeneratedField::Base), + "multiplier" => Ok(GeneratedField::Multiplier), + _ => Err(serde::de::Error::unknown_field(value, FIELDS)), + } + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = ValidatorUpdateFeeComponents; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct astria.protocol.fees.v1alpha1.ValidatorUpdateFeeComponents") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + let mut base__ = None; + let mut multiplier__ = None; + while let Some(k) = map_.next_key()? { + match k { + GeneratedField::Base => { + if base__.is_some() { + return Err(serde::de::Error::duplicate_field("base")); + } + base__ = map_.next_value()?; + } + GeneratedField::Multiplier => { + if multiplier__.is_some() { + return Err(serde::de::Error::duplicate_field("multiplier")); + } + multiplier__ = map_.next_value()?; + } + } + } + Ok(ValidatorUpdateFeeComponents { + base: base__, + multiplier: multiplier__, + }) + } + } + deserializer.deserialize_struct("astria.protocol.fees.v1alpha1.ValidatorUpdateFeeComponents", FIELDS, GeneratedVisitor) + } +} diff --git a/crates/astria-core/src/generated/astria.protocol.genesis.v1alpha1.rs b/crates/astria-core/src/generated/astria.protocol.genesis.v1alpha1.rs index cf46eec58d..73dfd41cc6 100644 --- a/crates/astria-core/src/generated/astria.protocol.genesis.v1alpha1.rs +++ b/crates/astria-core/src/generated/astria.protocol.genesis.v1alpha1.rs @@ -26,7 +26,7 @@ pub struct GenesisAppState { #[prost(string, repeated, tag = "9")] pub allowed_fee_assets: ::prost::alloc::vec::Vec<::prost::alloc::string::String>, #[prost(message, optional, tag = "10")] - pub fees: ::core::option::Option, + pub fees: ::core::option::Option, } impl ::prost::Name for GenesisAppState { const NAME: &'static str = "GenesisAppState"; @@ -92,38 +92,66 @@ impl ::prost::Name for IbcParameters { } #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] -pub struct Fees { +pub struct GenesisFees { #[prost(message, optional, tag = "1")] - pub transfer_base_fee: ::core::option::Option< - super::super::super::primitive::v1::Uint128, + pub bridge_lock: ::core::option::Option< + super::super::fees::v1alpha1::BridgeLockFeeComponents, >, #[prost(message, optional, tag = "2")] - pub sequence_base_fee: ::core::option::Option< - super::super::super::primitive::v1::Uint128, + pub bridge_sudo_change: ::core::option::Option< + super::super::fees::v1alpha1::BridgeSudoChangeFeeComponents, >, #[prost(message, optional, tag = "3")] - pub sequence_byte_cost_multiplier: ::core::option::Option< - super::super::super::primitive::v1::Uint128, + pub bridge_unlock: ::core::option::Option< + super::super::fees::v1alpha1::BridgeUnlockFeeComponents, >, #[prost(message, optional, tag = "4")] - pub init_bridge_account_base_fee: ::core::option::Option< - super::super::super::primitive::v1::Uint128, + pub fee_asset_change: ::core::option::Option< + super::super::fees::v1alpha1::FeeAssetChangeFeeComponents, >, #[prost(message, optional, tag = "5")] - pub bridge_lock_byte_cost_multiplier: ::core::option::Option< - super::super::super::primitive::v1::Uint128, + pub fee_change: ::core::option::Option< + super::super::fees::v1alpha1::FeeChangeFeeComponents, + >, + #[prost(message, optional, tag = "7")] + pub ibc_relay: ::core::option::Option< + super::super::fees::v1alpha1::IbcRelayFeeComponents, >, #[prost(message, optional, tag = "6")] - pub bridge_sudo_change_fee: ::core::option::Option< - super::super::super::primitive::v1::Uint128, + pub ibc_relayer_change: ::core::option::Option< + super::super::fees::v1alpha1::IbcRelayerChangeFeeComponents, >, - #[prost(message, optional, tag = "7")] - pub ics20_withdrawal_base_fee: ::core::option::Option< - super::super::super::primitive::v1::Uint128, + #[prost(message, optional, tag = "8")] + pub ibc_sudo_change: ::core::option::Option< + super::super::fees::v1alpha1::IbcSudoChangeFeeComponents, + >, + #[prost(message, optional, tag = "9")] + pub ics20_withdrawal: ::core::option::Option< + super::super::fees::v1alpha1::Ics20WithdrawalFeeComponents, + >, + #[prost(message, optional, tag = "10")] + pub init_bridge_account: ::core::option::Option< + super::super::fees::v1alpha1::InitBridgeAccountFeeComponents, + >, + #[prost(message, optional, tag = "11")] + pub sequence: ::core::option::Option< + super::super::fees::v1alpha1::SequenceFeeComponents, + >, + #[prost(message, optional, tag = "12")] + pub sudo_address_change: ::core::option::Option< + super::super::fees::v1alpha1::SudoAddressChangeFeeComponents, + >, + #[prost(message, optional, tag = "13")] + pub transfer: ::core::option::Option< + super::super::fees::v1alpha1::TransferFeeComponents, + >, + #[prost(message, optional, tag = "14")] + pub validator_update: ::core::option::Option< + super::super::fees::v1alpha1::ValidatorUpdateFeeComponents, >, } -impl ::prost::Name for Fees { - const NAME: &'static str = "Fees"; +impl ::prost::Name for GenesisFees { + const NAME: &'static str = "GenesisFees"; const PACKAGE: &'static str = "astria.protocol.genesis.v1alpha1"; fn full_name() -> ::prost::alloc::string::String { ::prost::alloc::format!("astria.protocol.genesis.v1alpha1.{}", Self::NAME) diff --git a/crates/astria-core/src/generated/astria.protocol.genesis.v1alpha1.serde.rs b/crates/astria-core/src/generated/astria.protocol.genesis.v1alpha1.serde.rs index 3ae22e9461..1334efdbbd 100644 --- a/crates/astria-core/src/generated/astria.protocol.genesis.v1alpha1.serde.rs +++ b/crates/astria-core/src/generated/astria.protocol.genesis.v1alpha1.serde.rs @@ -215,206 +215,6 @@ impl<'de> serde::Deserialize<'de> for AddressPrefixes { deserializer.deserialize_struct("astria.protocol.genesis.v1alpha1.AddressPrefixes", FIELDS, GeneratedVisitor) } } -impl serde::Serialize for Fees { - #[allow(deprecated)] - fn serialize(&self, serializer: S) -> std::result::Result - where - S: serde::Serializer, - { - use serde::ser::SerializeStruct; - let mut len = 0; - if self.transfer_base_fee.is_some() { - len += 1; - } - if self.sequence_base_fee.is_some() { - len += 1; - } - if self.sequence_byte_cost_multiplier.is_some() { - len += 1; - } - if self.init_bridge_account_base_fee.is_some() { - len += 1; - } - if self.bridge_lock_byte_cost_multiplier.is_some() { - len += 1; - } - if self.bridge_sudo_change_fee.is_some() { - len += 1; - } - if self.ics20_withdrawal_base_fee.is_some() { - len += 1; - } - let mut struct_ser = serializer.serialize_struct("astria.protocol.genesis.v1alpha1.Fees", len)?; - if let Some(v) = self.transfer_base_fee.as_ref() { - struct_ser.serialize_field("transferBaseFee", v)?; - } - if let Some(v) = self.sequence_base_fee.as_ref() { - struct_ser.serialize_field("sequenceBaseFee", v)?; - } - if let Some(v) = self.sequence_byte_cost_multiplier.as_ref() { - struct_ser.serialize_field("sequenceByteCostMultiplier", v)?; - } - if let Some(v) = self.init_bridge_account_base_fee.as_ref() { - struct_ser.serialize_field("initBridgeAccountBaseFee", v)?; - } - if let Some(v) = self.bridge_lock_byte_cost_multiplier.as_ref() { - struct_ser.serialize_field("bridgeLockByteCostMultiplier", v)?; - } - if let Some(v) = self.bridge_sudo_change_fee.as_ref() { - struct_ser.serialize_field("bridgeSudoChangeFee", v)?; - } - if let Some(v) = self.ics20_withdrawal_base_fee.as_ref() { - struct_ser.serialize_field("ics20WithdrawalBaseFee", v)?; - } - struct_ser.end() - } -} -impl<'de> serde::Deserialize<'de> for Fees { - #[allow(deprecated)] - fn deserialize(deserializer: D) -> std::result::Result - where - D: serde::Deserializer<'de>, - { - const FIELDS: &[&str] = &[ - "transfer_base_fee", - "transferBaseFee", - "sequence_base_fee", - "sequenceBaseFee", - "sequence_byte_cost_multiplier", - "sequenceByteCostMultiplier", - "init_bridge_account_base_fee", - "initBridgeAccountBaseFee", - "bridge_lock_byte_cost_multiplier", - "bridgeLockByteCostMultiplier", - "bridge_sudo_change_fee", - "bridgeSudoChangeFee", - "ics20_withdrawal_base_fee", - "ics20WithdrawalBaseFee", - ]; - - #[allow(clippy::enum_variant_names)] - enum GeneratedField { - TransferBaseFee, - SequenceBaseFee, - SequenceByteCostMultiplier, - InitBridgeAccountBaseFee, - BridgeLockByteCostMultiplier, - BridgeSudoChangeFee, - Ics20WithdrawalBaseFee, - } - 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 { - "transferBaseFee" | "transfer_base_fee" => Ok(GeneratedField::TransferBaseFee), - "sequenceBaseFee" | "sequence_base_fee" => Ok(GeneratedField::SequenceBaseFee), - "sequenceByteCostMultiplier" | "sequence_byte_cost_multiplier" => Ok(GeneratedField::SequenceByteCostMultiplier), - "initBridgeAccountBaseFee" | "init_bridge_account_base_fee" => Ok(GeneratedField::InitBridgeAccountBaseFee), - "bridgeLockByteCostMultiplier" | "bridge_lock_byte_cost_multiplier" => Ok(GeneratedField::BridgeLockByteCostMultiplier), - "bridgeSudoChangeFee" | "bridge_sudo_change_fee" => Ok(GeneratedField::BridgeSudoChangeFee), - "ics20WithdrawalBaseFee" | "ics20_withdrawal_base_fee" => Ok(GeneratedField::Ics20WithdrawalBaseFee), - _ => Err(serde::de::Error::unknown_field(value, FIELDS)), - } - } - } - deserializer.deserialize_identifier(GeneratedVisitor) - } - } - struct GeneratedVisitor; - impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { - type Value = Fees; - - fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - formatter.write_str("struct astria.protocol.genesis.v1alpha1.Fees") - } - - fn visit_map(self, mut map_: V) -> std::result::Result - where - V: serde::de::MapAccess<'de>, - { - let mut transfer_base_fee__ = None; - let mut sequence_base_fee__ = None; - let mut sequence_byte_cost_multiplier__ = None; - let mut init_bridge_account_base_fee__ = None; - let mut bridge_lock_byte_cost_multiplier__ = None; - let mut bridge_sudo_change_fee__ = None; - let mut ics20_withdrawal_base_fee__ = None; - while let Some(k) = map_.next_key()? { - match k { - GeneratedField::TransferBaseFee => { - if transfer_base_fee__.is_some() { - return Err(serde::de::Error::duplicate_field("transferBaseFee")); - } - transfer_base_fee__ = map_.next_value()?; - } - GeneratedField::SequenceBaseFee => { - if sequence_base_fee__.is_some() { - return Err(serde::de::Error::duplicate_field("sequenceBaseFee")); - } - sequence_base_fee__ = map_.next_value()?; - } - GeneratedField::SequenceByteCostMultiplier => { - if sequence_byte_cost_multiplier__.is_some() { - return Err(serde::de::Error::duplicate_field("sequenceByteCostMultiplier")); - } - sequence_byte_cost_multiplier__ = map_.next_value()?; - } - GeneratedField::InitBridgeAccountBaseFee => { - if init_bridge_account_base_fee__.is_some() { - return Err(serde::de::Error::duplicate_field("initBridgeAccountBaseFee")); - } - init_bridge_account_base_fee__ = map_.next_value()?; - } - GeneratedField::BridgeLockByteCostMultiplier => { - if bridge_lock_byte_cost_multiplier__.is_some() { - return Err(serde::de::Error::duplicate_field("bridgeLockByteCostMultiplier")); - } - bridge_lock_byte_cost_multiplier__ = map_.next_value()?; - } - GeneratedField::BridgeSudoChangeFee => { - if bridge_sudo_change_fee__.is_some() { - return Err(serde::de::Error::duplicate_field("bridgeSudoChangeFee")); - } - bridge_sudo_change_fee__ = map_.next_value()?; - } - GeneratedField::Ics20WithdrawalBaseFee => { - if ics20_withdrawal_base_fee__.is_some() { - return Err(serde::de::Error::duplicate_field("ics20WithdrawalBaseFee")); - } - ics20_withdrawal_base_fee__ = map_.next_value()?; - } - } - } - Ok(Fees { - transfer_base_fee: transfer_base_fee__, - sequence_base_fee: sequence_base_fee__, - sequence_byte_cost_multiplier: sequence_byte_cost_multiplier__, - init_bridge_account_base_fee: init_bridge_account_base_fee__, - bridge_lock_byte_cost_multiplier: bridge_lock_byte_cost_multiplier__, - bridge_sudo_change_fee: bridge_sudo_change_fee__, - ics20_withdrawal_base_fee: ics20_withdrawal_base_fee__, - }) - } - } - deserializer.deserialize_struct("astria.protocol.genesis.v1alpha1.Fees", FIELDS, GeneratedVisitor) - } -} impl serde::Serialize for GenesisAppState { #[allow(deprecated)] fn serialize(&self, serializer: S) -> std::result::Result @@ -667,6 +467,330 @@ impl<'de> serde::Deserialize<'de> for GenesisAppState { deserializer.deserialize_struct("astria.protocol.genesis.v1alpha1.GenesisAppState", FIELDS, GeneratedVisitor) } } +impl serde::Serialize for GenesisFees { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let mut len = 0; + if self.bridge_lock.is_some() { + len += 1; + } + if self.bridge_sudo_change.is_some() { + len += 1; + } + if self.bridge_unlock.is_some() { + len += 1; + } + if self.fee_asset_change.is_some() { + len += 1; + } + if self.fee_change.is_some() { + len += 1; + } + if self.ibc_relay.is_some() { + len += 1; + } + if self.ibc_relayer_change.is_some() { + len += 1; + } + if self.ibc_sudo_change.is_some() { + len += 1; + } + if self.ics20_withdrawal.is_some() { + len += 1; + } + if self.init_bridge_account.is_some() { + len += 1; + } + if self.sequence.is_some() { + len += 1; + } + if self.sudo_address_change.is_some() { + len += 1; + } + if self.transfer.is_some() { + len += 1; + } + if self.validator_update.is_some() { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("astria.protocol.genesis.v1alpha1.GenesisFees", len)?; + if let Some(v) = self.bridge_lock.as_ref() { + struct_ser.serialize_field("bridgeLock", v)?; + } + if let Some(v) = self.bridge_sudo_change.as_ref() { + struct_ser.serialize_field("bridgeSudoChange", v)?; + } + if let Some(v) = self.bridge_unlock.as_ref() { + struct_ser.serialize_field("bridgeUnlock", v)?; + } + if let Some(v) = self.fee_asset_change.as_ref() { + struct_ser.serialize_field("feeAssetChange", v)?; + } + if let Some(v) = self.fee_change.as_ref() { + struct_ser.serialize_field("feeChange", v)?; + } + if let Some(v) = self.ibc_relay.as_ref() { + struct_ser.serialize_field("ibcRelay", v)?; + } + if let Some(v) = self.ibc_relayer_change.as_ref() { + struct_ser.serialize_field("ibcRelayerChange", v)?; + } + if let Some(v) = self.ibc_sudo_change.as_ref() { + struct_ser.serialize_field("ibcSudoChange", v)?; + } + if let Some(v) = self.ics20_withdrawal.as_ref() { + struct_ser.serialize_field("ics20Withdrawal", v)?; + } + if let Some(v) = self.init_bridge_account.as_ref() { + struct_ser.serialize_field("initBridgeAccount", v)?; + } + if let Some(v) = self.sequence.as_ref() { + struct_ser.serialize_field("sequence", v)?; + } + if let Some(v) = self.sudo_address_change.as_ref() { + struct_ser.serialize_field("sudoAddressChange", v)?; + } + if let Some(v) = self.transfer.as_ref() { + struct_ser.serialize_field("transfer", v)?; + } + if let Some(v) = self.validator_update.as_ref() { + struct_ser.serialize_field("validatorUpdate", v)?; + } + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for GenesisFees { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "bridge_lock", + "bridgeLock", + "bridge_sudo_change", + "bridgeSudoChange", + "bridge_unlock", + "bridgeUnlock", + "fee_asset_change", + "feeAssetChange", + "fee_change", + "feeChange", + "ibc_relay", + "ibcRelay", + "ibc_relayer_change", + "ibcRelayerChange", + "ibc_sudo_change", + "ibcSudoChange", + "ics20_withdrawal", + "ics20Withdrawal", + "init_bridge_account", + "initBridgeAccount", + "sequence", + "sudo_address_change", + "sudoAddressChange", + "transfer", + "validator_update", + "validatorUpdate", + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + BridgeLock, + BridgeSudoChange, + BridgeUnlock, + FeeAssetChange, + FeeChange, + IbcRelay, + IbcRelayerChange, + IbcSudoChange, + Ics20Withdrawal, + InitBridgeAccount, + Sequence, + SudoAddressChange, + Transfer, + ValidatorUpdate, + } + 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 { + "bridgeLock" | "bridge_lock" => Ok(GeneratedField::BridgeLock), + "bridgeSudoChange" | "bridge_sudo_change" => Ok(GeneratedField::BridgeSudoChange), + "bridgeUnlock" | "bridge_unlock" => Ok(GeneratedField::BridgeUnlock), + "feeAssetChange" | "fee_asset_change" => Ok(GeneratedField::FeeAssetChange), + "feeChange" | "fee_change" => Ok(GeneratedField::FeeChange), + "ibcRelay" | "ibc_relay" => Ok(GeneratedField::IbcRelay), + "ibcRelayerChange" | "ibc_relayer_change" => Ok(GeneratedField::IbcRelayerChange), + "ibcSudoChange" | "ibc_sudo_change" => Ok(GeneratedField::IbcSudoChange), + "ics20Withdrawal" | "ics20_withdrawal" => Ok(GeneratedField::Ics20Withdrawal), + "initBridgeAccount" | "init_bridge_account" => Ok(GeneratedField::InitBridgeAccount), + "sequence" => Ok(GeneratedField::Sequence), + "sudoAddressChange" | "sudo_address_change" => Ok(GeneratedField::SudoAddressChange), + "transfer" => Ok(GeneratedField::Transfer), + "validatorUpdate" | "validator_update" => Ok(GeneratedField::ValidatorUpdate), + _ => Err(serde::de::Error::unknown_field(value, FIELDS)), + } + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = GenesisFees; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct astria.protocol.genesis.v1alpha1.GenesisFees") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + let mut bridge_lock__ = None; + let mut bridge_sudo_change__ = None; + let mut bridge_unlock__ = None; + let mut fee_asset_change__ = None; + let mut fee_change__ = None; + let mut ibc_relay__ = None; + let mut ibc_relayer_change__ = None; + let mut ibc_sudo_change__ = None; + let mut ics20_withdrawal__ = None; + let mut init_bridge_account__ = None; + let mut sequence__ = None; + let mut sudo_address_change__ = None; + let mut transfer__ = None; + let mut validator_update__ = None; + while let Some(k) = map_.next_key()? { + match k { + GeneratedField::BridgeLock => { + if bridge_lock__.is_some() { + return Err(serde::de::Error::duplicate_field("bridgeLock")); + } + bridge_lock__ = map_.next_value()?; + } + GeneratedField::BridgeSudoChange => { + if bridge_sudo_change__.is_some() { + return Err(serde::de::Error::duplicate_field("bridgeSudoChange")); + } + bridge_sudo_change__ = map_.next_value()?; + } + GeneratedField::BridgeUnlock => { + if bridge_unlock__.is_some() { + return Err(serde::de::Error::duplicate_field("bridgeUnlock")); + } + bridge_unlock__ = map_.next_value()?; + } + GeneratedField::FeeAssetChange => { + if fee_asset_change__.is_some() { + return Err(serde::de::Error::duplicate_field("feeAssetChange")); + } + fee_asset_change__ = map_.next_value()?; + } + GeneratedField::FeeChange => { + if fee_change__.is_some() { + return Err(serde::de::Error::duplicate_field("feeChange")); + } + fee_change__ = map_.next_value()?; + } + GeneratedField::IbcRelay => { + if ibc_relay__.is_some() { + return Err(serde::de::Error::duplicate_field("ibcRelay")); + } + ibc_relay__ = map_.next_value()?; + } + GeneratedField::IbcRelayerChange => { + if ibc_relayer_change__.is_some() { + return Err(serde::de::Error::duplicate_field("ibcRelayerChange")); + } + ibc_relayer_change__ = map_.next_value()?; + } + GeneratedField::IbcSudoChange => { + if ibc_sudo_change__.is_some() { + return Err(serde::de::Error::duplicate_field("ibcSudoChange")); + } + ibc_sudo_change__ = map_.next_value()?; + } + GeneratedField::Ics20Withdrawal => { + if ics20_withdrawal__.is_some() { + return Err(serde::de::Error::duplicate_field("ics20Withdrawal")); + } + ics20_withdrawal__ = map_.next_value()?; + } + GeneratedField::InitBridgeAccount => { + if init_bridge_account__.is_some() { + return Err(serde::de::Error::duplicate_field("initBridgeAccount")); + } + init_bridge_account__ = map_.next_value()?; + } + GeneratedField::Sequence => { + if sequence__.is_some() { + return Err(serde::de::Error::duplicate_field("sequence")); + } + sequence__ = map_.next_value()?; + } + GeneratedField::SudoAddressChange => { + if sudo_address_change__.is_some() { + return Err(serde::de::Error::duplicate_field("sudoAddressChange")); + } + sudo_address_change__ = map_.next_value()?; + } + GeneratedField::Transfer => { + if transfer__.is_some() { + return Err(serde::de::Error::duplicate_field("transfer")); + } + transfer__ = map_.next_value()?; + } + GeneratedField::ValidatorUpdate => { + if validator_update__.is_some() { + return Err(serde::de::Error::duplicate_field("validatorUpdate")); + } + validator_update__ = map_.next_value()?; + } + } + } + Ok(GenesisFees { + bridge_lock: bridge_lock__, + bridge_sudo_change: bridge_sudo_change__, + bridge_unlock: bridge_unlock__, + fee_asset_change: fee_asset_change__, + fee_change: fee_change__, + ibc_relay: ibc_relay__, + ibc_relayer_change: ibc_relayer_change__, + ibc_sudo_change: ibc_sudo_change__, + ics20_withdrawal: ics20_withdrawal__, + init_bridge_account: init_bridge_account__, + sequence: sequence__, + sudo_address_change: sudo_address_change__, + transfer: transfer__, + validator_update: validator_update__, + }) + } + } + deserializer.deserialize_struct("astria.protocol.genesis.v1alpha1.GenesisFees", FIELDS, GeneratedVisitor) + } +} impl serde::Serialize for IbcParameters { #[allow(deprecated)] fn serialize(&self, serializer: S) -> std::result::Result diff --git a/crates/astria-core/src/generated/astria.protocol.transactions.v1alpha1.rs b/crates/astria-core/src/generated/astria.protocol.transactions.v1alpha1.rs index ce9f4c94f6..7458478d08 100644 --- a/crates/astria-core/src/generated/astria.protocol.transactions.v1alpha1.rs +++ b/crates/astria-core/src/generated/astria.protocol.transactions.v1alpha1.rs @@ -399,35 +399,59 @@ impl ::prost::Name for BridgeSudoChange { #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct FeeChange { - /// note that the proto number ranges are doubled from that of `Action`. - /// this to accomodate both `base_fee` and `byte_cost_multiplier` for each action. - #[prost(oneof = "fee_change::Value", tags = "1, 2, 3, 20, 21, 22, 40")] - pub value: ::core::option::Option, + /// the new fee components values + #[prost( + oneof = "fee_change::FeeComponents", + tags = "1, 2, 3, 4, 5, 7, 6, 8, 9, 10, 11, 12, 13, 14" + )] + pub fee_components: ::core::option::Option, } /// Nested message and enum types in `FeeChange`. pub mod fee_change { - /// note that the proto number ranges are doubled from that of `Action`. - /// this to accomodate both `base_fee` and `byte_cost_multiplier` for each action. + /// the new fee components values #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Oneof)] - pub enum Value { - /// core protocol fees are defined on 1-20 + pub enum FeeComponents { #[prost(message, tag = "1")] - TransferBaseFee(super::super::super::super::primitive::v1::Uint128), + BridgeLock(super::super::super::fees::v1alpha1::BridgeLockFeeComponents), #[prost(message, tag = "2")] - SequenceBaseFee(super::super::super::super::primitive::v1::Uint128), + BridgeSudoChange( + super::super::super::fees::v1alpha1::BridgeSudoChangeFeeComponents, + ), #[prost(message, tag = "3")] - SequenceByteCostMultiplier(super::super::super::super::primitive::v1::Uint128), - /// bridge fees are defined on 20-39 - #[prost(message, tag = "20")] - InitBridgeAccountBaseFee(super::super::super::super::primitive::v1::Uint128), - #[prost(message, tag = "21")] - BridgeLockByteCostMultiplier(super::super::super::super::primitive::v1::Uint128), - #[prost(message, tag = "22")] - BridgeSudoChangeBaseFee(super::super::super::super::primitive::v1::Uint128), - /// ibc fees are defined on 40-59 - #[prost(message, tag = "40")] - Ics20WithdrawalBaseFee(super::super::super::super::primitive::v1::Uint128), + BridgeUnlock(super::super::super::fees::v1alpha1::BridgeUnlockFeeComponents), + #[prost(message, tag = "4")] + FeeAssetChange(super::super::super::fees::v1alpha1::FeeAssetChangeFeeComponents), + #[prost(message, tag = "5")] + FeeChange(super::super::super::fees::v1alpha1::FeeChangeFeeComponents), + #[prost(message, tag = "7")] + IbcRelay(super::super::super::fees::v1alpha1::IbcRelayFeeComponents), + #[prost(message, tag = "6")] + IbcRelayerChange( + super::super::super::fees::v1alpha1::IbcRelayerChangeFeeComponents, + ), + #[prost(message, tag = "8")] + IbcSudoChange(super::super::super::fees::v1alpha1::IbcSudoChangeFeeComponents), + #[prost(message, tag = "9")] + Ics20Withdrawal( + super::super::super::fees::v1alpha1::Ics20WithdrawalFeeComponents, + ), + #[prost(message, tag = "10")] + InitBridgeAccount( + super::super::super::fees::v1alpha1::InitBridgeAccountFeeComponents, + ), + #[prost(message, tag = "11")] + Sequence(super::super::super::fees::v1alpha1::SequenceFeeComponents), + #[prost(message, tag = "12")] + SudoAddressChange( + super::super::super::fees::v1alpha1::SudoAddressChangeFeeComponents, + ), + #[prost(message, tag = "13")] + Transfer(super::super::super::fees::v1alpha1::TransferFeeComponents), + #[prost(message, tag = "14")] + ValidatorUpdate( + super::super::super::fees::v1alpha1::ValidatorUpdateFeeComponents, + ), } } impl ::prost::Name for FeeChange { @@ -450,37 +474,6 @@ impl ::prost::Name for IbcSudoChange { ::prost::alloc::format!("astria.protocol.transactions.v1alpha1.{}", Self::NAME) } } -/// Response to a transaction fee ABCI query. -#[allow(clippy::derive_partial_eq_without_eq)] -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct TransactionFeeResponse { - #[prost(uint64, tag = "2")] - pub height: u64, - #[prost(message, repeated, tag = "3")] - pub fees: ::prost::alloc::vec::Vec, -} -impl ::prost::Name for TransactionFeeResponse { - const NAME: &'static str = "TransactionFeeResponse"; - const PACKAGE: &'static str = "astria.protocol.transactions.v1alpha1"; - fn full_name() -> ::prost::alloc::string::String { - ::prost::alloc::format!("astria.protocol.transactions.v1alpha1.{}", Self::NAME) - } -} -#[allow(clippy::derive_partial_eq_without_eq)] -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct TransactionFee { - #[prost(string, tag = "1")] - pub asset: ::prost::alloc::string::String, - #[prost(message, optional, tag = "2")] - pub fee: ::core::option::Option, -} -impl ::prost::Name for TransactionFee { - const NAME: &'static str = "TransactionFee"; - const PACKAGE: &'static str = "astria.protocol.transactions.v1alpha1"; - fn full_name() -> ::prost::alloc::string::String { - ::prost::alloc::format!("astria.protocol.transactions.v1alpha1.{}", Self::NAME) - } -} /// `SignedTransaction` is a transaction that has /// been signed by the given public key. /// It wraps an `UnsignedTransaction` with a diff --git a/crates/astria-core/src/generated/astria.protocol.transactions.v1alpha1.serde.rs b/crates/astria-core/src/generated/astria.protocol.transactions.v1alpha1.serde.rs index ea905cf97e..83d7600b95 100644 --- a/crates/astria-core/src/generated/astria.protocol.transactions.v1alpha1.serde.rs +++ b/crates/astria-core/src/generated/astria.protocol.transactions.v1alpha1.serde.rs @@ -896,32 +896,53 @@ impl serde::Serialize for FeeChange { { use serde::ser::SerializeStruct; let mut len = 0; - if self.value.is_some() { + if self.fee_components.is_some() { len += 1; } let mut struct_ser = serializer.serialize_struct("astria.protocol.transactions.v1alpha1.FeeChange", len)?; - if let Some(v) = self.value.as_ref() { + if let Some(v) = self.fee_components.as_ref() { match v { - fee_change::Value::TransferBaseFee(v) => { - struct_ser.serialize_field("transferBaseFee", v)?; + fee_change::FeeComponents::BridgeLock(v) => { + struct_ser.serialize_field("bridgeLock", v)?; + } + fee_change::FeeComponents::BridgeSudoChange(v) => { + struct_ser.serialize_field("bridgeSudoChange", v)?; } - fee_change::Value::SequenceBaseFee(v) => { - struct_ser.serialize_field("sequenceBaseFee", v)?; + fee_change::FeeComponents::BridgeUnlock(v) => { + struct_ser.serialize_field("bridgeUnlock", v)?; } - fee_change::Value::SequenceByteCostMultiplier(v) => { - struct_ser.serialize_field("sequenceByteCostMultiplier", v)?; + fee_change::FeeComponents::FeeAssetChange(v) => { + struct_ser.serialize_field("feeAssetChange", v)?; } - fee_change::Value::InitBridgeAccountBaseFee(v) => { - struct_ser.serialize_field("initBridgeAccountBaseFee", v)?; + fee_change::FeeComponents::FeeChange(v) => { + struct_ser.serialize_field("feeChange", v)?; } - fee_change::Value::BridgeLockByteCostMultiplier(v) => { - struct_ser.serialize_field("bridgeLockByteCostMultiplier", v)?; + fee_change::FeeComponents::IbcRelay(v) => { + struct_ser.serialize_field("ibcRelay", v)?; } - fee_change::Value::BridgeSudoChangeBaseFee(v) => { - struct_ser.serialize_field("bridgeSudoChangeBaseFee", v)?; + fee_change::FeeComponents::IbcRelayerChange(v) => { + struct_ser.serialize_field("ibcRelayerChange", v)?; } - fee_change::Value::Ics20WithdrawalBaseFee(v) => { - struct_ser.serialize_field("ics20WithdrawalBaseFee", v)?; + fee_change::FeeComponents::IbcSudoChange(v) => { + struct_ser.serialize_field("ibcSudoChange", v)?; + } + fee_change::FeeComponents::Ics20Withdrawal(v) => { + struct_ser.serialize_field("ics20Withdrawal", v)?; + } + fee_change::FeeComponents::InitBridgeAccount(v) => { + struct_ser.serialize_field("initBridgeAccount", v)?; + } + fee_change::FeeComponents::Sequence(v) => { + struct_ser.serialize_field("sequence", v)?; + } + fee_change::FeeComponents::SudoAddressChange(v) => { + struct_ser.serialize_field("sudoAddressChange", v)?; + } + fee_change::FeeComponents::Transfer(v) => { + struct_ser.serialize_field("transfer", v)?; + } + fee_change::FeeComponents::ValidatorUpdate(v) => { + struct_ser.serialize_field("validatorUpdate", v)?; } } } @@ -935,31 +956,50 @@ impl<'de> serde::Deserialize<'de> for FeeChange { D: serde::Deserializer<'de>, { const FIELDS: &[&str] = &[ - "transfer_base_fee", - "transferBaseFee", - "sequence_base_fee", - "sequenceBaseFee", - "sequence_byte_cost_multiplier", - "sequenceByteCostMultiplier", - "init_bridge_account_base_fee", - "initBridgeAccountBaseFee", - "bridge_lock_byte_cost_multiplier", - "bridgeLockByteCostMultiplier", - "bridge_sudo_change_base_fee", - "bridgeSudoChangeBaseFee", - "ics20_withdrawal_base_fee", - "ics20WithdrawalBaseFee", + "bridge_lock", + "bridgeLock", + "bridge_sudo_change", + "bridgeSudoChange", + "bridge_unlock", + "bridgeUnlock", + "fee_asset_change", + "feeAssetChange", + "fee_change", + "feeChange", + "ibc_relay", + "ibcRelay", + "ibc_relayer_change", + "ibcRelayerChange", + "ibc_sudo_change", + "ibcSudoChange", + "ics20_withdrawal", + "ics20Withdrawal", + "init_bridge_account", + "initBridgeAccount", + "sequence", + "sudo_address_change", + "sudoAddressChange", + "transfer", + "validator_update", + "validatorUpdate", ]; #[allow(clippy::enum_variant_names)] enum GeneratedField { - TransferBaseFee, - SequenceBaseFee, - SequenceByteCostMultiplier, - InitBridgeAccountBaseFee, - BridgeLockByteCostMultiplier, - BridgeSudoChangeBaseFee, - Ics20WithdrawalBaseFee, + BridgeLock, + BridgeSudoChange, + BridgeUnlock, + FeeAssetChange, + FeeChange, + IbcRelay, + IbcRelayerChange, + IbcSudoChange, + Ics20Withdrawal, + InitBridgeAccount, + Sequence, + SudoAddressChange, + Transfer, + ValidatorUpdate, } impl<'de> serde::Deserialize<'de> for GeneratedField { fn deserialize(deserializer: D) -> std::result::Result @@ -981,13 +1021,20 @@ impl<'de> serde::Deserialize<'de> for FeeChange { E: serde::de::Error, { match value { - "transferBaseFee" | "transfer_base_fee" => Ok(GeneratedField::TransferBaseFee), - "sequenceBaseFee" | "sequence_base_fee" => Ok(GeneratedField::SequenceBaseFee), - "sequenceByteCostMultiplier" | "sequence_byte_cost_multiplier" => Ok(GeneratedField::SequenceByteCostMultiplier), - "initBridgeAccountBaseFee" | "init_bridge_account_base_fee" => Ok(GeneratedField::InitBridgeAccountBaseFee), - "bridgeLockByteCostMultiplier" | "bridge_lock_byte_cost_multiplier" => Ok(GeneratedField::BridgeLockByteCostMultiplier), - "bridgeSudoChangeBaseFee" | "bridge_sudo_change_base_fee" => Ok(GeneratedField::BridgeSudoChangeBaseFee), - "ics20WithdrawalBaseFee" | "ics20_withdrawal_base_fee" => Ok(GeneratedField::Ics20WithdrawalBaseFee), + "bridgeLock" | "bridge_lock" => Ok(GeneratedField::BridgeLock), + "bridgeSudoChange" | "bridge_sudo_change" => Ok(GeneratedField::BridgeSudoChange), + "bridgeUnlock" | "bridge_unlock" => Ok(GeneratedField::BridgeUnlock), + "feeAssetChange" | "fee_asset_change" => Ok(GeneratedField::FeeAssetChange), + "feeChange" | "fee_change" => Ok(GeneratedField::FeeChange), + "ibcRelay" | "ibc_relay" => Ok(GeneratedField::IbcRelay), + "ibcRelayerChange" | "ibc_relayer_change" => Ok(GeneratedField::IbcRelayerChange), + "ibcSudoChange" | "ibc_sudo_change" => Ok(GeneratedField::IbcSudoChange), + "ics20Withdrawal" | "ics20_withdrawal" => Ok(GeneratedField::Ics20Withdrawal), + "initBridgeAccount" | "init_bridge_account" => Ok(GeneratedField::InitBridgeAccount), + "sequence" => Ok(GeneratedField::Sequence), + "sudoAddressChange" | "sudo_address_change" => Ok(GeneratedField::SudoAddressChange), + "transfer" => Ok(GeneratedField::Transfer), + "validatorUpdate" | "validator_update" => Ok(GeneratedField::ValidatorUpdate), _ => Err(serde::de::Error::unknown_field(value, FIELDS)), } } @@ -1007,62 +1054,111 @@ impl<'de> serde::Deserialize<'de> for FeeChange { where V: serde::de::MapAccess<'de>, { - let mut value__ = None; + let mut fee_components__ = None; while let Some(k) = map_.next_key()? { match k { - GeneratedField::TransferBaseFee => { - if value__.is_some() { - return Err(serde::de::Error::duplicate_field("transferBaseFee")); + GeneratedField::BridgeLock => { + if fee_components__.is_some() { + return Err(serde::de::Error::duplicate_field("bridgeLock")); } - value__ = map_.next_value::<::std::option::Option<_>>()?.map(fee_change::Value::TransferBaseFee) + fee_components__ = map_.next_value::<::std::option::Option<_>>()?.map(fee_change::FeeComponents::BridgeLock) ; } - GeneratedField::SequenceBaseFee => { - if value__.is_some() { - return Err(serde::de::Error::duplicate_field("sequenceBaseFee")); + GeneratedField::BridgeSudoChange => { + if fee_components__.is_some() { + return Err(serde::de::Error::duplicate_field("bridgeSudoChange")); } - value__ = map_.next_value::<::std::option::Option<_>>()?.map(fee_change::Value::SequenceBaseFee) + fee_components__ = map_.next_value::<::std::option::Option<_>>()?.map(fee_change::FeeComponents::BridgeSudoChange) ; } - GeneratedField::SequenceByteCostMultiplier => { - if value__.is_some() { - return Err(serde::de::Error::duplicate_field("sequenceByteCostMultiplier")); + GeneratedField::BridgeUnlock => { + if fee_components__.is_some() { + return Err(serde::de::Error::duplicate_field("bridgeUnlock")); } - value__ = map_.next_value::<::std::option::Option<_>>()?.map(fee_change::Value::SequenceByteCostMultiplier) + fee_components__ = map_.next_value::<::std::option::Option<_>>()?.map(fee_change::FeeComponents::BridgeUnlock) ; } - GeneratedField::InitBridgeAccountBaseFee => { - if value__.is_some() { - return Err(serde::de::Error::duplicate_field("initBridgeAccountBaseFee")); + GeneratedField::FeeAssetChange => { + if fee_components__.is_some() { + return Err(serde::de::Error::duplicate_field("feeAssetChange")); } - value__ = map_.next_value::<::std::option::Option<_>>()?.map(fee_change::Value::InitBridgeAccountBaseFee) + fee_components__ = map_.next_value::<::std::option::Option<_>>()?.map(fee_change::FeeComponents::FeeAssetChange) ; } - GeneratedField::BridgeLockByteCostMultiplier => { - if value__.is_some() { - return Err(serde::de::Error::duplicate_field("bridgeLockByteCostMultiplier")); + GeneratedField::FeeChange => { + if fee_components__.is_some() { + return Err(serde::de::Error::duplicate_field("feeChange")); } - value__ = map_.next_value::<::std::option::Option<_>>()?.map(fee_change::Value::BridgeLockByteCostMultiplier) + fee_components__ = map_.next_value::<::std::option::Option<_>>()?.map(fee_change::FeeComponents::FeeChange) ; } - GeneratedField::BridgeSudoChangeBaseFee => { - if value__.is_some() { - return Err(serde::de::Error::duplicate_field("bridgeSudoChangeBaseFee")); + GeneratedField::IbcRelay => { + if fee_components__.is_some() { + return Err(serde::de::Error::duplicate_field("ibcRelay")); } - value__ = map_.next_value::<::std::option::Option<_>>()?.map(fee_change::Value::BridgeSudoChangeBaseFee) + fee_components__ = map_.next_value::<::std::option::Option<_>>()?.map(fee_change::FeeComponents::IbcRelay) ; } - GeneratedField::Ics20WithdrawalBaseFee => { - if value__.is_some() { - return Err(serde::de::Error::duplicate_field("ics20WithdrawalBaseFee")); + GeneratedField::IbcRelayerChange => { + if fee_components__.is_some() { + return Err(serde::de::Error::duplicate_field("ibcRelayerChange")); + } + fee_components__ = map_.next_value::<::std::option::Option<_>>()?.map(fee_change::FeeComponents::IbcRelayerChange) +; + } + GeneratedField::IbcSudoChange => { + if fee_components__.is_some() { + return Err(serde::de::Error::duplicate_field("ibcSudoChange")); + } + fee_components__ = map_.next_value::<::std::option::Option<_>>()?.map(fee_change::FeeComponents::IbcSudoChange) +; + } + GeneratedField::Ics20Withdrawal => { + if fee_components__.is_some() { + return Err(serde::de::Error::duplicate_field("ics20Withdrawal")); + } + fee_components__ = map_.next_value::<::std::option::Option<_>>()?.map(fee_change::FeeComponents::Ics20Withdrawal) +; + } + GeneratedField::InitBridgeAccount => { + if fee_components__.is_some() { + return Err(serde::de::Error::duplicate_field("initBridgeAccount")); + } + fee_components__ = map_.next_value::<::std::option::Option<_>>()?.map(fee_change::FeeComponents::InitBridgeAccount) +; + } + GeneratedField::Sequence => { + if fee_components__.is_some() { + return Err(serde::de::Error::duplicate_field("sequence")); + } + fee_components__ = map_.next_value::<::std::option::Option<_>>()?.map(fee_change::FeeComponents::Sequence) +; + } + GeneratedField::SudoAddressChange => { + if fee_components__.is_some() { + return Err(serde::de::Error::duplicate_field("sudoAddressChange")); } - value__ = map_.next_value::<::std::option::Option<_>>()?.map(fee_change::Value::Ics20WithdrawalBaseFee) + fee_components__ = map_.next_value::<::std::option::Option<_>>()?.map(fee_change::FeeComponents::SudoAddressChange) +; + } + GeneratedField::Transfer => { + if fee_components__.is_some() { + return Err(serde::de::Error::duplicate_field("transfer")); + } + fee_components__ = map_.next_value::<::std::option::Option<_>>()?.map(fee_change::FeeComponents::Transfer) +; + } + GeneratedField::ValidatorUpdate => { + if fee_components__.is_some() { + return Err(serde::de::Error::duplicate_field("validatorUpdate")); + } + fee_components__ = map_.next_value::<::std::option::Option<_>>()?.map(fee_change::FeeComponents::ValidatorUpdate) ; } } } Ok(FeeChange { - value: value__, + fee_components: fee_components__, }) } } @@ -2175,225 +2271,6 @@ impl<'de> serde::Deserialize<'de> for SudoAddressChange { deserializer.deserialize_struct("astria.protocol.transactions.v1alpha1.SudoAddressChange", FIELDS, GeneratedVisitor) } } -impl serde::Serialize for TransactionFee { - #[allow(deprecated)] - fn serialize(&self, serializer: S) -> std::result::Result - where - S: serde::Serializer, - { - use serde::ser::SerializeStruct; - let mut len = 0; - if !self.asset.is_empty() { - len += 1; - } - if self.fee.is_some() { - len += 1; - } - let mut struct_ser = serializer.serialize_struct("astria.protocol.transactions.v1alpha1.TransactionFee", len)?; - if !self.asset.is_empty() { - struct_ser.serialize_field("asset", &self.asset)?; - } - if let Some(v) = self.fee.as_ref() { - struct_ser.serialize_field("fee", v)?; - } - struct_ser.end() - } -} -impl<'de> serde::Deserialize<'de> for TransactionFee { - #[allow(deprecated)] - fn deserialize(deserializer: D) -> std::result::Result - where - D: serde::Deserializer<'de>, - { - const FIELDS: &[&str] = &[ - "asset", - "fee", - ]; - - #[allow(clippy::enum_variant_names)] - enum GeneratedField { - Asset, - Fee, - } - 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 { - "asset" => Ok(GeneratedField::Asset), - "fee" => Ok(GeneratedField::Fee), - _ => Err(serde::de::Error::unknown_field(value, FIELDS)), - } - } - } - deserializer.deserialize_identifier(GeneratedVisitor) - } - } - struct GeneratedVisitor; - impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { - type Value = TransactionFee; - - fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - formatter.write_str("struct astria.protocol.transactions.v1alpha1.TransactionFee") - } - - fn visit_map(self, mut map_: V) -> std::result::Result - where - V: serde::de::MapAccess<'de>, - { - let mut asset__ = None; - let mut fee__ = None; - while let Some(k) = map_.next_key()? { - match k { - GeneratedField::Asset => { - if asset__.is_some() { - return Err(serde::de::Error::duplicate_field("asset")); - } - asset__ = Some(map_.next_value()?); - } - GeneratedField::Fee => { - if fee__.is_some() { - return Err(serde::de::Error::duplicate_field("fee")); - } - fee__ = map_.next_value()?; - } - } - } - Ok(TransactionFee { - asset: asset__.unwrap_or_default(), - fee: fee__, - }) - } - } - deserializer.deserialize_struct("astria.protocol.transactions.v1alpha1.TransactionFee", FIELDS, GeneratedVisitor) - } -} -impl serde::Serialize for TransactionFeeResponse { - #[allow(deprecated)] - fn serialize(&self, serializer: S) -> std::result::Result - where - S: serde::Serializer, - { - use serde::ser::SerializeStruct; - let mut len = 0; - if self.height != 0 { - len += 1; - } - if !self.fees.is_empty() { - len += 1; - } - let mut struct_ser = serializer.serialize_struct("astria.protocol.transactions.v1alpha1.TransactionFeeResponse", len)?; - if self.height != 0 { - #[allow(clippy::needless_borrow)] - struct_ser.serialize_field("height", ToString::to_string(&self.height).as_str())?; - } - if !self.fees.is_empty() { - struct_ser.serialize_field("fees", &self.fees)?; - } - struct_ser.end() - } -} -impl<'de> serde::Deserialize<'de> for TransactionFeeResponse { - #[allow(deprecated)] - fn deserialize(deserializer: D) -> std::result::Result - where - D: serde::Deserializer<'de>, - { - const FIELDS: &[&str] = &[ - "height", - "fees", - ]; - - #[allow(clippy::enum_variant_names)] - enum GeneratedField { - Height, - Fees, - } - 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 { - "height" => Ok(GeneratedField::Height), - "fees" => Ok(GeneratedField::Fees), - _ => Err(serde::de::Error::unknown_field(value, FIELDS)), - } - } - } - deserializer.deserialize_identifier(GeneratedVisitor) - } - } - struct GeneratedVisitor; - impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { - type Value = TransactionFeeResponse; - - fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - formatter.write_str("struct astria.protocol.transactions.v1alpha1.TransactionFeeResponse") - } - - fn visit_map(self, mut map_: V) -> std::result::Result - where - V: serde::de::MapAccess<'de>, - { - let mut height__ = None; - let mut fees__ = None; - while let Some(k) = map_.next_key()? { - match k { - GeneratedField::Height => { - if height__.is_some() { - return Err(serde::de::Error::duplicate_field("height")); - } - height__ = - Some(map_.next_value::<::pbjson::private::NumberDeserialize<_>>()?.0) - ; - } - GeneratedField::Fees => { - if fees__.is_some() { - return Err(serde::de::Error::duplicate_field("fees")); - } - fees__ = Some(map_.next_value()?); - } - } - } - Ok(TransactionFeeResponse { - height: height__.unwrap_or_default(), - fees: fees__.unwrap_or_default(), - }) - } - } - deserializer.deserialize_struct("astria.protocol.transactions.v1alpha1.TransactionFeeResponse", FIELDS, GeneratedVisitor) - } -} impl serde::Serialize for TransactionParams { #[allow(deprecated)] fn serialize(&self, serializer: S) -> std::result::Result diff --git a/crates/astria-core/src/generated/mod.rs b/crates/astria-core/src/generated/mod.rs index b48248efc8..526e3d8d06 100644 --- a/crates/astria-core/src/generated/mod.rs +++ b/crates/astria-core/src/generated/mod.rs @@ -97,6 +97,19 @@ pub mod protocol { pub mod v1alpha1; } #[path = ""] + pub mod fees { + #[path = "astria.protocol.fees.v1alpha1.rs"] + pub mod v1alpha1 { + include!("astria.protocol.fees.v1alpha1.rs"); + + #[cfg(feature = "serde")] + mod _serde_impls { + use super::*; + include!("astria.protocol.fees.v1alpha1.serde.rs"); + } + } + } + #[path = ""] pub mod genesis { pub mod v1alpha1 { include!("astria.protocol.genesis.v1alpha1.rs"); diff --git a/crates/astria-core/src/protocol/fees/mod.rs b/crates/astria-core/src/protocol/fees/mod.rs new file mode 100644 index 0000000000..32a5a9d4fd --- /dev/null +++ b/crates/astria-core/src/protocol/fees/mod.rs @@ -0,0 +1 @@ +pub mod v1alpha1; diff --git a/crates/astria-core/src/protocol/fees/v1alpha1.rs b/crates/astria-core/src/protocol/fees/v1alpha1.rs new file mode 100644 index 0000000000..f50523ca69 --- /dev/null +++ b/crates/astria-core/src/protocol/fees/v1alpha1.rs @@ -0,0 +1,247 @@ +use prost::Name as _; + +use crate::{ + generated::protocol::fees::v1alpha1 as raw, + primitive::v1::asset, + Protobuf, +}; + +#[derive(Debug, thiserror::Error)] +#[error("failed validating on-wire type `{on_wire}` as domain type")] +pub struct FeeComponentError { + on_wire: String, + source: FeeComponentErrorKind, +} + +impl FeeComponentError { + fn missing_field(on_wire: String, field: &'static str) -> Self { + Self { + on_wire, + source: FeeComponentErrorKind::MissingField { + field, + }, + } + } +} + +#[derive(Debug, thiserror::Error)] +enum FeeComponentErrorKind { + #[error("field `{field}` was not set")] + MissingField { field: &'static str }, +} + +macro_rules! impl_protobuf_for_fee_components { + ( $( $domain_ty:ty => $raw_ty:ty ),* $(,)?) => { + $( + impl Protobuf for $domain_ty { + type Error = FeeComponentError; + type Raw = $raw_ty; + + fn try_from_raw_ref(raw: &Self::Raw) -> Result { + let Self::Raw { + base, + multiplier, + } = raw; + Ok(Self { + base: base + .ok_or_else(|| Self::Error::missing_field(Self::Raw::full_name(), "base"))? + .into(), + multiplier: multiplier + .ok_or_else(|| Self::Error::missing_field(Self::Raw::full_name(), "multiplier"))? + .into(), + }) + } + + fn to_raw(&self) -> Self::Raw { + let Self { + base, + multiplier, + } = self; + Self::Raw { + base: Some(base.into()), + multiplier: Some(multiplier.into()), + } + } + } + )* + }; +} +impl_protobuf_for_fee_components!( + TransferFeeComponents => raw::TransferFeeComponents, + SequenceFeeComponents => raw::SequenceFeeComponents, + Ics20WithdrawalFeeComponents => raw::Ics20WithdrawalFeeComponents , + InitBridgeAccountFeeComponents => raw::InitBridgeAccountFeeComponents , + BridgeLockFeeComponents => raw::BridgeLockFeeComponents, + BridgeUnlockFeeComponents => raw::BridgeUnlockFeeComponents, + BridgeSudoChangeFeeComponents => raw::BridgeSudoChangeFeeComponents , + ValidatorUpdateFeeComponents => raw::ValidatorUpdateFeeComponents , + IbcRelayerChangeFeeComponents => raw::IbcRelayerChangeFeeComponents , + IbcRelayFeeComponents => raw::IbcRelayFeeComponents, + FeeAssetChangeFeeComponents => raw::FeeAssetChangeFeeComponents , + FeeChangeFeeComponents => raw::FeeChangeFeeComponents, + SudoAddressChangeFeeComponents => raw::SudoAddressChangeFeeComponents , + IbcSudoChangeFeeComponents => raw::IbcSudoChangeFeeComponents, +); + +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct TransferFeeComponents { + pub base: u128, + pub multiplier: u128, +} + +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct SequenceFeeComponents { + pub base: u128, + pub multiplier: u128, +} + +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct Ics20WithdrawalFeeComponents { + pub base: u128, + pub multiplier: u128, +} + +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct InitBridgeAccountFeeComponents { + pub base: u128, + pub multiplier: u128, +} + +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct BridgeLockFeeComponents { + pub base: u128, + pub multiplier: u128, +} + +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct BridgeUnlockFeeComponents { + pub base: u128, + pub multiplier: u128, +} + +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct BridgeSudoChangeFeeComponents { + pub base: u128, + pub multiplier: u128, +} + +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct IbcRelayFeeComponents { + pub base: u128, + pub multiplier: u128, +} + +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct ValidatorUpdateFeeComponents { + pub base: u128, + pub multiplier: u128, +} + +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct FeeAssetChangeFeeComponents { + pub base: u128, + pub multiplier: u128, +} + +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct FeeChangeFeeComponents { + pub base: u128, + pub multiplier: u128, +} + +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct IbcRelayerChangeFeeComponents { + pub base: u128, + pub multiplier: u128, +} + +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct SudoAddressChangeFeeComponents { + pub base: u128, + pub multiplier: u128, +} + +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct IbcSudoChangeFeeComponents { + pub base: u128, + pub multiplier: u128, +} + +#[derive(Debug, Clone)] +pub struct TransactionFeeResponse { + pub height: u64, + pub fees: Vec<(asset::Denom, u128)>, +} + +impl TransactionFeeResponse { + #[must_use] + pub fn into_raw(self) -> raw::TransactionFeeResponse { + raw::TransactionFeeResponse { + height: self.height, + fees: self + .fees + .into_iter() + .map( + |(asset, fee)| crate::generated::protocol::fees::v1alpha1::TransactionFee { + asset: asset.to_string(), + fee: Some(fee.into()), + }, + ) + .collect(), + } + } + + /// Attempt to convert from a raw protobuf [`raw::TransactionFeeResponse`]. + /// + /// # Errors + /// + /// - if the asset ID could not be converted from bytes + /// - if the fee was unset + pub fn try_from_raw( + proto: raw::TransactionFeeResponse, + ) -> Result { + let raw::TransactionFeeResponse { + height, + fees, + } = proto; + let fees = fees + .into_iter() + .map( + |crate::generated::protocol::fees::v1alpha1::TransactionFee { + asset, + fee, + }| { + let asset = asset.parse().map_err(TransactionFeeResponseError::asset)?; + let fee = fee.ok_or(TransactionFeeResponseError::unset_fee())?; + Ok((asset, fee.into())) + }, + ) + .collect::>()?; + Ok(Self { + height, + fees, + }) + } +} + +#[derive(Debug, thiserror::Error)] +#[error(transparent)] +pub struct TransactionFeeResponseError(TransactionFeeResponseErrorKind); + +impl TransactionFeeResponseError { + fn unset_fee() -> Self { + Self(TransactionFeeResponseErrorKind::UnsetFee) + } + + fn asset(inner: asset::ParseDenomError) -> Self { + Self(TransactionFeeResponseErrorKind::Asset(inner)) + } +} + +#[derive(Debug, thiserror::Error)] +enum TransactionFeeResponseErrorKind { + #[error("`fee` field is unset")] + UnsetFee, + #[error("failed to parse asset denom in the `assets` field")] + Asset(#[source] asset::ParseDenomError), +} diff --git a/crates/astria-core/src/protocol/genesis/snapshots/astria_core__protocol__genesis__v1alpha1__tests__genesis_state_is_unchanged.snap b/crates/astria-core/src/protocol/genesis/snapshots/astria_core__protocol__genesis__v1alpha1__tests__genesis_state_is_unchanged.snap index 3889c03f12..e9ce32088a 100644 --- a/crates/astria-core/src/protocol/genesis/snapshots/astria_core__protocol__genesis__v1alpha1__tests__genesis_state_is_unchanged.snap +++ b/crates/astria-core/src/protocol/genesis/snapshots/astria_core__protocol__genesis__v1alpha1__tests__genesis_state_is_unchanged.snap @@ -58,26 +58,79 @@ expression: genesis_state() "nria" ], "fees": { - "transferBaseFee": { - "lo": "12" + "bridgeLock": { + "base": { + "lo": "12" + }, + "multiplier": { + "lo": "1" + } + }, + "bridgeSudoChange": { + "base": { + "lo": "24" + }, + "multiplier": {} + }, + "bridgeUnlock": { + "base": { + "lo": "12" + }, + "multiplier": {} + }, + "feeAssetChange": { + "base": {}, + "multiplier": {} + }, + "feeChange": { + "base": {}, + "multiplier": {} }, - "sequenceBaseFee": { - "lo": "32" + "ibcRelay": { + "base": {}, + "multiplier": {} }, - "sequenceByteCostMultiplier": { - "lo": "1" + "ibcRelayerChange": { + "base": {}, + "multiplier": {} }, - "initBridgeAccountBaseFee": { - "lo": "48" + "ibcSudoChange": { + "base": {}, + "multiplier": {} }, - "bridgeLockByteCostMultiplier": { - "lo": "1" + "ics20Withdrawal": { + "base": { + "lo": "24" + }, + "multiplier": {} + }, + "initBridgeAccount": { + "base": { + "lo": "48" + }, + "multiplier": {} + }, + "sequence": { + "base": { + "lo": "32" + }, + "multiplier": { + "lo": "1" + } }, - "bridgeSudoChangeFee": { - "lo": "24" + "sudoAddressChange": { + "base": {}, + "multiplier": {} + }, + "transfer": { + "base": { + "lo": "12" + }, + "multiplier": {} }, - "ics20WithdrawalBaseFee": { - "lo": "24" + "validatorUpdate": { + "base": {}, + "multiplier": {} } } } diff --git a/crates/astria-core/src/protocol/genesis/v1alpha1.rs b/crates/astria-core/src/protocol/genesis/v1alpha1.rs index 79a69eaef4..fe49e93238 100644 --- a/crates/astria-core/src/protocol/genesis/v1alpha1.rs +++ b/crates/astria-core/src/protocol/genesis/v1alpha1.rs @@ -16,6 +16,23 @@ use crate::{ Bech32, Bech32m, }, + protocol::fees::v1alpha1::{ + BridgeLockFeeComponents, + BridgeSudoChangeFeeComponents, + BridgeUnlockFeeComponents, + FeeAssetChangeFeeComponents, + FeeChangeFeeComponents, + FeeComponentError, + IbcRelayFeeComponents, + IbcRelayerChangeFeeComponents, + IbcSudoChangeFeeComponents, + Ics20WithdrawalFeeComponents, + InitBridgeAccountFeeComponents, + SequenceFeeComponents, + SudoAddressChangeFeeComponents, + TransferFeeComponents, + ValidatorUpdateFeeComponents, + }, Protobuf, }; @@ -39,7 +56,7 @@ pub struct GenesisAppState { native_asset_base_denomination: asset::TracePrefixed, ibc_parameters: IBCParameters, allowed_fee_assets: Vec, - fees: Fees, + fees: GenesisFees, } impl GenesisAppState { @@ -89,7 +106,7 @@ impl GenesisAppState { } #[must_use] - pub fn fees(&self) -> &Fees { + pub fn fees(&self) -> &GenesisFees { &self.fees } @@ -199,7 +216,7 @@ impl Protobuf for GenesisAppState { let fees = fees .as_ref() .ok_or_else(|| Self::Error::field_not_set("fees")) - .and_then(|fees| Fees::try_from_raw_ref(fees).map_err(Self::Error::fees))?; + .and_then(|fees| GenesisFees::try_from_raw_ref(fees).map_err(Self::Error::fees))?; let this = Self { address_prefixes, @@ -542,80 +559,183 @@ impl From for IBCParameters { } #[derive(Clone, Debug)] -pub struct Fees { - pub transfer_base_fee: u128, - pub sequence_base_fee: u128, - pub sequence_byte_cost_multiplier: u128, - pub init_bridge_account_base_fee: u128, - pub bridge_lock_byte_cost_multiplier: u128, - pub bridge_sudo_change_fee: u128, - pub ics20_withdrawal_base_fee: u128, +pub struct GenesisFees { + pub sequence: SequenceFeeComponents, + pub transfer: TransferFeeComponents, + pub ics20_withdrawal: Ics20WithdrawalFeeComponents, + pub init_bridge_account: InitBridgeAccountFeeComponents, + pub bridge_lock: BridgeLockFeeComponents, + pub bridge_unlock: BridgeUnlockFeeComponents, + pub bridge_sudo_change: BridgeSudoChangeFeeComponents, + pub ibc_relay: IbcRelayFeeComponents, + pub validator_update: ValidatorUpdateFeeComponents, + pub fee_asset_change: FeeAssetChangeFeeComponents, + pub fee_change: FeeChangeFeeComponents, + pub ibc_relayer_change: IbcRelayerChangeFeeComponents, + pub sudo_address_change: SudoAddressChangeFeeComponents, + pub ibc_sudo_change: IbcSudoChangeFeeComponents, } -impl Protobuf for Fees { +impl Protobuf for GenesisFees { type Error = FeesError; - type Raw = raw::Fees; + type Raw = raw::GenesisFees; + #[expect( + clippy::too_many_lines, + reason = "barring use of a macro, all lines are necessary" + )] fn try_from_raw_ref(raw: &Self::Raw) -> Result { let Self::Raw { - transfer_base_fee, - sequence_base_fee, - sequence_byte_cost_multiplier, - init_bridge_account_base_fee, - bridge_lock_byte_cost_multiplier, - bridge_sudo_change_fee, - ics20_withdrawal_base_fee, + sequence, + transfer, + ics20_withdrawal, + init_bridge_account, + bridge_lock, + bridge_unlock, + bridge_sudo_change, + ibc_relay, + validator_update, + fee_asset_change, + fee_change, + ibc_relayer_change, + sudo_address_change, + ibc_sudo_change, } = raw; - let transfer_base_fee = transfer_base_fee - .ok_or_else(|| Self::Error::field_not_set("transfer_base_fee"))? - .into(); - let sequence_base_fee = sequence_base_fee - .ok_or_else(|| Self::Error::field_not_set("sequence_base_fee"))? - .into(); - let sequence_byte_cost_multiplier = sequence_byte_cost_multiplier - .ok_or_else(|| Self::Error::field_not_set("sequence_byte_cost_multiplier"))? - .into(); - let init_bridge_account_base_fee = init_bridge_account_base_fee - .ok_or_else(|| Self::Error::field_not_set("init_bridge_account_base_fee"))? - .into(); - let bridge_lock_byte_cost_multiplier = bridge_lock_byte_cost_multiplier - .ok_or_else(|| Self::Error::field_not_set("bridge_lock_byte_cost_multiplier"))? - .into(); - let bridge_sudo_change_fee = bridge_sudo_change_fee - .ok_or_else(|| Self::Error::field_not_set("bridge_sudo_change_fee"))? - .into(); - let ics20_withdrawal_base_fee = ics20_withdrawal_base_fee - .ok_or_else(|| Self::Error::field_not_set("ics20_withdrawal_base_fee"))? - .into(); + let sequence = SequenceFeeComponents::try_from_raw( + sequence + .clone() + .ok_or_else(|| Self::Error::field_not_set("sequence"))?, + ) + .map_err(|e| FeesError::fee_components("sequence", e))?; + let transfer = TransferFeeComponents::try_from_raw( + transfer + .clone() + .ok_or_else(|| Self::Error::field_not_set("transfer"))?, + ) + .map_err(|e| FeesError::fee_components("transfer", e))?; + let ics20_withdrawal = Ics20WithdrawalFeeComponents::try_from_raw( + ics20_withdrawal + .clone() + .ok_or_else(|| Self::Error::field_not_set("ics20_withdrawal"))?, + ) + .map_err(|e| FeesError::fee_components("ics20_withdrawal", e))?; + let init_bridge_account = InitBridgeAccountFeeComponents::try_from_raw( + init_bridge_account + .clone() + .ok_or_else(|| Self::Error::field_not_set("init_bridge_account"))?, + ) + .map_err(|e| FeesError::fee_components("init_bridge_account", e))?; + let bridge_lock = BridgeLockFeeComponents::try_from_raw( + bridge_lock + .clone() + .ok_or_else(|| Self::Error::field_not_set("bridge_lock"))?, + ) + .map_err(|e| FeesError::fee_components("bridge_lock", e))?; + let bridge_unlock = BridgeUnlockFeeComponents::try_from_raw( + bridge_unlock + .clone() + .ok_or_else(|| Self::Error::field_not_set("bridge_unlock"))?, + ) + .map_err(|e| FeesError::fee_components("bridge_unlock", e))?; + let bridge_sudo_change = BridgeSudoChangeFeeComponents::try_from_raw( + bridge_sudo_change + .clone() + .ok_or_else(|| Self::Error::field_not_set("bridge_sudo_change"))?, + ) + .map_err(|e| FeesError::fee_components("bridge_sudo_change", e))?; + let ibc_relay = IbcRelayFeeComponents::try_from_raw( + ibc_relay + .clone() + .ok_or_else(|| Self::Error::field_not_set("ibc_relay"))?, + ) + .map_err(|e| FeesError::fee_components("ibc_relay", e))?; + let validator_update = ValidatorUpdateFeeComponents::try_from_raw( + validator_update + .clone() + .ok_or_else(|| Self::Error::field_not_set("validator_update"))?, + ) + .map_err(|e| FeesError::fee_components("validator_update", e))?; + let fee_asset_change = FeeAssetChangeFeeComponents::try_from_raw( + fee_asset_change + .clone() + .ok_or_else(|| Self::Error::field_not_set("fee_asset_change"))?, + ) + .map_err(|e| FeesError::fee_components("fee_asset_change", e))?; + let fee_change = FeeChangeFeeComponents::try_from_raw( + fee_change + .clone() + .ok_or_else(|| Self::Error::field_not_set("fee_change"))?, + ) + .map_err(|e| FeesError::fee_components("fee_change", e))?; + let ibc_relayer_change = IbcRelayerChangeFeeComponents::try_from_raw( + ibc_relayer_change + .clone() + .ok_or_else(|| Self::Error::field_not_set("ibc_relayer_change"))?, + ) + .map_err(|e| FeesError::fee_components("ibc_relayer_change", e))?; + let sudo_address_change = SudoAddressChangeFeeComponents::try_from_raw( + sudo_address_change + .clone() + .ok_or_else(|| Self::Error::field_not_set("sudo_address_change"))?, + ) + .map_err(|e| FeesError::fee_components("sudo_address_change", e))?; + let ibc_sudo_change = IbcSudoChangeFeeComponents::try_from_raw( + ibc_sudo_change + .clone() + .ok_or_else(|| Self::Error::field_not_set("ibc_sudo_change"))?, + ) + .map_err(|e| FeesError::fee_components("ibc_sudo_change", e))?; + Ok(Self { - transfer_base_fee, - sequence_base_fee, - sequence_byte_cost_multiplier, - init_bridge_account_base_fee, - bridge_lock_byte_cost_multiplier, - bridge_sudo_change_fee, - ics20_withdrawal_base_fee, + sequence, + transfer, + ics20_withdrawal, + init_bridge_account, + bridge_lock, + bridge_unlock, + bridge_sudo_change, + ibc_relay, + validator_update, + fee_asset_change, + fee_change, + ibc_relayer_change, + sudo_address_change, + ibc_sudo_change, }) } fn to_raw(&self) -> Self::Raw { let Self { - transfer_base_fee, - sequence_base_fee, - sequence_byte_cost_multiplier, - init_bridge_account_base_fee, - bridge_lock_byte_cost_multiplier, - bridge_sudo_change_fee, - ics20_withdrawal_base_fee, + sequence, + transfer, + ics20_withdrawal, + init_bridge_account, + bridge_lock, + bridge_unlock, + bridge_sudo_change, + ibc_relay, + validator_update, + fee_asset_change, + fee_change, + ibc_relayer_change, + sudo_address_change, + ibc_sudo_change, } = self; Self::Raw { - transfer_base_fee: Some(transfer_base_fee.into()), - sequence_base_fee: Some(sequence_base_fee.into()), - sequence_byte_cost_multiplier: Some(sequence_byte_cost_multiplier.into()), - init_bridge_account_base_fee: Some(init_bridge_account_base_fee.into()), - bridge_lock_byte_cost_multiplier: Some(bridge_lock_byte_cost_multiplier.into()), - bridge_sudo_change_fee: Some(bridge_sudo_change_fee.into()), - ics20_withdrawal_base_fee: Some(ics20_withdrawal_base_fee.into()), + transfer: Some(transfer.to_raw()), + sequence: Some(sequence.to_raw()), + ics20_withdrawal: Some(ics20_withdrawal.to_raw()), + init_bridge_account: Some(init_bridge_account.to_raw()), + bridge_lock: Some(bridge_lock.to_raw()), + bridge_unlock: Some(bridge_unlock.to_raw()), + bridge_sudo_change: Some(bridge_sudo_change.to_raw()), + ibc_relay: Some(ibc_relay.to_raw()), + validator_update: Some(validator_update.to_raw()), + fee_asset_change: Some(fee_asset_change.to_raw()), + fee_change: Some(fee_change.to_raw()), + ibc_relayer_change: Some(ibc_relayer_change.to_raw()), + sudo_address_change: Some(sudo_address_change.to_raw()), + ibc_sudo_change: Some(ibc_sudo_change.to_raw()), } } } @@ -630,6 +750,13 @@ impl FeesError { name, }) } + + fn fee_components(field: &'static str, err: FeeComponentError) -> Self { + Self(FeesErrorKind::FeeComponentsConversion { + field, + source: err, + }) + } } #[derive(Debug, thiserror::Error)] @@ -637,6 +764,11 @@ impl FeesError { enum FeesErrorKind { #[error("field was not set: `{name}`")] FieldNotSet { name: &'static str }, + #[error("validating field `{field}` failed")] + FeeComponentsConversion { + field: &'static str, + source: FeeComponentError, + }, } #[cfg(test)] @@ -678,6 +810,7 @@ mod tests { .unwrap() } + #[expect(clippy::too_many_lines, reason = "for testing purposes")] fn proto_genesis_state() -> raw::GenesisAppState { raw::GenesisAppState { accounts: vec![ @@ -709,14 +842,105 @@ mod tests { outbound_ics20_transfers_enabled: true, }), allowed_fee_assets: vec!["nria".into()], - fees: Some(raw::Fees { - transfer_base_fee: Some(12.into()), - sequence_base_fee: Some(32.into()), - sequence_byte_cost_multiplier: Some(1.into()), - init_bridge_account_base_fee: Some(48.into()), - bridge_lock_byte_cost_multiplier: Some(1.into()), - bridge_sudo_change_fee: Some(24.into()), - ics20_withdrawal_base_fee: Some(24.into()), + fees: Some(raw::GenesisFees { + transfer: Some( + TransferFeeComponents { + base: 12, + multiplier: 0, + } + .to_raw(), + ), + sequence: Some( + SequenceFeeComponents { + base: 32, + multiplier: 1, + } + .to_raw(), + ), + init_bridge_account: Some( + InitBridgeAccountFeeComponents { + base: 48, + multiplier: 0, + } + .to_raw(), + ), + bridge_lock: Some( + BridgeLockFeeComponents { + base: 12, + multiplier: 1, + } + .to_raw(), + ), + bridge_unlock: Some( + BridgeUnlockFeeComponents { + base: 12, + multiplier: 0, + } + .to_raw(), + ), + bridge_sudo_change: Some( + BridgeSudoChangeFeeComponents { + base: 24, + multiplier: 0, + } + .to_raw(), + ), + ics20_withdrawal: Some( + Ics20WithdrawalFeeComponents { + base: 24, + multiplier: 0, + } + .to_raw(), + ), + ibc_relay: Some( + IbcRelayFeeComponents { + base: 0, + multiplier: 0, + } + .to_raw(), + ), + validator_update: Some( + ValidatorUpdateFeeComponents { + base: 0, + multiplier: 0, + } + .to_raw(), + ), + fee_asset_change: Some( + FeeAssetChangeFeeComponents { + base: 0, + multiplier: 0, + } + .to_raw(), + ), + fee_change: Some( + FeeChangeFeeComponents { + base: 0, + multiplier: 0, + } + .to_raw(), + ), + ibc_relayer_change: Some( + IbcRelayerChangeFeeComponents { + base: 0, + multiplier: 0, + } + .to_raw(), + ), + sudo_address_change: Some( + SudoAddressChangeFeeComponents { + base: 0, + multiplier: 0, + } + .to_raw(), + ), + ibc_sudo_change: Some( + IbcSudoChangeFeeComponents { + base: 0, + multiplier: 0, + } + .to_raw(), + ), }), } } diff --git a/crates/astria-core/src/protocol/mod.rs b/crates/astria-core/src/protocol/mod.rs index e285ce4d67..75a28697a8 100644 --- a/crates/astria-core/src/protocol/mod.rs +++ b/crates/astria-core/src/protocol/mod.rs @@ -8,6 +8,7 @@ pub mod abci; pub mod account; pub mod asset; pub mod bridge; +pub mod fees; pub mod genesis; pub mod memos; pub mod transaction; diff --git a/crates/astria-core/src/protocol/transaction/v1alpha1/action/group/mod.rs b/crates/astria-core/src/protocol/transaction/v1alpha1/action/group/mod.rs index 7c79c29306..ca95353dd0 100644 --- a/crates/astria-core/src/protocol/transaction/v1alpha1/action/group/mod.rs +++ b/crates/astria-core/src/protocol/transaction/v1alpha1/action/group/mod.rs @@ -15,7 +15,7 @@ use super::{ BridgeSudoChange, BridgeUnlock, FeeAssetChange, - FeeChangeKind, + FeeChange, IbcRelayerChange, IbcSudoChange, Ics20Withdrawal, @@ -51,7 +51,7 @@ impl_belong_to_group!( (BridgeLock, Group::BundleableGeneral), (BridgeUnlock, Group::BundleableGeneral), (BridgeSudoChange, Group::UnbundleableGeneral), - (FeeChangeKind, Group::BundleableSudo), + (FeeChange, Group::BundleableSudo), (FeeAssetChange, Group::BundleableSudo), (IbcRelay, Group::BundleableGeneral), (IbcSudoChange, Group::UnbundleableSudo), @@ -70,7 +70,7 @@ impl Action { Action::BridgeLock(_) => BridgeLock::GROUP, Action::BridgeUnlock(_) => BridgeUnlock::GROUP, Action::BridgeSudoChange(_) => BridgeSudoChange::GROUP, - Action::FeeChange(_) => FeeChangeKind::GROUP, + Action::FeeChange(_) => FeeChange::GROUP, Action::FeeAssetChange(_) => FeeAssetChange::GROUP, Action::Ibc(_) => IbcRelay::GROUP, Action::IbcSudoChange(_) => IbcSudoChange::GROUP, diff --git a/crates/astria-core/src/protocol/transaction/v1alpha1/action/group/tests.rs b/crates/astria-core/src/protocol/transaction/v1alpha1/action/group/tests.rs index c5cd6212e0..2918a28a90 100644 --- a/crates/astria-core/src/protocol/transaction/v1alpha1/action/group/tests.rs +++ b/crates/astria-core/src/protocol/transaction/v1alpha1/action/group/tests.rs @@ -19,7 +19,6 @@ use crate::{ BridgeUnlock, FeeAssetChange, FeeChange, - FeeChangeKind, IbcRelayerChange, IbcSudoChange, Ics20Withdrawal, @@ -27,6 +26,7 @@ use crate::{ Sequence, SudoAddressChange, Transfer, + TransferFeeComponents, ValidatorUpdate, }, }; @@ -104,10 +104,10 @@ fn from_list_of_actions_bundleable_sudo() { let asset: Denom = "nria".parse().unwrap(); let actions = vec![ - Action::FeeChange(FeeChange { - fee_change: FeeChangeKind::TransferBaseFee, - new_value: 100, - }), + Action::FeeChange(FeeChange::Transfer(TransferFeeComponents { + base: 100, + multiplier: 0, + })), Action::FeeAssetChange(FeeAssetChange::Addition(asset)), Action::IbcRelayerChange(IbcRelayerChange::Addition(address)), ]; diff --git a/crates/astria-core/src/protocol/transaction/v1alpha1/action/mod.rs b/crates/astria-core/src/protocol/transaction/v1alpha1/action/mod.rs index fb34593bd1..c4dc59bd52 100644 --- a/crates/astria-core/src/protocol/transaction/v1alpha1/action/mod.rs +++ b/crates/astria-core/src/protocol/transaction/v1alpha1/action/mod.rs @@ -7,6 +7,7 @@ use ibc_types::{ IdentifierError, }; use penumbra_ibc::IbcRelay; +use prost::Name as _; use super::raw; use crate::{ @@ -20,6 +21,23 @@ use crate::{ IncorrectRollupIdLength, RollupId, }, + protocol::fees::v1alpha1::{ + BridgeLockFeeComponents, + BridgeSudoChangeFeeComponents, + BridgeUnlockFeeComponents, + FeeAssetChangeFeeComponents, + FeeChangeFeeComponents, + FeeComponentError, + IbcRelayFeeComponents, + IbcRelayerChangeFeeComponents, + IbcSudoChangeFeeComponents, + Ics20WithdrawalFeeComponents, + InitBridgeAccountFeeComponents, + SequenceFeeComponents, + SudoAddressChangeFeeComponents, + TransferFeeComponents, + ValidatorUpdateFeeComponents, + }, Protobuf, }; @@ -1891,21 +1909,55 @@ enum BridgeSudoChangeErrorKind { InvalidFeeAsset(#[source] asset::ParseDenomError), } -#[derive(Debug, Clone)] -pub enum FeeChangeKind { - TransferBaseFee, - SequenceBaseFee, - SequenceByteCostMultiplier, - InitBridgeAccountBaseFee, - BridgeLockByteCostMultiplier, - BridgeSudoChangeBaseFee, - Ics20WithdrawalBaseFee, +#[derive(Debug, thiserror::Error)] +#[error(transparent)] +pub struct FeeChangeError(FeeChangeErrorKind); + +impl FeeChangeError { + fn field_unset(name: &'static str) -> Self { + Self(FeeChangeErrorKind::FieldUnset { + name, + }) + } +} + +impl From for FeeChangeError { + fn from(source: FeeComponentError) -> Self { + Self(FeeChangeErrorKind::FeeComponent { + source, + }) + } +} + +#[derive(Debug, thiserror::Error)] +#[error("failed to validate on-wire type `{}`", raw::FeeChange::full_name())] +enum FeeChangeErrorKind { + FeeComponent { + // NOTE: the name of the fee change variant is not specified because it is included in + // the source FeeComponentError. + #[from] + source: FeeComponentError, + }, + #[error("field `{name}` was not set")] + FieldUnset { name: &'static str }, } #[derive(Debug, Clone)] -pub struct FeeChange { - pub fee_change: FeeChangeKind, - pub new_value: u128, +pub enum FeeChange { + Transfer(TransferFeeComponents), + Sequence(SequenceFeeComponents), + Ics20Withdrawal(Ics20WithdrawalFeeComponents), + InitBridgeAccount(InitBridgeAccountFeeComponents), + BridgeLock(BridgeLockFeeComponents), + BridgeUnlock(BridgeUnlockFeeComponents), + BridgeSudoChange(BridgeSudoChangeFeeComponents), + IbcRelay(IbcRelayFeeComponents), + ValidatorUpdate(ValidatorUpdateFeeComponents), + FeeAssetChange(FeeAssetChangeFeeComponents), + FeeChange(FeeChangeFeeComponents), + IbcRelayerChange(IbcRelayerChangeFeeComponents), + SudoAddressChange(SudoAddressChangeFeeComponents), + IbcSudoChange(IbcSudoChangeFeeComponents), } impl Protobuf for FeeChange { @@ -1915,27 +1967,48 @@ impl Protobuf for FeeChange { #[must_use] fn to_raw(&self) -> raw::FeeChange { raw::FeeChange { - value: Some(match self.fee_change { - FeeChangeKind::TransferBaseFee => { - raw::fee_change::Value::TransferBaseFee(self.new_value.into()) + fee_components: Some(match &self { + Self::Transfer(fee_change) => { + raw::fee_change::FeeComponents::Transfer(fee_change.to_raw()) + } + Self::Sequence(fee_change) => { + raw::fee_change::FeeComponents::Sequence(fee_change.to_raw()) + } + Self::Ics20Withdrawal(fee_change) => { + raw::fee_change::FeeComponents::Ics20Withdrawal(fee_change.to_raw()) + } + Self::InitBridgeAccount(fee_change) => { + raw::fee_change::FeeComponents::InitBridgeAccount(fee_change.to_raw()) + } + Self::BridgeLock(fee_change) => { + raw::fee_change::FeeComponents::BridgeLock(fee_change.to_raw()) + } + Self::BridgeUnlock(fee_change) => { + raw::fee_change::FeeComponents::BridgeUnlock(fee_change.to_raw()) } - FeeChangeKind::SequenceBaseFee => { - raw::fee_change::Value::SequenceBaseFee(self.new_value.into()) + Self::BridgeSudoChange(fee_change) => { + raw::fee_change::FeeComponents::BridgeSudoChange(fee_change.to_raw()) } - FeeChangeKind::SequenceByteCostMultiplier => { - raw::fee_change::Value::SequenceByteCostMultiplier(self.new_value.into()) + Self::IbcRelay(fee_change) => { + raw::fee_change::FeeComponents::IbcRelay(fee_change.to_raw()) } - FeeChangeKind::InitBridgeAccountBaseFee => { - raw::fee_change::Value::InitBridgeAccountBaseFee(self.new_value.into()) + Self::ValidatorUpdate(fee_change) => { + raw::fee_change::FeeComponents::ValidatorUpdate(fee_change.to_raw()) } - FeeChangeKind::BridgeLockByteCostMultiplier => { - raw::fee_change::Value::BridgeLockByteCostMultiplier(self.new_value.into()) + Self::FeeAssetChange(fee_change) => { + raw::fee_change::FeeComponents::FeeAssetChange(fee_change.to_raw()) } - FeeChangeKind::BridgeSudoChangeBaseFee => { - raw::fee_change::Value::BridgeSudoChangeBaseFee(self.new_value.into()) + Self::FeeChange(fee_change) => { + raw::fee_change::FeeComponents::FeeChange(fee_change.to_raw()) } - FeeChangeKind::Ics20WithdrawalBaseFee => { - raw::fee_change::Value::Ics20WithdrawalBaseFee(self.new_value.into()) + Self::IbcRelayerChange(fee_change) => { + raw::fee_change::FeeComponents::IbcRelayerChange(fee_change.to_raw()) + } + Self::SudoAddressChange(fee_change) => { + raw::fee_change::FeeComponents::SudoAddressChange(fee_change.to_raw()) + } + Self::IbcSudoChange(fee_change) => { + raw::fee_change::FeeComponents::IbcSudoChange(fee_change.to_raw()) } }), } @@ -1947,51 +2020,55 @@ impl Protobuf for FeeChange { /// /// - if the fee change `value` field is missing /// - if the `new_value` field is missing - fn try_from_raw_ref(proto: &raw::FeeChange) -> Result { - let (fee_change, new_value) = match proto.value { - Some(raw::fee_change::Value::TransferBaseFee(new_value)) => { - (FeeChangeKind::TransferBaseFee, new_value) + fn try_from_raw_ref(proto: &raw::FeeChange) -> Result { + Ok(match &proto.fee_components { + Some(raw::fee_change::FeeComponents::Transfer(fee_change)) => { + Self::Transfer(TransferFeeComponents::try_from_raw_ref(fee_change)?) } - Some(raw::fee_change::Value::SequenceBaseFee(new_value)) => { - (FeeChangeKind::SequenceBaseFee, new_value) + Some(raw::fee_change::FeeComponents::Sequence(fee_change)) => { + Self::Sequence(SequenceFeeComponents::try_from_raw_ref(fee_change)?) } - Some(raw::fee_change::Value::SequenceByteCostMultiplier(new_value)) => { - (FeeChangeKind::SequenceByteCostMultiplier, new_value) + Some(raw::fee_change::FeeComponents::Ics20Withdrawal(fee_change)) => { + Self::Ics20Withdrawal(Ics20WithdrawalFeeComponents::try_from_raw_ref(fee_change)?) } - Some(raw::fee_change::Value::InitBridgeAccountBaseFee(new_value)) => { - (FeeChangeKind::InitBridgeAccountBaseFee, new_value) + Some(raw::fee_change::FeeComponents::InitBridgeAccount(fee_change)) => { + Self::InitBridgeAccount(InitBridgeAccountFeeComponents::try_from_raw_ref( + fee_change, + )?) } - Some(raw::fee_change::Value::BridgeLockByteCostMultiplier(new_value)) => { - (FeeChangeKind::BridgeLockByteCostMultiplier, new_value) + Some(raw::fee_change::FeeComponents::BridgeLock(fee_change)) => { + Self::BridgeLock(BridgeLockFeeComponents::try_from_raw_ref(fee_change)?) } - Some(raw::fee_change::Value::BridgeSudoChangeBaseFee(new_value)) => { - (FeeChangeKind::BridgeSudoChangeBaseFee, new_value) + Some(raw::fee_change::FeeComponents::BridgeUnlock(fee_change)) => { + Self::BridgeUnlock(BridgeUnlockFeeComponents::try_from_raw_ref(fee_change)?) } - Some(raw::fee_change::Value::Ics20WithdrawalBaseFee(new_value)) => { - (FeeChangeKind::Ics20WithdrawalBaseFee, new_value) + Some(raw::fee_change::FeeComponents::BridgeSudoChange(fee_change)) => { + Self::BridgeSudoChange(BridgeSudoChangeFeeComponents::try_from_raw_ref(fee_change)?) } - None => return Err(FeeChangeError::missing_value_to_change()), - }; - - Ok(Self { - fee_change, - new_value: new_value.into(), + Some(raw::fee_change::FeeComponents::IbcRelay(fee_change)) => { + Self::IbcRelay(IbcRelayFeeComponents::try_from_raw_ref(fee_change)?) + } + Some(raw::fee_change::FeeComponents::ValidatorUpdate(fee_change)) => { + Self::ValidatorUpdate(ValidatorUpdateFeeComponents::try_from_raw_ref(fee_change)?) + } + Some(raw::fee_change::FeeComponents::FeeAssetChange(fee_change)) => { + Self::FeeAssetChange(FeeAssetChangeFeeComponents::try_from_raw_ref(fee_change)?) + } + Some(raw::fee_change::FeeComponents::FeeChange(fee_change)) => { + Self::FeeChange(FeeChangeFeeComponents::try_from_raw_ref(fee_change)?) + } + Some(raw::fee_change::FeeComponents::IbcRelayerChange(fee_change)) => { + Self::IbcRelayerChange(IbcRelayerChangeFeeComponents::try_from_raw_ref(fee_change)?) + } + Some(raw::fee_change::FeeComponents::SudoAddressChange(fee_change)) => { + Self::SudoAddressChange(SudoAddressChangeFeeComponents::try_from_raw_ref( + fee_change, + )?) + } + Some(raw::fee_change::FeeComponents::IbcSudoChange(fee_change)) => { + Self::IbcSudoChange(IbcSudoChangeFeeComponents::try_from_raw_ref(fee_change)?) + } + None => return Err(FeeChangeError::field_unset("fee_components")), }) } } - -#[derive(Debug, thiserror::Error)] -#[error(transparent)] -pub struct FeeChangeError(FeeChangeErrorKind); - -impl FeeChangeError { - fn missing_value_to_change() -> Self { - Self(FeeChangeErrorKind::MissingValueToChange) - } -} - -#[derive(Debug, thiserror::Error)] -enum FeeChangeErrorKind { - #[error("the value which to change was missing")] - MissingValueToChange, -} diff --git a/crates/astria-core/src/protocol/transaction/v1alpha1/mod.rs b/crates/astria-core/src/protocol/transaction/v1alpha1/mod.rs index f91de71b22..42fa197238 100644 --- a/crates/astria-core/src/protocol/transaction/v1alpha1/mod.rs +++ b/crates/astria-core/src/protocol/transaction/v1alpha1/mod.rs @@ -13,7 +13,6 @@ use crate::{ }, generated::protocol::transactions::v1alpha1 as raw, primitive::v1::{ - asset, TransactionId, ADDRESS_LEN, }, @@ -498,88 +497,14 @@ impl TransactionParams { } } -#[derive(Debug, Clone)] -pub struct TransactionFeeResponse { - pub height: u64, - pub fees: Vec<(asset::Denom, u128)>, -} - -impl TransactionFeeResponse { - #[must_use] - pub fn into_raw(self) -> raw::TransactionFeeResponse { - raw::TransactionFeeResponse { - height: self.height, - fees: self - .fees - .into_iter() - .map(|(asset, fee)| raw::TransactionFee { - asset: asset.to_string(), - fee: Some(fee.into()), - }) - .collect(), - } - } - - /// Attempt to convert from a raw protobuf [`raw::TransactionFeeResponse`]. - /// - /// # Errors - /// - /// - if the asset ID could not be converted from bytes - /// - if the fee was unset - pub fn try_from_raw( - proto: raw::TransactionFeeResponse, - ) -> Result { - let raw::TransactionFeeResponse { - height, - fees, - } = proto; - let fees = fees - .into_iter() - .map( - |raw::TransactionFee { - asset, - fee, - }| { - let asset = asset.parse().map_err(TransactionFeeResponseError::asset)?; - let fee = fee.ok_or(TransactionFeeResponseError::unset_fee())?; - Ok((asset, fee.into())) - }, - ) - .collect::>()?; - Ok(Self { - height, - fees, - }) - } -} - -#[derive(Debug, thiserror::Error)] -#[error(transparent)] -pub struct TransactionFeeResponseError(TransactionFeeResponseErrorKind); - -impl TransactionFeeResponseError { - fn unset_fee() -> Self { - Self(TransactionFeeResponseErrorKind::UnsetFee) - } - - fn asset(inner: asset::ParseDenomError) -> Self { - Self(TransactionFeeResponseErrorKind::Asset(inner)) - } -} - -#[derive(Debug, thiserror::Error)] -enum TransactionFeeResponseErrorKind { - #[error("`fee` field is unset")] - UnsetFee, - #[error("failed to parse asset denom in the `assets` field")] - Asset(#[source] asset::ParseDenomError), -} - #[cfg(test)] mod tests { use super::*; use crate::{ - primitive::v1::Address, + primitive::v1::{ + asset, + Address, + }, protocol::transaction::v1alpha1::action::Transfer, }; const ASTRIA_ADDRESS_PREFIX: &str = "astria"; diff --git a/crates/astria-sequencer-client/src/extension_trait.rs b/crates/astria-sequencer-client/src/extension_trait.rs index a863911517..aeb153fa36 100644 --- a/crates/astria-sequencer-client/src/extension_trait.rs +++ b/crates/astria-sequencer-client/src/extension_trait.rs @@ -39,10 +39,8 @@ use astria_core::protocol::{ BridgeAccountInfoResponse, BridgeAccountLastTxHashResponse, }, - transaction::v1alpha1::{ - TransactionFeeResponse, - UnsignedTransaction, - }, + fees::v1alpha1::TransactionFeeResponse, + transaction::v1alpha1::UnsignedTransaction, }; pub use astria_core::{ primitive::v1::Address, @@ -628,7 +626,7 @@ pub trait SequencerClientExt: Client { .map_err(|e| Error::tendermint_rpc("abci_query", e))?; let proto_response = - astria_core::generated::protocol::transactions::v1alpha1::TransactionFeeResponse::decode( + astria_core::generated::protocol::fees::v1alpha1::TransactionFeeResponse::decode( &*response.value, ) .map_err(|e| { diff --git a/crates/astria-sequencer-client/src/tests/http.rs b/crates/astria-sequencer-client/src/tests/http.rs index a1223b9e75..60362c72ef 100644 --- a/crates/astria-sequencer-client/src/tests/http.rs +++ b/crates/astria-sequencer-client/src/tests/http.rs @@ -2,7 +2,10 @@ use std::time::Duration; use astria_core::{ crypto::SigningKey, - generated::protocol::asset::v1alpha1::AllowedFeeAssetsResponse, + generated::protocol::{ + asset::v1alpha1::AllowedFeeAssetsResponse, + fees::v1alpha1::TransactionFee, + }, primitive::v1::Address, protocol::transaction::v1alpha1::{ action::Transfer, @@ -321,10 +324,7 @@ async fn get_bridge_account_last_transaction_hash() { #[tokio::test] async fn get_transaction_fee() { - use astria_core::generated::protocol::transactions::v1alpha1::{ - TransactionFee, - TransactionFeeResponse, - }; + use astria_core::generated::protocol::fees::v1alpha1::TransactionFeeResponse; let MockSequencer { server, diff --git a/crates/astria-sequencer-utils/src/genesis_example.rs b/crates/astria-sequencer-utils/src/genesis_example.rs index 680dc0c307..cbe7ea5e22 100644 --- a/crates/astria-sequencer-utils/src/genesis_example.rs +++ b/crates/astria-sequencer-utils/src/genesis_example.rs @@ -7,13 +7,31 @@ use std::{ use astria_core::{ generated::protocol::genesis::v1alpha1::{ AddressPrefixes, + GenesisFees, IbcParameters, }, primitive::v1::Address, - protocol::genesis::v1alpha1::{ - Account, - Fees, - GenesisAppState, + protocol::{ + fees::v1alpha1::{ + BridgeLockFeeComponents, + BridgeSudoChangeFeeComponents, + BridgeUnlockFeeComponents, + FeeAssetChangeFeeComponents, + FeeChangeFeeComponents, + IbcRelayFeeComponents, + IbcRelayerChangeFeeComponents, + IbcSudoChangeFeeComponents, + Ics20WithdrawalFeeComponents, + InitBridgeAccountFeeComponents, + SequenceFeeComponents, + SudoAddressChangeFeeComponents, + TransferFeeComponents, + ValidatorUpdateFeeComponents, + }, + genesis::v1alpha1::{ + Account, + GenesisAppState, + }, }, Protobuf, }; @@ -72,6 +90,7 @@ fn address_prefixes() -> AddressPrefixes { } } +#[expect(clippy::too_many_lines, reason = "all lines reasonably necessary")] fn proto_genesis_state() -> astria_core::generated::protocol::genesis::v1alpha1::GenesisAppState { astria_core::generated::protocol::genesis::v1alpha1::GenesisAppState { accounts: accounts().into_iter().map(Protobuf::into_raw).collect(), @@ -87,18 +106,106 @@ fn proto_genesis_state() -> astria_core::generated::protocol::genesis::v1alpha1: outbound_ics20_transfers_enabled: true, }), allowed_fee_assets: vec!["nria".parse().unwrap()], - fees: Some( - Fees { - transfer_base_fee: 12, - sequence_base_fee: 32, - sequence_byte_cost_multiplier: 1, - init_bridge_account_base_fee: 48, - bridge_lock_byte_cost_multiplier: 1, - bridge_sudo_change_fee: 24, - ics20_withdrawal_base_fee: 24, - } - .into_raw(), - ), + fees: Some(GenesisFees { + transfer: Some( + TransferFeeComponents { + base: 12, + multiplier: 0, + } + .to_raw(), + ), + sequence: Some( + SequenceFeeComponents { + base: 32, + multiplier: 1, + } + .to_raw(), + ), + init_bridge_account: Some( + InitBridgeAccountFeeComponents { + base: 48, + multiplier: 0, + } + .to_raw(), + ), + bridge_lock: Some( + BridgeLockFeeComponents { + base: 12, + multiplier: 1, + } + .to_raw(), + ), + bridge_unlock: Some( + BridgeUnlockFeeComponents { + base: 12, + multiplier: 0, + } + .to_raw(), + ), + bridge_sudo_change: Some( + BridgeSudoChangeFeeComponents { + base: 24, + multiplier: 0, + } + .to_raw(), + ), + ics20_withdrawal: Some( + Ics20WithdrawalFeeComponents { + base: 24, + multiplier: 0, + } + .to_raw(), + ), + ibc_relay: Some( + IbcRelayFeeComponents { + base: 0, + multiplier: 0, + } + .to_raw(), + ), + validator_update: Some( + ValidatorUpdateFeeComponents { + base: 0, + multiplier: 0, + } + .to_raw(), + ), + fee_asset_change: Some( + FeeAssetChangeFeeComponents { + base: 0, + multiplier: 0, + } + .to_raw(), + ), + fee_change: Some( + FeeChangeFeeComponents { + base: 0, + multiplier: 0, + } + .to_raw(), + ), + ibc_relayer_change: Some( + IbcRelayerChangeFeeComponents { + base: 0, + multiplier: 0, + } + .to_raw(), + ), + sudo_address_change: Some( + SudoAddressChangeFeeComponents { + base: 0, + multiplier: 0, + } + .to_raw(), + ), + ibc_sudo_change: Some( + IbcSudoChangeFeeComponents { + base: 0, + multiplier: 0, + } + .to_raw(), + ), + }), } } diff --git a/crates/astria-sequencer/Cargo.toml b/crates/astria-sequencer/Cargo.toml index 5f5f2cec24..3f11ca4215 100644 --- a/crates/astria-sequencer/Cargo.toml +++ b/crates/astria-sequencer/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "astria-sequencer" -version = "0.17.0" +version = "0.18.0" edition = "2021" license = "MIT OR Apache-2.0" rust-version = "1.81.0" diff --git a/crates/astria-sequencer/src/accounts/action.rs b/crates/astria-sequencer/src/accounts/action.rs index 32b2a482b0..c2ac80f877 100644 --- a/crates/astria-sequencer/src/accounts/action.rs +++ b/crates/astria-sequencer/src/accounts/action.rs @@ -1,7 +1,6 @@ use astria_core::protocol::transaction::v1alpha1::action::Transfer; use astria_eyre::eyre::{ ensure, - OptionExt as _, Result, WrapErr as _, }; @@ -18,10 +17,6 @@ use crate::{ }, address::StateReadExt as _, app::ActionHandler, - assets::{ - StateReadExt as _, - StateWriteExt as _, - }, bridge::StateReadExt as _, transaction::StateReadExt as _, }; @@ -63,50 +58,16 @@ where S: StateWrite, TAddress: AddressBytes, { - let fee = state - .get_transfer_base_fee() + let from = from.address_bytes(); + state + .decrease_balance(from, &action.asset, action.amount) .await - .wrap_err("failed to get transfer base fee")?; + .wrap_err("failed decreasing `from` account balance")?; state - .get_and_increase_block_fees::(&action.fee_asset, fee) + .increase_balance(&action.to, &action.asset, action.amount) .await - .wrap_err("failed to add to block fees")?; - - // if fee payment asset is same asset as transfer asset, deduct fee - // from same balance as asset transferred - if action.asset.to_ibc_prefixed() == action.fee_asset.to_ibc_prefixed() { - // check_stateful should have already checked this arithmetic - let payment_amount = action - .amount - .checked_add(fee) - .expect("transfer amount plus fee should not overflow"); - - state - .decrease_balance(from, &action.asset, payment_amount) - .await - .wrap_err("failed decreasing `from` account balance")?; - state - .increase_balance(&action.to, &action.asset, action.amount) - .await - .wrap_err("failed increasing `to` account balance")?; - } else { - // otherwise, just transfer the transfer asset and deduct fee from fee asset balance - // later - state - .decrease_balance(from, &action.asset, action.amount) - .await - .wrap_err("failed decreasing `from` account balance")?; - state - .increase_balance(&action.to, &action.asset, action.amount) - .await - .wrap_err("failed increasing `to` account balance")?; + .wrap_err("failed increasing `to` account balance")?; - // deduct fee from fee asset balance - state - .decrease_balance(from, &action.fee_asset, fee) - .await - .wrap_err("failed decreasing `from` account balance for fee payment")?; - } Ok(()) } @@ -122,54 +83,17 @@ where state.ensure_base_prefix(&action.to).await.wrap_err( "failed ensuring that the destination address matches the permitted base prefix", )?; - ensure!( - state - .is_allowed_fee_asset(&action.fee_asset) - .await - .wrap_err("failed to check allowed fee assets in state")?, - "invalid fee asset", - ); - let fee = state - .get_transfer_base_fee() - .await - .wrap_err("failed to get transfer base fee")?; let transfer_asset = &action.asset; - let from_fee_balance = state - .get_account_balance(from, &action.fee_asset) + let from_transfer_balance = state + .get_account_balance(from, transfer_asset) .await - .wrap_err("failed getting `from` account balance for fee payment")?; - - // if fee asset is same as transfer asset, ensure accounts has enough funds - // to cover both the fee and the amount transferred - if action.fee_asset.to_ibc_prefixed() == transfer_asset.to_ibc_prefixed() { - let payment_amount = action - .amount - .checked_add(fee) - .ok_or_eyre("transfer amount plus fee overflowed")?; - - ensure!( - from_fee_balance >= payment_amount, - "insufficient funds for transfer and fee payment" - ); - } else { - // otherwise, check the fee asset account has enough to cover the fees, - // and the transfer asset account has enough to cover the transfer - ensure!( - from_fee_balance >= fee, - "insufficient funds for fee payment" - ); - - let from_transfer_balance = state - .get_account_balance(from, transfer_asset) - .await - .wrap_err("failed to get account balance in transfer check")?; - ensure!( - from_transfer_balance >= action.amount, - "insufficient funds for transfer" - ); - } + .wrap_err("failed to get account balance in transfer check")?; + ensure!( + from_transfer_balance >= action.amount, + "insufficient funds for transfer" + ); Ok(()) } diff --git a/crates/astria-sequencer/src/accounts/component.rs b/crates/astria-sequencer/src/accounts/component.rs index 6913eba06b..a6b9632c28 100644 --- a/crates/astria-sequencer/src/accounts/component.rs +++ b/crates/astria-sequencer/src/accounts/component.rs @@ -38,10 +38,6 @@ impl Component for AccountsComponent { .put_account_balance(&account.address, &native_asset, account.balance) .wrap_err("failed writing account balance to state")?; } - - state - .put_transfer_base_fee(app_state.fees().transfer_base_fee) - .wrap_err("failed to put transfer base fee")?; Ok(()) } diff --git a/crates/astria-sequencer/src/accounts/state_ext.rs b/crates/astria-sequencer/src/accounts/state_ext.rs index 5348d0ee2d..40de580e34 100644 --- a/crates/astria-sequencer/src/accounts/state_ext.rs +++ b/crates/astria-sequencer/src/accounts/state_ext.rs @@ -13,7 +13,6 @@ use astria_core::primitive::v1::asset; use astria_eyre::{ anyhow_to_eyre, eyre::{ - eyre, OptionExt as _, Result, WrapErr as _, @@ -179,21 +178,6 @@ pub(crate) trait StateReadExt: StateRead + crate::assets::StateReadExt { .and_then(|value| storage::Nonce::try_from(value).map(u32::from)) .wrap_err("invalid nonce bytes") } - - #[instrument(skip_all)] - async fn get_transfer_base_fee(&self) -> Result { - let bytes = self - .get_raw(keys::TRANSFER_BASE_FEE) - .await - .map_err(anyhow_to_eyre) - .wrap_err("failed reading raw transfer base fee from state")?; - let Some(bytes) = bytes else { - return Err(eyre!("transfer base fee not set")); - }; - StoredValue::deserialize(&bytes) - .and_then(|value| storage::Fee::try_from(value).map(u128::from)) - .wrap_err("invalid fee bytes") - } } impl StateReadExt for T {} @@ -281,15 +265,6 @@ pub(crate) trait StateWriteExt: StateWrite { .wrap_err("failed to store updated account balance in database")?; Ok(()) } - - #[instrument(skip_all)] - fn put_transfer_base_fee(&mut self, fee: u128) -> Result<()> { - let bytes = StoredValue::from(storage::Fee::from(fee)) - .serialize() - .wrap_err("failed to serialize fee")?; - self.put_raw(keys::TRANSFER_BASE_FEE.to_string(), bytes); - Ok(()) - } } impl StateWriteExt for T {} @@ -783,15 +758,4 @@ mod tests { .await .expect_err("should not be able to subtract larger balance than what existed"); } - - #[tokio::test] - async fn transfer_base_fee_round_trip() { - let storage = cnidarium::TempStorage::new().await.unwrap(); - let snapshot = storage.latest_snapshot(); - let mut state = StateDelta::new(snapshot); - - state.put_transfer_base_fee(123).unwrap(); - let retrieved_fee = state.get_transfer_base_fee().await.unwrap(); - assert_eq!(retrieved_fee, 123); - } } diff --git a/crates/astria-sequencer/src/accounts/storage/keys.rs b/crates/astria-sequencer/src/accounts/storage/keys.rs index e23e368bcf..ceb9f9c191 100644 --- a/crates/astria-sequencer/src/accounts/storage/keys.rs +++ b/crates/astria-sequencer/src/accounts/storage/keys.rs @@ -15,7 +15,6 @@ use crate::{ }, }; -pub(in crate::accounts) const TRANSFER_BASE_FEE: &str = "accounts/transfer_base_fee"; const COMPONENT_PREFIX: &str = "accounts/"; const BALANCE_PREFIX: &str = "balance/"; const NONCE: &str = "nonce"; @@ -86,14 +85,12 @@ mod tests { #[test] fn keys_should_not_change() { - insta::assert_snapshot!(TRANSFER_BASE_FEE); insta::assert_snapshot!(balance(&address(), &asset())); insta::assert_snapshot!(nonce(&address())); } #[test] fn keys_should_have_component_prefix() { - assert!(TRANSFER_BASE_FEE.starts_with(COMPONENT_PREFIX)); assert!(balance(&address(), &asset()).starts_with(COMPONENT_PREFIX)); assert!(nonce(&address()).starts_with(COMPONENT_PREFIX)); } diff --git a/crates/astria-sequencer/src/accounts/storage/mod.rs b/crates/astria-sequencer/src/accounts/storage/mod.rs index 8d61ec9f93..72b4488564 100644 --- a/crates/astria-sequencer/src/accounts/storage/mod.rs +++ b/crates/astria-sequencer/src/accounts/storage/mod.rs @@ -4,6 +4,5 @@ mod values; pub(crate) use values::Value; pub(super) use values::{ Balance, - Fee, Nonce, }; diff --git a/crates/astria-sequencer/src/accounts/storage/snapshots/astria_sequencer__accounts__storage__keys__tests__keys_should_not_change-2.snap b/crates/astria-sequencer/src/accounts/storage/snapshots/astria_sequencer__accounts__storage__keys__tests__keys_should_not_change-2.snap index f3ff1952e1..8f0588676b 100644 --- a/crates/astria-sequencer/src/accounts/storage/snapshots/astria_sequencer__accounts__storage__keys__tests__keys_should_not_change-2.snap +++ b/crates/astria-sequencer/src/accounts/storage/snapshots/astria_sequencer__accounts__storage__keys__tests__keys_should_not_change-2.snap @@ -1,6 +1,5 @@ --- source: crates/astria-sequencer/src/accounts/storage/keys.rs -assertion_line: 90 -expression: "balance(&address(), &asset())" +expression: nonce(&address()) --- -accounts/HAxJDxtVKNgXPF3kbRMRYOSywMM=/balance/ibc/be429a02d00837245167a2616674a979a2ac6f9806468b48a975b156ad711320 +accounts/HAxJDxtVKNgXPF3kbRMRYOSywMM=/nonce diff --git a/crates/astria-sequencer/src/accounts/storage/snapshots/astria_sequencer__accounts__storage__keys__tests__keys_should_not_change.snap b/crates/astria-sequencer/src/accounts/storage/snapshots/astria_sequencer__accounts__storage__keys__tests__keys_should_not_change.snap index a57562a2da..c2567996ff 100644 --- a/crates/astria-sequencer/src/accounts/storage/snapshots/astria_sequencer__accounts__storage__keys__tests__keys_should_not_change.snap +++ b/crates/astria-sequencer/src/accounts/storage/snapshots/astria_sequencer__accounts__storage__keys__tests__keys_should_not_change.snap @@ -1,6 +1,5 @@ --- source: crates/astria-sequencer/src/accounts/storage/keys.rs -assertion_line: 79 -expression: TRANSFER_BASE_FEE_KEY +expression: "balance(&address(), &asset())" --- -accounts/transfer_base_fee +accounts/HAxJDxtVKNgXPF3kbRMRYOSywMM=/balance/ibc/be429a02d00837245167a2616674a979a2ac6f9806468b48a975b156ad711320 diff --git a/crates/astria-sequencer/src/app/mod.rs b/crates/astria-sequencer/src/app/mod.rs index 30a39b0031..93df9f5fd6 100644 --- a/crates/astria-sequencer/src/app/mod.rs +++ b/crates/astria-sequencer/src/app/mod.rs @@ -8,8 +8,6 @@ pub(crate) mod test_utils; #[cfg(test)] mod tests_app; #[cfg(test)] -mod tests_block_fees; -#[cfg(test)] mod tests_block_ordering; #[cfg(test)] mod tests_breaking_changes; @@ -94,10 +92,7 @@ use crate::{ StateWriteExt as _, }, address::StateWriteExt as _, - assets::{ - StateReadExt as _, - StateWriteExt as _, - }, + assets::StateWriteExt as _, authority::{ component::{ AuthorityComponent, @@ -107,11 +102,16 @@ use crate::{ StateWriteExt as _, }, bridge::{ - component::BridgeComponent, StateReadExt as _, StateWriteExt as _, }, component::Component as _, + fees::{ + component::FeesComponent, + construct_tx_fee_event, + StateReadExt as _, + StateWriteExt as _, + }, grpc::StateWriteExt as _, ibc::component::IbcComponent, mempool::{ @@ -126,7 +126,6 @@ use crate::{ GeneratedCommitments, }, }, - sequence::component::SequenceComponent, transaction::InvalidNonce, }; @@ -311,6 +310,9 @@ impl App { } // call init_chain on all components + FeesComponent::init_chain(&mut state_tx, &genesis_state) + .await + .wrap_err("init_chain failed on FeesComponent")?; AccountsComponent::init_chain(&mut state_tx, &genesis_state) .await .wrap_err("init_chain failed on AccountsComponent")?; @@ -323,15 +325,9 @@ impl App { ) .await .wrap_err("init_chain failed on AuthorityComponent")?; - BridgeComponent::init_chain(&mut state_tx, &genesis_state) - .await - .wrap_err("init_chain failed on BridgeComponent")?; IbcComponent::init_chain(&mut state_tx, &genesis_state) .await .wrap_err("init_chain failed on IbcComponent")?; - SequenceComponent::init_chain(&mut state_tx, &genesis_state) - .await - .wrap_err("init_chain failed on SequenceComponent")?; state_tx.apply(); @@ -1144,15 +1140,12 @@ impl App { AuthorityComponent::begin_block(&mut arc_state_tx, begin_block) .await .wrap_err("begin_block failed on AuthorityComponent")?; - BridgeComponent::begin_block(&mut arc_state_tx, begin_block) - .await - .wrap_err("begin_block failed on BridgeComponent")?; IbcComponent::begin_block(&mut arc_state_tx, begin_block) .await .wrap_err("begin_block failed on IbcComponent")?; - SequenceComponent::begin_block(&mut arc_state_tx, begin_block) + FeesComponent::begin_block(&mut arc_state_tx, begin_block) .await - .wrap_err("begin_block failed on SequenceComponent")?; + .wrap_err("begin_block failed on FeesComponent")?; let state_tx = Arc::try_unwrap(arc_state_tx) .expect("components should not retain copies of shared state"); @@ -1214,15 +1207,12 @@ impl App { AuthorityComponent::end_block(&mut arc_state_tx, &end_block) .await .wrap_err("end_block failed on AuthorityComponent")?; - BridgeComponent::end_block(&mut arc_state_tx, &end_block) + FeesComponent::end_block(&mut arc_state_tx, &end_block) .await - .wrap_err("end_block failed on BridgeComponent")?; + .wrap_err("end_block failed on FeesComponent")?; IbcComponent::end_block(&mut arc_state_tx, &end_block) .await .wrap_err("end_block failed on IbcComponent")?; - SequenceComponent::end_block(&mut arc_state_tx, &end_block) - .await - .wrap_err("end_block failed on SequenceComponent")?; let mut state_tx = Arc::try_unwrap(arc_state_tx) .expect("components should not retain copies of shared state"); @@ -1238,22 +1228,17 @@ impl App { state_tx.clear_validator_updates(); // gather block fees and transfer them to the block proposer - let fees = self - .state - .get_block_fees() - .await - .wrap_err("failed to get block fees")?; + let fees = self.state.get_block_fees(); - for (asset, amount) in fees { + for fee in fees { state_tx - .increase_balance(fee_recipient, &asset, amount) + .increase_balance(fee_recipient, fee.asset(), fee.amount()) .await .wrap_err("failed to increase fee recipient balance")?; + let fee_event = construct_tx_fee_event(&fee); + state_tx.record(fee_event); } - // clear block fees - state_tx.clear_block_fees().await; - let events = self.apply(state_tx); Ok(abci::response::EndBlock { validator_updates: validator_updates diff --git a/crates/astria-sequencer/src/app/snapshots/astria_sequencer__app__tests_breaking_changes__app_execute_transaction_with_every_action_snapshot.snap b/crates/astria-sequencer/src/app/snapshots/astria_sequencer__app__tests_breaking_changes__app_execute_transaction_with_every_action_snapshot.snap index 00738ae01d..aa6a6111a1 100644 --- a/crates/astria-sequencer/src/app/snapshots/astria_sequencer__app__tests_breaking_changes__app_execute_transaction_with_every_action_snapshot.snap +++ b/crates/astria-sequencer/src/app/snapshots/astria_sequencer__app__tests_breaking_changes__app_execute_transaction_with_every_action_snapshot.snap @@ -1,39 +1,38 @@ --- source: crates/astria-sequencer/src/app/tests_breaking_changes.rs -assertion_line: 350 expression: app.app_hash.as_bytes() --- [ - 65, - 106, - 222, - 239, - 12, - 37, + 56, + 244, 108, - 130, - 83, - 78, - 58, - 50, - 178, - 164, - 93, - 245, - 18, - 12, - 205, - 129, - 14, - 158, - 134, - 107, - 50, - 190, - 88, - 71, - 110, - 9, - 148, - 233 + 40, + 240, + 244, + 218, + 23, + 86, + 66, + 168, + 85, + 165, + 226, + 63, + 103, + 34, + 73, + 94, + 23, + 3, + 123, + 221, + 180, + 154, + 52, + 96, + 24, + 154, + 94, + 20, + 240 ] diff --git a/crates/astria-sequencer/src/app/snapshots/astria_sequencer__app__tests_breaking_changes__app_finalize_block_snapshot.snap b/crates/astria-sequencer/src/app/snapshots/astria_sequencer__app__tests_breaking_changes__app_finalize_block_snapshot.snap index e36d49f879..6c343dfd6a 100644 --- a/crates/astria-sequencer/src/app/snapshots/astria_sequencer__app__tests_breaking_changes__app_finalize_block_snapshot.snap +++ b/crates/astria-sequencer/src/app/snapshots/astria_sequencer__app__tests_breaking_changes__app_finalize_block_snapshot.snap @@ -1,39 +1,38 @@ --- source: crates/astria-sequencer/src/app/tests_breaking_changes.rs -assertion_line: 157 expression: app.app_hash.as_bytes() --- [ - 65, - 14, - 103, - 92, - 219, - 56, - 251, - 135, + 168, + 252, + 185, + 106, + 52, 182, - 207, - 215, - 245, - 234, - 148, - 33, - 34, - 179, - 40, - 146, - 17, - 236, - 227, - 125, - 197, - 76, - 142, - 194, + 239, + 85, + 19, + 177, + 50, + 55, 180, - 9, - 157, - 73, - 63 + 63, + 139, + 137, + 236, + 248, + 193, + 147, + 219, + 108, + 52, + 102, + 6, + 24, + 188, + 57, + 136, + 136, + 187, + 149 ] diff --git a/crates/astria-sequencer/src/app/snapshots/astria_sequencer__app__tests_breaking_changes__app_genesis_snapshot.snap b/crates/astria-sequencer/src/app/snapshots/astria_sequencer__app__tests_breaking_changes__app_genesis_snapshot.snap index d1fd6ce989..5196b9e96f 100644 --- a/crates/astria-sequencer/src/app/snapshots/astria_sequencer__app__tests_breaking_changes__app_genesis_snapshot.snap +++ b/crates/astria-sequencer/src/app/snapshots/astria_sequencer__app__tests_breaking_changes__app_genesis_snapshot.snap @@ -1,39 +1,38 @@ --- source: crates/astria-sequencer/src/app/tests_breaking_changes.rs -assertion_line: 77 expression: app.app_hash.as_bytes() --- [ - 66, - 163, - 122, - 253, - 159, - 247, - 85, + 141, + 143, + 22, 105, - 254, - 245, - 230, - 91, - 25, - 173, - 179, - 227, - 231, - 2, - 69, - 165, - 1, - 169, - 100, - 210, - 97, - 33, - 91, - 87, - 140, - 222, - 83, - 57 + 180, + 22, + 206, + 62, + 93, + 252, + 155, + 197, + 4, + 119, + 39, + 198, + 195, + 166, + 244, + 101, + 55, + 186, + 78, + 218, + 246, + 134, + 159, + 225, + 197, + 16, + 182, + 10 ] diff --git a/crates/astria-sequencer/src/app/test_utils.rs b/crates/astria-sequencer/src/app/test_utils.rs index f988423d4a..a1a5ba9785 100644 --- a/crates/astria-sequencer/src/app/test_utils.rs +++ b/crates/astria-sequencer/src/app/test_utils.rs @@ -13,6 +13,22 @@ use astria_core::{ RollupId, }, protocol::{ + fees::v1alpha1::{ + BridgeLockFeeComponents, + BridgeSudoChangeFeeComponents, + BridgeUnlockFeeComponents, + FeeAssetChangeFeeComponents, + FeeChangeFeeComponents, + IbcRelayFeeComponents, + IbcRelayerChangeFeeComponents, + IbcSudoChangeFeeComponents, + Ics20WithdrawalFeeComponents, + InitBridgeAccountFeeComponents, + SequenceFeeComponents, + SudoAddressChangeFeeComponents, + TransferFeeComponents, + ValidatorUpdateFeeComponents, + }, genesis::v1alpha1::{ Account, AddressPrefixes, @@ -34,6 +50,7 @@ use astria_core::{ }, Protobuf, }; +use astria_eyre::eyre::WrapErr as _; use bytes::Bytes; use cnidarium::{ Snapshot, @@ -46,11 +63,9 @@ use crate::{ accounts::StateWriteExt, app::App, assets::StateWriteExt as AssetStateWriteExt, - bridge::StateWriteExt as BridgeStateWriteExt, - ibc::StateWriteExt as IbcStateWriteExt, + fees::StateWriteExt as _, mempool::Mempool, metrics::Metrics, - sequence::StateWriteExt as SequenceStateWriteExt, test_utils::{ astria_address_from_hex_string, nria, @@ -165,15 +180,64 @@ pub(crate) fn default_genesis_accounts() -> Vec { reason = "allow is only necessary when benchmark isn't enabled" )] #[cfg_attr(feature = "benchmark", allow(dead_code))] -pub(crate) fn default_fees() -> astria_core::protocol::genesis::v1alpha1::Fees { - astria_core::protocol::genesis::v1alpha1::Fees { - transfer_base_fee: 12, - sequence_base_fee: 32, - sequence_byte_cost_multiplier: 1, - init_bridge_account_base_fee: 48, - bridge_lock_byte_cost_multiplier: 1, - bridge_sudo_change_fee: 24, - ics20_withdrawal_base_fee: 24, +pub(crate) fn default_fees() -> astria_core::protocol::genesis::v1alpha1::GenesisFees { + astria_core::protocol::genesis::v1alpha1::GenesisFees { + transfer: TransferFeeComponents { + base: 12, + multiplier: 0, + }, + sequence: SequenceFeeComponents { + base: 32, + multiplier: 1, + }, + init_bridge_account: InitBridgeAccountFeeComponents { + base: 48, + multiplier: 0, + }, + bridge_lock: BridgeLockFeeComponents { + base: 12, // should reflect transfer fee + multiplier: 1, + }, + bridge_sudo_change: BridgeSudoChangeFeeComponents { + base: 24, + multiplier: 0, + }, + ics20_withdrawal: Ics20WithdrawalFeeComponents { + base: 24, + multiplier: 0, + }, + bridge_unlock: BridgeUnlockFeeComponents { + base: 12, // should reflect transfer fee + multiplier: 0, + }, + ibc_relay: IbcRelayFeeComponents { + base: 0, + multiplier: 0, + }, + validator_update: ValidatorUpdateFeeComponents { + base: 0, + multiplier: 0, + }, + fee_asset_change: FeeAssetChangeFeeComponents { + base: 0, + multiplier: 0, + }, + fee_change: FeeChangeFeeComponents { + base: 0, + multiplier: 0, + }, + ibc_relayer_change: IbcRelayerChangeFeeComponents { + base: 0, + multiplier: 0, + }, + sudo_address_change: SudoAddressChangeFeeComponents { + base: 0, + multiplier: 0, + }, + ibc_sudo_change: IbcSudoChangeFeeComponents { + base: 0, + multiplier: 0, + }, } } @@ -455,6 +519,7 @@ pub(crate) fn mock_state_put_account_nonce( #[expect( clippy::allow_attributes, clippy::allow_attributes_without_reason, + clippy::too_many_lines, reason = "allow is only necessary when benchmark isn't enabled" )] #[cfg_attr(feature = "benchmark", allow(dead_code))] @@ -487,15 +552,131 @@ pub(crate) async fn mock_state_getter() -> StateDelta { .unwrap(); // setup tx fees + let transfer_fees = TransferFeeComponents { + base: 0, + multiplier: 0, + }; + state + .put_transfer_fees(transfer_fees) + .wrap_err("failed to initiate transfer fee components") + .unwrap(); + + let sequence_fees = SequenceFeeComponents { + base: MOCK_SEQUENCE_FEE, + multiplier: 0, + }; + state + .put_sequence_fees(sequence_fees) + .wrap_err("failed to initiate sequence action fee components") + .unwrap(); + + let ics20_withdrawal_fees = Ics20WithdrawalFeeComponents { + base: 0, + multiplier: 0, + }; + state + .put_ics20_withdrawal_fees(ics20_withdrawal_fees) + .wrap_err("failed to initiate ics20 withdrawal fee components") + .unwrap(); + + let init_bridge_account_fees = InitBridgeAccountFeeComponents { + base: 0, + multiplier: 0, + }; + state + .put_init_bridge_account_fees(init_bridge_account_fees) + .wrap_err("failed to initiate init bridge account fee components") + .unwrap(); + + let bridge_lock_fees = BridgeLockFeeComponents { + base: 0, + multiplier: 0, + }; + state + .put_bridge_lock_fees(bridge_lock_fees) + .wrap_err("failed to initiate bridge lock fee components") + .unwrap(); + + let bridge_unlock_fees = BridgeUnlockFeeComponents { + base: 0, + multiplier: 0, + }; + state + .put_bridge_unlock_fees(bridge_unlock_fees) + .wrap_err("failed to initiate bridge unlock fee components") + .unwrap(); + + let bridge_sudo_change_fees = BridgeSudoChangeFeeComponents { + base: 0, + multiplier: 0, + }; + state + .put_bridge_sudo_change_fees(bridge_sudo_change_fees) + .wrap_err("failed to initiate bridge sudo change fee components") + .unwrap(); + + let ibc_relay_fees = IbcRelayFeeComponents { + base: 0, + multiplier: 0, + }; + state + .put_ibc_relay_fees(ibc_relay_fees) + .wrap_err("failed to initiate ibc relay fee components") + .unwrap(); + + let validator_update_fees = ValidatorUpdateFeeComponents { + base: 0, + multiplier: 0, + }; + state + .put_validator_update_fees(validator_update_fees) + .wrap_err("failed to initiate validator update fee components") + .unwrap(); + + let fee_asset_change_fees = FeeAssetChangeFeeComponents { + base: 0, + multiplier: 0, + }; + state + .put_fee_asset_change_fees(fee_asset_change_fees) + .wrap_err("failed to initiate fee asset change fee components") + .unwrap(); + + let fee_change_fees = FeeChangeFeeComponents { + base: 0, + multiplier: 0, + }; + state + .put_fee_change_fees(fee_change_fees) + .wrap_err("failed to initiate fee change fees fee components") + .unwrap(); + + let ibc_relayer_change_fees = IbcRelayerChangeFeeComponents { + base: 0, + multiplier: 0, + }; + state + .put_ibc_relayer_change_fees(ibc_relayer_change_fees) + .wrap_err("failed to initiate ibc relayer change fee components") + .unwrap(); + + let sudo_address_change_fees = SudoAddressChangeFeeComponents { + base: 0, + multiplier: 0, + }; + state + .put_sudo_address_change_fees(sudo_address_change_fees) + .wrap_err("failed to initiate sudo address change fee components") + .unwrap(); + + let ibc_sudo_change_fees = IbcSudoChangeFeeComponents { + base: 0, + multiplier: 0, + }; state - .put_sequence_action_base_fee(MOCK_SEQUENCE_FEE) + .put_ibc_sudo_change_fees(ibc_sudo_change_fees) + .wrap_err("failed to initiate ibc sudo change fee components") .unwrap(); - state.put_sequence_action_byte_cost_multiplier(0).unwrap(); - state.put_transfer_base_fee(0).unwrap(); - state.put_ics20_withdrawal_base_fee(0).unwrap(); - state.put_init_bridge_account_base_fee(0).unwrap(); - state.put_bridge_lock_byte_cost_multiplier(0).unwrap(); - state.put_bridge_sudo_change_base_fee(0).unwrap(); // put denoms as allowed fee asset state.put_allowed_fee_asset(&denom_0()).unwrap(); diff --git a/crates/astria-sequencer/src/app/tests_app/mempool.rs b/crates/astria-sequencer/src/app/tests_app/mempool.rs index 75c4d0db5c..a76c157d02 100644 --- a/crates/astria-sequencer/src/app/tests_app/mempool.rs +++ b/crates/astria-sequencer/src/app/tests_app/mempool.rs @@ -2,11 +2,11 @@ use std::collections::HashMap; use astria_core::{ protocol::{ + fees::v1alpha1::TransferFeeComponents, genesis::v1alpha1::Account, transaction::v1alpha1::{ action::{ FeeChange, - FeeChangeKind, Transfer, }, UnsignedTransaction, @@ -50,10 +50,10 @@ async fn trigger_cleaning() { // create tx which will cause mempool cleaning flag to be set let tx_trigger = UnsignedTransaction::builder() .actions(vec![ - FeeChange { - fee_change: FeeChangeKind::TransferBaseFee, - new_value: 10, - } + FeeChange::Transfer(TransferFeeComponents { + base: 10, + multiplier: 0, + }) .into(), ]) .chain_id("test") @@ -146,10 +146,10 @@ async fn do_not_trigger_cleaning() { // (wrong sudo signer) let tx_fail = UnsignedTransaction::builder() .actions(vec![ - FeeChange { - fee_change: FeeChangeKind::TransferBaseFee, - new_value: 10, - } + FeeChange::Transfer(TransferFeeComponents { + base: 10, + multiplier: 0, + }) .into(), ]) .chain_id("test") @@ -247,10 +247,10 @@ async fn maintenance_recosting_promotes() { // create tx which will enable recost tx to pass let tx_recost = UnsignedTransaction::builder() .actions(vec![ - FeeChange { - fee_change: FeeChangeKind::TransferBaseFee, - new_value: 10, // originally 12 - } + FeeChange::Transfer(TransferFeeComponents { + base: 10, + multiplier: 0, + }) .into(), ]) .chain_id("test") diff --git a/crates/astria-sequencer/src/app/tests_app/mod.rs b/crates/astria-sequencer/src/app/tests_app/mod.rs index fbc6a6b95d..56977ac144 100644 --- a/crates/astria-sequencer/src/app/tests_app/mod.rs +++ b/crates/astria-sequencer/src/app/tests_app/mod.rs @@ -59,6 +59,7 @@ use crate::{ ValidatorSet, }, bridge::StateWriteExt as _, + fees::StateReadExt as _, proposal::commitment::generate_rollup_datas_commitment, test_utils::{ astria_address, @@ -273,15 +274,15 @@ async fn app_transfer_block_fees_to_sudo() { app.commit(storage).await; // assert that transaction fees were transferred to the block proposer - let transfer_fee = app.state.get_transfer_base_fee().await.unwrap(); + let transfer_base_fee = app.state.get_transfer_fees().await.unwrap().base; assert_eq!( app.state .get_account_balance(&astria_address_from_hex_string(JUDY_ADDRESS), &nria()) .await .unwrap(), - transfer_fee, + transfer_base_fee, ); - assert_eq!(app.state.get_block_fees().await.unwrap().len(), 0); + assert_eq!(app.state.get_block_fees().len(), 0); } #[tokio::test] diff --git a/crates/astria-sequencer/src/app/tests_block_fees.rs b/crates/astria-sequencer/src/app/tests_block_fees.rs deleted file mode 100644 index b4eaf505d8..0000000000 --- a/crates/astria-sequencer/src/app/tests_block_fees.rs +++ /dev/null @@ -1,336 +0,0 @@ -use std::sync::Arc; - -use astria_core::{ - primitive::v1::RollupId, - protocol::transaction::v1alpha1::{ - action::{ - BridgeLock, - BridgeSudoChange, - InitBridgeAccount, - Sequence, - Transfer, - }, - UnsignedTransaction, - }, - sequencerblock::v1alpha1::block::Deposit, -}; -use cnidarium::StateDelta; -use tendermint::abci::EventAttributeIndexExt as _; - -use crate::{ - accounts::{ - StateReadExt as _, - StateWriteExt as _, - }, - app::test_utils::{ - get_alice_signing_key, - get_bridge_signing_key, - initialize_app, - BOB_ADDRESS, - }, - assets::StateReadExt as _, - bridge::{ - calculate_base_deposit_fee, - StateWriteExt as _, - }, - sequence::{ - calculate_fee_from_state, - StateWriteExt as _, - }, - test_utils::{ - astria_address, - astria_address_from_hex_string, - nria, - }, -}; - -#[tokio::test] -async fn transaction_execution_records_fee_event() { - let mut app = initialize_app(None, vec![]).await; - - // transfer funds from Alice to Bob - let alice = get_alice_signing_key(); - let bob_address = astria_address_from_hex_string(BOB_ADDRESS); - let value = 333_333; - let tx = UnsignedTransaction::builder() - .actions(vec![ - Transfer { - to: bob_address, - amount: value, - asset: nria().into(), - fee_asset: nria().into(), - } - .into(), - ]) - .chain_id("test") - .try_build() - .unwrap(); - let signed_tx = Arc::new(tx.into_signed(&alice)); - - let events = app.execute_transaction(signed_tx).await.unwrap(); - let transfer_fee = app.state.get_transfer_base_fee().await.unwrap(); - let event = events.first().unwrap(); - assert_eq!(event.kind, "tx.fees"); - assert_eq!( - event.attributes[0], - ("asset", nria().to_string()).index().into() - ); - assert_eq!( - event.attributes[1], - ("feeAmount", transfer_fee.to_string()).index().into() - ); - assert_eq!( - event.attributes[2], - ( - "actionType", - "astria.protocol.transactions.v1alpha1.Transfer" - ) - .index() - .into() - ); -} - -#[tokio::test] -async fn ensure_correct_block_fees_transfer() { - let mut app = initialize_app(None, vec![]).await; - let mut state_tx = StateDelta::new(app.state.clone()); - let transfer_base_fee = 1; - state_tx.put_transfer_base_fee(transfer_base_fee).unwrap(); - app.apply(state_tx); - - let alice = get_alice_signing_key(); - let bob_address = astria_address_from_hex_string(BOB_ADDRESS); - let actions = vec![ - Transfer { - to: bob_address, - amount: 1000, - asset: nria().into(), - fee_asset: nria().into(), - } - .into(), - ]; - - let tx = UnsignedTransaction::builder() - .actions(actions) - .chain_id("test") - .try_build() - .unwrap(); - let signed_tx = Arc::new(tx.into_signed(&alice)); - app.execute_transaction(signed_tx).await.unwrap(); - - let total_block_fees: u128 = app - .state - .get_block_fees() - .await - .unwrap() - .into_iter() - .map(|(_, fee)| fee) - .sum(); - assert_eq!(total_block_fees, transfer_base_fee); -} - -#[tokio::test] -async fn ensure_correct_block_fees_sequence() { - let mut app = initialize_app(None, vec![]).await; - let mut state_tx = StateDelta::new(app.state.clone()); - state_tx.put_sequence_action_base_fee(1).unwrap(); - state_tx - .put_sequence_action_byte_cost_multiplier(1) - .unwrap(); - app.apply(state_tx); - - let alice = get_alice_signing_key(); - let data = b"hello world".to_vec(); - - let actions = vec![ - Sequence { - rollup_id: RollupId::from_unhashed_bytes(b"testchainid"), - data: data.clone().into(), - fee_asset: nria().into(), - } - .into(), - ]; - - let tx = UnsignedTransaction::builder() - .actions(actions) - .chain_id("test") - .try_build() - .unwrap(); - let signed_tx = Arc::new(tx.into_signed(&alice)); - app.execute_transaction(signed_tx).await.unwrap(); - - let total_block_fees: u128 = app - .state - .get_block_fees() - .await - .unwrap() - .into_iter() - .map(|(_, fee)| fee) - .sum(); - let expected_fees = calculate_fee_from_state(&data, &app.state).await.unwrap(); - assert_eq!(total_block_fees, expected_fees); -} - -#[tokio::test] -async fn ensure_correct_block_fees_init_bridge_acct() { - let mut app = initialize_app(None, vec![]).await; - let mut state_tx = StateDelta::new(app.state.clone()); - let init_bridge_account_base_fee = 1; - state_tx - .put_init_bridge_account_base_fee(init_bridge_account_base_fee) - .unwrap(); - app.apply(state_tx); - - let alice = get_alice_signing_key(); - - let actions = vec![ - InitBridgeAccount { - rollup_id: RollupId::from_unhashed_bytes(b"testchainid"), - asset: nria().into(), - fee_asset: nria().into(), - sudo_address: None, - withdrawer_address: None, - } - .into(), - ]; - - let tx = UnsignedTransaction::builder() - .actions(actions) - .chain_id("test") - .try_build() - .unwrap(); - let signed_tx = Arc::new(tx.into_signed(&alice)); - app.execute_transaction(signed_tx).await.unwrap(); - - let total_block_fees: u128 = app - .state - .get_block_fees() - .await - .unwrap() - .into_iter() - .map(|(_, fee)| fee) - .sum(); - assert_eq!(total_block_fees, init_bridge_account_base_fee); -} - -#[tokio::test] -async fn ensure_correct_block_fees_bridge_lock() { - let alice = get_alice_signing_key(); - let bridge = get_bridge_signing_key(); - let bridge_address = astria_address(&bridge.address_bytes()); - let rollup_id = RollupId::from_unhashed_bytes(b"testchainid"); - let starting_index_of_action = 0; - - let mut app = initialize_app(None, vec![]).await; - let mut state_tx = StateDelta::new(app.state.clone()); - - let transfer_base_fee = 1; - let bridge_lock_byte_cost_multiplier = 1; - - state_tx.put_transfer_base_fee(transfer_base_fee).unwrap(); - state_tx - .put_bridge_lock_byte_cost_multiplier(bridge_lock_byte_cost_multiplier) - .unwrap(); - state_tx - .put_bridge_account_rollup_id(&bridge_address, rollup_id) - .unwrap(); - state_tx - .put_bridge_account_ibc_asset(&bridge_address, nria()) - .unwrap(); - app.apply(state_tx); - - let actions = vec![ - BridgeLock { - to: bridge_address, - amount: 1, - asset: nria().into(), - fee_asset: nria().into(), - destination_chain_address: rollup_id.to_string(), - } - .into(), - ]; - - let tx = UnsignedTransaction::builder() - .actions(actions) - .chain_id("test") - .try_build() - .unwrap(); - let signed_tx = Arc::new(tx.into_signed(&alice)); - app.execute_transaction(signed_tx.clone()).await.unwrap(); - - let test_deposit = Deposit { - bridge_address, - rollup_id, - amount: 1, - asset: nria().into(), - destination_chain_address: rollup_id.to_string(), - source_transaction_id: signed_tx.id(), - source_action_index: starting_index_of_action, - }; - - let total_block_fees: u128 = app - .state - .get_block_fees() - .await - .unwrap() - .into_iter() - .map(|(_, fee)| fee) - .sum(); - let expected_fees = transfer_base_fee - + (calculate_base_deposit_fee(&test_deposit).unwrap() * bridge_lock_byte_cost_multiplier); - assert_eq!(total_block_fees, expected_fees); -} - -#[tokio::test] -async fn ensure_correct_block_fees_bridge_sudo_change() { - let alice = get_alice_signing_key(); - let alice_address = astria_address(&alice.address_bytes()); - let bridge = get_bridge_signing_key(); - let bridge_address = astria_address(&bridge.address_bytes()); - - let mut app = initialize_app(None, vec![]).await; - let mut state_tx = StateDelta::new(app.state.clone()); - - let sudo_change_base_fee = 1; - state_tx - .put_bridge_sudo_change_base_fee(sudo_change_base_fee) - .unwrap(); - state_tx - .put_bridge_account_sudo_address(&bridge_address, alice_address) - .unwrap(); - state_tx - .increase_balance(&bridge_address, &nria(), 1) - .await - .unwrap(); - app.apply(state_tx); - - let actions = vec![ - BridgeSudoChange { - bridge_address, - new_sudo_address: None, - new_withdrawer_address: None, - fee_asset: nria().into(), - } - .into(), - ]; - - let tx = UnsignedTransaction::builder() - .actions(actions) - .chain_id("test") - .try_build() - .unwrap(); - let signed_tx = Arc::new(tx.into_signed(&alice)); - app.execute_transaction(signed_tx).await.unwrap(); - - let total_block_fees: u128 = app - .state - .get_block_fees() - .await - .unwrap() - .into_iter() - .map(|(_, fee)| fee) - .sum(); - assert_eq!(total_block_fees, sudo_change_base_fee); -} - -// TODO(https://github.com/astriaorg/astria/issues/1382): Add test to ensure correct block fees for ICS20 withdrawal diff --git a/crates/astria-sequencer/src/app/tests_execute_transaction.rs b/crates/astria-sequencer/src/app/tests_execute_transaction.rs index 2c64d35fe6..3edd9e08b6 100644 --- a/crates/astria-sequencer/src/app/tests_execute_transaction.rs +++ b/crates/astria-sequencer/src/app/tests_execute_transaction.rs @@ -7,6 +7,10 @@ use astria_core::{ RollupId, }, protocol::{ + fees::v1alpha1::{ + InitBridgeAccountFeeComponents, + SequenceFeeComponents, + }, genesis::v1alpha1::GenesisAppState, transaction::v1alpha1::{ action::{ @@ -44,20 +48,20 @@ use crate::{ }, ActionHandler as _, }, - assets::{ + authority::StateReadExt as _, + bridge::{ StateReadExt as _, StateWriteExt as _, }, - authority::StateReadExt as _, - bridge::{ + fees::{ StateReadExt as _, StateWriteExt as _, }, ibc::StateReadExt as _, - sequence::calculate_fee_from_state, test_utils::{ astria_address, astria_address_from_hex_string, + calculate_sequence_action_fee_from_state, nria, ASTRIA_PREFIX, }, @@ -127,13 +131,13 @@ async fn app_execute_transaction_transfer() { .unwrap(), value + 10u128.pow(19) ); - let transfer_fee = app.state.get_transfer_base_fee().await.unwrap(); + let transfer_base = app.state.get_transfer_fees().await.unwrap().base; assert_eq!( app.state .get_account_balance(&alice_address, &nria()) .await .unwrap(), - 10u128.pow(19) - (value + transfer_fee), + 10u128.pow(19) - (value + transfer_base), ); assert_eq!(app.state.get_account_nonce(&bob_address).await.unwrap(), 0); assert_eq!( @@ -193,13 +197,13 @@ async fn app_execute_transaction_transfer_not_native_token() { value, // transferred amount ); - let transfer_fee = app.state.get_transfer_base_fee().await.unwrap(); + let transfer_base = app.state.get_transfer_fees().await.unwrap().base; assert_eq!( app.state .get_account_balance(&alice_address, &nria()) .await .unwrap(), - 10u128.pow(19) - transfer_fee, // genesis balance - fee + 10u128.pow(19) - transfer_base, // genesis balance - fee ); assert_eq!( app.state @@ -253,20 +257,20 @@ async fn app_execute_transaction_transfer_balance_too_low_for_fee() { #[tokio::test] async fn app_execute_transaction_sequence() { - use crate::sequence::StateWriteExt as _; - let mut app = initialize_app(None, vec![]).await; let mut state_tx = StateDelta::new(app.state.clone()); - state_tx.put_sequence_action_base_fee(0).unwrap(); state_tx - .put_sequence_action_byte_cost_multiplier(1) + .put_sequence_fees(SequenceFeeComponents { + base: 0, + multiplier: 1, + }) .unwrap(); app.apply(state_tx); let alice = get_alice_signing_key(); let alice_address = astria_address(&alice.address_bytes()); let data = Bytes::from_static(b"hello world"); - let fee = calculate_fee_from_state(&data, &app.state).await.unwrap(); + let fee = calculate_sequence_action_fee_from_state(&data, &app.state).await; let tx = UnsignedTransaction::builder() .actions(vec![ @@ -593,7 +597,12 @@ async fn app_execute_transaction_init_bridge_account_ok() { let mut app = initialize_app(None, vec![]).await; let mut state_tx = StateDelta::new(app.state.clone()); let fee = 12; // arbitrary - state_tx.put_init_bridge_account_base_fee(fee).unwrap(); + state_tx + .put_init_bridge_account_fees(InitBridgeAccountFeeComponents { + base: fee, + multiplier: 0, + }) + .unwrap(); app.apply(state_tx); let rollup_id = RollupId::from_unhashed_bytes(b"testchainid"); @@ -724,11 +733,6 @@ async fn app_execute_transaction_bridge_lock_action_ok() { let signed_tx = Arc::new(tx.into_signed(&alice)); - let alice_before_balance = app - .state - .get_account_balance(&alice_address, &nria()) - .await - .unwrap(); let bridge_before_balance = app .state .get_account_balance(&bridge_address, &nria()) @@ -740,7 +744,6 @@ async fn app_execute_transaction_bridge_lock_action_ok() { app.state.get_account_nonce(&alice_address).await.unwrap(), 1 ); - let transfer_fee = app.state.get_transfer_base_fee().await.unwrap(); let expected_deposit = Deposit { bridge_address, rollup_id, @@ -751,20 +754,6 @@ async fn app_execute_transaction_bridge_lock_action_ok() { source_action_index: starting_index_of_action, }; - let fee = transfer_fee - + app - .state - .get_bridge_lock_byte_cost_multiplier() - .await - .unwrap() - * crate::bridge::calculate_base_deposit_fee(&expected_deposit).unwrap(); - assert_eq!( - app.state - .get_account_balance(&alice_address, &nria()) - .await - .unwrap(), - alice_before_balance - (amount + fee) - ); assert_eq!( app.state .get_account_balance(&bridge_address, &nria()) @@ -918,9 +907,7 @@ async fn app_stateful_check_fails_insufficient_total_balance() { // figure out needed fee for a single transfer let data = Bytes::from_static(b"hello world"); - let fee = calculate_fee_from_state(&data, &app.state.clone()) - .await - .unwrap(); + let fee = calculate_sequence_action_fee_from_state(&data, &app.state.clone()).await; // transfer just enough to cover single sequence fee with data let signed_tx = UnsignedTransaction::builder() @@ -1005,9 +992,9 @@ async fn app_execute_transaction_bridge_lock_unlock_action_ok() { // give bridge eoa funds so it can pay for the // unlock transfer action - let transfer_fee = app.state.get_transfer_base_fee().await.unwrap(); + let transfer_base = app.state.get_transfer_fees().await.unwrap().base; state_tx - .put_account_balance(&bridge_address, &nria(), transfer_fee) + .put_account_balance(&bridge_address, &nria(), transfer_base) .unwrap(); // create bridge account @@ -1238,3 +1225,40 @@ async fn app_execute_transaction_ibc_sudo_change_error() { .to_string(); assert!(res.contains("signer is not the sudo key")); } + +#[tokio::test] +async fn transaction_execution_records_fee_event() { + let mut app = initialize_app(None, vec![]).await; + + // transfer funds from Alice to Bob + let alice = get_alice_signing_key(); + let bob_address = astria_address_from_hex_string(BOB_ADDRESS); + let value = 333_333; + let tx = UnsignedTransaction::builder() + .actions(vec![ + Transfer { + to: bob_address, + amount: value, + asset: nria().into(), + fee_asset: nria().into(), + } + .into(), + ]) + .chain_id("test") + .try_build() + .unwrap(); + let signed_tx = Arc::new(tx.into_signed(&alice)); + app.execute_transaction(signed_tx).await.unwrap(); + + let sudo_address = app.state.get_sudo_address().await.unwrap(); + let end_block = app.end_block(1, &sudo_address).await.unwrap(); + + let events = end_block.events; + let event = events.first().unwrap(); + assert_eq!(event.kind, "tx.fees"); + assert_eq!(event.attributes[0].key, "actionName"); + assert_eq!(event.attributes[1].key, "asset"); + assert_eq!(event.attributes[2].key, "feeAmount"); + assert_eq!(event.attributes[3].key, "sourceTransactionId"); + assert_eq!(event.attributes[4].key, "sourceActionIndex"); +} diff --git a/crates/astria-sequencer/src/assets/query.rs b/crates/astria-sequencer/src/assets/query.rs index 37d300e5cf..c3ce285e3c 100644 --- a/crates/astria-sequencer/src/assets/query.rs +++ b/crates/astria-sequencer/src/assets/query.rs @@ -1,9 +1,6 @@ use astria_core::{ primitive::v1::asset, - protocol::{ - abci::AbciErrorCode, - asset::v1alpha1::AllowedFeeAssetsResponse, - }, + protocol::abci::AbciErrorCode, }; use astria_eyre::eyre::WrapErr as _; use cnidarium::Storage; @@ -109,55 +106,3 @@ fn preprocess_request(params: &[(String, String)]) -> Result, -) -> response::Query { - // get last snapshot - let snapshot = storage.latest_snapshot(); - - // get height from snapshot - let height = match snapshot.get_block_height().await { - Ok(height) => height, - Err(err) => { - return response::Query { - code: Code::Err(AbciErrorCode::INTERNAL_ERROR.value()), - info: AbciErrorCode::INTERNAL_ERROR.info(), - log: format!("failed getting block height: {err:#}"), - ..response::Query::default() - }; - } - }; - - // get ids from snapshot at height - let fee_assets = match snapshot.get_allowed_fee_assets().await { - Ok(fee_assets) => fee_assets, - Err(err) => { - return response::Query { - code: Code::Err(AbciErrorCode::INTERNAL_ERROR.value()), - info: AbciErrorCode::INTERNAL_ERROR.info(), - log: format!("failed to retrieve allowed fee assets: {err:#}"), - ..response::Query::default() - }; - } - }; - - let payload = AllowedFeeAssetsResponse { - height, - fee_assets: fee_assets.into_iter().map(Into::into).collect(), - } - .into_raw() - .encode_to_vec() - .into(); - - let height = tendermint::block::Height::try_from(height).expect("height must fit into an i64"); - response::Query { - code: tendermint::abci::Code::Ok, - key: request.path.into_bytes().into(), - value: payload, - height, - ..response::Query::default() - } -} diff --git a/crates/astria-sequencer/src/assets/snapshots/astria_sequencer__assets__state_ext__tests__storage_keys_are_unchanged-2.snap b/crates/astria-sequencer/src/assets/snapshots/astria_sequencer__assets__state_ext__tests__storage_keys_are_unchanged-2.snap new file mode 100644 index 0000000000..09bc835438 --- /dev/null +++ b/crates/astria-sequencer/src/assets/snapshots/astria_sequencer__assets__state_ext__tests__storage_keys_are_unchanged-2.snap @@ -0,0 +1,6 @@ +--- +source: crates/astria-sequencer/src/assets/state_ext.rs +expression: fee_asset_key(trace_prefixed) +--- +fee_asset/ce072174ebc356c6ead681d61ab417ee72fdedd8155eb76478ece374bb04dc1d + diff --git a/crates/astria-sequencer/src/assets/state_ext.rs b/crates/astria-sequencer/src/assets/state_ext.rs index 0597bc5801..ff2dc685ab 100644 --- a/crates/astria-sequencer/src/assets/state_ext.rs +++ b/crates/astria-sequencer/src/assets/state_ext.rs @@ -1,17 +1,10 @@ -use std::{ - borrow::Cow, - fmt::Display, -}; +use std::borrow::Cow; -use astria_core::{ - primitive::v1::asset, - Protobuf, -}; +use astria_core::primitive::v1::asset; use astria_eyre::{ anyhow_to_eyre, eyre::{ bail, - OptionExt as _, Result, WrapErr as _, }, @@ -21,35 +14,16 @@ use cnidarium::{ StateRead, StateWrite, }; -use futures::StreamExt as _; -use tendermint::abci::{ - Event, - EventAttributeIndexExt as _, -}; use tracing::instrument; use super::storage::{ self, keys::{ self, - extract_asset_from_block_fees_key, - extract_asset_from_fee_asset_key, }, }; use crate::storage::StoredValue; -/// Creates `abci::Event` of kind `tx.fees` for sequencer fee reporting -fn construct_tx_fee_event(asset: &T, fee_amount: u128) -> Event { - Event::new( - "tx.fees", - [ - ("asset", asset.to_string()).index(), - ("feeAmount", fee_amount.to_string()).index(), - ("actionType", P::full_name()).index(), - ], - ) -} - #[async_trait] pub(crate) trait StateReadExt: StateRead { #[instrument(skip_all)] @@ -103,55 +77,6 @@ pub(crate) trait StateReadExt: StateRead { }) .wrap_err("invalid ibc asset bytes") } - - #[instrument(skip_all)] - async fn get_block_fees(&self) -> Result> { - let mut fees = Vec::new(); - - let mut stream = - std::pin::pin!(self.nonverifiable_prefix_raw(keys::BLOCK_FEES_PREFIX.as_bytes())); - while let Some(Ok((key, bytes))) = stream.next().await { - let asset = - extract_asset_from_block_fees_key(&key).wrap_err("failed to extract asset")?; - - let fee = StoredValue::deserialize(&bytes) - .and_then(|value| storage::Fee::try_from(value).map(u128::from)) - .context("invalid block fees bytes")?; - - fees.push((asset, fee)); - } - - Ok(fees) - } - - #[instrument(skip_all)] - async fn is_allowed_fee_asset<'a, TAsset>(&self, asset: &'a TAsset) -> Result - where - TAsset: Sync, - &'a TAsset: Into>, - { - Ok(self - .nonverifiable_get_raw(keys::fee_asset(asset).as_bytes()) - .await - .map_err(anyhow_to_eyre) - .wrap_err("failed to read raw fee asset from state")? - .is_some()) - } - - #[instrument(skip_all)] - async fn get_allowed_fee_assets(&self) -> Result> { - let mut assets = Vec::new(); - - let mut stream = - std::pin::pin!(self.nonverifiable_prefix_raw(keys::FEE_ASSET_PREFIX.as_bytes())); - while let Some(Ok((key, _))) = stream.next().await { - let asset = - extract_asset_from_fee_asset_key(&key).wrap_err("failed to extract asset")?; - assets.push(asset); - } - - Ok(assets) - } } impl StateReadExt for T {} @@ -176,85 +101,12 @@ pub(crate) trait StateWriteExt: StateWrite { self.put_raw(key, bytes); Ok(()) } - - /// Adds `amount` to the block fees for `asset`. - #[instrument(skip_all)] - async fn get_and_increase_block_fees<'a, P, TAsset>( - &mut self, - asset: &'a TAsset, - amount: u128, - ) -> Result<()> - where - TAsset: Sync + Display, - &'a TAsset: Into>, - P: Protobuf, - { - let tx_fee_event = construct_tx_fee_event::(asset, amount); - let block_fees_key = keys::block_fees(asset); - - let current_amount = self - .nonverifiable_get_raw(block_fees_key.as_bytes()) - .await - .map_err(anyhow_to_eyre) - .wrap_err("failed to read raw block fees from state")? - .map(|bytes| { - StoredValue::deserialize(&bytes) - .and_then(|value| storage::Fee::try_from(value).map(u128::from)) - .context("invalid block fees bytes") - }) - .transpose()? - .unwrap_or_default(); - - let new_amount = current_amount - .checked_add(amount) - .ok_or_eyre("block fees overflowed u128")?; - let bytes = StoredValue::from(storage::Fee::from(new_amount)) - .serialize() - .wrap_err("failed to serialize block fees")?; - self.nonverifiable_put_raw(block_fees_key.into_bytes(), bytes); - - self.record(tx_fee_event); - - Ok(()) - } - - #[instrument(skip_all)] - async fn clear_block_fees(&mut self) { - let mut stream = - std::pin::pin!(self.nonverifiable_prefix_raw(keys::BLOCK_FEES_PREFIX.as_bytes())); - while let Some(Ok((key, _))) = stream.next().await { - self.nonverifiable_delete(key); - } - } - - #[instrument(skip_all)] - fn delete_allowed_fee_asset<'a, TAsset>(&mut self, asset: &'a TAsset) - where - &'a TAsset: Into>, - { - self.nonverifiable_delete(keys::fee_asset(asset).into_bytes()); - } - - #[instrument(skip_all)] - fn put_allowed_fee_asset<'a, TAsset>(&mut self, asset: &'a TAsset) -> Result<()> - where - &'a TAsset: Into>, - { - let bytes = StoredValue::Unit - .serialize() - .context("failed to serialize unit for allowed fee asset")?; - self.nonverifiable_put_raw(keys::fee_asset(asset).into_bytes(), bytes); - Ok(()) - } } impl StateWriteExt for T {} #[cfg(test)] mod tests { - use std::collections::HashSet; - - use astria_core::protocol::transaction::v1alpha1::action::Transfer; use cnidarium::StateDelta; use super::*; @@ -269,9 +121,6 @@ mod tests { fn asset_1() -> asset::Denom { "asset_1".parse().unwrap() } - fn asset_2() -> asset::Denom { - "asset_2".parse().unwrap() - } #[tokio::test] async fn native_asset() { @@ -308,74 +157,6 @@ mod tests { ); } - #[tokio::test] - async fn block_fee_read_and_increase() { - let storage = cnidarium::TempStorage::new().await.unwrap(); - let snapshot = storage.latest_snapshot(); - let mut state = StateDelta::new(snapshot); - - // doesn't exist at first - let fee_balances_orig = state.get_block_fees().await.unwrap(); - assert!(fee_balances_orig.is_empty()); - - // can write - let asset = asset_0(); - let amount = 100u128; - state - .get_and_increase_block_fees::(&asset, amount) - .await - .unwrap(); - - // holds expected - let fee_balances_updated = state.get_block_fees().await.unwrap(); - assert_eq!( - fee_balances_updated[0], - (asset.to_ibc_prefixed(), amount), - "fee balances are not what they were expected to be" - ); - } - - #[tokio::test] - async fn block_fee_read_and_increase_can_delete() { - let storage = cnidarium::TempStorage::new().await.unwrap(); - let snapshot = storage.latest_snapshot(); - let mut state = StateDelta::new(snapshot); - - // can write - let asset_first = asset_0(); - let asset_second = asset_1(); - let amount_first = 100u128; - let amount_second = 200u128; - - state - .get_and_increase_block_fees::(&asset_first, amount_first) - .await - .unwrap(); - state - .get_and_increase_block_fees::(&asset_second, amount_second) - .await - .unwrap(); - // holds expected - let fee_balances = HashSet::<_>::from_iter(state.get_block_fees().await.unwrap()); - assert_eq!( - fee_balances, - HashSet::from_iter(vec![ - (asset_first.to_ibc_prefixed(), amount_first), - (asset_second.to_ibc_prefixed(), amount_second) - ]), - "returned fee balance vector not what was expected" - ); - - // can delete - state.clear_block_fees().await; - - let fee_balances_updated = state.get_block_fees().await.unwrap(); - assert!( - fee_balances_updated.is_empty(), - "fee balances were expected to be deleted but were not" - ); - } - #[tokio::test] async fn get_ibc_asset_non_existent() { let storage = cnidarium::TempStorage::new().await.unwrap(); @@ -492,114 +273,4 @@ mod tests { "original ibc asset was not what was expected" ); } - - #[tokio::test] - async fn is_allowed_fee_asset() { - let storage = cnidarium::TempStorage::new().await.unwrap(); - let snapshot = storage.latest_snapshot(); - let mut state = StateDelta::new(snapshot); - - // non-existent fees assets return false - let asset = asset_0(); - assert!( - !state - .is_allowed_fee_asset(&asset) - .await - .expect("checking for allowed fee asset should not fail"), - "fee asset was expected to return false" - ); - - // existent fee assets return true - state.put_allowed_fee_asset(&asset).unwrap(); - assert!( - state - .is_allowed_fee_asset(&asset) - .await - .expect("checking for allowed fee asset should not fail"), - "fee asset was expected to be allowed" - ); - } - - #[tokio::test] - async fn can_delete_allowed_fee_assets_simple() { - let storage = cnidarium::TempStorage::new().await.unwrap(); - let snapshot = storage.latest_snapshot(); - let mut state = StateDelta::new(snapshot); - - // setup fee asset - let asset = asset_0(); - state.put_allowed_fee_asset(&asset).unwrap(); - assert!( - state - .is_allowed_fee_asset(&asset) - .await - .expect("checking for allowed fee asset should not fail"), - "fee asset was expected to be allowed" - ); - - // see can get fee asset - let assets = state.get_allowed_fee_assets().await.unwrap(); - assert_eq!( - assets, - vec![asset.to_ibc_prefixed()], - "expected returned allowed fee assets to match what was written in" - ); - - // can delete - state.delete_allowed_fee_asset(&asset); - - // see is deleted - let assets = state.get_allowed_fee_assets().await.unwrap(); - assert!(assets.is_empty(), "fee assets should be empty post delete"); - } - - #[tokio::test] - async fn can_delete_allowed_fee_assets_complex() { - let storage = cnidarium::TempStorage::new().await.unwrap(); - let snapshot = storage.latest_snapshot(); - let mut state = StateDelta::new(snapshot); - - // setup fee assets - let asset_first = asset_0(); - state.put_allowed_fee_asset(&asset_first).unwrap(); - assert!( - state - .is_allowed_fee_asset(&asset_first) - .await - .expect("checking for allowed fee asset should not fail"), - "fee asset was expected to be allowed" - ); - let asset_second = asset_1(); - state.put_allowed_fee_asset(&asset_second).unwrap(); - assert!( - state - .is_allowed_fee_asset(&asset_second) - .await - .expect("checking for allowed fee asset should not fail"), - "fee asset was expected to be allowed" - ); - let asset_third = asset_2(); - state.put_allowed_fee_asset(&asset_third).unwrap(); - assert!( - state - .is_allowed_fee_asset(&asset_third) - .await - .expect("checking for allowed fee asset should not fail"), - "fee asset was expected to be allowed" - ); - - // can delete - state.delete_allowed_fee_asset(&asset_second); - - // see is deleted - let assets = HashSet::<_>::from_iter(state.get_allowed_fee_assets().await.unwrap()); - assert_eq!( - assets, - HashSet::from_iter(vec![ - asset_first.to_ibc_prefixed(), - asset_third.to_ibc_prefixed() - ]), - "delete for allowed fee asset did not behave as expected" - ); - } } diff --git a/crates/astria-sequencer/src/assets/storage/keys.rs b/crates/astria-sequencer/src/assets/storage/keys.rs index 028678f0cd..4cfb588aad 100644 --- a/crates/astria-sequencer/src/assets/storage/keys.rs +++ b/crates/astria-sequencer/src/assets/storage/keys.rs @@ -1,17 +1,10 @@ use std::borrow::Cow; use astria_core::primitive::v1::asset::IbcPrefixed; -use astria_eyre::eyre::{ - eyre, - Result, - WrapErr as _, -}; use crate::storage::keys::Asset; pub(in crate::assets) const NATIVE_ASSET: &str = "assets/native_asset"; -pub(in crate::assets) const BLOCK_FEES_PREFIX: &str = "assets/block_fees/"; -pub(in crate::assets) const FEE_ASSET_PREFIX: &str = "assets/fee_asset/"; /// Example: `assets/ibc/0101....0101`. /// |64 hex chars| @@ -22,41 +15,6 @@ where format!("assets/{}", Asset::from(asset)) } -pub(in crate::assets) fn fee_asset<'a, TAsset>(asset: &'a TAsset) -> String -where - &'a TAsset: Into>, -{ - format!("{FEE_ASSET_PREFIX}{}", Asset::from(asset)) -} - -pub(in crate::assets) fn block_fees<'a, TAsset>(asset: &'a TAsset) -> String -where - &'a TAsset: Into>, -{ - format!("{BLOCK_FEES_PREFIX}{}", Asset::from(asset)) -} - -pub(in crate::assets) fn extract_asset_from_fee_asset_key(key: &[u8]) -> Result { - extract_asset_from_key(key, FEE_ASSET_PREFIX) - .wrap_err("failed to extract asset from fee asset key") -} - -pub(in crate::assets) fn extract_asset_from_block_fees_key(key: &[u8]) -> Result { - extract_asset_from_key(key, BLOCK_FEES_PREFIX) - .wrap_err("failed to extract asset from fee asset key") -} - -fn extract_asset_from_key(key: &[u8], prefix: &str) -> Result { - let key_str = std::str::from_utf8(key) - .wrap_err_with(|| format!("key `{}` not valid utf8", telemetry::display::hex(key),))?; - let suffix = key_str - .strip_prefix(prefix) - .ok_or_else(|| eyre!("key `{key_str}` did not have prefix `{prefix}`"))?; - suffix.parse().wrap_err_with(|| { - format!("failed to parse suffix `{suffix}` of key `{key_str}` as an ibc-prefixed asset",) - }) -} - #[cfg(test)] mod tests { use astria_core::primitive::v1::asset::Denom; @@ -73,34 +31,11 @@ mod tests { fn keys_should_not_change() { insta::assert_snapshot!(NATIVE_ASSET); insta::assert_snapshot!(asset(&test_asset())); - insta::assert_snapshot!(fee_asset(&test_asset())); - insta::assert_snapshot!(block_fees(&test_asset())); } #[test] fn keys_should_have_component_prefix() { assert!(NATIVE_ASSET.starts_with(COMPONENT_PREFIX)); assert!(asset(&test_asset()).starts_with(COMPONENT_PREFIX)); - assert!(fee_asset(&test_asset()).starts_with(COMPONENT_PREFIX)); - assert!(block_fees(&test_asset()).starts_with(COMPONENT_PREFIX)); - } - - #[test] - fn prefixes_should_be_prefixes_of_relevant_keys() { - assert!(fee_asset(&test_asset()).starts_with(FEE_ASSET_PREFIX)); - assert!(block_fees(&test_asset()).starts_with(BLOCK_FEES_PREFIX)); - } - - #[test] - fn should_extract_asset_from_key() { - let asset = IbcPrefixed::new([1; 32]); - - let key = fee_asset(&asset); - let recovered_asset = extract_asset_from_fee_asset_key(key.as_bytes()).unwrap(); - assert_eq!(asset, recovered_asset); - - let key = block_fees(&asset); - let recovered_asset = extract_asset_from_block_fees_key(key.as_bytes()).unwrap(); - assert_eq!(asset, recovered_asset); } } diff --git a/crates/astria-sequencer/src/assets/storage/mod.rs b/crates/astria-sequencer/src/assets/storage/mod.rs index 8551a44c91..15f61bebf1 100644 --- a/crates/astria-sequencer/src/assets/storage/mod.rs +++ b/crates/astria-sequencer/src/assets/storage/mod.rs @@ -1,8 +1,5 @@ pub(super) mod keys; mod values; +pub(super) use values::TracePrefixedDenom; pub(crate) use values::Value; -pub(super) use values::{ - Fee, - TracePrefixedDenom, -}; diff --git a/crates/astria-sequencer/src/authority/action.rs b/crates/astria-sequencer/src/authority/action.rs index 8386c748c6..aa63a8c641 100644 --- a/crates/astria-sequencer/src/authority/action.rs +++ b/crates/astria-sequencer/src/authority/action.rs @@ -1,6 +1,4 @@ use astria_core::protocol::transaction::v1alpha1::action::{ - FeeChange, - FeeChangeKind, IbcSudoChange, SudoAddressChange, ValidatorUpdate, @@ -14,16 +12,13 @@ use astria_eyre::eyre::{ use cnidarium::StateWrite; use crate::{ - accounts::StateWriteExt as _, address::StateReadExt as _, app::ActionHandler, authority::{ StateReadExt as _, StateWriteExt as _, }, - bridge::StateWriteExt as _, ibc::StateWriteExt as _, - sequence::StateWriteExt as _, transaction::StateReadExt as _, }; @@ -106,52 +101,6 @@ impl ActionHandler for SudoAddressChange { } } -#[async_trait::async_trait] -impl ActionHandler for FeeChange { - async fn check_stateless(&self) -> Result<()> { - Ok(()) - } - - /// check that the signer of the transaction is the current sudo address, - /// as only that address can change the fee - async fn check_and_execute(&self, mut state: S) -> Result<()> { - let from = state - .get_transaction_context() - .expect("transaction source must be present in state when executing an action") - .address_bytes(); - // ensure signer is the valid `sudo` key in state - let sudo_address = state - .get_sudo_address() - .await - .wrap_err("failed to get sudo address from state")?; - ensure!(sudo_address == from, "signer is not the sudo key"); - - match self.fee_change { - FeeChangeKind::TransferBaseFee => state - .put_transfer_base_fee(self.new_value) - .wrap_err("failed to put transfer base fee"), - FeeChangeKind::SequenceBaseFee => state - .put_sequence_action_base_fee(self.new_value) - .wrap_err("failed to put sequence action base fee"), - FeeChangeKind::SequenceByteCostMultiplier => state - .put_sequence_action_byte_cost_multiplier(self.new_value) - .wrap_err("failed to put sequence action byte cost multiplier"), - FeeChangeKind::InitBridgeAccountBaseFee => state - .put_init_bridge_account_base_fee(self.new_value) - .wrap_err("failed to put init bridge account base fee"), - FeeChangeKind::BridgeLockByteCostMultiplier => state - .put_bridge_lock_byte_cost_multiplier(self.new_value) - .wrap_err("failed to put bridge lock byte cost multiplier"), - FeeChangeKind::BridgeSudoChangeBaseFee => state - .put_bridge_sudo_change_base_fee(self.new_value) - .wrap_err("failed to put bridge sudo change base fee"), - FeeChangeKind::Ics20WithdrawalBaseFee => state - .put_ics20_withdrawal_base_fee(self.new_value) - .wrap_err("failed to put ics20 withdrawal base fee"), - } - } -} - #[async_trait::async_trait] impl ActionHandler for IbcSudoChange { async fn check_stateless(&self) -> Result<()> { @@ -179,120 +128,3 @@ impl ActionHandler for IbcSudoChange { Ok(()) } } - -#[cfg(test)] -mod tests { - use astria_core::primitive::v1::TransactionId; - use cnidarium::StateDelta; - - use super::*; - use crate::{ - accounts::StateReadExt as _, - bridge::StateReadExt as _, - ibc::StateReadExt as _, - sequence::StateReadExt as _, - transaction::{ - StateWriteExt as _, - TransactionContext, - }, - }; - - #[tokio::test] - async fn fee_change_action_executes() { - let storage = cnidarium::TempStorage::new().await.unwrap(); - let snapshot = storage.latest_snapshot(); - let mut state = StateDelta::new(snapshot); - let transfer_fee = 12; - - state.put_transaction_context(TransactionContext { - address_bytes: [1; 20], - transaction_id: TransactionId::new([0; 32]), - source_action_index: 0, - }); - state.put_sudo_address([1; 20]).unwrap(); - - state.put_transfer_base_fee(transfer_fee).unwrap(); - - let fee_change = FeeChange { - fee_change: FeeChangeKind::TransferBaseFee, - new_value: 10, - }; - - fee_change.check_and_execute(&mut state).await.unwrap(); - assert_eq!(state.get_transfer_base_fee().await.unwrap(), 10); - - let sequence_base_fee = 5; - state - .put_sequence_action_base_fee(sequence_base_fee) - .unwrap(); - - let fee_change = FeeChange { - fee_change: FeeChangeKind::SequenceBaseFee, - new_value: 3, - }; - - fee_change.check_and_execute(&mut state).await.unwrap(); - assert_eq!(state.get_sequence_action_base_fee().await.unwrap(), 3); - - let sequence_byte_cost_multiplier = 2; - state - .put_sequence_action_byte_cost_multiplier(sequence_byte_cost_multiplier) - .unwrap(); - - let fee_change = FeeChange { - fee_change: FeeChangeKind::SequenceByteCostMultiplier, - new_value: 4, - }; - - fee_change.check_and_execute(&mut state).await.unwrap(); - assert_eq!( - state - .get_sequence_action_byte_cost_multiplier() - .await - .unwrap(), - 4 - ); - - let init_bridge_account_base_fee = 1; - state - .put_init_bridge_account_base_fee(init_bridge_account_base_fee) - .unwrap(); - - let fee_change = FeeChange { - fee_change: FeeChangeKind::InitBridgeAccountBaseFee, - new_value: 2, - }; - - fee_change.check_and_execute(&mut state).await.unwrap(); - assert_eq!(state.get_init_bridge_account_base_fee().await.unwrap(), 2); - - let bridge_lock_byte_cost_multiplier = 1; - state - .put_bridge_lock_byte_cost_multiplier(bridge_lock_byte_cost_multiplier) - .unwrap(); - - let fee_change = FeeChange { - fee_change: FeeChangeKind::BridgeLockByteCostMultiplier, - new_value: 2, - }; - - fee_change.check_and_execute(&mut state).await.unwrap(); - assert_eq!( - state.get_bridge_lock_byte_cost_multiplier().await.unwrap(), - 2 - ); - - let ics20_withdrawal_base_fee = 1; - state - .put_ics20_withdrawal_base_fee(ics20_withdrawal_base_fee) - .unwrap(); - - let fee_change = FeeChange { - fee_change: FeeChangeKind::Ics20WithdrawalBaseFee, - new_value: 2, - }; - - fee_change.check_and_execute(&mut state).await.unwrap(); - assert_eq!(state.get_ics20_withdrawal_base_fee().await.unwrap(), 2); - } -} diff --git a/crates/astria-sequencer/src/bridge/bridge_lock_action.rs b/crates/astria-sequencer/src/bridge/bridge_lock_action.rs index 4adffe9172..8b29655129 100644 --- a/crates/astria-sequencer/src/bridge/bridge_lock_action.rs +++ b/crates/astria-sequencer/src/bridge/bridge_lock_action.rs @@ -14,17 +14,12 @@ use astria_eyre::eyre::{ use cnidarium::StateWrite; use crate::{ - accounts::{ - action::{ - check_transfer, - execute_transfer, - }, - StateReadExt as _, - StateWriteExt as _, + accounts::action::{ + check_transfer, + execute_transfer, }, address::StateReadExt as _, app::ActionHandler, - assets::StateWriteExt as _, bridge::{ StateReadExt as _, StateWriteExt as _, @@ -33,10 +28,6 @@ use crate::{ utils::create_deposit_event, }; -/// The base byte length of a deposit, as determined by -/// [`tests::get_base_deposit_fee()`]. -const DEPOSIT_BASE_FEE: u128 = 16; - #[async_trait::async_trait] impl ActionHandler for BridgeLock { async fn check_stateless(&self) -> Result<()> { @@ -68,15 +59,6 @@ impl ActionHandler for BridgeLock { "asset ID is not authorized for transfer to bridge account", ); - let from_balance = state - .get_account_balance(&from, &self.fee_asset) - .await - .wrap_err("failed to get sender account balance")?; - let transfer_fee = state - .get_transfer_base_fee() - .await - .context("failed to get transfer base fee")?; - let source_transaction_id = state .get_transaction_context() .expect("current source should be set before executing action") @@ -97,15 +79,6 @@ impl ActionHandler for BridgeLock { }; let deposit_abci_event = create_deposit_event(&deposit); - let byte_cost_multiplier = state - .get_bridge_lock_byte_cost_multiplier() - .await - .wrap_err("failed to get byte cost multiplier")?; - let fee = byte_cost_multiplier - .saturating_mul(calculate_base_deposit_fee(&deposit).unwrap_or(u128::MAX)) - .saturating_add(transfer_fee); - ensure!(from_balance >= fee, "insufficient funds for fee payment"); - let transfer_action = Transfer { to: self.to, asset: self.asset.clone(), @@ -114,231 +87,10 @@ impl ActionHandler for BridgeLock { }; check_transfer(&transfer_action, &from, &state).await?; - // Executes the transfer and deducts transfer feeds. - // FIXME: This is a very roundabout way of paying for fees. IMO it would be - // better to just duplicate this entire logic here so that we don't call out - // to the transfer-action logic. execute_transfer(&transfer_action, &from, &mut state).await?; - // so we just deduct the bridge lock byte multiplier fee. - // FIXME: similar to what is mentioned there: this should be reworked so that - // the fee deduction logic for these actions are defined fully independently - // (even at the cost of duplicating code). - let byte_cost_multiplier = state - .get_bridge_lock_byte_cost_multiplier() - .await - .wrap_err("failed to get byte cost multiplier")?; - let fee = byte_cost_multiplier - .saturating_mul(calculate_base_deposit_fee(&deposit).unwrap_or(u128::MAX)); - state - .get_and_increase_block_fees::(&self.fee_asset, fee) - .await - .wrap_err("failed to add to block fees")?; - state - .decrease_balance(&from, &self.fee_asset, fee) - .await - .wrap_err("failed to deduct fee from account balance")?; - - state.record(deposit_abci_event); state.cache_deposit_event(deposit); + state.record(deposit_abci_event); Ok(()) } } - -/// Returns a modified byte length of the deposit event. Length is calculated with reasonable values -/// for all fields except `asset` and `destination_chain_address`, ergo it may not be representative -/// of on-wire length. -pub(crate) fn calculate_base_deposit_fee(deposit: &Deposit) -> Option { - deposit - .asset - .display_len() - .checked_add(deposit.destination_chain_address.len()) - .and_then(|var_len| { - DEPOSIT_BASE_FEE.checked_add(u128::try_from(var_len).expect( - "converting a usize to a u128 should work on any currently existing machine", - )) - }) -} - -#[cfg(test)] -mod tests { - use astria_core::primitive::v1::{ - asset::{ - self, - }, - Address, - RollupId, - TransactionId, - ADDRESS_LEN, - ROLLUP_ID_LEN, - TRANSACTION_ID_LEN, - }; - use cnidarium::StateDelta; - - use super::*; - use crate::{ - address::StateWriteExt as _, - test_utils::{ - assert_eyre_error, - astria_address, - ASTRIA_PREFIX, - }, - transaction::{ - StateWriteExt as _, - TransactionContext, - }, - }; - - fn test_asset() -> asset::Denom { - "test".parse().unwrap() - } - - #[tokio::test] - async fn execute_fee_calc() { - let storage = cnidarium::TempStorage::new().await.unwrap(); - let snapshot = storage.latest_snapshot(); - let mut state = StateDelta::new(snapshot); - let transfer_fee = 12; - - let from_address = astria_address(&[2; 20]); - let transaction_id = TransactionId::new([0; 32]); - state.put_transaction_context(TransactionContext { - address_bytes: from_address.bytes(), - transaction_id, - source_action_index: 0, - }); - state.put_base_prefix(ASTRIA_PREFIX.to_string()).unwrap(); - - state.put_transfer_base_fee(transfer_fee).unwrap(); - state.put_bridge_lock_byte_cost_multiplier(2).unwrap(); - - let bridge_address = astria_address(&[1; 20]); - let asset = test_asset(); - let bridge_lock = BridgeLock { - to: bridge_address, - asset: asset.clone(), - amount: 100, - fee_asset: asset.clone(), - destination_chain_address: "someaddress".to_string(), - }; - - let rollup_id = RollupId::from_unhashed_bytes(b"test_rollup_id"); - state - .put_bridge_account_rollup_id(&bridge_address, rollup_id) - .unwrap(); - state - .put_bridge_account_ibc_asset(&bridge_address, asset.clone()) - .unwrap(); - state.put_allowed_fee_asset(&asset).unwrap(); - - // not enough balance; should fail - state - .put_account_balance(&from_address, &asset, transfer_fee) - .unwrap(); - assert_eyre_error( - &bridge_lock.check_and_execute(&mut state).await.unwrap_err(), - "insufficient funds for fee payment", - ); - - // enough balance; should pass - let expected_deposit_fee = transfer_fee - + calculate_base_deposit_fee(&Deposit { - bridge_address, - rollup_id, - amount: 100, - asset: asset.clone(), - destination_chain_address: "someaddress".to_string(), - source_transaction_id: transaction_id, - source_action_index: 0, - }) - .unwrap() - * 2; - state - .put_account_balance(&from_address, &asset, 100 + expected_deposit_fee) - .unwrap(); - bridge_lock.check_and_execute(&mut state).await.unwrap(); - } - - #[test] - fn calculated_base_deposit_fee_matches_expected_value() { - assert_correct_base_deposit_fee(&Deposit { - amount: u128::MAX, - source_action_index: u64::MAX, - ..reference_deposit() - }); - assert_correct_base_deposit_fee(&Deposit { - asset: "test_asset".parse().unwrap(), - ..reference_deposit() - }); - assert_correct_base_deposit_fee(&Deposit { - destination_chain_address: "someaddresslonger".to_string(), - ..reference_deposit() - }); - - // Ensure calculated length is as expected with absurd string - // lengths (have tested up to 99999999, but this makes testing very slow) - let absurd_string: String = ['a'; u16::MAX as usize].iter().collect(); - assert_correct_base_deposit_fee(&Deposit { - asset: absurd_string.parse().unwrap(), - ..reference_deposit() - }); - assert_correct_base_deposit_fee(&Deposit { - destination_chain_address: absurd_string, - ..reference_deposit() - }); - } - - #[track_caller] - #[expect( - clippy::arithmetic_side_effects, - reason = "adding length of strings will never overflow u128 on currently existing machines" - )] - fn assert_correct_base_deposit_fee(deposit: &Deposit) { - let calculated_len = calculate_base_deposit_fee(deposit).unwrap(); - let expected_len = DEPOSIT_BASE_FEE - + deposit.asset.to_string().len() as u128 - + deposit.destination_chain_address.len() as u128; - assert_eq!(calculated_len, expected_len); - } - - /// Used to determine the base deposit byte length for `get_deposit_byte_len()`. This is based - /// on "reasonable" values for all fields except `asset` and `destination_chain_address`. These - /// are empty strings, whose length will be added to the base cost at the time of - /// calculation. - /// - /// This test determines 165 bytes for an average deposit with empty `asset` and - /// `destination_chain_address`, which is divided by 10 to get our base byte length of 16. This - /// is to allow for more flexibility in overall fees (we have more flexibility multiplying by a - /// lower number, and if we want fees to be higher we can just raise the multiplier). - #[test] - fn get_base_deposit_fee() { - use prost::Message as _; - let bridge_address = Address::builder() - .prefix("astria-bridge") - .slice(&[0u8; ADDRESS_LEN][..]) - .try_build() - .unwrap(); - let raw_deposit = astria_core::generated::sequencerblock::v1alpha1::Deposit { - bridge_address: Some(bridge_address.to_raw()), - rollup_id: Some(RollupId::from_unhashed_bytes([0; ROLLUP_ID_LEN]).to_raw()), - amount: Some(1000.into()), - asset: String::new(), - destination_chain_address: String::new(), - source_transaction_id: Some(TransactionId::new([0; TRANSACTION_ID_LEN]).to_raw()), - source_action_index: 0, - }; - assert_eq!(DEPOSIT_BASE_FEE, raw_deposit.encoded_len() as u128 / 10); - } - - fn reference_deposit() -> Deposit { - Deposit { - bridge_address: astria_address(&[1; 20]), - rollup_id: RollupId::from_unhashed_bytes(b"test_rollup_id"), - amount: 0, - asset: "test".parse().unwrap(), - destination_chain_address: "someaddress".to_string(), - source_transaction_id: TransactionId::new([0; 32]), - source_action_index: 0, - } - } -} diff --git a/crates/astria-sequencer/src/bridge/bridge_sudo_change_action.rs b/crates/astria-sequencer/src/bridge/bridge_sudo_change_action.rs index ce2047d372..36b2677791 100644 --- a/crates/astria-sequencer/src/bridge/bridge_sudo_change_action.rs +++ b/crates/astria-sequencer/src/bridge/bridge_sudo_change_action.rs @@ -8,13 +8,8 @@ use astria_eyre::eyre::{ use cnidarium::StateWrite; use crate::{ - accounts::StateWriteExt as _, address::StateReadExt as _, app::ActionHandler, - assets::{ - StateReadExt as _, - StateWriteExt as _, - }, bridge::state_ext::{ StateReadExt as _, StateWriteExt as _, @@ -49,14 +44,6 @@ impl ActionHandler for BridgeSudoChange { .wrap_err("failed check for base prefix of new withdrawer address")?; } - ensure!( - state - .is_allowed_fee_asset(&self.fee_asset) - .await - .wrap_err("failed to check allowed fee assets in state")?, - "invalid fee asset", - ); - // check that the sender of this tx is the authorized sudo address for the bridge account let Some(sudo_address) = state .get_bridge_account_sudo_address(&self.bridge_address) @@ -73,19 +60,6 @@ impl ActionHandler for BridgeSudoChange { "unauthorized for bridge sudo change action", ); - let fee = state - .get_bridge_sudo_change_base_fee() - .await - .wrap_err("failed to get bridge sudo change fee")?; - state - .get_and_increase_block_fees::(&self.fee_asset, fee) - .await - .wrap_err("failed to add to block fees")?; - state - .decrease_balance(&self.bridge_address, &self.fee_asset, fee) - .await - .wrap_err("failed to decrease balance for bridge sudo change fee")?; - if let Some(sudo_address) = self.new_sudo_address { state .put_bridge_account_sudo_address(&self.bridge_address, sudo_address) @@ -104,15 +78,20 @@ impl ActionHandler for BridgeSudoChange { #[cfg(test)] mod tests { - use astria_core::primitive::v1::{ - asset, - TransactionId, + use astria_core::{ + primitive::v1::{ + asset, + TransactionId, + }, + protocol::fees::v1alpha1::BridgeSudoChangeFeeComponents, }; use cnidarium::StateDelta; use super::*; use crate::{ + accounts::StateWriteExt as _, address::StateWriteExt as _, + fees::StateWriteExt as _, test_utils::{ astria_address, ASTRIA_PREFIX, @@ -179,7 +158,12 @@ mod tests { source_action_index: 0, }); state.put_base_prefix(ASTRIA_PREFIX.to_string()).unwrap(); - state.put_bridge_sudo_change_base_fee(10).unwrap(); + state + .put_bridge_sudo_change_fees(BridgeSudoChangeFeeComponents { + base: 10, + multiplier: 0, + }) + .unwrap(); let fee_asset = test_asset(); state.put_allowed_fee_asset(&fee_asset).unwrap(); diff --git a/crates/astria-sequencer/src/bridge/bridge_unlock_action.rs b/crates/astria-sequencer/src/bridge/bridge_unlock_action.rs index 1661f9692a..64e45b0da3 100644 --- a/crates/astria-sequencer/src/bridge/bridge_unlock_action.rs +++ b/crates/astria-sequencer/src/bridge/bridge_unlock_action.rs @@ -108,7 +108,10 @@ mod tests { RollupId, TransactionId, }, - protocol::transaction::v1alpha1::action::BridgeUnlock, + protocol::{ + fees::v1alpha1::BridgeUnlockFeeComponents, + transaction::v1alpha1::action::BridgeUnlock, + }, }; use cnidarium::StateDelta; @@ -116,8 +119,8 @@ mod tests { accounts::StateWriteExt as _, address::StateWriteExt as _, app::ActionHandler as _, - assets::StateWriteExt as _, bridge::StateWriteExt as _, + fees::StateWriteExt as _, test_utils::{ assert_eyre_error, astria_address, @@ -232,7 +235,12 @@ mod tests { let asset = test_asset(); let transfer_fee = 10; let transfer_amount = 100; - state.put_transfer_base_fee(transfer_fee).unwrap(); + state + .put_bridge_unlock_fees(BridgeUnlockFeeComponents { + base: transfer_fee, + multiplier: 0, + }) + .unwrap(); let to_address = astria_address(&[2; 20]); let rollup_id = RollupId::from_unhashed_bytes(b"test_rollup_id"); diff --git a/crates/astria-sequencer/src/bridge/component.rs b/crates/astria-sequencer/src/bridge/component.rs deleted file mode 100644 index e844de463a..0000000000 --- a/crates/astria-sequencer/src/bridge/component.rs +++ /dev/null @@ -1,52 +0,0 @@ -use std::sync::Arc; - -use astria_core::protocol::genesis::v1alpha1::GenesisAppState; -use astria_eyre::eyre::{ - Result, - WrapErr, -}; -use tendermint::abci::request::{ - BeginBlock, - EndBlock, -}; -use tracing::instrument; - -use super::state_ext::StateWriteExt; -use crate::component::Component; - -#[derive(Default)] -pub(crate) struct BridgeComponent; - -#[async_trait::async_trait] -impl Component for BridgeComponent { - type AppState = GenesisAppState; - - #[instrument(name = "BridgeComponent::init_chain", skip_all)] - async fn init_chain(mut state: S, app_state: &Self::AppState) -> Result<()> { - state - .put_init_bridge_account_base_fee(app_state.fees().init_bridge_account_base_fee) - .wrap_err("failed to put init bridge account base fee")?; - state - .put_bridge_lock_byte_cost_multiplier(app_state.fees().bridge_lock_byte_cost_multiplier) - .wrap_err("failed to put bridge lock byte cost multiplier")?; - state - .put_bridge_sudo_change_base_fee(app_state.fees().bridge_sudo_change_fee) - .wrap_err("failed to put bridge sudo change base fee") - } - - #[instrument(name = "BridgeComponent::begin_block", skip_all)] - async fn begin_block( - _state: &mut Arc, - _begin_block: &BeginBlock, - ) -> Result<()> { - Ok(()) - } - - #[instrument(name = "BridgeComponent::end_block", skip_all)] - async fn end_block( - _state: &mut Arc, - _end_block: &EndBlock, - ) -> Result<()> { - Ok(()) - } -} diff --git a/crates/astria-sequencer/src/bridge/init_bridge_account_action.rs b/crates/astria-sequencer/src/bridge/init_bridge_account_action.rs index 5422e89fa1..adb7d30bb5 100644 --- a/crates/astria-sequencer/src/bridge/init_bridge_account_action.rs +++ b/crates/astria-sequencer/src/bridge/init_bridge_account_action.rs @@ -4,23 +4,14 @@ use astria_core::{ }; use astria_eyre::eyre::{ bail, - ensure, Result, WrapErr as _, }; use cnidarium::StateWrite; use crate::{ - accounts::{ - StateReadExt as _, - StateWriteExt as _, - }, address::StateReadExt as _, app::ActionHandler, - assets::{ - StateReadExt as _, - StateWriteExt as _, - }, bridge::state_ext::{ StateReadExt as _, StateWriteExt as _, @@ -52,16 +43,6 @@ impl ActionHandler for InitBridgeAccount { .wrap_err("failed check for base prefix of sudo address")?; } - ensure!( - state.is_allowed_fee_asset(&self.fee_asset).await?, - "invalid fee asset", - ); - - let fee = state - .get_init_bridge_account_base_fee() - .await - .wrap_err("failed to get base fee for initializing bridge account")?; - // this prevents the address from being registered as a bridge account // if it's been previously initialized as a bridge account. // @@ -82,39 +63,21 @@ impl ActionHandler for InitBridgeAccount { bail!("bridge account already exists"); } - let balance = state - .get_account_balance(&from, &self.fee_asset) - .await - .wrap_err("failed getting `from` account balance for fee payment")?; - - ensure!( - balance >= fee, - "insufficient funds for bridge account initialization", - ); - state .put_bridge_account_rollup_id(&from, self.rollup_id) .wrap_err("failed to put bridge account rollup id")?; state .put_bridge_account_ibc_asset(&from, &self.asset) .wrap_err("failed to put asset ID")?; - state - .put_bridge_account_sudo_address(&from, self.sudo_address.map_or(from, Address::bytes)) - .wrap_err("failed to put bridge account sudo address")?; - state - .put_bridge_account_withdrawer_address( - &from, - self.withdrawer_address.map_or(from, Address::bytes), - ) - .wrap_err("failed to put bridge account withdrawer address")?; - state - .get_and_increase_block_fees::(&self.fee_asset, fee) - .await - .wrap_err("failed to get and increase block fees")?; - state - .decrease_balance(&from, &self.fee_asset, fee) - .await - .wrap_err("failed to deduct fee from account balance")?; + state.put_bridge_account_sudo_address( + &from, + self.sudo_address.map_or(from, Address::bytes), + )?; + state.put_bridge_account_withdrawer_address( + &from, + self.withdrawer_address.map_or(from, Address::bytes), + )?; + Ok(()) } } diff --git a/crates/astria-sequencer/src/bridge/mod.rs b/crates/astria-sequencer/src/bridge/mod.rs index 33a5abaeb9..5aec65349a 100644 --- a/crates/astria-sequencer/src/bridge/mod.rs +++ b/crates/astria-sequencer/src/bridge/mod.rs @@ -1,13 +1,11 @@ mod bridge_lock_action; mod bridge_sudo_change_action; mod bridge_unlock_action; -pub(crate) mod component; pub(crate) mod init_bridge_account_action; pub(crate) mod query; mod state_ext; pub(crate) mod storage; -pub(crate) use bridge_lock_action::calculate_base_deposit_fee; pub(crate) use state_ext::{ StateReadExt, StateWriteExt, diff --git a/crates/astria-sequencer/src/bridge/state_ext.rs b/crates/astria-sequencer/src/bridge/state_ext.rs index c00b6f14d0..c1c0749512 100644 --- a/crates/astria-sequencer/src/bridge/state_ext.rs +++ b/crates/astria-sequencer/src/bridge/state_ext.rs @@ -157,45 +157,6 @@ pub(crate) trait StateReadExt: StateRead + address::StateReadExt { .context("invalid deposits bytes") } - #[instrument(skip_all)] - async fn get_init_bridge_account_base_fee(&self) -> Result { - let bytes = self - .get_raw(keys::INIT_BRIDGE_ACCOUNT_BASE_FEE) - .await - .map_err(anyhow_to_eyre) - .wrap_err("failed reading raw init bridge account base fee from state")? - .ok_or_eyre("init bridge account base fee not found")?; - StoredValue::deserialize(&bytes) - .and_then(|value| storage::Fee::try_from(value).map(u128::from)) - .wrap_err("invalid fee bytes") - } - - #[instrument(skip_all)] - async fn get_bridge_lock_byte_cost_multiplier(&self) -> Result { - let bytes = self - .get_raw(keys::BRIDGE_LOCK_BYTE_COST_MULTIPLIER) - .await - .map_err(anyhow_to_eyre) - .wrap_err("failed reading raw bridge lock byte cost multiplier from state")? - .ok_or_eyre("bridge lock byte cost multiplier not found")?; - StoredValue::deserialize(&bytes) - .and_then(|value| storage::Fee::try_from(value).map(u128::from)) - .wrap_err("invalid bridge lock byte cost multiplier bytes") - } - - #[instrument(skip_all)] - async fn get_bridge_sudo_change_base_fee(&self) -> Result { - let bytes = self - .get_raw(keys::BRIDGE_SUDO_CHANGE_FEE) - .await - .map_err(anyhow_to_eyre) - .wrap_err("failed reading raw bridge sudo change fee from state")? - .ok_or_eyre("bridge sudo change fee not found")?; - StoredValue::deserialize(&bytes) - .and_then(|value| storage::Fee::try_from(value).map(u128::from)) - .wrap_err("invalid bridge sudo change fee bytes") - } - #[instrument(skip_all)] async fn get_last_transaction_id_for_bridge_account( &self, @@ -348,33 +309,6 @@ pub(crate) trait StateWriteExt: StateWrite { Ok(()) } - #[instrument(skip_all)] - fn put_init_bridge_account_base_fee(&mut self, fee: u128) -> Result<()> { - let bytes = StoredValue::from(storage::Fee::from(fee)) - .serialize() - .context("failed to serialize bridge account base fee")?; - self.put_raw(keys::INIT_BRIDGE_ACCOUNT_BASE_FEE.to_string(), bytes); - Ok(()) - } - - #[instrument(skip_all)] - fn put_bridge_lock_byte_cost_multiplier(&mut self, fee: u128) -> Result<()> { - let bytes = StoredValue::from(storage::Fee::from(fee)) - .serialize() - .context("failed to serialize bridge lock byte cost multiplier")?; - self.put_raw(keys::BRIDGE_LOCK_BYTE_COST_MULTIPLIER.to_string(), bytes); - Ok(()) - } - - #[instrument(skip_all)] - fn put_bridge_sudo_change_base_fee(&mut self, fee: u128) -> Result<()> { - let bytes = StoredValue::from(storage::Fee::from(fee)) - .serialize() - .context("failed to serialize bridge sudo change base fee")?; - self.put_raw(keys::BRIDGE_SUDO_CHANGE_FEE.to_string(), bytes); - Ok(()) - } - #[instrument(skip_all)] fn put_last_transaction_id_for_bridge_account( &mut self, @@ -714,39 +648,6 @@ mod tests { ); } - #[tokio::test] - async fn init_bridge_account_base_fee_round_trip() { - let storage = cnidarium::TempStorage::new().await.unwrap(); - let snapshot = storage.latest_snapshot(); - let mut state = StateDelta::new(snapshot); - - state.put_init_bridge_account_base_fee(123).unwrap(); - let retrieved_fee = state.get_init_bridge_account_base_fee().await.unwrap(); - assert_eq!(retrieved_fee, 123); - } - - #[tokio::test] - async fn bridge_lock_byte_cost_multiplier_round_trip() { - let storage = cnidarium::TempStorage::new().await.unwrap(); - let snapshot = storage.latest_snapshot(); - let mut state = StateDelta::new(snapshot); - - state.put_bridge_lock_byte_cost_multiplier(123).unwrap(); - let retrieved_fee = state.get_bridge_lock_byte_cost_multiplier().await.unwrap(); - assert_eq!(retrieved_fee, 123); - } - - #[tokio::test] - async fn bridge_sudo_change_base_fee_round_trip() { - let storage = cnidarium::TempStorage::new().await.unwrap(); - let snapshot = storage.latest_snapshot(); - let mut state = StateDelta::new(snapshot); - - state.put_bridge_sudo_change_base_fee(123).unwrap(); - let retrieved_fee = state.get_bridge_sudo_change_base_fee().await.unwrap(); - assert_eq!(retrieved_fee, 123); - } - #[tokio::test] async fn last_transaction_id_for_bridge_account_round_trip() { let storage = cnidarium::TempStorage::new().await.unwrap(); diff --git a/crates/astria-sequencer/src/bridge/storage/keys.rs b/crates/astria-sequencer/src/bridge/storage/keys.rs index 028d06eaea..a8618637b4 100644 --- a/crates/astria-sequencer/src/bridge/storage/keys.rs +++ b/crates/astria-sequencer/src/bridge/storage/keys.rs @@ -9,11 +9,6 @@ use crate::{ storage::keys::AccountPrefixer, }; -pub(in crate::bridge) const INIT_BRIDGE_ACCOUNT_BASE_FEE: &str = "bridge/init_account_fee"; -pub(in crate::bridge) const BRIDGE_LOCK_BYTE_COST_MULTIPLIER: &str = - "bridge/lock_byte_cost_multiplier"; -pub(in crate::bridge) const BRIDGE_SUDO_CHANGE_FEE: &str = "bridge/sudo_change_fee"; - pub(in crate::bridge) const BRIDGE_ACCOUNT_PREFIX: &str = "bridge/account/"; const BRIDGE_ACCOUNT_SUDO_PREFIX: &str = "bridge/sudo/"; const BRIDGE_ACCOUNT_WITHDRAWER_PREFIX: &str = "bridge/withdrawer/"; @@ -96,9 +91,6 @@ mod tests { #[test] fn keys_should_not_change() { - insta::assert_snapshot!(INIT_BRIDGE_ACCOUNT_BASE_FEE); - insta::assert_snapshot!(BRIDGE_LOCK_BYTE_COST_MULTIPLIER); - insta::assert_snapshot!(BRIDGE_SUDO_CHANGE_FEE); insta::assert_snapshot!(DEPOSITS_EPHEMERAL); insta::assert_snapshot!(rollup_id(&address())); insta::assert_snapshot!(asset_id(&address())); @@ -111,9 +103,6 @@ mod tests { #[test] fn keys_should_have_component_prefix() { - assert!(INIT_BRIDGE_ACCOUNT_BASE_FEE.starts_with(COMPONENT_PREFIX)); - assert!(BRIDGE_LOCK_BYTE_COST_MULTIPLIER.starts_with(COMPONENT_PREFIX)); - assert!(BRIDGE_SUDO_CHANGE_FEE.starts_with(COMPONENT_PREFIX)); assert!(DEPOSITS_EPHEMERAL.starts_with(COMPONENT_PREFIX)); assert!(rollup_id(&address()).starts_with(COMPONENT_PREFIX)); assert!(asset_id(&address()).starts_with(COMPONENT_PREFIX)); diff --git a/crates/astria-sequencer/src/bridge/storage/mod.rs b/crates/astria-sequencer/src/bridge/storage/mod.rs index c52ffe69e2..f244fedb9b 100644 --- a/crates/astria-sequencer/src/bridge/storage/mod.rs +++ b/crates/astria-sequencer/src/bridge/storage/mod.rs @@ -6,7 +6,6 @@ pub(super) use values::{ AddressBytes, BlockHeight, Deposits, - Fee, IbcPrefixedDenom, RollupId, TransactionId, diff --git a/crates/astria-sequencer/src/bridge/storage/snapshots/astria_sequencer__bridge__storage__keys__tests__keys_should_not_change-2.snap b/crates/astria-sequencer/src/bridge/storage/snapshots/astria_sequencer__bridge__storage__keys__tests__keys_should_not_change-2.snap index 8fdfb69d89..2f5eda8379 100644 --- a/crates/astria-sequencer/src/bridge/storage/snapshots/astria_sequencer__bridge__storage__keys__tests__keys_should_not_change-2.snap +++ b/crates/astria-sequencer/src/bridge/storage/snapshots/astria_sequencer__bridge__storage__keys__tests__keys_should_not_change-2.snap @@ -1,6 +1,5 @@ --- source: crates/astria-sequencer/src/bridge/storage/keys.rs -assertion_line: 96 -expression: BRIDGE_LOCK_BYTE_COST_MULTIPLIER_KEY +expression: rollup_id(&address()) --- -bridge/lock_byte_cost_multiplier +bridge/account/HAxJDxtVKNgXPF3kbRMRYOSywMM=/rollup_id diff --git a/crates/astria-sequencer/src/bridge/storage/snapshots/astria_sequencer__bridge__storage__keys__tests__keys_should_not_change-3.snap b/crates/astria-sequencer/src/bridge/storage/snapshots/astria_sequencer__bridge__storage__keys__tests__keys_should_not_change-3.snap index 9b95261abd..60280f1fc3 100644 --- a/crates/astria-sequencer/src/bridge/storage/snapshots/astria_sequencer__bridge__storage__keys__tests__keys_should_not_change-3.snap +++ b/crates/astria-sequencer/src/bridge/storage/snapshots/astria_sequencer__bridge__storage__keys__tests__keys_should_not_change-3.snap @@ -1,6 +1,5 @@ --- source: crates/astria-sequencer/src/bridge/storage/keys.rs -assertion_line: 97 -expression: BRIDGE_SUDO_CHANGE_FEE_KEY +expression: asset_id(&address()) --- -bridge/sudo_change_fee +bridge/account/HAxJDxtVKNgXPF3kbRMRYOSywMM=/asset_id diff --git a/crates/astria-sequencer/src/bridge/storage/snapshots/astria_sequencer__bridge__storage__keys__tests__keys_should_not_change-4.snap b/crates/astria-sequencer/src/bridge/storage/snapshots/astria_sequencer__bridge__storage__keys__tests__keys_should_not_change-4.snap index 5be3e6da08..3764e29048 100644 --- a/crates/astria-sequencer/src/bridge/storage/snapshots/astria_sequencer__bridge__storage__keys__tests__keys_should_not_change-4.snap +++ b/crates/astria-sequencer/src/bridge/storage/snapshots/astria_sequencer__bridge__storage__keys__tests__keys_should_not_change-4.snap @@ -1,6 +1,5 @@ --- source: crates/astria-sequencer/src/bridge/storage/keys.rs -assertion_line: 98 -expression: DEPOSITS_EPHEMERAL_KEY +expression: bridge_account_sudo_address(&address()) --- -bridge/deposits +bridge/sudo/HAxJDxtVKNgXPF3kbRMRYOSywMM= diff --git a/crates/astria-sequencer/src/bridge/storage/snapshots/astria_sequencer__bridge__storage__keys__tests__keys_should_not_change-5.snap b/crates/astria-sequencer/src/bridge/storage/snapshots/astria_sequencer__bridge__storage__keys__tests__keys_should_not_change-5.snap index 6d98ac4b82..8cd0e79522 100644 --- a/crates/astria-sequencer/src/bridge/storage/snapshots/astria_sequencer__bridge__storage__keys__tests__keys_should_not_change-5.snap +++ b/crates/astria-sequencer/src/bridge/storage/snapshots/astria_sequencer__bridge__storage__keys__tests__keys_should_not_change-5.snap @@ -1,6 +1,5 @@ --- source: crates/astria-sequencer/src/bridge/storage/keys.rs -assertion_line: 99 -expression: rollup_id(&address()) +expression: bridge_account_withdrawer_address(&address()) --- -bridge/account/HAxJDxtVKNgXPF3kbRMRYOSywMM=/rollup_id +bridge/withdrawer/HAxJDxtVKNgXPF3kbRMRYOSywMM= diff --git a/crates/astria-sequencer/src/bridge/storage/snapshots/astria_sequencer__bridge__storage__keys__tests__keys_should_not_change-6.snap b/crates/astria-sequencer/src/bridge/storage/snapshots/astria_sequencer__bridge__storage__keys__tests__keys_should_not_change-6.snap index 3cb63bc740..0bc1461a19 100644 --- a/crates/astria-sequencer/src/bridge/storage/snapshots/astria_sequencer__bridge__storage__keys__tests__keys_should_not_change-6.snap +++ b/crates/astria-sequencer/src/bridge/storage/snapshots/astria_sequencer__bridge__storage__keys__tests__keys_should_not_change-6.snap @@ -1,6 +1,5 @@ --- source: crates/astria-sequencer/src/bridge/storage/keys.rs -assertion_line: 100 -expression: asset_id(&address()) +expression: "bridge_account_withdrawal_event(&address(), \"the-event\")" --- -bridge/account/HAxJDxtVKNgXPF3kbRMRYOSywMM=/asset_id +bridge/account/HAxJDxtVKNgXPF3kbRMRYOSywMM=/withdrawal_event/the-event diff --git a/crates/astria-sequencer/src/bridge/storage/snapshots/astria_sequencer__bridge__storage__keys__tests__keys_should_not_change-7.snap b/crates/astria-sequencer/src/bridge/storage/snapshots/astria_sequencer__bridge__storage__keys__tests__keys_should_not_change-7.snap index 71464a1920..b6505d19fa 100644 --- a/crates/astria-sequencer/src/bridge/storage/snapshots/astria_sequencer__bridge__storage__keys__tests__keys_should_not_change-7.snap +++ b/crates/astria-sequencer/src/bridge/storage/snapshots/astria_sequencer__bridge__storage__keys__tests__keys_should_not_change-7.snap @@ -1,6 +1,5 @@ --- source: crates/astria-sequencer/src/bridge/storage/keys.rs -assertion_line: 101 -expression: bridge_account_sudo_address(&address()) +expression: "deposit(&[1; 32], &RollupId::new([2; 32]))" --- -bridge/sudo/HAxJDxtVKNgXPF3kbRMRYOSywMM= +bridge/deposit/AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQE=/AgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgI= diff --git a/crates/astria-sequencer/src/bridge/storage/snapshots/astria_sequencer__bridge__storage__keys__tests__keys_should_not_change-8.snap b/crates/astria-sequencer/src/bridge/storage/snapshots/astria_sequencer__bridge__storage__keys__tests__keys_should_not_change-8.snap index a1385b069f..95b5defb09 100644 --- a/crates/astria-sequencer/src/bridge/storage/snapshots/astria_sequencer__bridge__storage__keys__tests__keys_should_not_change-8.snap +++ b/crates/astria-sequencer/src/bridge/storage/snapshots/astria_sequencer__bridge__storage__keys__tests__keys_should_not_change-8.snap @@ -1,6 +1,5 @@ --- source: crates/astria-sequencer/src/bridge/storage/keys.rs -assertion_line: 102 -expression: bridge_account_withdrawer_address(&address()) +expression: last_transaction_id_for_bridge_account(&address()) --- -bridge/withdrawer/HAxJDxtVKNgXPF3kbRMRYOSywMM= +bridge/account/HAxJDxtVKNgXPF3kbRMRYOSywMM=/last_tx diff --git a/crates/astria-sequencer/src/bridge/storage/snapshots/astria_sequencer__bridge__storage__keys__tests__keys_should_not_change.snap b/crates/astria-sequencer/src/bridge/storage/snapshots/astria_sequencer__bridge__storage__keys__tests__keys_should_not_change.snap index 2ab66a494a..46b4c9424d 100644 --- a/crates/astria-sequencer/src/bridge/storage/snapshots/astria_sequencer__bridge__storage__keys__tests__keys_should_not_change.snap +++ b/crates/astria-sequencer/src/bridge/storage/snapshots/astria_sequencer__bridge__storage__keys__tests__keys_should_not_change.snap @@ -1,6 +1,5 @@ --- source: crates/astria-sequencer/src/bridge/storage/keys.rs -assertion_line: 95 -expression: INIT_BRIDGE_ACCOUNT_BASE_FEE_KEY +expression: DEPOSITS_EPHEMERAL --- -bridge/init_account_fee +bridge/deposits diff --git a/crates/astria-sequencer/src/bridge/storage/values/fee.rs b/crates/astria-sequencer/src/bridge/storage/values/fee.rs deleted file mode 100644 index 993531e713..0000000000 --- a/crates/astria-sequencer/src/bridge/storage/values/fee.rs +++ /dev/null @@ -1,42 +0,0 @@ -use astria_eyre::eyre::bail; -use borsh::{ - BorshDeserialize, - BorshSerialize, -}; - -use super::{ - Value, - ValueImpl, -}; - -#[derive(Debug, BorshSerialize, BorshDeserialize)] -pub(in crate::bridge) struct Fee(u128); - -impl From for Fee { - fn from(fee: u128) -> Self { - Fee(fee) - } -} - -impl From for u128 { - fn from(fee: Fee) -> Self { - fee.0 - } -} - -impl<'a> From for crate::storage::StoredValue<'a> { - fn from(fee: Fee) -> Self { - crate::storage::StoredValue::Bridge(Value(ValueImpl::Fee(fee))) - } -} - -impl<'a> TryFrom> for Fee { - type Error = astria_eyre::eyre::Error; - - fn try_from(value: crate::storage::StoredValue<'a>) -> Result { - let crate::storage::StoredValue::Bridge(Value(ValueImpl::Fee(fee))) = value else { - bail!("bridge stored value type mismatch: expected fee, found {value:?}"); - }; - Ok(fee) - } -} diff --git a/crates/astria-sequencer/src/bridge/storage/values/mod.rs b/crates/astria-sequencer/src/bridge/storage/values/mod.rs index 0b5c5e8125..37a23e100b 100644 --- a/crates/astria-sequencer/src/bridge/storage/values/mod.rs +++ b/crates/astria-sequencer/src/bridge/storage/values/mod.rs @@ -1,7 +1,6 @@ mod address_bytes; mod block_height; mod deposits; -mod fee; mod ibc_prefixed_denom; mod rollup_id; mod transaction_id; @@ -15,7 +14,6 @@ pub(in crate::bridge) use self::{ address_bytes::AddressBytes, block_height::BlockHeight, deposits::Deposits, - fee::Fee, ibc_prefixed_denom::IbcPrefixedDenom, rollup_id::RollupId, transaction_id::TransactionId, @@ -31,6 +29,5 @@ enum ValueImpl<'a> { AddressBytes(AddressBytes<'a>), BlockHeight(BlockHeight), Deposits(Deposits<'a>), - Fee(Fee), TransactionId(TransactionId<'a>), } diff --git a/crates/astria-sequencer/src/fee_asset_change.rs b/crates/astria-sequencer/src/fee_asset_change.rs deleted file mode 100644 index f12cc9dd93..0000000000 --- a/crates/astria-sequencer/src/fee_asset_change.rs +++ /dev/null @@ -1,61 +0,0 @@ -use astria_core::protocol::transaction::v1alpha1::action::FeeAssetChange; -use astria_eyre::eyre::{ - bail, - ensure, - Result, - WrapErr as _, -}; -use async_trait::async_trait; -use cnidarium::StateWrite; - -use crate::{ - app::ActionHandler, - assets::{ - StateReadExt as _, - StateWriteExt as _, - }, - authority::StateReadExt as _, - transaction::StateReadExt as _, -}; - -#[async_trait] -impl ActionHandler for FeeAssetChange { - async fn check_stateless(&self) -> Result<()> { - Ok(()) - } - - async fn check_and_execute(&self, mut state: S) -> Result<()> { - let from = state - .get_transaction_context() - .expect("transaction source must be present in state when executing an action") - .address_bytes(); - let authority_sudo_address = state - .get_sudo_address() - .await - .wrap_err("failed to get authority sudo address")?; - ensure!( - authority_sudo_address == from, - "unauthorized address for fee asset change" - ); - match self { - FeeAssetChange::Addition(asset) => { - state - .put_allowed_fee_asset(asset) - .context("failed to write allowed fee asset to state")?; - } - FeeAssetChange::Removal(asset) => { - state.delete_allowed_fee_asset(asset); - - if state - .get_allowed_fee_assets() - .await - .wrap_err("failed to retrieve allowed fee assets")? - .is_empty() - { - bail!("cannot remove last allowed fee asset"); - } - } - } - Ok(()) - } -} diff --git a/crates/astria-sequencer/src/fees/action.rs b/crates/astria-sequencer/src/fees/action.rs new file mode 100644 index 0000000000..0eb2519436 --- /dev/null +++ b/crates/astria-sequencer/src/fees/action.rs @@ -0,0 +1,257 @@ +use astria_core::protocol::transaction::v1alpha1::action::{ + FeeAssetChange, + FeeChange, +}; +use astria_eyre::eyre::{ + self, + bail, + ensure, + WrapErr as _, +}; +use cnidarium::StateWrite; + +use crate::{ + app::ActionHandler, + authority::StateReadExt as _, + fees::{ + StateReadExt as _, + StateWriteExt as _, + }, + transaction::StateReadExt as _, +}; + +#[async_trait::async_trait] +impl ActionHandler for FeeChange { + async fn check_stateless(&self) -> eyre::Result<()> { + Ok(()) + } + + /// check that the signer of the transaction is the current sudo address, + /// as only that address can change the fee + async fn check_and_execute(&self, mut state: S) -> eyre::Result<()> { + let from = state + .get_transaction_context() + .expect("transaction source must be present in state when executing an action") + .address_bytes(); + // ensure signer is the valid `sudo` key in state + let sudo_address = state + .get_sudo_address() + .await + .wrap_err("failed to get sudo address from state")?; + ensure!(sudo_address == from, "signer is not the sudo key"); + + match self { + Self::Transfer(fees) => state + .put_transfer_fees(*fees) + .wrap_err("failed to put transfer fees"), + Self::Sequence(fees) => state + .put_sequence_fees(*fees) + .wrap_err("failed to put sequence fees"), + Self::Ics20Withdrawal(fees) => state + .put_ics20_withdrawal_fees(*fees) + .wrap_err("failed to put ics20 withdrawal fees"), + Self::InitBridgeAccount(fees) => state + .put_init_bridge_account_fees(*fees) + .wrap_err("failed to put init bridge account fees"), + Self::BridgeLock(fees) => state + .put_bridge_lock_fees(*fees) + .wrap_err("failed to put bridge lock fees"), + Self::BridgeUnlock(fees) => state + .put_bridge_unlock_fees(*fees) + .wrap_err("failed to put bridge unlock fees"), + Self::BridgeSudoChange(fees) => state + .put_bridge_sudo_change_fees(*fees) + .wrap_err("failed to put bridge sudo change fees"), + Self::IbcRelay(fees) => state + .put_ibc_relay_fees(*fees) + .wrap_err("failed to put ibc relay fees"), + Self::ValidatorUpdate(fees) => state + .put_validator_update_fees(*fees) + .wrap_err("failed to put validator update fees"), + Self::FeeAssetChange(fees) => state + .put_fee_asset_change_fees(*fees) + .wrap_err("failed to put fee asset change fees"), + Self::FeeChange(fees) => state + .put_fee_change_fees(*fees) + .wrap_err("failed to put fee change fees"), + Self::IbcRelayerChange(fees) => state + .put_ibc_relayer_change_fees(*fees) + .wrap_err("failed to put ibc relayer change fees"), + Self::SudoAddressChange(fees) => state + .put_sudo_address_change_fees(*fees) + .wrap_err("failed to put sudo address change fees"), + Self::IbcSudoChange(fees) => state + .put_ibc_sudo_change_fees(*fees) + .wrap_err("failed to put ibc sudo change fees"), + } + } +} + +#[async_trait::async_trait] +impl ActionHandler for FeeAssetChange { + async fn check_stateless(&self) -> eyre::Result<()> { + Ok(()) + } + + async fn check_and_execute(&self, mut state: S) -> eyre::Result<()> { + let from = state + .get_transaction_context() + .expect("transaction source must be present in state when executing an action") + .address_bytes(); + let authority_sudo_address = state + .get_sudo_address() + .await + .wrap_err("failed to get authority sudo address")?; + ensure!( + authority_sudo_address == from, + "unauthorized address for fee asset change" + ); + match self { + FeeAssetChange::Addition(asset) => { + state + .put_allowed_fee_asset(asset) + .context("failed to write allowed fee asset to state")?; + } + FeeAssetChange::Removal(asset) => { + state.delete_allowed_fee_asset(asset); + + if state + .get_allowed_fee_assets() + .await + .wrap_err("failed to retrieve allowed fee assets")? + .is_empty() + { + bail!("cannot remove last allowed fee asset"); + } + } + } + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use astria_core::{ + primitive::v1::TransactionId, + protocol::{ + fees::v1alpha1::{ + BridgeLockFeeComponents, + Ics20WithdrawalFeeComponents, + InitBridgeAccountFeeComponents, + SequenceFeeComponents, + TransferFeeComponents, + }, + transaction::v1alpha1::action::FeeChange, + }, + }; + use cnidarium::StateDelta; + + use crate::{ + app::ActionHandler as _, + authority::StateWriteExt as _, + fees::{ + StateReadExt as _, + StateWriteExt as _, + }, + transaction::{ + StateWriteExt as _, + TransactionContext, + }, + }; + + #[tokio::test] + async fn fee_change_action_executes() { + let storage = cnidarium::TempStorage::new().await.unwrap(); + let snapshot = storage.latest_snapshot(); + let mut state = StateDelta::new(snapshot); + let transfer_fee = 12; + + state.put_transaction_context(TransactionContext { + address_bytes: [1; 20], + transaction_id: TransactionId::new([0; 32]), + source_action_index: 0, + }); + state.put_sudo_address([1; 20]).unwrap(); + + state + .put_transfer_fees(TransferFeeComponents { + base: transfer_fee, + multiplier: 0, + }) + .unwrap(); + + let fee_change = FeeChange::Transfer(TransferFeeComponents { + base: 10, + multiplier: 0, + }); + + fee_change.check_and_execute(&mut state).await.unwrap(); + assert_eq!(state.get_transfer_fees().await.unwrap().base, 10); + + let sequence_base = 5; + let sequence_cost_multiplier = 2; + state + .put_sequence_fees(SequenceFeeComponents { + base: sequence_base, + multiplier: sequence_cost_multiplier, + }) + .unwrap(); + + let fee_change = FeeChange::Sequence(SequenceFeeComponents { + base: 3, + multiplier: 4, + }); + + fee_change.check_and_execute(&mut state).await.unwrap(); + assert_eq!(state.get_sequence_fees().await.unwrap().base, 3); + assert_eq!(state.get_sequence_fees().await.unwrap().multiplier, 4); + + let init_bridge_account_base = 1; + state + .put_init_bridge_account_fees(InitBridgeAccountFeeComponents { + base: init_bridge_account_base, + multiplier: 0, + }) + .unwrap(); + + let fee_change = FeeChange::InitBridgeAccount(InitBridgeAccountFeeComponents { + base: 2, + multiplier: 0, + }); + + fee_change.check_and_execute(&mut state).await.unwrap(); + assert_eq!(state.get_init_bridge_account_fees().await.unwrap().base, 2); + + let bridge_lock_cost_multiplier = 1; + state + .put_bridge_lock_fees(BridgeLockFeeComponents { + base: 0, + multiplier: bridge_lock_cost_multiplier, + }) + .unwrap(); + + let fee_change = FeeChange::BridgeLock(BridgeLockFeeComponents { + base: 0, + multiplier: 2, + }); + + fee_change.check_and_execute(&mut state).await.unwrap(); + assert_eq!(state.get_bridge_lock_fees().await.unwrap().multiplier, 2); + + let ics20_withdrawal_base = 1; + state + .put_ics20_withdrawal_fees(Ics20WithdrawalFeeComponents { + base: ics20_withdrawal_base, + multiplier: 0, + }) + .unwrap(); + + let fee_change = FeeChange::Ics20Withdrawal(Ics20WithdrawalFeeComponents { + base: 2, + multiplier: 0, + }); + + fee_change.check_and_execute(&mut state).await.unwrap(); + assert_eq!(state.get_ics20_withdrawal_fees().await.unwrap().base, 2); + } +} diff --git a/crates/astria-sequencer/src/fees/component.rs b/crates/astria-sequencer/src/fees/component.rs new file mode 100644 index 0000000000..3b55dec6b0 --- /dev/null +++ b/crates/astria-sequencer/src/fees/component.rs @@ -0,0 +1,119 @@ +use std::sync::Arc; + +use astria_core::protocol::genesis::v1alpha1::GenesisAppState; +use astria_eyre::eyre::{ + Result, + WrapErr as _, +}; +use tendermint::abci::request::{ + BeginBlock, + EndBlock, +}; +use tracing::instrument; + +use crate::{ + component::Component, + fees, +}; + +#[derive(Default)] +pub(crate) struct FeesComponent; + +#[async_trait::async_trait] +impl Component for FeesComponent { + type AppState = GenesisAppState; + + #[instrument(name = "FeesComponent::init_chain", skip_all)] + async fn init_chain(mut state: S, app_state: &Self::AppState) -> Result<()> + where + S: fees::StateWriteExt + fees::StateReadExt, + { + let transfer_fees = app_state.fees().transfer; + state + .put_transfer_fees(transfer_fees) + .wrap_err("failed to store transfer fee components")?; + + let sequence_fees = app_state.fees().sequence; + state + .put_sequence_fees(sequence_fees) + .wrap_err("failed to store sequence action fee components")?; + + let ics20_withdrawal_fees = app_state.fees().ics20_withdrawal; + state + .put_ics20_withdrawal_fees(ics20_withdrawal_fees) + .wrap_err("failed to store ics20 withdrawal fee components")?; + + let init_bridge_account_fees = app_state.fees().init_bridge_account; + state + .put_init_bridge_account_fees(init_bridge_account_fees) + .wrap_err("failed to store init bridge account fee components")?; + + let bridge_lock_fees = app_state.fees().bridge_lock; + state + .put_bridge_lock_fees(bridge_lock_fees) + .wrap_err("failed to store bridge lock fee components")?; + + let bridge_unlock_fees = app_state.fees().bridge_unlock; + state + .put_bridge_unlock_fees(bridge_unlock_fees) + .wrap_err("failed to store bridge unlock fee components")?; + + let bridge_sudo_change_fees = app_state.fees().bridge_sudo_change; + state + .put_bridge_sudo_change_fees(bridge_sudo_change_fees) + .wrap_err("failed to store bridge sudo change fee components")?; + + let ibc_relay_fees = app_state.fees().ibc_relay; + state + .put_ibc_relay_fees(ibc_relay_fees) + .wrap_err("failed to store ibc relay fee components")?; + + let validator_update_fees = app_state.fees().validator_update; + state + .put_validator_update_fees(validator_update_fees) + .wrap_err("failed to store validator update fee components")?; + + let fee_asset_change_fees = app_state.fees().fee_asset_change; + state + .put_fee_asset_change_fees(fee_asset_change_fees) + .wrap_err("failed to store fee asset change fee components")?; + + let fee_change_fees = app_state.fees().fee_change; + state + .put_fee_change_fees(fee_change_fees) + .wrap_err("failed to store fee change fee components")?; + + let ibc_relayer_change_fees = app_state.fees().ibc_relayer_change; + state + .put_ibc_relayer_change_fees(ibc_relayer_change_fees) + .wrap_err("failed to store ibc relayer change fee components")?; + + let sudo_address_change_fees = app_state.fees().sudo_address_change; + state + .put_sudo_address_change_fees(sudo_address_change_fees) + .wrap_err("failed to store sudo address change fee components")?; + + let ibc_sudo_change_fees = app_state.fees().ibc_sudo_change; + state + .put_ibc_sudo_change_fees(ibc_sudo_change_fees) + .wrap_err("failed to store ibc sudo change fee components")?; + + Ok(()) + } + + #[instrument(name = "FeesComponent::begin_block", skip_all)] + async fn begin_block( + _state: &mut Arc, + _begin_block: &BeginBlock, + ) -> Result<()> { + Ok(()) + } + + #[instrument(name = "FeesComponent::end_block", skip_all)] + async fn end_block( + _state: &mut Arc, + _end_block: &EndBlock, + ) -> Result<()> { + Ok(()) + } +} diff --git a/crates/astria-sequencer/src/fees/mod.rs b/crates/astria-sequencer/src/fees/mod.rs new file mode 100644 index 0000000000..690bc19922 --- /dev/null +++ b/crates/astria-sequencer/src/fees/mod.rs @@ -0,0 +1,380 @@ +use astria_core::{ + primitive::v1::{ + asset, + TransactionId, + }, + protocol::transaction::{ + self, + v1alpha1::action::{ + BridgeLock, + BridgeSudoChange, + BridgeUnlock, + FeeAssetChange, + FeeChange, + IbcRelayerChange, + IbcSudoChange, + InitBridgeAccount, + Sequence, + SudoAddressChange, + Transfer, + ValidatorUpdate, + }, + }, + Protobuf, +}; +use astria_eyre::eyre::{ + self, + ensure, + WrapErr as _, +}; +use cnidarium::StateWrite; +use penumbra_ibc::IbcRelay; +use tendermint::abci::{ + Event, + EventAttributeIndexExt as _, +}; +use tracing::{ + instrument, + Level, +}; + +use crate::{ + accounts::StateWriteExt as _, + transaction::StateReadExt as _, +}; + +pub(crate) mod action; +pub(crate) mod component; +pub(crate) mod query; +mod state_ext; +pub(crate) mod storage; + +#[cfg(test)] +mod tests; + +pub(crate) use state_ext::{ + StateReadExt, + StateWriteExt, +}; + +/// The base byte length of a deposit, as determined by +/// [`tests::get_base_deposit_fee()`]. +const DEPOSIT_BASE_FEE: u128 = 16; + +#[async_trait::async_trait] +pub(crate) trait FeeHandler { + async fn check_and_pay_fees(&self, state: S) -> eyre::Result<()>; + + fn variable_component(&self) -> u128; +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub(crate) struct Fee { + action_name: String, + asset: asset::Denom, + amount: u128, + source_transaction_id: TransactionId, + source_action_index: u64, +} + +impl Fee { + pub(crate) fn asset(&self) -> &asset::Denom { + &self.asset + } + + pub(crate) fn amount(&self) -> u128 { + self.amount + } +} + +#[async_trait::async_trait] +impl FeeHandler for Transfer { + #[instrument(skip_all, err)] + async fn check_and_pay_fees(&self, state: S) -> eyre::Result<()> { + let fees = state + .get_transfer_fees() + .await + .wrap_err("transfer fees not found, so this action is disabled")?; + check_and_pay_fees(self, fees.base, fees.multiplier, state, &self.fee_asset).await + } + + #[instrument(skip_all)] + fn variable_component(&self) -> u128 { + 0 + } +} + +#[async_trait::async_trait] +impl FeeHandler for BridgeLock { + #[instrument(skip_all, err)] + async fn check_and_pay_fees(&self, state: S) -> eyre::Result<()> { + let fees = state + .get_bridge_lock_fees() + .await + .wrap_err("bridge lock fees not found, so this action is disabled")?; + check_and_pay_fees(self, fees.base, fees.multiplier, state, &self.fee_asset).await + } + + #[instrument(skip_all)] + fn variable_component(&self) -> u128 { + base_deposit_fee(&self.asset, &self.destination_chain_address) + } +} + +#[async_trait::async_trait] +impl FeeHandler for BridgeSudoChange { + #[instrument(skip_all, err)] + async fn check_and_pay_fees(&self, state: S) -> eyre::Result<()> { + let fees = state + .get_bridge_sudo_change_fees() + .await + .wrap_err("bridge sudo change fees not found, so this action is disabled")?; + check_and_pay_fees(self, fees.base, fees.multiplier, state, &self.fee_asset).await + } + + #[instrument(skip_all)] + fn variable_component(&self) -> u128 { + 0 + } +} + +#[async_trait::async_trait] +impl FeeHandler for BridgeUnlock { + #[instrument(skip_all, err)] + async fn check_and_pay_fees(&self, state: S) -> eyre::Result<()> { + let fees = state + .get_bridge_unlock_fees() + .await + .wrap_err("bridge unlock fees not found, so this action is disabled")?; + check_and_pay_fees(self, fees.base, fees.multiplier, state, &self.fee_asset).await + } + + #[instrument(skip_all)] + fn variable_component(&self) -> u128 { + 0 + } +} + +#[async_trait::async_trait] +impl FeeHandler for InitBridgeAccount { + #[instrument(skip_all, err)] + async fn check_and_pay_fees(&self, state: S) -> eyre::Result<()> { + let fees = state + .get_init_bridge_account_fees() + .await + .wrap_err("init bridge account fees not found, so this action is disabled")?; + check_and_pay_fees(self, fees.base, fees.multiplier, state, &self.fee_asset).await + } + + #[instrument(skip_all)] + fn variable_component(&self) -> u128 { + 0 + } +} + +#[async_trait::async_trait] +impl FeeHandler for transaction::v1alpha1::action::Ics20Withdrawal { + #[instrument(skip_all, err)] + async fn check_and_pay_fees(&self, state: S) -> eyre::Result<()> { + let fees = state + .get_ics20_withdrawal_fees() + .await + .wrap_err("ics20 withdrawal fees not found, so this action is disabled")?; + check_and_pay_fees(self, fees.base, fees.multiplier, state, &self.fee_asset).await + } + + #[instrument(skip_all)] + fn variable_component(&self) -> u128 { + 0 + } +} + +#[async_trait::async_trait] +impl FeeHandler for Sequence { + #[instrument(skip_all, err)] + async fn check_and_pay_fees(&self, state: S) -> eyre::Result<()> { + let fees = state + .get_sequence_fees() + .await + .wrap_err("sequence fees not found, so this action is disabled")?; + check_and_pay_fees(self, fees.base, fees.multiplier, state, &self.fee_asset).await + } + + #[instrument(skip_all)] + fn variable_component(&self) -> u128 { + u128::try_from(self.data.len()) + .expect("converting a usize to a u128 should work on any currently existing machine") + } +} + +#[async_trait::async_trait] +impl FeeHandler for ValidatorUpdate { + #[instrument(skip_all, err)] + async fn check_and_pay_fees(&self, state: S) -> eyre::Result<()> { + state + .get_validator_update_fees() + .await + .wrap_err("validator update fees not found, so this action is disabled")?; + Ok(()) + } + + fn variable_component(&self) -> u128 { + 0 + } +} + +#[async_trait::async_trait] +impl FeeHandler for SudoAddressChange { + #[instrument(skip_all, err)] + async fn check_and_pay_fees(&self, state: S) -> eyre::Result<()> { + state + .get_sudo_address_change_fees() + .await + .wrap_err("sudo address change fees not found, so this action is disabled")?; + Ok(()) + } + + fn variable_component(&self) -> u128 { + 0 + } +} + +#[async_trait::async_trait] +impl FeeHandler for FeeChange { + #[instrument(skip_all, err)] + async fn check_and_pay_fees(&self, state: S) -> eyre::Result<()> { + state + .get_fee_change_fees() + .await + .wrap_err("fee change fees not found, so this action is disabled")?; + Ok(()) + } + + fn variable_component(&self) -> u128 { + 0 + } +} + +#[async_trait::async_trait] +impl FeeHandler for IbcSudoChange { + #[instrument(skip_all, err)] + async fn check_and_pay_fees(&self, state: S) -> eyre::Result<()> { + state + .get_ibc_sudo_change_fees() + .await + .wrap_err("ibc sudo change fees not found, so this action is disabled")?; + Ok(()) + } + + fn variable_component(&self) -> u128 { + 0 + } +} + +#[async_trait::async_trait] +impl FeeHandler for IbcRelayerChange { + #[instrument(skip_all, err)] + async fn check_and_pay_fees(&self, state: S) -> eyre::Result<()> { + state + .get_ibc_relayer_change_fees() + .await + .wrap_err("ibc relayer change fees not found, so this action is disabled")?; + Ok(()) + } + + fn variable_component(&self) -> u128 { + 0 + } +} + +#[async_trait::async_trait] +impl FeeHandler for FeeAssetChange { + #[instrument(skip_all, err)] + async fn check_and_pay_fees(&self, state: S) -> eyre::Result<()> { + state + .get_fee_asset_change_fees() + .await + .wrap_err("fee asset change fees not found, so this action is disabled")?; + Ok(()) + } + + fn variable_component(&self) -> u128 { + 0 + } +} + +#[async_trait::async_trait] +impl FeeHandler for IbcRelay { + #[instrument(skip_all, err)] + async fn check_and_pay_fees(&self, state: S) -> eyre::Result<()> { + state + .get_ibc_relay_fees() + .await + .wrap_err("ibc relay fees not found, so this action is disabled")?; + Ok(()) + } + + fn variable_component(&self) -> u128 { + 0 + } +} + +#[instrument(skip_all, err(level = Level::WARN))] +async fn check_and_pay_fees( + act: &T, + base: u128, + multiplier: u128, + mut state: S, + fee_asset: &asset::Denom, +) -> eyre::Result<()> { + let total_fees = base.saturating_add(act.variable_component().saturating_mul(multiplier)); + let transaction_context = state + .get_transaction_context() + .expect("transaction source must be present in state when executing an action"); + let from = transaction_context.address_bytes(); + let transaction_id = transaction_context.transaction_id; + let source_action_index = transaction_context.source_action_index; + + ensure!( + state + .is_allowed_fee_asset(fee_asset) + .await + .wrap_err("failed to check allowed fee assets in state")?, + "invalid fee asset", + ); + state + .add_fee_to_block_fees::<_, T>(fee_asset, total_fees, transaction_id, source_action_index) + .wrap_err("failed to add to block fees")?; + state + .decrease_balance(&from, fee_asset, total_fees) + .await + .wrap_err("failed to decrease balance for fee payment")?; + Ok(()) +} + +/// Returns a modified byte length of the deposit event. Length is calculated with reasonable values +/// for all fields except `asset` and `destination_chain_address`, ergo it may not be representative +/// of on-wire length. +fn base_deposit_fee(asset: &asset::Denom, destination_chain_address: &str) -> u128 { + u128::try_from( + asset + .display_len() + .saturating_add(destination_chain_address.len()), + ) + .expect("converting a usize to a u128 should work on any currently existing machine") + .saturating_add(DEPOSIT_BASE_FEE) +} + +/// Creates `abci::Event` of kind `tx.fees` for sequencer fee reporting +pub(crate) fn construct_tx_fee_event(fee: &Fee) -> Event { + Event::new( + "tx.fees", + [ + ("actionName", fee.action_name.to_string()).index(), + ("asset", fee.asset.to_string()).index(), + ("feeAmount", fee.amount.to_string()).index(), + ("sourceTransactionId", fee.source_transaction_id.to_string()).index(), + ("sourceActionIndex", fee.source_action_index.to_string()).index(), + ], + ) +} diff --git a/crates/astria-sequencer/src/fees/query.rs b/crates/astria-sequencer/src/fees/query.rs new file mode 100644 index 0000000000..c125fb315a --- /dev/null +++ b/crates/astria-sequencer/src/fees/query.rs @@ -0,0 +1,66 @@ +use astria_core::protocol::{ + abci::AbciErrorCode, + asset::v1alpha1::AllowedFeeAssetsResponse, +}; +use cnidarium::Storage; +use prost::Message as _; +use tendermint::abci::{ + request, + response, + Code, +}; + +use super::StateReadExt as _; +use crate::app::StateReadExt as _; + +pub(crate) async fn allowed_fee_assets_request( + storage: Storage, + request: request::Query, + _params: Vec<(String, String)>, +) -> response::Query { + // get last snapshot + let snapshot = storage.latest_snapshot(); + + // get height from snapshot + let height = match snapshot.get_block_height().await { + Ok(height) => height, + Err(err) => { + return response::Query { + code: Code::Err(AbciErrorCode::INTERNAL_ERROR.value()), + info: AbciErrorCode::INTERNAL_ERROR.info(), + log: format!("failed getting block height: {err:#}"), + ..response::Query::default() + }; + } + }; + + // get ids from snapshot at height + let fee_assets = match snapshot.get_allowed_fee_assets().await { + Ok(fee_assets) => fee_assets, + Err(err) => { + return response::Query { + code: Code::Err(AbciErrorCode::INTERNAL_ERROR.value()), + info: AbciErrorCode::INTERNAL_ERROR.info(), + log: format!("failed to retrieve allowed fee assets: {err:#}"), + ..response::Query::default() + }; + } + }; + + let payload = AllowedFeeAssetsResponse { + height, + fee_assets: fee_assets.into_iter().map(Into::into).collect(), + } + .into_raw() + .encode_to_vec() + .into(); + + let height = tendermint::block::Height::try_from(height).expect("height must fit into an i64"); + response::Query { + code: tendermint::abci::Code::Ok, + key: request.path.into_bytes().into(), + value: payload, + height, + ..response::Query::default() + } +} diff --git a/crates/astria-sequencer/src/fees/state_ext.rs b/crates/astria-sequencer/src/fees/state_ext.rs new file mode 100644 index 0000000000..25a851560a --- /dev/null +++ b/crates/astria-sequencer/src/fees/state_ext.rs @@ -0,0 +1,968 @@ +use std::borrow::Cow; + +use astria_core::{ + primitive::v1::{ + asset, + TransactionId, + }, + protocol::fees::v1alpha1::{ + BridgeLockFeeComponents, + BridgeSudoChangeFeeComponents, + BridgeUnlockFeeComponents, + FeeAssetChangeFeeComponents, + FeeChangeFeeComponents, + IbcRelayFeeComponents, + IbcRelayerChangeFeeComponents, + IbcSudoChangeFeeComponents, + Ics20WithdrawalFeeComponents, + InitBridgeAccountFeeComponents, + SequenceFeeComponents, + SudoAddressChangeFeeComponents, + TransferFeeComponents, + ValidatorUpdateFeeComponents, + }, + Protobuf, +}; +use astria_eyre::{ + anyhow_to_eyre, + eyre::{ + eyre, + Result, + WrapErr as _, + }, +}; +use async_trait::async_trait; +use cnidarium::{ + StateRead, + StateWrite, +}; +use futures::StreamExt as _; +use tracing::instrument; + +use super::{ + storage::{ + self, + keys::{ + self, + extract_asset_from_allowed_asset_key, + }, + }, + Fee, + FeeHandler, +}; +use crate::storage::StoredValue; + +#[async_trait] +pub(crate) trait StateReadExt: StateRead { + #[instrument(skip_all)] + fn get_block_fees(&self) -> Vec { + self.object_get(keys::BLOCK).unwrap_or_default() + } + + #[instrument(skip_all)] + async fn get_transfer_fees(&self) -> Result { + let bytes = self + .get_raw(keys::TRANSFER) + .await + .map_err(anyhow_to_eyre) + .wrap_err("failed reading raw transfer fee components from state")?; + let Some(bytes) = bytes else { + return Err(eyre!("transfer fee components not set")); + }; + StoredValue::deserialize(&bytes) + .and_then(|value| { + storage::TransferFeeComponentsStorage::try_from(value) + .map(TransferFeeComponents::from) + }) + .wrap_err("invalid fees bytes") + } + + #[instrument(skip_all)] + async fn get_sequence_fees(&self) -> Result { + let bytes = self + .get_raw(keys::SEQUENCE) + .await + .map_err(anyhow_to_eyre) + .wrap_err("failed reading raw sequence fee components from state")?; + let Some(bytes) = bytes else { + return Err(eyre!("sequence fee components not set")); + }; + StoredValue::deserialize(&bytes) + .and_then(|value| { + storage::SequenceFeeComponentsStorage::try_from(value) + .map(SequenceFeeComponents::from) + }) + .wrap_err("invalid fees bytes") + } + + #[instrument(skip_all)] + async fn get_ics20_withdrawal_fees(&self) -> Result { + let bytes = self + .get_raw(keys::ICS20_WITHDRAWAL) + .await + .map_err(anyhow_to_eyre) + .wrap_err("failed reading raw ics20 withdrawal fee components from state")?; + let Some(bytes) = bytes else { + return Err(eyre!("ics20 withdrawal fee components not set")); + }; + StoredValue::deserialize(&bytes) + .and_then(|value| { + storage::Ics20WithdrawalFeeComponentsStorage::try_from(value) + .map(Ics20WithdrawalFeeComponents::from) + }) + .wrap_err("invalid fees bytes") + } + + #[instrument(skip_all)] + async fn get_init_bridge_account_fees(&self) -> Result { + let bytes = self + .get_raw(keys::INIT_BRIDGE_ACCOUNT) + .await + .map_err(anyhow_to_eyre) + .wrap_err("failed reading raw init bridge account fee components from state")?; + let Some(bytes) = bytes else { + return Err(eyre!("init bridge account fee components not set")); + }; + StoredValue::deserialize(&bytes) + .and_then(|value| { + storage::InitBridgeAccountFeeComponentsStorage::try_from(value) + .map(InitBridgeAccountFeeComponents::from) + }) + .wrap_err("invalid fees bytes") + } + + #[instrument(skip_all)] + async fn get_bridge_lock_fees(&self) -> Result { + let bytes = self + .get_raw(keys::BRIDGE_LOCK) + .await + .map_err(anyhow_to_eyre) + .wrap_err("failed reading raw bridge lock fee components from state")?; + let Some(bytes) = bytes else { + return Err(eyre!("bridge lock fee components not set")); + }; + StoredValue::deserialize(&bytes) + .and_then(|value| { + storage::BridgeLockFeeComponentsStorage::try_from(value) + .map(BridgeLockFeeComponents::from) + }) + .wrap_err("invalid fees bytes") + } + + #[instrument(skip_all)] + async fn get_bridge_unlock_fees(&self) -> Result { + let bytes = self + .get_raw(keys::BRIDGE_UNLOCK) + .await + .map_err(anyhow_to_eyre) + .wrap_err("failed reading raw bridge unlock fee components from state")?; + let Some(bytes) = bytes else { + return Err(eyre!("bridge unlock fee components not set")); + }; + StoredValue::deserialize(&bytes) + .and_then(|value| { + storage::BridgeUnlockFeeComponentsStorage::try_from(value) + .map(BridgeUnlockFeeComponents::from) + }) + .wrap_err("invalid fees bytes") + } + + #[instrument(skip_all)] + async fn get_bridge_sudo_change_fees(&self) -> Result { + let bytes = self + .get_raw(keys::BRIDGE_SUDO_CHANGE) + .await + .map_err(anyhow_to_eyre) + .wrap_err("failed reading raw bridge sudo change fee components from state")?; + let Some(bytes) = bytes else { + return Err(eyre!("bridge sudo change fee components not set")); + }; + StoredValue::deserialize(&bytes) + .and_then(|value| { + storage::BridgeSudoChangeFeeComponentsStorage::try_from(value) + .map(BridgeSudoChangeFeeComponents::from) + }) + .wrap_err("invalid fees bytes") + } + + #[instrument(skip_all)] + async fn get_ibc_relay_fees(&self) -> Result { + let bytes = self + .get_raw(keys::IBC_RELAY) + .await + .map_err(anyhow_to_eyre) + .wrap_err("failed reading raw ibc relay fee components from state")?; + let Some(bytes) = bytes else { + return Err(eyre!("ibc relay fee components not set")); + }; + StoredValue::deserialize(&bytes) + .and_then(|value| { + storage::IbcRelayFeeComponentsStorage::try_from(value) + .map(IbcRelayFeeComponents::from) + }) + .wrap_err("invalid fees bytes") + } + + #[instrument(skip_all)] + async fn get_validator_update_fees(&self) -> Result { + let bytes = self + .get_raw(keys::VALIDATOR_UPDATE) + .await + .map_err(anyhow_to_eyre) + .wrap_err("failed reading raw validator update fee components from state")?; + let Some(bytes) = bytes else { + return Err(eyre!("validator update fee components not set")); + }; + StoredValue::deserialize(&bytes) + .and_then(|value| { + storage::ValidatorUpdateFeeComponentsStorage::try_from(value) + .map(ValidatorUpdateFeeComponents::from) + }) + .wrap_err("invalid fees bytes") + } + + #[instrument(skip_all)] + async fn get_fee_asset_change_fees(&self) -> Result { + let bytes = self + .get_raw(keys::FEE_ASSET_CHANGE) + .await + .map_err(anyhow_to_eyre) + .wrap_err("failed reading raw fee asset change fee components from state")?; + let Some(bytes) = bytes else { + return Err(eyre!("fee asset change fee components not set")); + }; + StoredValue::deserialize(&bytes) + .and_then(|value| { + storage::FeeAssetChangeFeeComponentsStorage::try_from(value) + .map(FeeAssetChangeFeeComponents::from) + }) + .wrap_err("invalid fees bytes") + } + + #[instrument(skip_all)] + async fn get_fee_change_fees(&self) -> Result { + let bytes = self + .get_raw(keys::FEE_CHANGE) + .await + .map_err(anyhow_to_eyre) + .wrap_err("failed reading raw fee change fee components from state")?; + let Some(bytes) = bytes else { + return Err(eyre!("fee change fee components not set")); + }; + StoredValue::deserialize(&bytes) + .and_then(|value| { + storage::FeeChangeFeeComponentsStorage::try_from(value) + .map(FeeChangeFeeComponents::from) + }) + .wrap_err("invalid fees bytes") + } + + #[instrument(skip_all)] + async fn get_ibc_relayer_change_fees(&self) -> Result { + let bytes = self + .get_raw(keys::IBC_RELAYER_CHANGE) + .await + .map_err(anyhow_to_eyre) + .wrap_err("failed reading raw ibc relayer change fee components from state")?; + let Some(bytes) = bytes else { + return Err(eyre!("ibc relayer change fee components not set")); + }; + StoredValue::deserialize(&bytes) + .and_then(|value| { + storage::IbcRelayerChangeFeeComponentsStorage::try_from(value) + .map(IbcRelayerChangeFeeComponents::from) + }) + .wrap_err("invalid fees bytes") + } + + #[instrument(skip_all)] + async fn get_sudo_address_change_fees(&self) -> Result { + let bytes = self + .get_raw(keys::SUDO_ADDRESS_CHANGE) + .await + .map_err(anyhow_to_eyre) + .wrap_err("failed reading raw sudo address change fee components from state")?; + let Some(bytes) = bytes else { + return Err(eyre!("sudo address change fee components not set")); + }; + StoredValue::deserialize(&bytes) + .and_then(|value| { + storage::SudoAddressChangeFeeComponentsStorage::try_from(value) + .map(SudoAddressChangeFeeComponents::from) + }) + .wrap_err("invalid fees bytes") + } + + #[instrument(skip_all)] + async fn get_ibc_sudo_change_fees(&self) -> Result { + let bytes = self + .get_raw(keys::IBC_SUDO_CHANGE) + .await + .map_err(anyhow_to_eyre) + .wrap_err("failed reading raw ibc sudo change fee components from state")?; + let Some(bytes) = bytes else { + return Err(eyre!("ibc sudo change fee components not set")); + }; + StoredValue::deserialize(&bytes) + .and_then(|value| { + storage::IbcSudoChangeFeeComponentsStorage::try_from(value) + .map(IbcSudoChangeFeeComponents::from) + }) + .wrap_err("invalid fees bytes") + } + + #[instrument(skip_all)] + async fn is_allowed_fee_asset<'a, TAsset>(&self, asset: &'a TAsset) -> Result + where + TAsset: Sync, + &'a TAsset: Into>, + { + Ok(self + .get_raw(&keys::allowed_asset(asset)) + .await + .map_err(anyhow_to_eyre) + .wrap_err("failed to read raw fee asset from state")? + .is_some()) + } + + #[instrument(skip_all)] + async fn get_allowed_fee_assets(&self) -> Result> { + let mut assets = Vec::new(); + + let mut stream = std::pin::pin!(self.prefix_raw(keys::ALLOWED_ASSET_PREFIX)); + while let Some(Ok((key, _))) = stream.next().await { + let asset = + extract_asset_from_allowed_asset_key(&key).wrap_err("failed to extract asset")?; + assets.push(asset); + } + + Ok(assets) + } +} + +impl StateReadExt for T {} + +#[async_trait] +pub(crate) trait StateWriteExt: StateWrite { + /// Constructs and adds `Fee` object to the block fees vec. + #[instrument(skip_all)] + fn add_fee_to_block_fees<'a, TAsset, T: FeeHandler + Protobuf>( + &mut self, + asset: &'a TAsset, + amount: u128, + source_transaction_id: TransactionId, + source_action_index: u64, + ) -> Result<()> + where + TAsset: Sync + std::fmt::Display, + asset::IbcPrefixed: From<&'a TAsset>, + { + let current_fees: Option> = self.object_get(keys::BLOCK); + + let fee = Fee { + action_name: T::full_name(), + asset: asset::IbcPrefixed::from(asset).into(), + amount, + source_transaction_id, + source_action_index, + }; + let new_fees = if let Some(mut fees) = current_fees { + fees.push(fee); + fees + } else { + vec![fee] + }; + + self.object_put(keys::BLOCK, new_fees); + Ok(()) + } + + #[instrument(skip_all)] + fn put_transfer_fees(&mut self, fees: TransferFeeComponents) -> Result<()> { + let bytes = StoredValue::from(storage::TransferFeeComponentsStorage::from(fees)) + .serialize() + .wrap_err("failed to serialize fees")?; + self.put_raw(keys::TRANSFER.to_string(), bytes); + Ok(()) + } + + #[instrument(skip_all)] + fn put_sequence_fees(&mut self, fees: SequenceFeeComponents) -> Result<()> { + let bytes = StoredValue::from(storage::SequenceFeeComponentsStorage::from(fees)) + .serialize() + .wrap_err("failed to serialize fees")?; + self.put_raw(keys::SEQUENCE.to_string(), bytes); + Ok(()) + } + + #[instrument(skip_all)] + fn put_ics20_withdrawal_fees(&mut self, fees: Ics20WithdrawalFeeComponents) -> Result<()> { + let bytes = StoredValue::from(storage::Ics20WithdrawalFeeComponentsStorage::from(fees)) + .serialize() + .wrap_err("failed to serialize fees")?; + self.put_raw(keys::ICS20_WITHDRAWAL.to_string(), bytes); + Ok(()) + } + + #[instrument(skip_all)] + fn put_init_bridge_account_fees(&mut self, fees: InitBridgeAccountFeeComponents) -> Result<()> { + let bytes = StoredValue::from(storage::InitBridgeAccountFeeComponentsStorage::from(fees)) + .serialize() + .wrap_err("failed to serialize fees")?; + self.put_raw(keys::INIT_BRIDGE_ACCOUNT.to_string(), bytes); + Ok(()) + } + + #[instrument(skip_all)] + fn put_bridge_lock_fees(&mut self, fees: BridgeLockFeeComponents) -> Result<()> { + let bytes = StoredValue::from(storage::BridgeLockFeeComponentsStorage::from(fees)) + .serialize() + .wrap_err("failed to serialize fees")?; + self.put_raw(keys::BRIDGE_LOCK.to_string(), bytes); + Ok(()) + } + + #[instrument(skip_all)] + fn put_bridge_unlock_fees(&mut self, fees: BridgeUnlockFeeComponents) -> Result<()> { + let bytes = StoredValue::from(storage::BridgeUnlockFeeComponentsStorage::from(fees)) + .serialize() + .wrap_err("failed to serialize fees")?; + self.put_raw(keys::BRIDGE_UNLOCK.to_string(), bytes); + Ok(()) + } + + #[instrument(skip_all)] + fn put_bridge_sudo_change_fees(&mut self, fees: BridgeSudoChangeFeeComponents) -> Result<()> { + let bytes = StoredValue::from(storage::BridgeSudoChangeFeeComponentsStorage::from(fees)) + .serialize() + .wrap_err("failed to serialize fees")?; + self.put_raw(keys::BRIDGE_SUDO_CHANGE.to_string(), bytes); + Ok(()) + } + + #[instrument(skip_all)] + fn put_ibc_relay_fees(&mut self, fees: IbcRelayFeeComponents) -> Result<()> { + let bytes = StoredValue::from(storage::IbcRelayFeeComponentsStorage::from(fees)) + .serialize() + .wrap_err("failed to serialize fees")?; + self.put_raw(keys::IBC_RELAY.to_string(), bytes); + Ok(()) + } + + #[instrument(skip_all)] + fn put_validator_update_fees(&mut self, fees: ValidatorUpdateFeeComponents) -> Result<()> { + let bytes = StoredValue::from(storage::ValidatorUpdateFeeComponentsStorage::from(fees)) + .serialize() + .wrap_err("failed to serialize fees")?; + self.put_raw(keys::VALIDATOR_UPDATE.to_string(), bytes); + Ok(()) + } + + #[instrument(skip_all)] + fn put_fee_asset_change_fees(&mut self, fees: FeeAssetChangeFeeComponents) -> Result<()> { + let bytes = StoredValue::from(storage::FeeAssetChangeFeeComponentsStorage::from(fees)) + .serialize() + .wrap_err("failed to serialize fees")?; + self.put_raw(keys::FEE_ASSET_CHANGE.to_string(), bytes); + Ok(()) + } + + #[instrument(skip_all)] + fn put_fee_change_fees(&mut self, fees: FeeChangeFeeComponents) -> Result<()> { + let bytes = StoredValue::from(storage::FeeChangeFeeComponentsStorage::from(fees)) + .serialize() + .wrap_err("failed to serialize fees")?; + self.put_raw(keys::FEE_CHANGE.to_string(), bytes); + Ok(()) + } + + #[instrument(skip_all)] + fn put_ibc_relayer_change_fees(&mut self, fees: IbcRelayerChangeFeeComponents) -> Result<()> { + let bytes = StoredValue::from(storage::IbcRelayerChangeFeeComponentsStorage::from(fees)) + .serialize() + .wrap_err("failed to serialize fees")?; + self.put_raw(keys::IBC_RELAYER_CHANGE.to_string(), bytes); + Ok(()) + } + + #[instrument(skip_all)] + fn put_sudo_address_change_fees(&mut self, fees: SudoAddressChangeFeeComponents) -> Result<()> { + let bytes = StoredValue::from(storage::SudoAddressChangeFeeComponentsStorage::from(fees)) + .serialize() + .wrap_err("failed to serialize fees")?; + self.put_raw(keys::SUDO_ADDRESS_CHANGE.to_string(), bytes); + Ok(()) + } + + #[instrument(skip_all)] + fn put_ibc_sudo_change_fees(&mut self, fees: IbcSudoChangeFeeComponents) -> Result<()> { + let bytes = StoredValue::from(storage::IbcSudoChangeFeeComponentsStorage::from(fees)) + .serialize() + .wrap_err("failed to serialize fees")?; + self.put_raw(keys::IBC_SUDO_CHANGE.to_string(), bytes); + Ok(()) + } + + #[instrument(skip_all)] + fn delete_allowed_fee_asset<'a, TAsset>(&mut self, asset: &'a TAsset) + where + &'a TAsset: Into>, + { + self.delete(keys::allowed_asset(asset)); + } + + #[instrument(skip_all)] + fn put_allowed_fee_asset<'a, TAsset>(&mut self, asset: &'a TAsset) -> Result<()> + where + &'a TAsset: Into>, + { + let bytes = StoredValue::Unit + .serialize() + .context("failed to serialize unit for allowed fee asset")?; + self.put_raw(keys::allowed_asset(asset), bytes); + Ok(()) + } +} + +impl StateWriteExt for T {} + +#[cfg(test)] +mod tests { + use std::collections::HashSet; + + use astria_core::protocol::transaction::v1alpha1::action::Transfer; + use cnidarium::StateDelta; + + use super::*; + use crate::app::test_utils::initialize_app_with_storage; + + fn asset_0() -> asset::Denom { + "asset_0".parse().unwrap() + } + + fn asset_1() -> asset::Denom { + "asset_1".parse().unwrap() + } + + fn asset_2() -> asset::Denom { + "asset_2".parse().unwrap() + } + + #[tokio::test] + async fn block_fee_read_and_increase() { + let (_, storage) = initialize_app_with_storage(None, vec![]).await; + let snapshot = storage.latest_snapshot(); + let mut state = StateDelta::new(snapshot); + + // doesn't exist at first + let fee_balances_orig = state.get_block_fees(); + assert!(fee_balances_orig.is_empty()); + + // can write + let asset = asset_0(); + let amount = 100u128; + state + .add_fee_to_block_fees::<_, Transfer>(&asset, amount, TransactionId::new([0; 32]), 0) + .unwrap(); + + // holds expected + let fee_balances_updated = state.get_block_fees(); + assert_eq!( + fee_balances_updated[0], + Fee { + action_name: "astria.protocol.transactions.v1alpha1.Transfer".to_string(), + asset: asset.to_ibc_prefixed().into(), + amount, + source_transaction_id: TransactionId::new([0; 32]), + source_action_index: 0 + }, + "fee balances are not what they were expected to be" + ); + } + + #[tokio::test] + async fn block_fee_read_and_increase_can_delete() { + let (_, storage) = initialize_app_with_storage(None, vec![]).await; + let snapshot = storage.latest_snapshot(); + let mut state = StateDelta::new(snapshot); + + // can write + let asset_first = asset_0(); + let asset_second = asset_1(); + let amount_first = 100u128; + let amount_second = 200u128; + + state + .add_fee_to_block_fees::<_, Transfer>( + &asset_first, + amount_first, + TransactionId::new([0; 32]), + 0, + ) + .unwrap(); + state + .add_fee_to_block_fees::<_, Transfer>( + &asset_second, + amount_second, + TransactionId::new([0; 32]), + 1, + ) + .unwrap(); + // holds expected + let fee_balances = HashSet::<_>::from_iter(state.get_block_fees()); + assert_eq!( + fee_balances, + HashSet::from_iter(vec![ + Fee { + action_name: "astria.protocol.transactions.v1alpha1.Transfer".to_string(), + asset: asset_first.to_ibc_prefixed().into(), + amount: amount_first, + source_transaction_id: TransactionId::new([0; 32]), + source_action_index: 0 + }, + Fee { + action_name: "astria.protocol.transactions.v1alpha1.Transfer".to_string(), + asset: asset_second.to_ibc_prefixed().into(), + amount: amount_second, + source_transaction_id: TransactionId::new([0; 32]), + source_action_index: 1 + }, + ]), + "returned fee balance vector not what was expected" + ); + } + + #[tokio::test] + async fn transfer_fees_round_trip() { + let storage = cnidarium::TempStorage::new().await.unwrap(); + let snapshot = storage.latest_snapshot(); + let mut state = StateDelta::new(snapshot); + + let fee_components = TransferFeeComponents { + base: 123, + multiplier: 1, + }; + + state.put_transfer_fees(fee_components).unwrap(); + let retrieved_fee = state.get_transfer_fees().await.unwrap(); + assert_eq!(retrieved_fee, fee_components); + } + + #[tokio::test] + async fn sequence_fees_round_trip() { + let storage = cnidarium::TempStorage::new().await.unwrap(); + let snapshot = storage.latest_snapshot(); + let mut state = StateDelta::new(snapshot); + + let fee_components = SequenceFeeComponents { + base: 123, + multiplier: 1, + }; + + state.put_sequence_fees(fee_components).unwrap(); + let retrieved_fee = state.get_sequence_fees().await.unwrap(); + assert_eq!(retrieved_fee, fee_components); + } + + #[tokio::test] + async fn ics20_withdrawal_fees_round_trip() { + let storage = cnidarium::TempStorage::new().await.unwrap(); + let snapshot = storage.latest_snapshot(); + let mut state = StateDelta::new(snapshot); + + let fee_components = Ics20WithdrawalFeeComponents { + base: 123, + multiplier: 1, + }; + + state.put_ics20_withdrawal_fees(fee_components).unwrap(); + let retrieved_fee = state.get_ics20_withdrawal_fees().await.unwrap(); + assert_eq!(retrieved_fee, fee_components); + } + + #[tokio::test] + async fn init_bridge_account_fees_round_trip() { + let storage = cnidarium::TempStorage::new().await.unwrap(); + let snapshot = storage.latest_snapshot(); + let mut state = StateDelta::new(snapshot); + + let fee_components = InitBridgeAccountFeeComponents { + base: 123, + multiplier: 1, + }; + + state.put_init_bridge_account_fees(fee_components).unwrap(); + let retrieved_fee = state.get_init_bridge_account_fees().await.unwrap(); + assert_eq!(retrieved_fee, fee_components); + } + + #[tokio::test] + async fn bridge_lock_fees_round_trip() { + let storage = cnidarium::TempStorage::new().await.unwrap(); + let snapshot = storage.latest_snapshot(); + let mut state = StateDelta::new(snapshot); + + let fee_components = BridgeLockFeeComponents { + base: 123, + multiplier: 1, + }; + + state.put_bridge_lock_fees(fee_components).unwrap(); + let retrieved_fee = state.get_bridge_lock_fees().await.unwrap(); + assert_eq!(retrieved_fee, fee_components); + } + + #[tokio::test] + async fn bridge_unlock_fees_round_trip() { + let storage = cnidarium::TempStorage::new().await.unwrap(); + let snapshot = storage.latest_snapshot(); + let mut state = StateDelta::new(snapshot); + + let fee_components = BridgeUnlockFeeComponents { + base: 123, + multiplier: 1, + }; + + state.put_bridge_unlock_fees(fee_components).unwrap(); + let retrieved_fee = state.get_bridge_unlock_fees().await.unwrap(); + assert_eq!(retrieved_fee, fee_components); + } + + #[tokio::test] + async fn bridge_sudo_change_fees_round_trip() { + let storage = cnidarium::TempStorage::new().await.unwrap(); + let snapshot = storage.latest_snapshot(); + let mut state = StateDelta::new(snapshot); + + let fee_components = BridgeSudoChangeFeeComponents { + base: 123, + multiplier: 1, + }; + + state.put_bridge_sudo_change_fees(fee_components).unwrap(); + let retrieved_fee = state.get_bridge_sudo_change_fees().await.unwrap(); + assert_eq!(retrieved_fee, fee_components); + } + + #[tokio::test] + async fn ibc_relay_fees_round_trip() { + let storage = cnidarium::TempStorage::new().await.unwrap(); + let snapshot = storage.latest_snapshot(); + let mut state = StateDelta::new(snapshot); + + let fee_components = IbcRelayFeeComponents { + base: 123, + multiplier: 1, + }; + + state.put_ibc_relay_fees(fee_components).unwrap(); + let retrieved_fee = state.get_ibc_relay_fees().await.unwrap(); + assert_eq!(retrieved_fee, fee_components); + } + + #[tokio::test] + async fn validator_update_fees_round_trip() { + let storage = cnidarium::TempStorage::new().await.unwrap(); + let snapshot = storage.latest_snapshot(); + let mut state = StateDelta::new(snapshot); + + let fee_components = ValidatorUpdateFeeComponents { + base: 123, + multiplier: 1, + }; + + state.put_validator_update_fees(fee_components).unwrap(); + let retrieved_fee = state.get_validator_update_fees().await.unwrap(); + assert_eq!(retrieved_fee, fee_components); + } + + #[tokio::test] + async fn fee_asset_change_fees_round_trip() { + let storage = cnidarium::TempStorage::new().await.unwrap(); + let snapshot = storage.latest_snapshot(); + let mut state = StateDelta::new(snapshot); + + let fee_components = FeeAssetChangeFeeComponents { + base: 123, + multiplier: 1, + }; + + state.put_fee_asset_change_fees(fee_components).unwrap(); + let retrieved_fee = state.get_fee_asset_change_fees().await.unwrap(); + assert_eq!(retrieved_fee, fee_components); + } + + #[tokio::test] + async fn fee_change_fees_round_trip() { + let storage = cnidarium::TempStorage::new().await.unwrap(); + let snapshot = storage.latest_snapshot(); + let mut state = StateDelta::new(snapshot); + + let fee_components = FeeChangeFeeComponents { + base: 123, + multiplier: 1, + }; + + state.put_fee_change_fees(fee_components).unwrap(); + let retrieved_fee = state.get_fee_change_fees().await.unwrap(); + assert_eq!(retrieved_fee, fee_components); + } + + #[tokio::test] + async fn ibc_relayer_change_fees_round_trip() { + let storage = cnidarium::TempStorage::new().await.unwrap(); + let snapshot = storage.latest_snapshot(); + let mut state = StateDelta::new(snapshot); + + let fee_components = IbcRelayerChangeFeeComponents { + base: 123, + multiplier: 1, + }; + + state.put_ibc_relayer_change_fees(fee_components).unwrap(); + let retrieved_fee = state.get_ibc_relayer_change_fees().await.unwrap(); + assert_eq!(retrieved_fee, fee_components); + } + + #[tokio::test] + async fn sudo_address_change_fees_round_trip() { + let storage = cnidarium::TempStorage::new().await.unwrap(); + let snapshot = storage.latest_snapshot(); + let mut state = StateDelta::new(snapshot); + + let fee_components = SudoAddressChangeFeeComponents { + base: 123, + multiplier: 1, + }; + + state.put_sudo_address_change_fees(fee_components).unwrap(); + let retrieved_fee = state.get_sudo_address_change_fees().await.unwrap(); + assert_eq!(retrieved_fee, fee_components); + } + + #[tokio::test] + async fn ibc_sudo_change_fees_round_trip() { + let storage = cnidarium::TempStorage::new().await.unwrap(); + let snapshot = storage.latest_snapshot(); + let mut state = StateDelta::new(snapshot); + + let fee_components = IbcSudoChangeFeeComponents { + base: 123, + multiplier: 1, + }; + + state.put_ibc_sudo_change_fees(fee_components).unwrap(); + let retrieved_fee = state.get_ibc_sudo_change_fees().await.unwrap(); + assert_eq!(retrieved_fee, fee_components); + } + + #[tokio::test] + async fn is_allowed_fee_asset() { + let storage = cnidarium::TempStorage::new().await.unwrap(); + let snapshot = storage.latest_snapshot(); + let mut state = StateDelta::new(snapshot); + + // non-existent fees assets return false + let asset = asset_0(); + assert!( + !state + .is_allowed_fee_asset(&asset) + .await + .expect("checking for allowed fee asset should not fail"), + "fee asset was expected to return false" + ); + + // existent fee assets return true + state.put_allowed_fee_asset(&asset).unwrap(); + assert!( + state + .is_allowed_fee_asset(&asset) + .await + .expect("checking for allowed fee asset should not fail"), + "fee asset was expected to be allowed" + ); + } + + #[tokio::test] + async fn can_delete_allowed_fee_assets_simple() { + let storage = cnidarium::TempStorage::new().await.unwrap(); + let snapshot = storage.latest_snapshot(); + let mut state = StateDelta::new(snapshot); + + // setup fee asset + let asset = asset_0(); + state.put_allowed_fee_asset(&asset).unwrap(); + assert!( + state + .is_allowed_fee_asset(&asset) + .await + .expect("checking for allowed fee asset should not fail"), + "fee asset was expected to be allowed" + ); + + // see can get fee asset + let assets = state.get_allowed_fee_assets().await.unwrap(); + assert_eq!( + assets, + vec![asset.to_ibc_prefixed()], + "expected returned allowed fee assets to match what was written in" + ); + + // can delete + state.delete_allowed_fee_asset(&asset); + + // see is deleted + let assets = state.get_allowed_fee_assets().await.unwrap(); + assert!(assets.is_empty(), "fee assets should be empty post delete"); + } + + #[tokio::test] + async fn can_delete_allowed_fee_assets_complex() { + let storage = cnidarium::TempStorage::new().await.unwrap(); + let snapshot = storage.latest_snapshot(); + let mut state = StateDelta::new(snapshot); + + // setup fee assets + let asset_first = asset_0(); + state.put_allowed_fee_asset(&asset_first).unwrap(); + assert!( + state + .is_allowed_fee_asset(&asset_first) + .await + .expect("checking for allowed fee asset should not fail"), + "fee asset was expected to be allowed" + ); + let asset_second = asset_1(); + state.put_allowed_fee_asset(&asset_second).unwrap(); + assert!( + state + .is_allowed_fee_asset(&asset_second) + .await + .expect("checking for allowed fee asset should not fail"), + "fee asset was expected to be allowed" + ); + let asset_third = asset_2(); + state.put_allowed_fee_asset(&asset_third).unwrap(); + assert!( + state + .is_allowed_fee_asset(&asset_third) + .await + .expect("checking for allowed fee asset should not fail"), + "fee asset was expected to be allowed" + ); + + // can delete + state.delete_allowed_fee_asset(&asset_second); + + // see is deleted + let assets = HashSet::<_>::from_iter(state.get_allowed_fee_assets().await.unwrap()); + assert_eq!( + assets, + HashSet::from_iter(vec![ + asset_first.to_ibc_prefixed(), + asset_third.to_ibc_prefixed() + ]), + "delete for allowed fee asset did not behave as expected" + ); + } +} diff --git a/crates/astria-sequencer/src/fees/storage/keys.rs b/crates/astria-sequencer/src/fees/storage/keys.rs new file mode 100644 index 0000000000..7c5572acc4 --- /dev/null +++ b/crates/astria-sequencer/src/fees/storage/keys.rs @@ -0,0 +1,120 @@ +use std::borrow::Cow; + +use astria_core::primitive::v1::asset::IbcPrefixed; +use astria_eyre::eyre::{ + self, + eyre, + Context as _, +}; + +use crate::storage::keys::Asset; + +pub(in crate::fees) const TRANSFER: &str = "fees/transfer"; +pub(in crate::fees) const SEQUENCE: &str = "fees/sequence"; +pub(in crate::fees) const ICS20_WITHDRAWAL: &str = "fees/ics20_withdrawal"; +pub(in crate::fees) const INIT_BRIDGE_ACCOUNT: &str = "fees/init_bridge_account"; +pub(in crate::fees) const BRIDGE_LOCK: &str = "fees/bridge_lock"; +pub(in crate::fees) const BRIDGE_UNLOCK: &str = "fees/bridge_unlock"; +pub(in crate::fees) const BRIDGE_SUDO_CHANGE: &str = "fees/bridge_sudo_change"; +pub(in crate::fees) const IBC_RELAY: &str = "fees/ibc_relay"; +pub(in crate::fees) const VALIDATOR_UPDATE: &str = "fees/validator_update"; +pub(in crate::fees) const FEE_ASSET_CHANGE: &str = "fees/fee_asset_change"; +pub(in crate::fees) const FEE_CHANGE: &str = "fees/fee_change"; +pub(in crate::fees) const IBC_RELAYER_CHANGE: &str = "fees/ibc_relayer_change"; +pub(in crate::fees) const SUDO_ADDRESS_CHANGE: &str = "fees/sudo_address_change"; +pub(in crate::fees) const IBC_SUDO_CHANGE: &str = "fees/ibc_sudo_change"; +pub(in crate::fees) const BLOCK: &str = "fees/block"; // NOTE: `BLOCK` is only used in the ephemeral store. +pub(in crate::fees) const ALLOWED_ASSET_PREFIX: &str = "fees/allowed_asset/"; + +pub(in crate::fees) fn allowed_asset<'a, TAsset>(asset: &'a TAsset) -> String +where + &'a TAsset: Into>, +{ + format!("{ALLOWED_ASSET_PREFIX}{}", Asset::from(asset)) +} + +pub(in crate::fees) fn extract_asset_from_allowed_asset_key( + key: &str, +) -> eyre::Result { + extract_asset_from_key(key, ALLOWED_ASSET_PREFIX) + .wrap_err("failed to extract asset from fee asset key") +} + +fn extract_asset_from_key(key: &str, prefix: &str) -> eyre::Result { + let suffix = key + .strip_prefix(prefix) + .ok_or_else(|| eyre!("key `{key}` did not have prefix `{prefix}`"))?; + suffix.parse().wrap_err_with(|| { + format!("failed to parse suffix `{suffix}` of key `{key}` as an ibc-prefixed asset",) + }) +} + +#[cfg(test)] +mod tests { + use astria_core::primitive::v1::asset::Denom; + use insta::assert_snapshot; + + use super::*; + + const COMPONENT_PREFIX: &str = "fees/"; + + fn test_asset() -> Denom { + "an/asset/with/a/prefix".parse().unwrap() + } + + #[test] + fn keys_should_not_change() { + // NOTE: This helper struct is just to avoid having 14 snapshot files to contend with. + // NOTE: `BLOCK` is only used in the ephemeral store, so isn't included here. + assert_snapshot!("bridge_lock_fees_key", BRIDGE_LOCK); + assert_snapshot!("bridge_sudo_change_fees_key", BRIDGE_SUDO_CHANGE); + assert_snapshot!("bridge_unlock_fees_key", BRIDGE_UNLOCK); + assert_snapshot!("fee_asset_change_fees_key", FEE_ASSET_CHANGE); + assert_snapshot!("allowed_asset_prefix", ALLOWED_ASSET_PREFIX); + assert_snapshot!("fee_change_fees_key", FEE_CHANGE); + assert_snapshot!("ibc_relay_fees_key", IBC_RELAY); + assert_snapshot!("ibc_relayer_change_fees_key", IBC_RELAYER_CHANGE); + assert_snapshot!("ibc_sudo_change_fees_key", IBC_SUDO_CHANGE); + assert_snapshot!("ics20_withdrawal_fees_key", ICS20_WITHDRAWAL); + assert_snapshot!("init_bridge_account_fees_key", INIT_BRIDGE_ACCOUNT); + assert_snapshot!("sequence_fees_key", SEQUENCE); + assert_snapshot!("sudo_address_change_fees_key", SUDO_ADDRESS_CHANGE); + assert_snapshot!("transer_fees_key", TRANSFER); + assert_snapshot!("validator_update_fees_key", VALIDATOR_UPDATE); + assert_snapshot!("allowed_asset_key", allowed_asset(&test_asset())); + } + + #[test] + fn keys_should_have_component_prefix() { + assert!(TRANSFER.starts_with(COMPONENT_PREFIX)); + assert!(SEQUENCE.starts_with(COMPONENT_PREFIX)); + assert!(ICS20_WITHDRAWAL.starts_with(COMPONENT_PREFIX)); + assert!(INIT_BRIDGE_ACCOUNT.starts_with(COMPONENT_PREFIX)); + assert!(BRIDGE_LOCK.starts_with(COMPONENT_PREFIX)); + assert!(BRIDGE_UNLOCK.starts_with(COMPONENT_PREFIX)); + assert!(BRIDGE_SUDO_CHANGE.starts_with(COMPONENT_PREFIX)); + assert!(IBC_RELAY.starts_with(COMPONENT_PREFIX)); + assert!(VALIDATOR_UPDATE.starts_with(COMPONENT_PREFIX)); + assert!(FEE_ASSET_CHANGE.starts_with(COMPONENT_PREFIX)); + assert!(FEE_CHANGE.starts_with(COMPONENT_PREFIX)); + assert!(IBC_RELAYER_CHANGE.starts_with(COMPONENT_PREFIX)); + assert!(SUDO_ADDRESS_CHANGE.starts_with(COMPONENT_PREFIX)); + assert!(IBC_SUDO_CHANGE.starts_with(COMPONENT_PREFIX)); + assert!(ALLOWED_ASSET_PREFIX.starts_with(COMPONENT_PREFIX)); + assert!(allowed_asset(&test_asset()).starts_with(COMPONENT_PREFIX)); + } + + #[test] + fn prefixes_should_be_prefixes_of_relevant_keys() { + assert!(allowed_asset(&test_asset()).starts_with(ALLOWED_ASSET_PREFIX)); + } + + #[test] + fn should_extract_asset_from_key() { + let asset = IbcPrefixed::new([1; 32]); + + let key = allowed_asset(&asset); + let recovered_asset = extract_asset_from_allowed_asset_key(&key).unwrap(); + assert_eq!(asset, recovered_asset); + } +} diff --git a/crates/astria-sequencer/src/fees/storage/mod.rs b/crates/astria-sequencer/src/fees/storage/mod.rs new file mode 100644 index 0000000000..54a4fecef3 --- /dev/null +++ b/crates/astria-sequencer/src/fees/storage/mod.rs @@ -0,0 +1,20 @@ +pub(super) mod keys; +mod values; + +pub(crate) use values::Value; +pub(super) use values::{ + BridgeLockFeeComponentsStorage, + BridgeSudoChangeFeeComponentsStorage, + BridgeUnlockFeeComponentsStorage, + FeeAssetChangeFeeComponentsStorage, + FeeChangeFeeComponentsStorage, + IbcRelayFeeComponentsStorage, + IbcRelayerChangeFeeComponentsStorage, + IbcSudoChangeFeeComponentsStorage, + Ics20WithdrawalFeeComponentsStorage, + InitBridgeAccountFeeComponentsStorage, + SequenceFeeComponentsStorage, + SudoAddressChangeFeeComponentsStorage, + TransferFeeComponentsStorage, + ValidatorUpdateFeeComponentsStorage, +}; diff --git a/crates/astria-sequencer/src/fees/storage/snapshots/astria_sequencer__fees__storage__keys__tests__allowed_asset_key.snap b/crates/astria-sequencer/src/fees/storage/snapshots/astria_sequencer__fees__storage__keys__tests__allowed_asset_key.snap new file mode 100644 index 0000000000..126d70b93b --- /dev/null +++ b/crates/astria-sequencer/src/fees/storage/snapshots/astria_sequencer__fees__storage__keys__tests__allowed_asset_key.snap @@ -0,0 +1,5 @@ +--- +source: crates/astria-sequencer/src/fees/storage/keys.rs +expression: allowed_asset(&test_asset()) +--- +fees/allowed_asset/ibc/be429a02d00837245167a2616674a979a2ac6f9806468b48a975b156ad711320 diff --git a/crates/astria-sequencer/src/fees/storage/snapshots/astria_sequencer__fees__storage__keys__tests__allowed_asset_prefix.snap b/crates/astria-sequencer/src/fees/storage/snapshots/astria_sequencer__fees__storage__keys__tests__allowed_asset_prefix.snap new file mode 100644 index 0000000000..4ac406120f --- /dev/null +++ b/crates/astria-sequencer/src/fees/storage/snapshots/astria_sequencer__fees__storage__keys__tests__allowed_asset_prefix.snap @@ -0,0 +1,5 @@ +--- +source: crates/astria-sequencer/src/fees/storage/keys.rs +expression: ALLOWED_ASSET_PREFIX +--- +fees/allowed_asset/ diff --git a/crates/astria-sequencer/src/fees/storage/snapshots/astria_sequencer__fees__storage__keys__tests__bridge_lock_fees_key.snap b/crates/astria-sequencer/src/fees/storage/snapshots/astria_sequencer__fees__storage__keys__tests__bridge_lock_fees_key.snap new file mode 100644 index 0000000000..ef5f1db644 --- /dev/null +++ b/crates/astria-sequencer/src/fees/storage/snapshots/astria_sequencer__fees__storage__keys__tests__bridge_lock_fees_key.snap @@ -0,0 +1,5 @@ +--- +source: crates/astria-sequencer/src/fees/storage/keys.rs +expression: BRIDGE_LOCK +--- +fees/bridge_lock diff --git a/crates/astria-sequencer/src/fees/storage/snapshots/astria_sequencer__fees__storage__keys__tests__bridge_sudo_change_fees_key.snap b/crates/astria-sequencer/src/fees/storage/snapshots/astria_sequencer__fees__storage__keys__tests__bridge_sudo_change_fees_key.snap new file mode 100644 index 0000000000..fdd09e5817 --- /dev/null +++ b/crates/astria-sequencer/src/fees/storage/snapshots/astria_sequencer__fees__storage__keys__tests__bridge_sudo_change_fees_key.snap @@ -0,0 +1,5 @@ +--- +source: crates/astria-sequencer/src/fees/storage/keys.rs +expression: BRIDGE_SUDO_CHANGE +--- +fees/bridge_sudo_change diff --git a/crates/astria-sequencer/src/fees/storage/snapshots/astria_sequencer__fees__storage__keys__tests__bridge_unlock_fees_key.snap b/crates/astria-sequencer/src/fees/storage/snapshots/astria_sequencer__fees__storage__keys__tests__bridge_unlock_fees_key.snap new file mode 100644 index 0000000000..b78c4ef3d8 --- /dev/null +++ b/crates/astria-sequencer/src/fees/storage/snapshots/astria_sequencer__fees__storage__keys__tests__bridge_unlock_fees_key.snap @@ -0,0 +1,5 @@ +--- +source: crates/astria-sequencer/src/fees/storage/keys.rs +expression: BRIDGE_UNLOCK +--- +fees/bridge_unlock diff --git a/crates/astria-sequencer/src/fees/storage/snapshots/astria_sequencer__fees__storage__keys__tests__fee_asset_change_fees_key.snap b/crates/astria-sequencer/src/fees/storage/snapshots/astria_sequencer__fees__storage__keys__tests__fee_asset_change_fees_key.snap new file mode 100644 index 0000000000..34f0f3260c --- /dev/null +++ b/crates/astria-sequencer/src/fees/storage/snapshots/astria_sequencer__fees__storage__keys__tests__fee_asset_change_fees_key.snap @@ -0,0 +1,5 @@ +--- +source: crates/astria-sequencer/src/fees/storage/keys.rs +expression: FEE_ASSET_CHANGE +--- +fees/fee_asset_change diff --git a/crates/astria-sequencer/src/fees/storage/snapshots/astria_sequencer__fees__storage__keys__tests__fee_change_fees_key.snap b/crates/astria-sequencer/src/fees/storage/snapshots/astria_sequencer__fees__storage__keys__tests__fee_change_fees_key.snap new file mode 100644 index 0000000000..1a06a31d55 --- /dev/null +++ b/crates/astria-sequencer/src/fees/storage/snapshots/astria_sequencer__fees__storage__keys__tests__fee_change_fees_key.snap @@ -0,0 +1,5 @@ +--- +source: crates/astria-sequencer/src/fees/storage/keys.rs +expression: FEE_CHANGE +--- +fees/fee_change diff --git a/crates/astria-sequencer/src/fees/storage/snapshots/astria_sequencer__fees__storage__keys__tests__ibc_relay_fees_key.snap b/crates/astria-sequencer/src/fees/storage/snapshots/astria_sequencer__fees__storage__keys__tests__ibc_relay_fees_key.snap new file mode 100644 index 0000000000..35c47020de --- /dev/null +++ b/crates/astria-sequencer/src/fees/storage/snapshots/astria_sequencer__fees__storage__keys__tests__ibc_relay_fees_key.snap @@ -0,0 +1,5 @@ +--- +source: crates/astria-sequencer/src/fees/storage/keys.rs +expression: IBC_RELAY +--- +fees/ibc_relay diff --git a/crates/astria-sequencer/src/fees/storage/snapshots/astria_sequencer__fees__storage__keys__tests__ibc_relayer_change_fees_key.snap b/crates/astria-sequencer/src/fees/storage/snapshots/astria_sequencer__fees__storage__keys__tests__ibc_relayer_change_fees_key.snap new file mode 100644 index 0000000000..450aedb826 --- /dev/null +++ b/crates/astria-sequencer/src/fees/storage/snapshots/astria_sequencer__fees__storage__keys__tests__ibc_relayer_change_fees_key.snap @@ -0,0 +1,5 @@ +--- +source: crates/astria-sequencer/src/fees/storage/keys.rs +expression: IBC_RELAYER_CHANGE +--- +fees/ibc_relayer_change diff --git a/crates/astria-sequencer/src/fees/storage/snapshots/astria_sequencer__fees__storage__keys__tests__ibc_sudo_change_fees_key.snap b/crates/astria-sequencer/src/fees/storage/snapshots/astria_sequencer__fees__storage__keys__tests__ibc_sudo_change_fees_key.snap new file mode 100644 index 0000000000..951e70eede --- /dev/null +++ b/crates/astria-sequencer/src/fees/storage/snapshots/astria_sequencer__fees__storage__keys__tests__ibc_sudo_change_fees_key.snap @@ -0,0 +1,5 @@ +--- +source: crates/astria-sequencer/src/fees/storage/keys.rs +expression: IBC_SUDO_CHANGE +--- +fees/ibc_sudo_change diff --git a/crates/astria-sequencer/src/fees/storage/snapshots/astria_sequencer__fees__storage__keys__tests__ics20_withdrawal_fees_key.snap b/crates/astria-sequencer/src/fees/storage/snapshots/astria_sequencer__fees__storage__keys__tests__ics20_withdrawal_fees_key.snap new file mode 100644 index 0000000000..e880397c1f --- /dev/null +++ b/crates/astria-sequencer/src/fees/storage/snapshots/astria_sequencer__fees__storage__keys__tests__ics20_withdrawal_fees_key.snap @@ -0,0 +1,5 @@ +--- +source: crates/astria-sequencer/src/fees/storage/keys.rs +expression: ICS20_WITHDRAWAL +--- +fees/ics20_withdrawal diff --git a/crates/astria-sequencer/src/fees/storage/snapshots/astria_sequencer__fees__storage__keys__tests__init_bridge_account_fees_key.snap b/crates/astria-sequencer/src/fees/storage/snapshots/astria_sequencer__fees__storage__keys__tests__init_bridge_account_fees_key.snap new file mode 100644 index 0000000000..2dc1f38fd0 --- /dev/null +++ b/crates/astria-sequencer/src/fees/storage/snapshots/astria_sequencer__fees__storage__keys__tests__init_bridge_account_fees_key.snap @@ -0,0 +1,5 @@ +--- +source: crates/astria-sequencer/src/fees/storage/keys.rs +expression: INIT_BRIDGE_ACCOUNT +--- +fees/init_bridge_account diff --git a/crates/astria-sequencer/src/fees/storage/snapshots/astria_sequencer__fees__storage__keys__tests__sequence_fees_key.snap b/crates/astria-sequencer/src/fees/storage/snapshots/astria_sequencer__fees__storage__keys__tests__sequence_fees_key.snap new file mode 100644 index 0000000000..3807479857 --- /dev/null +++ b/crates/astria-sequencer/src/fees/storage/snapshots/astria_sequencer__fees__storage__keys__tests__sequence_fees_key.snap @@ -0,0 +1,5 @@ +--- +source: crates/astria-sequencer/src/fees/storage/keys.rs +expression: SEQUENCE +--- +fees/sequence diff --git a/crates/astria-sequencer/src/fees/storage/snapshots/astria_sequencer__fees__storage__keys__tests__sudo_address_change_fees_key.snap b/crates/astria-sequencer/src/fees/storage/snapshots/astria_sequencer__fees__storage__keys__tests__sudo_address_change_fees_key.snap new file mode 100644 index 0000000000..0bb7fbe718 --- /dev/null +++ b/crates/astria-sequencer/src/fees/storage/snapshots/astria_sequencer__fees__storage__keys__tests__sudo_address_change_fees_key.snap @@ -0,0 +1,5 @@ +--- +source: crates/astria-sequencer/src/fees/storage/keys.rs +expression: SUDO_ADDRESS_CHANGE +--- +fees/sudo_address_change diff --git a/crates/astria-sequencer/src/fees/storage/snapshots/astria_sequencer__fees__storage__keys__tests__transer_fees_key.snap b/crates/astria-sequencer/src/fees/storage/snapshots/astria_sequencer__fees__storage__keys__tests__transer_fees_key.snap new file mode 100644 index 0000000000..b0539e07d2 --- /dev/null +++ b/crates/astria-sequencer/src/fees/storage/snapshots/astria_sequencer__fees__storage__keys__tests__transer_fees_key.snap @@ -0,0 +1,5 @@ +--- +source: crates/astria-sequencer/src/fees/storage/keys.rs +expression: TRANSFER +--- +fees/transfer diff --git a/crates/astria-sequencer/src/fees/storage/snapshots/astria_sequencer__fees__storage__keys__tests__validator_update_fees_key.snap b/crates/astria-sequencer/src/fees/storage/snapshots/astria_sequencer__fees__storage__keys__tests__validator_update_fees_key.snap new file mode 100644 index 0000000000..9d5fd29457 --- /dev/null +++ b/crates/astria-sequencer/src/fees/storage/snapshots/astria_sequencer__fees__storage__keys__tests__validator_update_fees_key.snap @@ -0,0 +1,5 @@ +--- +source: crates/astria-sequencer/src/fees/storage/keys.rs +expression: VALIDATOR_UPDATE +--- +fees/validator_update diff --git a/crates/astria-sequencer/src/fees/storage/values.rs b/crates/astria-sequencer/src/fees/storage/values.rs new file mode 100644 index 0000000000..906371cb44 --- /dev/null +++ b/crates/astria-sequencer/src/fees/storage/values.rs @@ -0,0 +1,206 @@ +use astria_core::protocol::fees::v1alpha1::{ + BridgeLockFeeComponents, + BridgeSudoChangeFeeComponents, + BridgeUnlockFeeComponents, + FeeAssetChangeFeeComponents, + FeeChangeFeeComponents, + IbcRelayFeeComponents, + IbcRelayerChangeFeeComponents, + IbcSudoChangeFeeComponents, + Ics20WithdrawalFeeComponents, + InitBridgeAccountFeeComponents, + SequenceFeeComponents, + SudoAddressChangeFeeComponents, + TransferFeeComponents, + ValidatorUpdateFeeComponents, +}; +use astria_eyre::eyre::bail; +use borsh::{ + BorshDeserialize, + BorshSerialize, +}; + +#[derive(Debug, BorshSerialize, BorshDeserialize)] +pub(crate) struct Value(ValueImpl); + +#[derive(Debug, BorshSerialize, BorshDeserialize)] +#[expect( + clippy::enum_variant_names, + reason = "want to make it clear that these are fees and not actions" +)] +enum ValueImpl { + TransferFees(TransferFeeComponentsStorage), + SequenceFees(SequenceFeeComponentsStorage), + Ics20WithdrawalFees(Ics20WithdrawalFeeComponentsStorage), + InitBridgeAccountFees(InitBridgeAccountFeeComponentsStorage), + BridgeLockFees(BridgeLockFeeComponentsStorage), + BridgeUnlockFees(BridgeUnlockFeeComponentsStorage), + BridgeSudoChangeFees(BridgeSudoChangeFeeComponentsStorage), + IbcRelayFees(IbcRelayFeeComponentsStorage), + ValidatorUpdateFees(ValidatorUpdateFeeComponentsStorage), + FeeAssetChangeFees(FeeAssetChangeFeeComponentsStorage), + FeeChangeFees(FeeChangeFeeComponentsStorage), + IbcRelayerChangeFees(IbcRelayerChangeFeeComponentsStorage), + IbcSudoChangeFees(IbcSudoChangeFeeComponentsStorage), + SudoAddressChangeFees(SudoAddressChangeFeeComponentsStorage), +} + +macro_rules! impl_from_for_fee_component{ + ( $( $domain_ty:ty => $storage_ty:ty),* $(,)? ) => { + $( + impl From<$domain_ty> for $storage_ty { + fn from(val: $domain_ty) -> Self { + Self{base: val.base, multiplier: val.multiplier} + } + } + impl From<$storage_ty> for $domain_ty { + fn from(val: $storage_ty) -> Self { + Self{base: val.base, multiplier: val.multiplier} + } + } + )* + } +} + +macro_rules! impl_from_for_fee_storage { + ( $( $storage_ty:ty => $value_impl:ident),* $(,)? ) => { + $( + impl<'a> From<$storage_ty> for crate::storage::StoredValue<'a> { + fn from(fees: $storage_ty) -> Self { + crate::storage::StoredValue::Fees(Value(ValueImpl::$value_impl(fees))) + } + } + impl<'a> TryFrom> for $storage_ty { + type Error = astria_eyre::eyre::Error; + + fn try_from(value: crate::storage::StoredValue<'a>) -> Result { + let crate::storage::StoredValue::Fees(Value(ValueImpl::$value_impl(fees))) = value else { + let value_impl_ty = concat!("ValueImpl::", stringify!($value_impl)); + bail!( + "fees stored value type mismatch: expected {value_impl_ty}, found {value:?}" + ); + }; + Ok(fees) + } + } + )* + } +} + +#[derive(Debug, BorshSerialize, BorshDeserialize)] +pub(in crate::fees) struct TransferFeeComponentsStorage { + pub base: u128, + pub multiplier: u128, +} + +#[derive(Debug, BorshSerialize, BorshDeserialize)] +pub(in crate::fees) struct SequenceFeeComponentsStorage { + pub base: u128, + pub multiplier: u128, +} + +#[derive(Debug, BorshSerialize, BorshDeserialize)] +pub(in crate::fees) struct Ics20WithdrawalFeeComponentsStorage { + pub base: u128, + pub multiplier: u128, +} + +#[derive(Debug, BorshSerialize, BorshDeserialize)] +pub(in crate::fees) struct InitBridgeAccountFeeComponentsStorage { + pub base: u128, + pub multiplier: u128, +} + +#[derive(Debug, BorshSerialize, BorshDeserialize)] +pub(in crate::fees) struct BridgeLockFeeComponentsStorage { + pub base: u128, + pub multiplier: u128, +} + +#[derive(Debug, BorshSerialize, BorshDeserialize)] +pub(in crate::fees) struct BridgeUnlockFeeComponentsStorage { + pub base: u128, + pub multiplier: u128, +} + +#[derive(Debug, BorshSerialize, BorshDeserialize)] +pub(in crate::fees) struct BridgeSudoChangeFeeComponentsStorage { + pub base: u128, + pub multiplier: u128, +} + +#[derive(Debug, BorshSerialize, BorshDeserialize)] +pub(in crate::fees) struct IbcRelayFeeComponentsStorage { + pub base: u128, + pub multiplier: u128, +} + +#[derive(Debug, BorshSerialize, BorshDeserialize)] +pub(in crate::fees) struct ValidatorUpdateFeeComponentsStorage { + pub base: u128, + pub multiplier: u128, +} + +#[derive(Debug, BorshSerialize, BorshDeserialize)] +pub(in crate::fees) struct FeeAssetChangeFeeComponentsStorage { + pub base: u128, + pub multiplier: u128, +} + +#[derive(Debug, BorshSerialize, BorshDeserialize)] +pub(in crate::fees) struct FeeChangeFeeComponentsStorage { + pub base: u128, + pub multiplier: u128, +} + +#[derive(Debug, BorshSerialize, BorshDeserialize)] +pub(in crate::fees) struct IbcRelayerChangeFeeComponentsStorage { + pub base: u128, + pub multiplier: u128, +} + +#[derive(Debug, BorshSerialize, BorshDeserialize)] +pub(in crate::fees) struct IbcSudoChangeFeeComponentsStorage { + pub base: u128, + pub multiplier: u128, +} + +#[derive(Debug, BorshSerialize, BorshDeserialize)] +pub(in crate::fees) struct SudoAddressChangeFeeComponentsStorage { + pub base: u128, + pub multiplier: u128, +} + +impl_from_for_fee_component!( + TransferFeeComponents => TransferFeeComponentsStorage, + SequenceFeeComponents => SequenceFeeComponentsStorage, + Ics20WithdrawalFeeComponents => Ics20WithdrawalFeeComponentsStorage, + InitBridgeAccountFeeComponents => InitBridgeAccountFeeComponentsStorage, + BridgeLockFeeComponents => BridgeLockFeeComponentsStorage, + BridgeUnlockFeeComponents => BridgeUnlockFeeComponentsStorage, + BridgeSudoChangeFeeComponents => BridgeSudoChangeFeeComponentsStorage, + IbcRelayFeeComponents => IbcRelayFeeComponentsStorage, + ValidatorUpdateFeeComponents => ValidatorUpdateFeeComponentsStorage, + FeeAssetChangeFeeComponents => FeeAssetChangeFeeComponentsStorage, + FeeChangeFeeComponents => FeeChangeFeeComponentsStorage, + IbcRelayerChangeFeeComponents => IbcRelayerChangeFeeComponentsStorage, + IbcSudoChangeFeeComponents => IbcSudoChangeFeeComponentsStorage, + SudoAddressChangeFeeComponents => SudoAddressChangeFeeComponentsStorage, +); + +impl_from_for_fee_storage!( + TransferFeeComponentsStorage => TransferFees, + SequenceFeeComponentsStorage => SequenceFees, + Ics20WithdrawalFeeComponentsStorage => Ics20WithdrawalFees, + InitBridgeAccountFeeComponentsStorage => InitBridgeAccountFees, + BridgeLockFeeComponentsStorage => BridgeLockFees, + BridgeUnlockFeeComponentsStorage => BridgeUnlockFees, + BridgeSudoChangeFeeComponentsStorage => BridgeSudoChangeFees, + IbcRelayFeeComponentsStorage => IbcRelayFees, + ValidatorUpdateFeeComponentsStorage => ValidatorUpdateFees, + FeeAssetChangeFeeComponentsStorage => FeeAssetChangeFees, + FeeChangeFeeComponentsStorage => FeeChangeFees, + IbcRelayerChangeFeeComponentsStorage => IbcRelayerChangeFees, + IbcSudoChangeFeeComponentsStorage => IbcSudoChangeFees, + SudoAddressChangeFeeComponentsStorage => SudoAddressChangeFees, +); diff --git a/crates/astria-sequencer/src/fees/tests.rs b/crates/astria-sequencer/src/fees/tests.rs new file mode 100644 index 0000000000..dbfa66725e --- /dev/null +++ b/crates/astria-sequencer/src/fees/tests.rs @@ -0,0 +1,469 @@ +use std::sync::Arc; + +use astria_core::{ + primitive::v1::{ + asset, + Address, + RollupId, + TransactionId, + ADDRESS_LEN, + ROLLUP_ID_LEN, + TRANSACTION_ID_LEN, + }, + protocol::{ + fees::v1alpha1::{ + BridgeLockFeeComponents, + BridgeSudoChangeFeeComponents, + InitBridgeAccountFeeComponents, + SequenceFeeComponents, + TransferFeeComponents, + }, + transaction::v1alpha1::{ + action::{ + BridgeLock, + BridgeSudoChange, + InitBridgeAccount, + Sequence, + Transfer, + }, + UnsignedTransaction, + }, + }, + sequencerblock::v1alpha1::block::Deposit, +}; +use cnidarium::StateDelta; + +use super::base_deposit_fee; +use crate::{ + accounts::StateWriteExt as _, + address::StateWriteExt as _, + app::{ + test_utils::{ + get_alice_signing_key, + get_bridge_signing_key, + initialize_app_with_storage, + BOB_ADDRESS, + }, + ActionHandler as _, + }, + bridge::StateWriteExt as _, + fees::{ + StateReadExt as _, + StateWriteExt as _, + DEPOSIT_BASE_FEE, + }, + test_utils::{ + assert_eyre_error, + astria_address, + astria_address_from_hex_string, + calculate_sequence_action_fee_from_state, + nria, + ASTRIA_PREFIX, + }, + transaction::{ + StateWriteExt as _, + TransactionContext, + }, +}; + +fn test_asset() -> asset::Denom { + "test".parse().unwrap() +} + +#[tokio::test] +async fn ensure_correct_block_fees_transfer() { + let (_, storage) = initialize_app_with_storage(None, vec![]).await; + let snapshot = storage.latest_snapshot(); + let mut state = StateDelta::new(snapshot); + let transfer_base = 1; + state + .put_transfer_fees(TransferFeeComponents { + base: transfer_base, + multiplier: 0, + }) + .unwrap(); + + let alice = get_alice_signing_key(); + let bob_address = astria_address_from_hex_string(BOB_ADDRESS); + let actions = vec![ + Transfer { + to: bob_address, + amount: 1000, + asset: nria().into(), + fee_asset: nria().into(), + } + .into(), + ]; + + let tx = UnsignedTransaction::builder() + .actions(actions) + .chain_id("test") + .try_build() + .unwrap(); + let signed_tx = Arc::new(tx.into_signed(&alice)); + signed_tx.check_and_execute(&mut state).await.unwrap(); + + let total_block_fees: u128 = state + .get_block_fees() + .into_iter() + .map(|fee| fee.amount()) + .sum(); + assert_eq!(total_block_fees, transfer_base); +} + +#[tokio::test] +async fn ensure_correct_block_fees_sequence() { + let (_, storage) = initialize_app_with_storage(None, vec![]).await; + let snapshot = storage.latest_snapshot(); + let mut state = StateDelta::new(snapshot); + state + .put_sequence_fees(SequenceFeeComponents { + base: 1, + multiplier: 1, + }) + .unwrap(); + + let alice = get_alice_signing_key(); + let data = b"hello world".to_vec(); + + let actions = vec![ + Sequence { + rollup_id: RollupId::from_unhashed_bytes(b"testchainid"), + data: data.clone().into(), + fee_asset: nria().into(), + } + .into(), + ]; + + let tx = UnsignedTransaction::builder() + .actions(actions) + .chain_id("test") + .try_build() + .unwrap(); + let signed_tx = Arc::new(tx.into_signed(&alice)); + signed_tx.check_and_execute(&mut state).await.unwrap(); + let total_block_fees: u128 = state + .get_block_fees() + .into_iter() + .map(|fee| fee.amount()) + .sum(); + let expected_fees = calculate_sequence_action_fee_from_state(&data, &state).await; + assert_eq!(total_block_fees, expected_fees); +} + +#[tokio::test] +async fn ensure_correct_block_fees_init_bridge_acct() { + let (_, storage) = initialize_app_with_storage(None, vec![]).await; + let snapshot = storage.latest_snapshot(); + let mut state = StateDelta::new(snapshot); + let init_bridge_account_base = 1; + state + .put_init_bridge_account_fees(InitBridgeAccountFeeComponents { + base: init_bridge_account_base, + multiplier: 0, + }) + .unwrap(); + + let alice = get_alice_signing_key(); + + let actions = vec![ + InitBridgeAccount { + rollup_id: RollupId::from_unhashed_bytes(b"testchainid"), + asset: nria().into(), + fee_asset: nria().into(), + sudo_address: None, + withdrawer_address: None, + } + .into(), + ]; + + let tx = UnsignedTransaction::builder() + .actions(actions) + .chain_id("test") + .try_build() + .unwrap(); + let signed_tx = Arc::new(tx.into_signed(&alice)); + signed_tx.check_and_execute(&mut state).await.unwrap(); + + let total_block_fees: u128 = state + .get_block_fees() + .into_iter() + .map(|fee| fee.amount()) + .sum(); + assert_eq!(total_block_fees, init_bridge_account_base); +} + +#[tokio::test] +async fn ensure_correct_block_fees_bridge_lock() { + let alice = get_alice_signing_key(); + let bridge = get_bridge_signing_key(); + let bridge_address = astria_address(&bridge.address_bytes()); + let rollup_id = RollupId::from_unhashed_bytes(b"testchainid"); + let starting_index_of_action = 0; + + let (_, storage) = initialize_app_with_storage(None, vec![]).await; + let snapshot = storage.latest_snapshot(); + let mut state = StateDelta::new(snapshot); + + let transfer_base = 1; + let bridge_lock_byte_cost_multiplier = 1; + + state + .put_transfer_fees(TransferFeeComponents { + base: transfer_base, + multiplier: 0, + }) + .unwrap(); + state + .put_bridge_lock_fees(BridgeLockFeeComponents { + base: transfer_base, + multiplier: bridge_lock_byte_cost_multiplier, + }) + .unwrap(); + state + .put_bridge_account_rollup_id(&bridge_address, rollup_id) + .unwrap(); + state + .put_bridge_account_ibc_asset(&bridge_address, nria()) + .unwrap(); + + let actions = vec![ + BridgeLock { + to: bridge_address, + amount: 1, + asset: nria().into(), + fee_asset: nria().into(), + destination_chain_address: rollup_id.to_string(), + } + .into(), + ]; + + let tx = UnsignedTransaction::builder() + .actions(actions) + .chain_id("test") + .try_build() + .unwrap(); + let signed_tx = Arc::new(tx.into_signed(&alice)); + signed_tx.check_and_execute(&mut state).await.unwrap(); + + let test_deposit = Deposit { + bridge_address, + rollup_id, + amount: 1, + asset: nria().into(), + destination_chain_address: rollup_id.to_string(), + source_transaction_id: signed_tx.id(), + source_action_index: starting_index_of_action, + }; + + let total_block_fees: u128 = state + .get_block_fees() + .into_iter() + .map(|fee| fee.amount()) + .sum(); + let expected_fees = transfer_base + + (base_deposit_fee(&test_deposit.asset, &test_deposit.destination_chain_address) + * bridge_lock_byte_cost_multiplier); + assert_eq!(total_block_fees, expected_fees); +} + +#[tokio::test] +async fn ensure_correct_block_fees_bridge_sudo_change() { + let alice = get_alice_signing_key(); + let alice_address = astria_address(&alice.address_bytes()); + let bridge = get_bridge_signing_key(); + let bridge_address = astria_address(&bridge.address_bytes()); + + let (_, storage) = initialize_app_with_storage(None, vec![]).await; + let snapshot = storage.latest_snapshot(); + let mut state = StateDelta::new(snapshot); + + let sudo_change_base = 1; + state + .put_bridge_sudo_change_fees(BridgeSudoChangeFeeComponents { + base: sudo_change_base, + multiplier: 0, + }) + .unwrap(); + state + .put_bridge_account_sudo_address(&bridge_address, alice_address) + .unwrap(); + state + .increase_balance(&bridge_address, &nria(), 1) + .await + .unwrap(); + + let actions = vec![ + BridgeSudoChange { + bridge_address, + new_sudo_address: None, + new_withdrawer_address: None, + fee_asset: nria().into(), + } + .into(), + ]; + + let tx = UnsignedTransaction::builder() + .actions(actions) + .chain_id("test") + .try_build() + .unwrap(); + let signed_tx = Arc::new(tx.into_signed(&alice)); + signed_tx.check_and_execute(&mut state).await.unwrap(); + + let total_block_fees: u128 = state + .get_block_fees() + .into_iter() + .map(|fee| fee.amount()) + .sum(); + assert_eq!(total_block_fees, sudo_change_base); +} + +#[tokio::test] +async fn bridge_lock_fee_calculation_works_as_expected() { + let storage = cnidarium::TempStorage::new().await.unwrap(); + let snapshot = storage.latest_snapshot(); + let mut state = StateDelta::new(snapshot); + let transfer_fee = 12; + + let from_address = astria_address(&[2; 20]); + let transaction_id = TransactionId::new([0; 32]); + state.put_transaction_context(TransactionContext { + address_bytes: from_address.bytes(), + transaction_id, + source_action_index: 0, + }); + state.put_base_prefix(ASTRIA_PREFIX.to_string()).unwrap(); + + let transfer_fees = TransferFeeComponents { + base: transfer_fee, + multiplier: 0, + }; + state.put_transfer_fees(transfer_fees).unwrap(); + + let bridge_lock_fees = BridgeLockFeeComponents { + base: transfer_fee, + multiplier: 2, + }; + state.put_bridge_lock_fees(bridge_lock_fees).unwrap(); + + let bridge_address = astria_address(&[1; 20]); + let asset = test_asset(); + let bridge_lock = BridgeLock { + to: bridge_address, + asset: asset.clone(), + amount: 100, + fee_asset: asset.clone(), + destination_chain_address: "someaddress".to_string(), + }; + + let rollup_id = RollupId::from_unhashed_bytes(b"test_rollup_id"); + state + .put_bridge_account_rollup_id(&bridge_address, rollup_id) + .unwrap(); + state + .put_bridge_account_ibc_asset(&bridge_address, asset.clone()) + .unwrap(); + state.put_allowed_fee_asset(&asset).unwrap(); + + // not enough balance; should fail + state + .put_account_balance(&from_address, &asset, transfer_fee) + .unwrap(); + assert_eyre_error( + &bridge_lock.check_and_execute(&mut state).await.unwrap_err(), + "insufficient funds for transfer", + ); + + // enough balance; should pass + let expected_deposit_fee = transfer_fee + base_deposit_fee(&asset, "someaddress") * 2; + state + .put_account_balance(&from_address, &asset, 100 + expected_deposit_fee) + .unwrap(); + bridge_lock.check_and_execute(&mut state).await.unwrap(); +} + +#[test] +fn calculated_base_deposit_fee_matches_expected_value() { + assert_correct_base_deposit_fee(&Deposit { + amount: u128::MAX, + source_action_index: u64::MAX, + ..reference_deposit() + }); + assert_correct_base_deposit_fee(&Deposit { + asset: "test_asset".parse().unwrap(), + ..reference_deposit() + }); + assert_correct_base_deposit_fee(&Deposit { + destination_chain_address: "someaddresslonger".to_string(), + ..reference_deposit() + }); + + // Ensure calculated length is as expected with absurd string + // lengths (have tested up to 99999999, but this makes testing very slow) + let absurd_string: String = ['a'; u16::MAX as usize].iter().collect(); + assert_correct_base_deposit_fee(&Deposit { + asset: absurd_string.parse().unwrap(), + ..reference_deposit() + }); + assert_correct_base_deposit_fee(&Deposit { + destination_chain_address: absurd_string, + ..reference_deposit() + }); +} + +#[track_caller] +#[expect( + clippy::arithmetic_side_effects, + reason = "adding length of strings will never overflow u128 on currently existing machines" +)] +fn assert_correct_base_deposit_fee(deposit: &Deposit) { + let calculated_len = base_deposit_fee(&deposit.asset, &deposit.destination_chain_address); + let expected_len = DEPOSIT_BASE_FEE + + deposit.asset.to_string().len() as u128 + + deposit.destination_chain_address.len() as u128; + assert_eq!(calculated_len, expected_len); +} + +/// Used to determine the base deposit byte length for `get_deposit_byte_len()`. This is based +/// on "reasonable" values for all fields except `asset` and `destination_chain_address`. These +/// are empty strings, whose length will be added to the base cost at the time of +/// calculation. +/// +/// This test determines 165 bytes for an average deposit with empty `asset` and +/// `destination_chain_address`, which is divided by 10 to get our base byte length of 16. This +/// is to allow for more flexibility in overall fees (we have more flexibility multiplying by a +/// lower number, and if we want fees to be higher we can just raise the multiplier). +#[test] +fn get_base_deposit_fee() { + use prost::Message as _; + let bridge_address = Address::builder() + .prefix("astria-bridge") + .slice(&[0u8; ADDRESS_LEN][..]) + .try_build() + .unwrap(); + let raw_deposit = astria_core::generated::sequencerblock::v1alpha1::Deposit { + bridge_address: Some(bridge_address.to_raw()), + rollup_id: Some(RollupId::from_unhashed_bytes([0; ROLLUP_ID_LEN]).to_raw()), + amount: Some(1000.into()), + asset: String::new(), + destination_chain_address: String::new(), + source_transaction_id: Some(TransactionId::new([0; TRANSACTION_ID_LEN]).to_raw()), + source_action_index: 0, + }; + assert_eq!(DEPOSIT_BASE_FEE, raw_deposit.encoded_len() as u128 / 10); +} + +fn reference_deposit() -> Deposit { + Deposit { + bridge_address: astria_address(&[1; 20]), + rollup_id: RollupId::from_unhashed_bytes(b"test_rollup_id"), + amount: 0, + asset: "test".parse().unwrap(), + destination_chain_address: "someaddress".to_string(), + source_transaction_id: TransactionId::new([0; 32]), + source_action_index: 0, + } +} + +// TODO(https://github.com/astriaorg/astria/issues/1382): Add test to ensure correct block fees for ICS20 withdrawal diff --git a/crates/astria-sequencer/src/ibc/component.rs b/crates/astria-sequencer/src/ibc/component.rs index a96f6acbbb..f7578db26e 100644 --- a/crates/astria-sequencer/src/ibc/component.rs +++ b/crates/astria-sequencer/src/ibc/component.rs @@ -50,9 +50,6 @@ impl Component for IbcComponent { .wrap_err("failed to write IBC relayer address")?; } - state - .put_ics20_withdrawal_base_fee(app_state.fees().ics20_withdrawal_base_fee) - .wrap_err("failed to write ics20 withdrawal base fee")?; Ok(()) } diff --git a/crates/astria-sequencer/src/ibc/ics20_withdrawal.rs b/crates/astria-sequencer/src/ibc/ics20_withdrawal.rs index d7093c5455..1fbb69d4e8 100644 --- a/crates/astria-sequencer/src/ibc/ics20_withdrawal.rs +++ b/crates/astria-sequencer/src/ibc/ics20_withdrawal.rs @@ -45,7 +45,6 @@ use crate::{ ActionHandler, StateReadExt as _, }, - assets::StateWriteExt as _, bridge::{ StateReadExt as _, StateWriteExt as _, @@ -214,11 +213,6 @@ impl ActionHandler for action::Ics20Withdrawal { .await .wrap_err("failed establishing which account to withdraw funds from")?; - let fee = state - .get_ics20_withdrawal_base_fee() - .await - .wrap_err("failed to get ics20 withdrawal base fee")?; - let current_timestamp = state .get_block_timestamp() .await @@ -234,21 +228,11 @@ impl ActionHandler for action::Ics20Withdrawal { .wrap_err("packet failed send check")? }; - state - .get_and_increase_block_fees::(self.fee_asset(), fee) - .await - .wrap_err("failed to get and increase block fees")?; - state .decrease_balance(withdrawal_target, self.denom(), self.amount()) .await .wrap_err("failed to decrease sender or bridge balance")?; - state - .decrease_balance(&from, self.fee_asset(), fee) - .await - .wrap_err("failed to subtract fee from sender balance")?; - // if we're the source, move tokens to the escrow account, // otherwise the tokens are just burned if is_source(packet.source_port(), packet.source_channel(), self.denom()) { diff --git a/crates/astria-sequencer/src/ibc/state_ext.rs b/crates/astria-sequencer/src/ibc/state_ext.rs index 2ee556d3fa..5bc28cf21a 100644 --- a/crates/astria-sequencer/src/ibc/state_ext.rs +++ b/crates/astria-sequencer/src/ibc/state_ext.rs @@ -87,21 +87,6 @@ pub(crate) trait StateReadExt: StateRead { .wrap_err("failed to read ibc relayer key from state")? .is_some()) } - - #[instrument(skip_all)] - async fn get_ics20_withdrawal_base_fee(&self) -> Result { - let Some(bytes) = self - .get_raw(keys::ICS20_WITHDRAWAL_BASE_FEE) - .await - .map_err(anyhow_to_eyre) - .wrap_err("failed reading ics20 withdrawal fee from state")? - else { - bail!("ics20 withdrawal fee not found"); - }; - StoredValue::deserialize(&bytes) - .and_then(|value| storage::Fee::try_from(value).map(u128::from)) - .wrap_err("invalid ics20 withdrawal base fee bytes") - } } impl StateReadExt for T {} @@ -172,15 +157,6 @@ pub(crate) trait StateWriteExt: StateWrite { fn delete_ibc_relayer_address(&mut self, address: &T) { self.delete(keys::ibc_relayer(address)); } - - #[instrument(skip_all)] - fn put_ics20_withdrawal_base_fee(&mut self, fee: u128) -> Result<()> { - let bytes = StoredValue::from(storage::Fee::from(fee)) - .serialize() - .wrap_err("failed to serialize ics20 withdrawal base fee")?; - self.put_raw(keys::ICS20_WITHDRAWAL_BASE_FEE.to_string(), bytes); - Ok(()) - } } impl StateWriteExt for T {} @@ -473,15 +449,4 @@ mod tests { "set balance for channel/asset pair not what was expected" ); } - - #[tokio::test] - async fn ics20_withdrawal_base_fee_round_trip() { - let storage = cnidarium::TempStorage::new().await.unwrap(); - let snapshot = storage.latest_snapshot(); - let mut state = StateDelta::new(snapshot); - - state.put_ics20_withdrawal_base_fee(123).unwrap(); - let retrieved_fee = state.get_ics20_withdrawal_base_fee().await.unwrap(); - assert_eq!(retrieved_fee, 123); - } } diff --git a/crates/astria-sequencer/src/ibc/storage/keys.rs b/crates/astria-sequencer/src/ibc/storage/keys.rs index 8184c64e42..00cc0b5fd8 100644 --- a/crates/astria-sequencer/src/ibc/storage/keys.rs +++ b/crates/astria-sequencer/src/ibc/storage/keys.rs @@ -12,7 +12,6 @@ use crate::{ }; pub(in crate::ibc) const IBC_SUDO: &str = "ibc/sudo"; -pub(in crate::ibc) const ICS20_WITHDRAWAL_BASE_FEE: &str = "ibc/ics20_withdrawal_base_fee"; const IBC_RELAYER_PREFIX: &str = "ibc/relayer/"; /// Example: `ibc/channel-xxx/balance/ibc/0101....0101`. @@ -58,7 +57,6 @@ mod tests { #[test] fn keys_should_not_change() { insta::assert_snapshot!(IBC_SUDO); - insta::assert_snapshot!(ICS20_WITHDRAWAL_BASE_FEE); insta::assert_snapshot!(channel_balance(&channel_id(), &asset())); insta::assert_snapshot!(ibc_relayer(&address())); } @@ -66,7 +64,6 @@ mod tests { #[test] fn keys_should_have_component_prefix() { assert!(IBC_SUDO.starts_with(COMPONENT_PREFIX)); - assert!(ICS20_WITHDRAWAL_BASE_FEE.starts_with(COMPONENT_PREFIX)); assert!(channel_balance(&channel_id(), &asset()).starts_with(COMPONENT_PREFIX)); assert!(ibc_relayer(&address()).starts_with(COMPONENT_PREFIX)); } diff --git a/crates/astria-sequencer/src/ibc/storage/mod.rs b/crates/astria-sequencer/src/ibc/storage/mod.rs index 1f76fd5b36..ce326e4d54 100644 --- a/crates/astria-sequencer/src/ibc/storage/mod.rs +++ b/crates/astria-sequencer/src/ibc/storage/mod.rs @@ -5,5 +5,4 @@ pub(crate) use values::Value; pub(super) use values::{ AddressBytes, Balance, - Fee, }; diff --git a/crates/astria-sequencer/src/ibc/storage/snapshots/astria_sequencer__ibc__storage__keys__tests__keys_should_not_change-2.snap b/crates/astria-sequencer/src/ibc/storage/snapshots/astria_sequencer__ibc__storage__keys__tests__keys_should_not_change-2.snap index 16047d71da..09b7acb15e 100644 --- a/crates/astria-sequencer/src/ibc/storage/snapshots/astria_sequencer__ibc__storage__keys__tests__keys_should_not_change-2.snap +++ b/crates/astria-sequencer/src/ibc/storage/snapshots/astria_sequencer__ibc__storage__keys__tests__keys_should_not_change-2.snap @@ -1,6 +1,5 @@ --- source: crates/astria-sequencer/src/ibc/storage/keys.rs -assertion_line: 64 -expression: ICS20_WITHDRAWAL_BASE_FEE_KEY +expression: "channel_balance(&channel_id(), &asset())" --- -ibc/ics20_withdrawal_base_fee +ibc/channel-5/balance/ibc/be429a02d00837245167a2616674a979a2ac6f9806468b48a975b156ad711320 diff --git a/crates/astria-sequencer/src/ibc/storage/snapshots/astria_sequencer__ibc__storage__keys__tests__keys_should_not_change-3.snap b/crates/astria-sequencer/src/ibc/storage/snapshots/astria_sequencer__ibc__storage__keys__tests__keys_should_not_change-3.snap index 43ea3e793d..c57f4881da 100644 --- a/crates/astria-sequencer/src/ibc/storage/snapshots/astria_sequencer__ibc__storage__keys__tests__keys_should_not_change-3.snap +++ b/crates/astria-sequencer/src/ibc/storage/snapshots/astria_sequencer__ibc__storage__keys__tests__keys_should_not_change-3.snap @@ -1,6 +1,5 @@ --- source: crates/astria-sequencer/src/ibc/storage/keys.rs -assertion_line: 62 -expression: "channel_balance(&channel_id(), &asset())" +expression: ibc_relayer(&address()) --- -ibc/channel-5/balance/ibc/be429a02d00837245167a2616674a979a2ac6f9806468b48a975b156ad711320 +ibc/relayer/HAxJDxtVKNgXPF3kbRMRYOSywMM= diff --git a/crates/astria-sequencer/src/ibc/storage/values.rs b/crates/astria-sequencer/src/ibc/storage/values.rs index fc783a5353..f1a0783f89 100644 --- a/crates/astria-sequencer/src/ibc/storage/values.rs +++ b/crates/astria-sequencer/src/ibc/storage/values.rs @@ -24,7 +24,6 @@ pub(crate) struct Value<'a>(ValueImpl<'a>); enum ValueImpl<'a> { Balance(Balance), AddressBytes(AddressBytes<'a>), - Fee(Fee), } #[derive(Debug, BorshSerialize, BorshDeserialize)] @@ -97,35 +96,3 @@ impl<'a> TryFrom> for AddressBytes<'a> { Ok(address) } } - -#[derive(Debug, BorshSerialize, BorshDeserialize)] -pub(in crate::ibc) struct Fee(u128); - -impl From for Fee { - fn from(fee: u128) -> Self { - Fee(fee) - } -} - -impl From for u128 { - fn from(fee: Fee) -> Self { - fee.0 - } -} - -impl<'a> From for crate::storage::StoredValue<'a> { - fn from(fee: Fee) -> Self { - crate::storage::StoredValue::Ibc(Value(ValueImpl::Fee(fee))) - } -} - -impl<'a> TryFrom> for Fee { - type Error = astria_eyre::eyre::Error; - - fn try_from(value: crate::storage::StoredValue<'a>) -> Result { - let crate::storage::StoredValue::Ibc(Value(ValueImpl::Fee(fee))) = value else { - bail!("ibc stored value type mismatch: expected fee, found {value:?}"); - }; - Ok(fee) - } -} diff --git a/crates/astria-sequencer/src/lib.rs b/crates/astria-sequencer/src/lib.rs index 55bf2dfa5b..ddb45ae7d8 100644 --- a/crates/astria-sequencer/src/lib.rs +++ b/crates/astria-sequencer/src/lib.rs @@ -9,7 +9,7 @@ pub(crate) mod bridge; mod build_info; pub(crate) mod component; pub mod config; -pub(crate) mod fee_asset_change; +pub(crate) mod fees; pub(crate) mod grpc; pub(crate) mod ibc; mod mempool; diff --git a/crates/astria-sequencer/src/sequence/action.rs b/crates/astria-sequencer/src/sequence/action.rs index ccc4728494..d8f65205be 100644 --- a/crates/astria-sequencer/src/sequence/action.rs +++ b/crates/astria-sequencer/src/sequence/action.rs @@ -1,25 +1,11 @@ use astria_core::protocol::transaction::v1alpha1::action::Sequence; use astria_eyre::eyre::{ ensure, - OptionExt as _, Result, - WrapErr as _, }; use cnidarium::StateWrite; -use crate::{ - accounts::{ - StateReadExt as _, - StateWriteExt as _, - }, - app::ActionHandler, - assets::{ - StateReadExt, - StateWriteExt, - }, - sequence, - transaction::StateReadExt as _, -}; +use crate::app::ActionHandler; #[async_trait::async_trait] impl ActionHandler for Sequence { @@ -33,78 +19,7 @@ impl ActionHandler for Sequence { Ok(()) } - async fn check_and_execute(&self, mut state: S) -> Result<()> { - let from = state - .get_transaction_context() - .expect("transaction source must be present in state when executing an action") - .address_bytes(); - - ensure!( - state - .is_allowed_fee_asset(&self.fee_asset) - .await - .wrap_err("failed accessing state to check if fee is allowed")?, - "invalid fee asset", - ); - - let curr_balance = state - .get_account_balance(&from, &self.fee_asset) - .await - .wrap_err("failed getting `from` account balance for fee payment")?; - let fee = calculate_fee_from_state(&self.data, &state) - .await - .wrap_err("calculated fee overflows u128")?; - ensure!(curr_balance >= fee, "insufficient funds"); - - state - .get_and_increase_block_fees::(&self.fee_asset, fee) - .await - .wrap_err("failed to add to block fees")?; - state - .decrease_balance(&from, &self.fee_asset, fee) - .await - .wrap_err("failed updating `from` account balance")?; + async fn check_and_execute(&self, _state: S) -> Result<()> { Ok(()) } } - -/// Calculates the fee for a sequence `Action` based on the length of the `data`. -pub(crate) async fn calculate_fee_from_state( - data: &[u8], - state: &S, -) -> Result { - let base_fee = state - .get_sequence_action_base_fee() - .await - .wrap_err("failed to get base fee")?; - let fee_per_byte = state - .get_sequence_action_byte_cost_multiplier() - .await - .wrap_err("failed to get fee per byte")?; - calculate_fee(data, fee_per_byte, base_fee).ok_or_eyre("calculated fee overflows u128") -} - -/// Calculates the fee for a sequence `Action` based on the length of the `data`. -/// Returns `None` if the fee overflows `u128`. -fn calculate_fee(data: &[u8], fee_per_byte: u128, base_fee: u128) -> Option { - base_fee.checked_add( - fee_per_byte.checked_mul( - data.len() - .try_into() - .expect("a usize should always convert to a u128"), - )?, - ) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn calculate_fee_ok() { - assert_eq!(calculate_fee(&[], 1, 0), Some(0)); - assert_eq!(calculate_fee(&[0], 1, 0), Some(1)); - assert_eq!(calculate_fee(&[0u8; 10], 1, 0), Some(10)); - assert_eq!(calculate_fee(&[0u8; 10], 1, 100), Some(110)); - } -} diff --git a/crates/astria-sequencer/src/sequence/component.rs b/crates/astria-sequencer/src/sequence/component.rs deleted file mode 100644 index 98a0b87c98..0000000000 --- a/crates/astria-sequencer/src/sequence/component.rs +++ /dev/null @@ -1,51 +0,0 @@ -use std::sync::Arc; - -use astria_core::protocol::genesis::v1alpha1::GenesisAppState; -use astria_eyre::eyre::{ - Result, - WrapErr as _, -}; -use tendermint::abci::request::{ - BeginBlock, - EndBlock, -}; -use tracing::instrument; - -use super::state_ext::StateWriteExt; -use crate::component::Component; - -#[derive(Default)] -pub(crate) struct SequenceComponent; - -#[async_trait::async_trait] -impl Component for SequenceComponent { - type AppState = GenesisAppState; - - #[instrument(name = "SequenceComponent::init_chain", skip_all)] - async fn init_chain(mut state: S, app_state: &Self::AppState) -> Result<()> { - state - .put_sequence_action_base_fee(app_state.fees().sequence_base_fee) - .wrap_err("failed to put sequence action base fee")?; - state - .put_sequence_action_byte_cost_multiplier( - app_state.fees().sequence_byte_cost_multiplier, - ) - .wrap_err("failed to put sequence action byte cost multiplier") - } - - #[instrument(name = "SequenceComponent::begin_block", skip_all)] - async fn begin_block( - _state: &mut Arc, - _begin_block: &BeginBlock, - ) -> Result<()> { - Ok(()) - } - - #[instrument(name = "SequenceComponent::end_block", skip_all)] - async fn end_block( - _state: &mut Arc, - _end_block: &EndBlock, - ) -> Result<()> { - Ok(()) - } -} diff --git a/crates/astria-sequencer/src/sequence/mod.rs b/crates/astria-sequencer/src/sequence/mod.rs index 5a59df2ed4..f04dbbc6c9 100644 --- a/crates/astria-sequencer/src/sequence/mod.rs +++ b/crates/astria-sequencer/src/sequence/mod.rs @@ -1,10 +1 @@ pub(crate) mod action; -pub(crate) mod component; -mod state_ext; -pub(crate) mod storage; - -pub(crate) use action::calculate_fee_from_state; -pub(crate) use state_ext::{ - StateReadExt, - StateWriteExt, -}; diff --git a/crates/astria-sequencer/src/sequence/state_ext.rs b/crates/astria-sequencer/src/sequence/state_ext.rs deleted file mode 100644 index 6a03abbbe2..0000000000 --- a/crates/astria-sequencer/src/sequence/state_ext.rs +++ /dev/null @@ -1,112 +0,0 @@ -use astria_eyre::{ - anyhow_to_eyre, - eyre::{ - OptionExt as _, - Result, - WrapErr as _, - }, -}; -use async_trait::async_trait; -use cnidarium::{ - StateRead, - StateWrite, -}; -use tracing::instrument; - -use super::storage::{ - self, - keys, -}; -use crate::storage::StoredValue; - -#[async_trait] -pub(crate) trait StateReadExt: StateRead { - #[instrument(skip_all)] - async fn get_sequence_action_base_fee(&self) -> Result { - let bytes = self - .get_raw(keys::SEQUENCE_ACTION_BASE_FEE) - .await - .map_err(anyhow_to_eyre) - .wrap_err("failed reading raw sequence action base fee from state")? - .ok_or_eyre("sequence action base fee not found")?; - StoredValue::deserialize(&bytes) - .and_then(|value| storage::Fee::try_from(value).map(u128::from)) - .wrap_err("invalid sequence action base fee bytes") - } - - #[instrument(skip_all)] - async fn get_sequence_action_byte_cost_multiplier(&self) -> Result { - let bytes = self - .get_raw(keys::SEQUENCE_ACTION_BYTE_COST_MULTIPLIER) - .await - .map_err(anyhow_to_eyre) - .wrap_err("failed reading raw sequence action byte cost multiplier from state")? - .ok_or_eyre("sequence action byte cost multiplier not found")?; - StoredValue::deserialize(&bytes) - .and_then(|value| storage::Fee::try_from(value).map(u128::from)) - .wrap_err("invalid sequence action byte cost multiplier bytes") - } -} - -impl StateReadExt for T {} - -#[async_trait] -pub(crate) trait StateWriteExt: StateWrite { - #[instrument(skip_all)] - fn put_sequence_action_base_fee(&mut self, fee: u128) -> Result<()> { - let bytes = StoredValue::from(storage::Fee::from(fee)) - .serialize() - .context("failed to serialize sequence action base fee")?; - self.put_raw(keys::SEQUENCE_ACTION_BASE_FEE.to_string(), bytes); - Ok(()) - } - - #[instrument(skip_all)] - fn put_sequence_action_byte_cost_multiplier(&mut self, fee: u128) -> Result<()> { - let bytes = StoredValue::from(storage::Fee::from(fee)) - .serialize() - .context("failed to serialize sequence action byte cost multiplier")?; - self.put_raw( - keys::SEQUENCE_ACTION_BYTE_COST_MULTIPLIER.to_string(), - bytes, - ); - Ok(()) - } -} - -impl StateWriteExt for T {} - -#[cfg(test)] -mod tests { - use cnidarium::StateDelta; - - use super::*; - - #[tokio::test] - async fn sequence_action_base_fee() { - let storage = cnidarium::TempStorage::new().await.unwrap(); - let snapshot = storage.latest_snapshot(); - let mut state = StateDelta::new(snapshot); - - let fee = 42; - state.put_sequence_action_base_fee(fee).unwrap(); - assert_eq!(state.get_sequence_action_base_fee().await.unwrap(), fee); - } - - #[tokio::test] - async fn sequence_action_byte_cost_multiplier() { - let storage = cnidarium::TempStorage::new().await.unwrap(); - let snapshot = storage.latest_snapshot(); - let mut state = StateDelta::new(snapshot); - - let fee = 42; - state.put_sequence_action_byte_cost_multiplier(fee).unwrap(); - assert_eq!( - state - .get_sequence_action_byte_cost_multiplier() - .await - .unwrap(), - fee - ); - } -} diff --git a/crates/astria-sequencer/src/sequence/storage/keys.rs b/crates/astria-sequencer/src/sequence/storage/keys.rs deleted file mode 100644 index de28eef30e..0000000000 --- a/crates/astria-sequencer/src/sequence/storage/keys.rs +++ /dev/null @@ -1,22 +0,0 @@ -pub(in crate::sequence) const SEQUENCE_ACTION_BASE_FEE: &str = "sequence/base_fee"; -pub(in crate::sequence) const SEQUENCE_ACTION_BYTE_COST_MULTIPLIER: &str = - "sequence/byte_cost_multiplier"; - -#[cfg(test)] -mod tests { - use super::*; - - const COMPONENT_PREFIX: &str = "sequence/"; - - #[test] - fn keys_should_not_change() { - insta::assert_snapshot!(SEQUENCE_ACTION_BASE_FEE); - insta::assert_snapshot!(SEQUENCE_ACTION_BYTE_COST_MULTIPLIER); - } - - #[test] - fn keys_should_have_component_prefix() { - assert!(SEQUENCE_ACTION_BASE_FEE.starts_with(COMPONENT_PREFIX)); - assert!(SEQUENCE_ACTION_BYTE_COST_MULTIPLIER.starts_with(COMPONENT_PREFIX)); - } -} diff --git a/crates/astria-sequencer/src/sequence/storage/mod.rs b/crates/astria-sequencer/src/sequence/storage/mod.rs deleted file mode 100644 index 92e06f015f..0000000000 --- a/crates/astria-sequencer/src/sequence/storage/mod.rs +++ /dev/null @@ -1,5 +0,0 @@ -pub(super) mod keys; -mod values; - -pub(super) use values::Fee; -pub(crate) use values::Value; diff --git a/crates/astria-sequencer/src/sequence/storage/snapshots/astria_sequencer__sequence__storage__keys__tests__keys_should_not_change-2.snap b/crates/astria-sequencer/src/sequence/storage/snapshots/astria_sequencer__sequence__storage__keys__tests__keys_should_not_change-2.snap deleted file mode 100644 index 1c2afa9bca..0000000000 --- a/crates/astria-sequencer/src/sequence/storage/snapshots/astria_sequencer__sequence__storage__keys__tests__keys_should_not_change-2.snap +++ /dev/null @@ -1,6 +0,0 @@ ---- -source: crates/astria-sequencer/src/sequence/storage/keys.rs -assertion_line: 14 -expression: SEQUENCE_ACTION_BYTE_COST_MULTIPLIER_KEY ---- -sequence/byte_cost_multiplier diff --git a/crates/astria-sequencer/src/sequence/storage/snapshots/astria_sequencer__sequence__storage__keys__tests__keys_should_not_change.snap b/crates/astria-sequencer/src/sequence/storage/snapshots/astria_sequencer__sequence__storage__keys__tests__keys_should_not_change.snap deleted file mode 100644 index d1d9fd3513..0000000000 --- a/crates/astria-sequencer/src/sequence/storage/snapshots/astria_sequencer__sequence__storage__keys__tests__keys_should_not_change.snap +++ /dev/null @@ -1,6 +0,0 @@ ---- -source: crates/astria-sequencer/src/sequence/storage/keys.rs -assertion_line: 13 -expression: SEQUENCE_ACTION_BASE_FEE_KEY ---- -sequence/base_fee diff --git a/crates/astria-sequencer/src/sequence/storage/values.rs b/crates/astria-sequencer/src/sequence/storage/values.rs deleted file mode 100644 index 0d107a518b..0000000000 --- a/crates/astria-sequencer/src/sequence/storage/values.rs +++ /dev/null @@ -1,45 +0,0 @@ -use astria_eyre::eyre::bail; -use borsh::{ - BorshDeserialize, - BorshSerialize, -}; - -#[derive(Debug, BorshSerialize, BorshDeserialize)] -pub(crate) struct Value(ValueImpl); - -#[derive(Debug, BorshSerialize, BorshDeserialize)] -enum ValueImpl { - Fee(Fee), -} - -#[derive(Debug, BorshSerialize, BorshDeserialize)] -pub(in crate::sequence) struct Fee(u128); - -impl From for Fee { - fn from(fee: u128) -> Self { - Fee(fee) - } -} - -impl From for u128 { - fn from(fee: Fee) -> Self { - fee.0 - } -} - -impl<'a> From for crate::storage::StoredValue<'a> { - fn from(fee: Fee) -> Self { - crate::storage::StoredValue::Sequence(Value(ValueImpl::Fee(fee))) - } -} - -impl<'a> TryFrom> for Fee { - type Error = astria_eyre::eyre::Error; - - fn try_from(value: crate::storage::StoredValue<'a>) -> Result { - let crate::storage::StoredValue::Sequence(Value(ValueImpl::Fee(fee))) = value else { - bail!("sequence stored value type mismatch: expected fee, found {value:?}"); - }; - Ok(fee) - } -} diff --git a/crates/astria-sequencer/src/service/info/mod.rs b/crates/astria-sequencer/src/service/info/mod.rs index ba106fdfe4..3ced16ae01 100644 --- a/crates/astria-sequencer/src/service/info/mod.rs +++ b/crates/astria-sequencer/src/service/info/mod.rs @@ -66,7 +66,7 @@ impl Info { query_router .insert( "asset/allowed_fee_assets", - crate::assets::query::allowed_fee_assets_request, + crate::fees::query::allowed_fee_assets_request, ) .wrap_err("invalid path: `asset/allowed_fee_asset_ids`")?; query_router @@ -201,7 +201,8 @@ mod tests { StateWriteExt as _, }, app::StateWriteExt as _, - assets::{ + assets::StateWriteExt as _, + fees::{ StateReadExt as _, StateWriteExt as _, }, diff --git a/crates/astria-sequencer/src/storage/stored_value.rs b/crates/astria-sequencer/src/storage/stored_value.rs index 3cdc657084..07164615e7 100644 --- a/crates/astria-sequencer/src/storage/stored_value.rs +++ b/crates/astria-sequencer/src/storage/stored_value.rs @@ -14,7 +14,7 @@ pub(crate) enum StoredValue<'a> { Assets(crate::assets::storage::Value<'a>), Accounts(crate::accounts::storage::Value), Authority(crate::authority::storage::Value<'a>), - Sequence(crate::sequence::storage::Value), + Fees(crate::fees::storage::Value), Bridge(crate::bridge::storage::Value<'a>), Ibc(crate::ibc::storage::Value<'a>), App(crate::app::storage::Value<'a>), diff --git a/crates/astria-sequencer/src/test_utils.rs b/crates/astria-sequencer/src/test_utils.rs index 2fcf2b9d95..72f5219e43 100644 --- a/crates/astria-sequencer/src/test_utils.rs +++ b/crates/astria-sequencer/src/test_utils.rs @@ -3,6 +3,8 @@ use astria_core::primitive::v1::{ Address, Bech32, }; +#[cfg(test)] +use astria_core::protocol::fees::v1alpha1::SequenceFeeComponents; pub(crate) const ASTRIA_PREFIX: &str = "astria"; pub(crate) const ASTRIA_COMPAT_PREFIX: &str = "astriacompat"; @@ -59,3 +61,25 @@ pub(crate) fn assert_eyre_error(error: &astria_eyre::eyre::Error, expected: &'st "error contained different message\n\texpected: {expected}\n\tfull_error: {msg}", ); } + +/// Calculates the fee for a sequence `Action` based on the length of the `data`. +#[cfg(test)] +pub(crate) async fn calculate_sequence_action_fee_from_state( + data: &[u8], + state: &S, +) -> u128 { + let SequenceFeeComponents { + base, + multiplier, + } = state.get_sequence_fees().await.unwrap(); + base.checked_add( + multiplier + .checked_mul( + data.len() + .try_into() + .expect("a usize should always convert to a u128"), + ) + .expect("fee multiplication should not overflow"), + ) + .expect("fee addition should not overflow") +} diff --git a/crates/astria-sequencer/src/transaction/checks.rs b/crates/astria-sequencer/src/transaction/checks.rs index 9530de421c..b8a3661539 100644 --- a/crates/astria-sequencer/src/transaction/checks.rs +++ b/crates/astria-sequencer/src/transaction/checks.rs @@ -1,18 +1,31 @@ use std::collections::HashMap; use astria_core::{ - primitive::v1::{ - asset, - RollupId, - TransactionId, - }, - protocol::transaction::v1alpha1::{ - action::{ - Action, - BridgeLock, + primitive::v1::asset, + protocol::{ + fees::v1alpha1::{ + BridgeLockFeeComponents, + BridgeSudoChangeFeeComponents, + BridgeUnlockFeeComponents, + Ics20WithdrawalFeeComponents, + InitBridgeAccountFeeComponents, + SequenceFeeComponents, + TransferFeeComponents, + }, + transaction::v1alpha1::{ + action::{ + Action, + BridgeLock, + BridgeSudoChange, + BridgeUnlock, + Ics20Withdrawal, + InitBridgeAccount, + Sequence, + Transfer, + }, + SignedTransaction, + UnsignedTransaction, }, - SignedTransaction, - UnsignedTransaction, }, }; use astria_eyre::eyre::{ @@ -27,7 +40,10 @@ use crate::{ accounts::StateReadExt as _, app::StateReadExt as _, bridge::StateReadExt as _, - ibc::StateReadExt as _, + fees::{ + FeeHandler, + StateReadExt as _, + }, }; #[instrument(skip_all)] @@ -48,64 +64,58 @@ pub(crate) async fn get_fees_for_transaction( tx: &UnsignedTransaction, state: &S, ) -> Result> { - let transfer_fee = state - .get_transfer_base_fee() + let transfer_fees = state + .get_transfer_fees() + .await + .wrap_err("failed to get transfer fees")?; + let sequence_fees = state + .get_sequence_fees() + .await + .wrap_err("failed to get sequence fees")?; + let ics20_withdrawal_fees = state + .get_ics20_withdrawal_fees() .await - .wrap_err("failed to get transfer base fee")?; - let ics20_withdrawal_fee = state - .get_ics20_withdrawal_base_fee() + .wrap_err("failed to get ics20 withdrawal fees")?; + let init_bridge_account_fees = state + .get_init_bridge_account_fees() .await - .wrap_err("failed to get ics20 withdrawal base fee")?; - let init_bridge_account_fee = state - .get_init_bridge_account_base_fee() + .wrap_err("failed to get init bridge account fees")?; + let bridge_lock_fees = state + .get_bridge_lock_fees() .await - .wrap_err("failed to get init bridge account base fee")?; - let bridge_lock_byte_cost_multiplier = state - .get_bridge_lock_byte_cost_multiplier() + .wrap_err("failed to get bridge lock fees")?; + let bridge_unlock_fees = state + .get_bridge_unlock_fees() .await - .wrap_err("failed to get bridge lock byte cost multiplier")?; - let bridge_sudo_change_fee = state - .get_bridge_sudo_change_base_fee() + .wrap_err("failed to get bridge unlock fees")?; + let bridge_sudo_change_fees = state + .get_bridge_sudo_change_fees() .await - .wrap_err("failed to get bridge sudo change fee")?; + .wrap_err("failed to get bridge sudo change fees")?; let mut fees_by_asset = HashMap::new(); - for (i, action) in tx.actions().iter().enumerate() { + for action in tx.actions() { match action { Action::Transfer(act) => { - transfer_update_fees(&act.fee_asset, &mut fees_by_asset, transfer_fee); + transfer_update_fees(act, &mut fees_by_asset, &transfer_fees); } Action::Sequence(act) => { - sequence_update_fees(state, &act.fee_asset, &mut fees_by_asset, &act.data).await?; + sequence_update_fees(act, &mut fees_by_asset, &sequence_fees); + } + Action::Ics20Withdrawal(act) => { + ics20_withdrawal_updates_fees(act, &mut fees_by_asset, &ics20_withdrawal_fees); } - Action::Ics20Withdrawal(act) => ics20_withdrawal_updates_fees( - &act.fee_asset, - &mut fees_by_asset, - ics20_withdrawal_fee, - ), Action::InitBridgeAccount(act) => { - fees_by_asset - .entry(act.fee_asset.to_ibc_prefixed()) - .and_modify(|amt| *amt = amt.saturating_add(init_bridge_account_fee)) - .or_insert(init_bridge_account_fee); + init_bridge_account_update_fees(act, &mut fees_by_asset, &init_bridge_account_fees); } Action::BridgeLock(act) => { - bridge_lock_update_fees( - act, - &mut fees_by_asset, - transfer_fee, - bridge_lock_byte_cost_multiplier, - i as u64, - ); + bridge_lock_update_fees(act, &mut fees_by_asset, &bridge_lock_fees); } Action::BridgeUnlock(act) => { - bridge_unlock_update_fees(&act.fee_asset, &mut fees_by_asset, transfer_fee); + bridge_unlock_update_fees(act, &mut fees_by_asset, &bridge_unlock_fees); } Action::BridgeSudoChange(act) => { - fees_by_asset - .entry(act.fee_asset.to_ibc_prefixed()) - .and_modify(|amt| *amt = amt.saturating_add(bridge_sudo_change_fee)) - .or_insert(bridge_sudo_change_fee); + bridge_sudo_change_update_fees(act, &mut fees_by_asset, &bridge_sudo_change_fees); } Action::ValidatorUpdate(_) | Action::SudoAddressChange(_) @@ -143,7 +153,6 @@ pub(crate) async fn check_balance_for_total_fees_and_transfers( asset ); } - Ok(()) } @@ -209,82 +218,123 @@ pub(crate) async fn get_total_transaction_cost( } fn transfer_update_fees( - fee_asset: &asset::Denom, + act: &Transfer, fees_by_asset: &mut HashMap, - transfer_fee: u128, + transfer_fees: &TransferFeeComponents, ) { + let total_fees = calculate_total_fees( + transfer_fees.base, + transfer_fees.multiplier, + act.variable_component(), + ); fees_by_asset - .entry(fee_asset.to_ibc_prefixed()) - .and_modify(|amt| *amt = amt.saturating_add(transfer_fee)) - .or_insert(transfer_fee); + .entry(act.fee_asset.to_ibc_prefixed()) + .and_modify(|amt| *amt = amt.saturating_add(total_fees)) + .or_insert(total_fees); } -async fn sequence_update_fees( - state: &S, - fee_asset: &asset::Denom, +fn sequence_update_fees( + act: &Sequence, fees_by_asset: &mut HashMap, - data: &[u8], -) -> Result<()> { - let fee = crate::sequence::calculate_fee_from_state(data, state) - .await - .wrap_err("fee for sequence action overflowed; data too large")?; + sequence_fees: &SequenceFeeComponents, +) { + let total_fees = calculate_total_fees( + sequence_fees.base, + sequence_fees.multiplier, + act.variable_component(), + ); fees_by_asset - .entry(fee_asset.to_ibc_prefixed()) - .and_modify(|amt| *amt = amt.saturating_add(fee)) - .or_insert(fee); - Ok(()) + .entry(act.fee_asset.to_ibc_prefixed()) + .and_modify(|amt| *amt = amt.saturating_add(total_fees)) + .or_insert(total_fees); } fn ics20_withdrawal_updates_fees( - fee_asset: &asset::Denom, + act: &Ics20Withdrawal, fees_by_asset: &mut HashMap, - ics20_withdrawal_fee: u128, + ics20_withdrawal_fees: &Ics20WithdrawalFeeComponents, ) { + let total_fees = calculate_total_fees( + ics20_withdrawal_fees.base, + ics20_withdrawal_fees.multiplier, + act.variable_component(), + ); fees_by_asset - .entry(fee_asset.to_ibc_prefixed()) - .and_modify(|amt| *amt = amt.saturating_add(ics20_withdrawal_fee)) - .or_insert(ics20_withdrawal_fee); + .entry(act.fee_asset.to_ibc_prefixed()) + .and_modify(|amt| *amt = amt.saturating_add(total_fees)) + .or_insert(total_fees); } fn bridge_lock_update_fees( act: &BridgeLock, fees_by_asset: &mut HashMap, - transfer_fee: u128, - bridge_lock_byte_cost_multiplier: u128, - tx_index_of_action: u64, + bridge_lock_fees: &BridgeLockFeeComponents, ) { - use astria_core::sequencerblock::v1alpha1::block::Deposit; - - let expected_deposit_fee = transfer_fee.saturating_add( - crate::bridge::calculate_base_deposit_fee(&Deposit { - bridge_address: act.to, - // rollup ID doesn't matter here, as this is only used as a size-check - rollup_id: RollupId::from_unhashed_bytes([0; 32]), - amount: act.amount, - asset: act.asset.clone(), - destination_chain_address: act.destination_chain_address.clone(), - source_transaction_id: TransactionId::new([0; 32]), - source_action_index: tx_index_of_action, - }) - .unwrap() - .saturating_mul(bridge_lock_byte_cost_multiplier), + let total_fees = calculate_total_fees( + bridge_lock_fees.base, + bridge_lock_fees.multiplier, + act.variable_component(), ); fees_by_asset .entry(act.asset.to_ibc_prefixed()) - .and_modify(|amt| *amt = amt.saturating_add(expected_deposit_fee)) - .or_insert(expected_deposit_fee); + .and_modify(|amt| *amt = amt.saturating_add(total_fees)) + .or_insert(total_fees); +} + +fn init_bridge_account_update_fees( + act: &InitBridgeAccount, + fees_by_asset: &mut HashMap, + init_bridge_account_fees: &InitBridgeAccountFeeComponents, +) { + let total_fees = calculate_total_fees( + init_bridge_account_fees.base, + init_bridge_account_fees.multiplier, + act.variable_component(), + ); + + fees_by_asset + .entry(act.fee_asset.to_ibc_prefixed()) + .and_modify(|amt| *amt = amt.saturating_add(total_fees)) + .or_insert(total_fees); } fn bridge_unlock_update_fees( - fee_asset: &asset::Denom, + act: &BridgeUnlock, + fees_by_asset: &mut HashMap, + bridge_lock_fees: &BridgeUnlockFeeComponents, +) { + let total_fees = calculate_total_fees( + bridge_lock_fees.base, + bridge_lock_fees.multiplier, + act.variable_component(), + ); + + fees_by_asset + .entry(act.fee_asset.to_ibc_prefixed()) + .and_modify(|amt| *amt = amt.saturating_add(total_fees)) + .or_insert(total_fees); +} + +fn bridge_sudo_change_update_fees( + act: &BridgeSudoChange, fees_by_asset: &mut HashMap, - transfer_fee: u128, + bridge_sudo_change_fees: &BridgeSudoChangeFeeComponents, ) { + let total_fees = calculate_total_fees( + bridge_sudo_change_fees.base, + bridge_sudo_change_fees.multiplier, + act.variable_component(), + ); + fees_by_asset - .entry(fee_asset.to_ibc_prefixed()) - .and_modify(|amt| *amt = amt.saturating_add(transfer_fee)) - .or_insert(transfer_fee); + .entry(act.fee_asset.to_ibc_prefixed()) + .and_modify(|amt| *amt = amt.saturating_add(total_fees)) + .or_insert(total_fees); +} + +fn calculate_total_fees(base: u128, multiplier: u128, computed_cost_base: u128) -> u128 { + base.saturating_add(computed_cost_base.saturating_mul(multiplier)) } #[cfg(test)] @@ -295,9 +345,20 @@ mod tests { RollupId, ADDRESS_LEN, }, - protocol::transaction::v1alpha1::action::{ - Sequence, - Transfer, + protocol::{ + fees::v1alpha1::{ + BridgeLockFeeComponents, + BridgeSudoChangeFeeComponents, + BridgeUnlockFeeComponents, + Ics20WithdrawalFeeComponents, + InitBridgeAccountFeeComponents, + SequenceFeeComponents, + TransferFeeComponents, + }, + transaction::v1alpha1::action::{ + Sequence, + Transfer, + }, }, }; use bytes::Bytes; @@ -312,13 +373,15 @@ mod tests { }, app::test_utils::*, assets::StateWriteExt as _, - bridge::StateWriteExt as _, - ibc::StateWriteExt as _, - sequence::StateWriteExt as _, - test_utils::ASTRIA_PREFIX, + fees::StateWriteExt as _, + test_utils::{ + calculate_sequence_action_fee_from_state, + ASTRIA_PREFIX, + }, }; #[tokio::test] + #[expect(clippy::too_many_lines, reason = "it's a test")] async fn check_balance_total_fees_transfers_ok() { let storage = cnidarium::TempStorage::new().await.unwrap(); let snapshot = storage.latest_snapshot(); @@ -328,22 +391,75 @@ mod tests { state_tx .put_native_asset(crate::test_utils::nria()) .unwrap(); - state_tx.put_transfer_base_fee(12).unwrap(); - state_tx.put_sequence_action_base_fee(0).unwrap(); + let transfer_fees = TransferFeeComponents { + base: 12, + multiplier: 0, + }; state_tx - .put_sequence_action_byte_cost_multiplier(1) + .put_transfer_fees(transfer_fees) + .wrap_err("failed to initiate transfer fee components") + .unwrap(); + + let sequence_fees = SequenceFeeComponents { + base: 0, + multiplier: 1, + }; + state_tx + .put_sequence_fees(sequence_fees) + .wrap_err("failed to initiate sequence action fee components") + .unwrap(); + + let ics20_withdrawal_fees = Ics20WithdrawalFeeComponents { + base: 1, + multiplier: 0, + }; + state_tx + .put_ics20_withdrawal_fees(ics20_withdrawal_fees) + .wrap_err("failed to initiate ics20 withdrawal fee components") + .unwrap(); + + let init_bridge_account_fees = InitBridgeAccountFeeComponents { + base: 12, + multiplier: 0, + }; + state_tx + .put_init_bridge_account_fees(init_bridge_account_fees) + .wrap_err("failed to initiate init bridge account fee components") + .unwrap(); + + let bridge_lock_fees = BridgeLockFeeComponents { + base: 0, + multiplier: 1, + }; + state_tx + .put_bridge_lock_fees(bridge_lock_fees) + .wrap_err("failed to initiate bridge lock fee components") + .unwrap(); + + let bridge_unlock_fees = BridgeUnlockFeeComponents { + base: 0, + multiplier: 0, + }; + state_tx + .put_bridge_unlock_fees(bridge_unlock_fees) + .wrap_err("failed to initiate bridge unlock fee components") + .unwrap(); + + let bridge_sudo_change_fees = BridgeSudoChangeFeeComponents { + base: 24, + multiplier: 0, + }; + state_tx + .put_bridge_sudo_change_fees(bridge_sudo_change_fees) + .wrap_err("failed to initiate bridge sudo change fee components") .unwrap(); - state_tx.put_ics20_withdrawal_base_fee(1).unwrap(); - state_tx.put_init_bridge_account_base_fee(12).unwrap(); - state_tx.put_bridge_lock_byte_cost_multiplier(1).unwrap(); - state_tx.put_bridge_sudo_change_base_fee(24).unwrap(); let other_asset = "other".parse::().unwrap(); let alice = get_alice_signing_key(); let amount = 100; let data = Bytes::from_static(&[0; 32]); - let transfer_fee = state_tx.get_transfer_base_fee().await.unwrap(); + let transfer_fee = state_tx.get_transfer_fees().await.unwrap().base; state_tx .increase_balance( &state_tx @@ -351,10 +467,7 @@ mod tests { .await .unwrap(), &crate::test_utils::nria(), - transfer_fee - + crate::sequence::calculate_fee_from_state(&data, &state_tx) - .await - .unwrap(), + transfer_fee + calculate_sequence_action_fee_from_state(&data, &state_tx).await, ) .await .unwrap(); @@ -397,6 +510,7 @@ mod tests { } #[tokio::test] + #[expect(clippy::too_many_lines, reason = "it's a test")] async fn check_balance_total_fees_and_transfers_insufficient_other_asset_balance() { let storage = cnidarium::TempStorage::new().await.unwrap(); let snapshot = storage.latest_snapshot(); @@ -406,22 +520,75 @@ mod tests { state_tx .put_native_asset(crate::test_utils::nria()) .unwrap(); - state_tx.put_transfer_base_fee(12).unwrap(); - state_tx.put_sequence_action_base_fee(0).unwrap(); + let transfer_fees = TransferFeeComponents { + base: 12, + multiplier: 0, + }; + state_tx + .put_transfer_fees(transfer_fees) + .wrap_err("failed to initiate transfer fee components") + .unwrap(); + + let sequence_fees = SequenceFeeComponents { + base: 0, + multiplier: 1, + }; + state_tx + .put_sequence_fees(sequence_fees) + .wrap_err("failed to initiate sequence action fee components") + .unwrap(); + + let ics20_withdrawal_fees = Ics20WithdrawalFeeComponents { + base: 1, + multiplier: 0, + }; + state_tx + .put_ics20_withdrawal_fees(ics20_withdrawal_fees) + .wrap_err("failed to initiate ics20 withdrawal fee components") + .unwrap(); + + let init_bridge_account_fees = InitBridgeAccountFeeComponents { + base: 12, + multiplier: 0, + }; + state_tx + .put_init_bridge_account_fees(init_bridge_account_fees) + .wrap_err("failed to initiate init bridge account fee components") + .unwrap(); + + let bridge_lock_fees = BridgeLockFeeComponents { + base: 0, + multiplier: 1, + }; + state_tx + .put_bridge_lock_fees(bridge_lock_fees) + .wrap_err("failed to initiate bridge lock fee components") + .unwrap(); + + let bridge_unlock_fees = BridgeUnlockFeeComponents { + base: 0, + multiplier: 0, + }; + state_tx + .put_bridge_unlock_fees(bridge_unlock_fees) + .wrap_err("failed to initiate bridge unlock fee components") + .unwrap(); + + let bridge_sudo_change_fees = BridgeSudoChangeFeeComponents { + base: 24, + multiplier: 0, + }; state_tx - .put_sequence_action_byte_cost_multiplier(1) + .put_bridge_sudo_change_fees(bridge_sudo_change_fees) + .wrap_err("failed to initiate bridge sudo change fee components") .unwrap(); - state_tx.put_ics20_withdrawal_base_fee(1).unwrap(); - state_tx.put_init_bridge_account_base_fee(12).unwrap(); - state_tx.put_bridge_lock_byte_cost_multiplier(1).unwrap(); - state_tx.put_bridge_sudo_change_base_fee(24).unwrap(); let other_asset = "other".parse::().unwrap(); let alice = get_alice_signing_key(); let amount = 100; let data = Bytes::from_static(&[0; 32]); - let transfer_fee = state_tx.get_transfer_base_fee().await.unwrap(); + let transfer_fee = state_tx.get_transfer_fees().await.unwrap().base; state_tx .increase_balance( &state_tx @@ -429,10 +596,7 @@ mod tests { .await .unwrap(), &crate::test_utils::nria(), - transfer_fee - + crate::sequence::calculate_fee_from_state(&data, &state_tx) - .await - .unwrap(), + transfer_fee + calculate_sequence_action_fee_from_state(&data, &state_tx).await, ) .await .unwrap(); diff --git a/crates/astria-sequencer/src/transaction/mod.rs b/crates/astria-sequencer/src/transaction/mod.rs index 47225cacfb..68d5b012a9 100644 --- a/crates/astria-sequencer/src/transaction/mod.rs +++ b/crates/astria-sequencer/src/transaction/mod.rs @@ -45,6 +45,7 @@ use crate::{ StateReadExt as _, StateWriteExt as _, }, + fees::FeeHandler, ibc::{ host_interface::AstriaHost, StateReadExt as _, @@ -216,28 +217,22 @@ impl ActionHandler for SignedTransaction { state.put_transaction_context(transaction_context); match action { - Action::Transfer(act) => act - .check_and_execute(&mut state) + Action::Transfer(act) => check_execute_and_pay_fees(act, &mut state) .await .wrap_err("executing transfer action failed")?, - Action::Sequence(act) => act - .check_and_execute(&mut state) + Action::Sequence(act) => check_execute_and_pay_fees(act, &mut state) .await .wrap_err("executing sequence action failed")?, - Action::ValidatorUpdate(act) => act - .check_and_execute(&mut state) + Action::ValidatorUpdate(act) => check_execute_and_pay_fees(act, &mut state) .await .wrap_err("executing validor update")?, - Action::SudoAddressChange(act) => act - .check_and_execute(&mut state) + Action::SudoAddressChange(act) => check_execute_and_pay_fees(act, &mut state) .await .wrap_err("executing sudo address change failed")?, - Action::IbcSudoChange(act) => act - .check_and_execute(&mut state) + Action::IbcSudoChange(act) => check_execute_and_pay_fees(act, &mut state) .await .wrap_err("executing ibc sudo change failed")?, - Action::FeeChange(act) => act - .check_and_execute(&mut state) + Action::FeeChange(act) => check_execute_and_pay_fees(act, &mut state) .await .wrap_err("executing fee change failed")?, Action::Ibc(act) => { @@ -261,32 +256,25 @@ impl ActionHandler for SignedTransaction { .map_err(anyhow_to_eyre) .wrap_err("failed executing ibc action")?; } - Action::Ics20Withdrawal(act) => act - .check_and_execute(&mut state) + Action::Ics20Withdrawal(act) => check_execute_and_pay_fees(act, &mut state) .await .wrap_err("failed executing ics20 withdrawal")?, - Action::IbcRelayerChange(act) => act - .check_and_execute(&mut state) + Action::IbcRelayerChange(act) => check_execute_and_pay_fees(act, &mut state) .await .wrap_err("failed executing ibc relayer change")?, - Action::FeeAssetChange(act) => act - .check_and_execute(&mut state) + Action::FeeAssetChange(act) => check_execute_and_pay_fees(act, &mut state) .await .wrap_err("failed executing fee asseet change")?, - Action::InitBridgeAccount(act) => act - .check_and_execute(&mut state) + Action::InitBridgeAccount(act) => check_execute_and_pay_fees(act, &mut state) .await .wrap_err("failed executing init bridge account")?, - Action::BridgeLock(act) => act - .check_and_execute(&mut state) + Action::BridgeLock(act) => check_execute_and_pay_fees(act, &mut state) .await .wrap_err("failed executing bridge lock")?, - Action::BridgeUnlock(act) => act - .check_and_execute(&mut state) + Action::BridgeUnlock(act) => check_execute_and_pay_fees(act, &mut state) .await .wrap_err("failed executing bridge unlock")?, - Action::BridgeSudoChange(act) => act - .check_and_execute(&mut state) + Action::BridgeSudoChange(act) => check_execute_and_pay_fees(act, &mut state) .await .wrap_err("failed executing bridge sudo change")?, } @@ -297,3 +285,12 @@ impl ActionHandler for SignedTransaction { Ok(()) } } + +async fn check_execute_and_pay_fees( + action: &T, + mut state: S, +) -> Result<()> { + action.check_and_execute(&mut state).await?; + action.check_and_pay_fees(&mut state).await?; + Ok(()) +} diff --git a/crates/astria-sequencer/src/transaction/query.rs b/crates/astria-sequencer/src/transaction/query.rs index a1e910afff..ed3a439fb2 100644 --- a/crates/astria-sequencer/src/transaction/query.rs +++ b/crates/astria-sequencer/src/transaction/query.rs @@ -24,7 +24,7 @@ pub(crate) async fn transaction_fee_request( request: request::Query, _params: Vec<(String, String)>, ) -> response::Query { - use astria_core::protocol::transaction::v1alpha1::TransactionFeeResponse; + use astria_core::protocol::fees::v1alpha1::TransactionFeeResponse; let tx = match preprocess_request(&request) { Ok(tx) => tx, diff --git a/proto/protocolapis/astria/protocol/fees/v1alpha1/types.proto b/proto/protocolapis/astria/protocol/fees/v1alpha1/types.proto new file mode 100644 index 0000000000..707c8d6618 --- /dev/null +++ b/proto/protocolapis/astria/protocol/fees/v1alpha1/types.proto @@ -0,0 +1,86 @@ +syntax = "proto3"; + +package astria.protocol.fees.v1alpha1; + +import "astria/primitive/v1/types.proto"; + +message TransactionFee { + string asset = 1; + astria.primitive.v1.Uint128 fee = 2; +} + +message TransferFeeComponents { + astria.primitive.v1.Uint128 base = 1; + astria.primitive.v1.Uint128 multiplier = 2; +} + +message SequenceFeeComponents { + astria.primitive.v1.Uint128 base = 1; + astria.primitive.v1.Uint128 multiplier = 2; +} + +message InitBridgeAccountFeeComponents { + astria.primitive.v1.Uint128 base = 1; + astria.primitive.v1.Uint128 multiplier = 2; +} + +message BridgeLockFeeComponents { + astria.primitive.v1.Uint128 base = 1; + astria.primitive.v1.Uint128 multiplier = 2; +} + +message BridgeUnlockFeeComponents { + astria.primitive.v1.Uint128 base = 1; + astria.primitive.v1.Uint128 multiplier = 2; +} + +message BridgeSudoChangeFeeComponents { + astria.primitive.v1.Uint128 base = 1; + astria.primitive.v1.Uint128 multiplier = 2; +} + +message Ics20WithdrawalFeeComponents { + astria.primitive.v1.Uint128 base = 1; + astria.primitive.v1.Uint128 multiplier = 2; +} + +message IbcRelayFeeComponents { + astria.primitive.v1.Uint128 base = 1; + astria.primitive.v1.Uint128 multiplier = 2; +} + +message ValidatorUpdateFeeComponents { + astria.primitive.v1.Uint128 base = 1; + astria.primitive.v1.Uint128 multiplier = 2; +} + +message FeeAssetChangeFeeComponents { + astria.primitive.v1.Uint128 base = 1; + astria.primitive.v1.Uint128 multiplier = 2; +} + +message FeeChangeFeeComponents { + astria.primitive.v1.Uint128 base = 1; + astria.primitive.v1.Uint128 multiplier = 2; +} + +message IbcRelayerChangeFeeComponents { + astria.primitive.v1.Uint128 base = 1; + astria.primitive.v1.Uint128 multiplier = 2; +} + +message SudoAddressChangeFeeComponents { + astria.primitive.v1.Uint128 base = 1; + astria.primitive.v1.Uint128 multiplier = 2; +} + +message IbcSudoChangeFeeComponents { + astria.primitive.v1.Uint128 base = 1; + astria.primitive.v1.Uint128 multiplier = 2; +} + +// Response to a transaction fee ABCI query. +message TransactionFeeResponse { + uint64 height = 2; + repeated astria.protocol.fees.v1alpha1.TransactionFee fees = 3; +} diff --git a/proto/protocolapis/astria/protocol/genesis/v1alpha1/types.proto b/proto/protocolapis/astria/protocol/genesis/v1alpha1/types.proto index caf199db59..5b9d9243cc 100644 --- a/proto/protocolapis/astria/protocol/genesis/v1alpha1/types.proto +++ b/proto/protocolapis/astria/protocol/genesis/v1alpha1/types.proto @@ -3,6 +3,7 @@ syntax = "proto3"; package astria.protocol.genesis.v1alpha1; import "astria/primitive/v1/types.proto"; +import "astria/protocol/fees/v1alpha1/types.proto"; message GenesisAppState { string chain_id = 1; @@ -14,7 +15,7 @@ message GenesisAppState { string native_asset_base_denomination = 7; IbcParameters ibc_parameters = 8; repeated string allowed_fee_assets = 9; - Fees fees = 10; + GenesisFees fees = 10; } message Account { @@ -40,12 +41,19 @@ message IbcParameters { bool outbound_ics20_transfers_enabled = 3; } -message Fees { - astria.primitive.v1.Uint128 transfer_base_fee = 1; - astria.primitive.v1.Uint128 sequence_base_fee = 2; - astria.primitive.v1.Uint128 sequence_byte_cost_multiplier = 3; - astria.primitive.v1.Uint128 init_bridge_account_base_fee = 4; - astria.primitive.v1.Uint128 bridge_lock_byte_cost_multiplier = 5; - astria.primitive.v1.Uint128 bridge_sudo_change_fee = 6; - astria.primitive.v1.Uint128 ics20_withdrawal_base_fee = 7; +message GenesisFees { + astria.protocol.fees.v1alpha1.BridgeLockFeeComponents bridge_lock = 1; + astria.protocol.fees.v1alpha1.BridgeSudoChangeFeeComponents bridge_sudo_change = 2; + astria.protocol.fees.v1alpha1.BridgeUnlockFeeComponents bridge_unlock = 3; + astria.protocol.fees.v1alpha1.FeeAssetChangeFeeComponents fee_asset_change = 4; + astria.protocol.fees.v1alpha1.FeeChangeFeeComponents fee_change = 5; + astria.protocol.fees.v1alpha1.IbcRelayFeeComponents ibc_relay = 7; + astria.protocol.fees.v1alpha1.IbcRelayerChangeFeeComponents ibc_relayer_change = 6; + astria.protocol.fees.v1alpha1.IbcSudoChangeFeeComponents ibc_sudo_change = 8; + astria.protocol.fees.v1alpha1.Ics20WithdrawalFeeComponents ics20_withdrawal = 9; + astria.protocol.fees.v1alpha1.InitBridgeAccountFeeComponents init_bridge_account = 10; + astria.protocol.fees.v1alpha1.SequenceFeeComponents sequence = 11; + astria.protocol.fees.v1alpha1.SudoAddressChangeFeeComponents sudo_address_change = 12; + astria.protocol.fees.v1alpha1.TransferFeeComponents transfer = 13; + astria.protocol.fees.v1alpha1.ValidatorUpdateFeeComponents validator_update = 14; } diff --git a/proto/protocolapis/astria/protocol/transactions/v1alpha1/action.proto b/proto/protocolapis/astria/protocol/transactions/v1alpha1/action.proto index 25607965cf..a10760a93d 100644 --- a/proto/protocolapis/astria/protocol/transactions/v1alpha1/action.proto +++ b/proto/protocolapis/astria/protocol/transactions/v1alpha1/action.proto @@ -3,6 +3,7 @@ syntax = "proto3"; package astria.protocol.transactions.v1alpha1; import "astria/primitive/v1/types.proto"; +import "astria/protocol/fees/v1alpha1/types.proto"; import "astria_vendored/penumbra/core/component/ibc/v1/ibc.proto"; import "astria_vendored/tendermint/abci/types.proto"; @@ -219,35 +220,25 @@ message BridgeSudoChange { } message FeeChange { - // note that the proto number ranges are doubled from that of `Action`. - // this to accomodate both `base_fee` and `byte_cost_multiplier` for each action. - oneof value { - // core protocol fees are defined on 1-20 - astria.primitive.v1.Uint128 transfer_base_fee = 1; - astria.primitive.v1.Uint128 sequence_base_fee = 2; - astria.primitive.v1.Uint128 sequence_byte_cost_multiplier = 3; - - // bridge fees are defined on 20-39 - astria.primitive.v1.Uint128 init_bridge_account_base_fee = 20; - astria.primitive.v1.Uint128 bridge_lock_byte_cost_multiplier = 21; - astria.primitive.v1.Uint128 bridge_sudo_change_base_fee = 22; - - // ibc fees are defined on 40-59 - astria.primitive.v1.Uint128 ics20_withdrawal_base_fee = 40; + // the new fee components values + oneof fee_components { + astria.protocol.fees.v1alpha1.BridgeLockFeeComponents bridge_lock = 1; + astria.protocol.fees.v1alpha1.BridgeSudoChangeFeeComponents bridge_sudo_change = 2; + astria.protocol.fees.v1alpha1.BridgeUnlockFeeComponents bridge_unlock = 3; + astria.protocol.fees.v1alpha1.FeeAssetChangeFeeComponents fee_asset_change = 4; + astria.protocol.fees.v1alpha1.FeeChangeFeeComponents fee_change = 5; + astria.protocol.fees.v1alpha1.IbcRelayFeeComponents ibc_relay = 7; + astria.protocol.fees.v1alpha1.IbcRelayerChangeFeeComponents ibc_relayer_change = 6; + astria.protocol.fees.v1alpha1.IbcSudoChangeFeeComponents ibc_sudo_change = 8; + astria.protocol.fees.v1alpha1.Ics20WithdrawalFeeComponents ics20_withdrawal = 9; + astria.protocol.fees.v1alpha1.InitBridgeAccountFeeComponents init_bridge_account = 10; + astria.protocol.fees.v1alpha1.SequenceFeeComponents sequence = 11; + astria.protocol.fees.v1alpha1.SudoAddressChangeFeeComponents sudo_address_change = 12; + astria.protocol.fees.v1alpha1.TransferFeeComponents transfer = 13; + astria.protocol.fees.v1alpha1.ValidatorUpdateFeeComponents validator_update = 14; } } message IbcSudoChange { astria.primitive.v1.Address new_address = 1; } - -// Response to a transaction fee ABCI query. -message TransactionFeeResponse { - uint64 height = 2; - repeated TransactionFee fees = 3; -} - -message TransactionFee { - string asset = 1; - astria.primitive.v1.Uint128 fee = 2; -}