diff --git a/Cargo.lock b/Cargo.lock index 063e3a9d2b..f5f4485d1a 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", @@ -786,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/composer/Chart.yaml b/charts/composer/Chart.yaml index a5e2ac72fe..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.4 +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/_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/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/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..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: "" @@ -50,6 +51,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..f9ed82cec8 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.6 - 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: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 e2cf556597..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.2 +version: 0.6.4 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.6 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 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-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"}"#; 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 935b11fabd..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, }; @@ -141,6 +140,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 +190,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 +259,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, @@ -273,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")?; @@ -285,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(); @@ -332,7 +366,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 +420,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 +455,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); @@ -1105,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"); @@ -1175,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"); @@ -1199,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 e215d1cee1..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] @@ -501,6 +502,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 +510,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 +535,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 +554,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 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; -}