diff --git a/Cargo.lock b/Cargo.lock index 53c3799a4..0ae98611b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -550,10 +550,10 @@ dependencies = [ "rand 0.8.5", "regex", "reqwest", - "secrecy", "serde", "serde_json", "sha2 0.10.8", + "tempfile", "tendermint 0.34.1", "tendermint-rpc", "thiserror", @@ -663,6 +663,7 @@ dependencies = [ "tracing", "walkdir", "which", + "zeroize", ] [[package]] @@ -815,7 +816,6 @@ dependencies = [ "const_format", "cosmrs", "dirs", - "ed25519-consensus", "futures", "futures-bounded", "hex", @@ -853,7 +853,6 @@ dependencies = [ "tracing", "tryhard", "wiremock", - "zeroize", ] [[package]] @@ -2596,7 +2595,6 @@ dependencies = [ "curve25519-dalek-ng", "hex", "rand_core 0.6.4", - "serde", "sha2 0.9.9", "thiserror", "zeroize", @@ -6875,16 +6873,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "secrecy" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9bd1c54ea06cfd2f6b63219704de0b9b4f72dcc2b8fdef820be6cd799780e91e" -dependencies = [ - "serde", - "zeroize", -] - [[package]] name = "security-framework" version = "2.10.0" diff --git a/Cargo.toml b/Cargo.toml index 30da97aed..8d389b412 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -55,7 +55,9 @@ bytes = "1" celestia-tendermint = "0.32.1" celestia-types = "0.1.1" clap = "4.5.4" -ed25519-consensus = "2.1.0" +ed25519-consensus = { version = "2.1.0", default-features = false, features = [ + "std", +] } ethers = "2.0.11" futures = "0.3" hex = "0.4" diff --git a/charts/evm-rollup/Chart.yaml b/charts/evm-rollup/Chart.yaml index 405efed2e..974d2dae2 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.16.1 +version: 0.16.2 # 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/configmap.yaml b/charts/evm-rollup/templates/configmap.yaml index c4b6ba8b7..9dd0f6095 100644 --- a/charts/evm-rollup/templates/configmap.yaml +++ b/charts/evm-rollup/templates/configmap.yaml @@ -63,11 +63,12 @@ data: OTEL_EXPORTER_OTLP_HEADERS: "{{ .Values.config.rollup.otel.otlpHeaders }}" OTEL_EXPORTER_OTLP_TRACE_HEADERS: "{{ .Values.config.rollup.otel.traceHeaders }}" OTEL_SERVICE_NAME: "{{ tpl .Values.config.rollup.otel.serviceNamePrefix . }}-composer" + {{- if not .Values.global.dev }} {{- if not .Values.secretProvider.enabled }} - ASTRIA_COMPOSER_PRIVATE_KEY: "{{ .Values.config.sequencer.privateKey }}" + ASTRIA_COMPOSER_PRIVATE_KEY: "{{ .Values.config.sequencer.privateKey.devContent }}" {{- end }} - {{- if not .Values.global.dev }} {{- else }} + ASTRIA_COMPOSER_PRIVATE_KEY_FILE: "/var/secrets/{{ .Values.config.sequencer.privateKey.secret.filename }}" {{- end }} --- apiVersion: v1 @@ -259,3 +260,15 @@ metadata: data: VISUALIZER__SERVER__GRPC__ENABLED: "false" VISUALIZER__SERVER__HTTP__ADDR: "0.0.0.0:8151" +--- +{{- if not .Values.secretProvider.enabled }} +apiVersion: v1 +kind: ConfigMap +metadata: + namespace: {{ include "rollup.namespace" . }} + name: sequencer-private-key +data: + {{ .Values.config.sequencer.privateKey.secret.filename }}: | + {{ .Values.config.sequencer.privateKey.devContent }} +--- +{{- end }} diff --git a/charts/evm-rollup/templates/statefulsets.yaml b/charts/evm-rollup/templates/statefulsets.yaml index 95e191645..af91ec498 100644 --- a/charts/evm-rollup/templates/statefulsets.yaml +++ b/charts/evm-rollup/templates/statefulsets.yaml @@ -56,8 +56,8 @@ spec: {{- else }} - --state.scheme=path {{- end }} - {{ if .Values.config.rollup.metrics.enabled }} - - --metrics + {{ if .Values.config.rollup.metrics.enabled }} + - --metrics - --metrics.addr=0.0.0.0 - --metrics.port={{ .Values.ports.metrics }} {{- end }} @@ -79,12 +79,12 @@ spec: name: ws-rpc - containerPort: {{ .Values.ports.executionGRPC }} name: execution-grpc - {{- if .Values.config.rollup.metrics.enabled }} + {{- if .Values.config.rollup.metrics.enabled }} - containerPort: {{ .Values.ports.metrics }} name: geth-metr {{- end }} resources: - {{- toYaml .Values.resources.geth | trim | nindent 12 }} + {{- toYaml .Values.resources.geth | trim | nindent 12 }} - name: composer image: {{ include "composer.image" . }} command: [ "/usr/local/bin/astria-composer" ] @@ -93,20 +93,10 @@ spec: envFrom: - configMapRef: name: {{ .Values.config.rollup.name }}-composer-env - {{- if .Values.secretProvider.enabled }} - env: - - name: ASTRIA_COMPOSER_PRIVATE_KEY - valueFrom: - secretKeyRef: - name: sequencer-private-key - key: {{ .Values.secretProvider.secrets.sequencerPrivateKey.key }} - {{- end }} volumeMounts: - {{- if .Values.secretProvider.enabled }} - mountPath: "/var/secrets" name: sequencer-private-key - {{- end }} - {{- if .Values.config.rollup.metrics.enabled }} + {{- if .Values.config.rollup.metrics.enabled }} ports: - containerPort: {{ .Values.ports.composerMetrics }} name: composer-metr @@ -123,7 +113,7 @@ spec: name: {{ .Values.config.rollup.name }}-conductor-env resources: {{- toYaml .Values.resources.conductor | trim | nindent 12 }} - {{- if .Values.config.rollup.metrics.enabled }} + {{- if .Values.config.rollup.metrics.enabled }} ports: - containerPort: {{ .Values.ports.conductorMetrics }} name: conductor-metr @@ -140,14 +130,17 @@ spec: {{- else }} emptyDir: {} {{- end }} - {{- if .Values.secretProvider.enabled }} - name: sequencer-private-key + {{- if .Values.secretProvider.enabled }} csi: driver: secrets-store.csi.k8s.io readOnly: true volumeAttributes: secretProviderClass: sequencer-private-key - {{- end }} + {{- else }} + configMap: + name: sequencer-private-key + {{- end }} --- {{- if .Values.config.blockscout.enabled }} apiVersion: apps/v1 diff --git a/charts/evm-rollup/values.yaml b/charts/evm-rollup/values.yaml index ce1dea640..e4dbe3ad6 100644 --- a/charts/evm-rollup/values.yaml +++ b/charts/evm-rollup/values.yaml @@ -173,7 +173,11 @@ config: # Private key which is used for wrapping txs for sequencer submission # Note: When secretProvider.enabled is true the secret provided by # `sequencerPrivateKey` is used instead of this value. - privateKey: "2bd806c97f0e00af1a1fc3328fa763a9269723c8db8fac4f93af71db186d6e90" + privateKey: + devContent: "2bd806c97f0e00af1a1fc3328fa763a9269723c8db8fac4f93af71db186d6e90" + secret: + filename: "key.hex" + resourceName: "projects/$PROJECT_ID/secrets/sequencerPrivateKey/versions/latest" celestia: # if config.rollup.executionLevel is NOT 'SoftOnly' AND celestia-node is not enabled diff --git a/crates/astria-cli/src/commands/sequencer.rs b/crates/astria-cli/src/commands/sequencer.rs index 532bad811..4534dee5f 100644 --- a/crates/astria-cli/src/commands/sequencer.rs +++ b/crates/astria-cli/src/commands/sequencer.rs @@ -1,4 +1,5 @@ use astria_core::{ + crypto::SigningKey, primitive::v1::asset, protocol::transaction::v1alpha1::{ action::{ @@ -31,10 +32,7 @@ use color_eyre::{ Context, }, }; -use ed25519_consensus::{ - SigningKey, - VerificationKeyBytes, -}; +use ed25519_consensus::VerificationKeyBytes; use rand::rngs::OsRng; use crate::cli::sequencer::{ diff --git a/crates/astria-composer/Cargo.toml b/crates/astria-composer/Cargo.toml index f81a08288..2a81a79b6 100644 --- a/crates/astria-composer/Cargo.toml +++ b/crates/astria-composer/Cargo.toml @@ -21,7 +21,6 @@ telemetry = { package = "astria-telemetry", path = "../astria-telemetry", featur ] } pin-project-lite = "0.2.13" -secrecy = { version = "0.8", features = ["serde"] } tonic-health = "0.10.2" async-trait = { workspace = true } @@ -70,6 +69,7 @@ test_utils = { package = "astria-test-utils", path = "../astria-test-utils", fea "geth", ] } insta = { workspace = true, features = ["json"] } +tempfile = { workspace = true } tokio-test = { workspace = true } astria-core = { path = "../astria-core", features = ["client"] } tendermint-rpc = { workspace = true } diff --git a/crates/astria-composer/local.env.example b/crates/astria-composer/local.env.example index e52b99a1a..915df6266 100644 --- a/crates/astria-composer/local.env.example +++ b/crates/astria-composer/local.env.example @@ -35,9 +35,9 @@ ASTRIA_COMPOSER_SEQUENCER_CHAIN_ID="astria-dev-1" # names are sha256 hashed and used as the `rollup_id` in `SequenceAction`s ASTRIA_COMPOSER_ROLLUPS="astriachain::ws://127.0.0.1:8545" -# Private key for the sequencer account used for signing transactions -# Must be a hex-encoded 32-byte array (64-character hex string) -ASTRIA_COMPOSER_PRIVATE_KEY="2bd806c97f0e00af1a1fc3328fa763a9269723c8db8fac4f93af71db186d6e90" +# The path to the file storing the private key for the sequencer account used for signing +# transactions. The file should contain a hex-encoded Ed25519 secret key. +ASTRIA_COMPOSER_PRIVATE_KEY_FILE=/path/to/priv_sequencer_key.json # Block time in milliseconds, used to force submitting of finished bundles. # Should match the sequencer node configuration for 'timeout_commit', as diff --git a/crates/astria-composer/src/composer.rs b/crates/astria-composer/src/composer.rs index 6cefab843..90a01b56b 100644 --- a/crates/astria-composer/src/composer.rs +++ b/crates/astria-composer/src/composer.rs @@ -120,7 +120,7 @@ impl Composer { let (executor, executor_handle) = executor::Builder { sequencer_url: cfg.sequencer_url.clone(), sequencer_chain_id: cfg.sequencer_chain_id.clone(), - private_key: cfg.private_key.clone(), + private_key_file: cfg.private_key_file.clone(), block_time_ms: cfg.block_time_ms, max_bytes_per_bundle: cfg.max_bytes_per_bundle, bundle_queue_capacity: cfg.bundle_queue_capacity, diff --git a/crates/astria-composer/src/config.rs b/crates/astria-composer/src/config.rs index 4a60762f0..0dbc55bb8 100644 --- a/crates/astria-composer/src/config.rs +++ b/crates/astria-composer/src/config.rs @@ -1,14 +1,8 @@ use std::net::SocketAddr; -use secrecy::{ - zeroize::ZeroizeOnDrop, - ExposeSecret as _, - SecretString, -}; use serde::{ Deserialize, Serialize, - Serializer, }; // this is a config, may have many boolean values @@ -31,9 +25,8 @@ pub struct Config { /// A list of :: pairs pub rollups: String, - /// Private key for the sequencer account used for signing transactions - #[serde(serialize_with = "serialize_private_key")] - pub private_key: SecretString, + /// Path to private key for the sequencer account used for signing transactions + pub private_key_file: String, /// Sequencer block time in milliseconds #[serde(alias = "max_submit_interval_ms")] @@ -69,22 +62,6 @@ impl config::Config for Config { const PREFIX: &'static str = "ASTRIA_COMPOSER_"; } -impl ZeroizeOnDrop for Config {} - -fn serialize_private_key(key: &SecretString, s: S) -> Result -where - S: Serializer, -{ - use serde::ser::Error as _; - let mut raw_key = key.expose_secret().clone().into_bytes(); - if let Some(sub_slice) = raw_key.get_mut(4..) { - sub_slice.fill(b'#'); - } - let sanitized_key = std::str::from_utf8(&raw_key) - .map_err(|_| S::Error::custom("private key hex contained non-ascii characters"))?; - s.serialize_str(sanitized_key) -} - #[cfg(test)] mod tests { use super::Config; diff --git a/crates/astria-composer/src/executor/builder.rs b/crates/astria-composer/src/executor/builder.rs index fa2c05a58..973bc1302 100644 --- a/crates/astria-composer/src/executor/builder.rs +++ b/crates/astria-composer/src/executor/builder.rs @@ -1,21 +1,18 @@ -use std::time::Duration; +use std::{ + fs, + path::Path, + time::Duration, +}; use astria_core::{ + crypto::SigningKey, primitive::v1::Address, protocol::transaction::v1alpha1::action::SequenceAction, }; -use astria_eyre::{ +use astria_eyre::eyre::{ + self, eyre, - eyre::{ - eyre, - WrapErr as _, - }, -}; -use ed25519_consensus::SigningKey; -use secrecy::{ - ExposeSecret, - SecretString, - Zeroize, + WrapErr as _, }; use tokio::sync::watch; use tokio_util::sync::CancellationToken; @@ -28,7 +25,7 @@ use crate::{ pub(crate) struct Builder { pub(crate) sequencer_url: String, pub(crate) sequencer_chain_id: String, - pub(crate) private_key: SecretString, + pub(crate) private_key_file: String, pub(crate) block_time_ms: u64, pub(crate) max_bytes_per_bundle: usize, pub(crate) bundle_queue_capacity: usize, @@ -40,7 +37,7 @@ impl Builder { let Self { sequencer_url, sequencer_chain_id, - private_key, + private_key_file, block_time_ms, max_bytes_per_bundle, bundle_queue_capacity, @@ -49,12 +46,10 @@ impl Builder { let sequencer_client = sequencer_client::HttpClient::new(sequencer_url.as_str()) .wrap_err("failed constructing sequencer client")?; let (status, _) = watch::channel(Status::new()); - let mut private_key_bytes: [u8; 32] = hex::decode(private_key.expose_secret()) - .wrap_err("failed to decode private key bytes from hex string")? - .try_into() - .map_err(|_| eyre!("invalid private key length; must be 32 bytes"))?; - let sequencer_key = SigningKey::from(private_key_bytes); - private_key_bytes.zeroize(); + + let sequencer_key = read_signing_key_from_file(&private_key_file).wrap_err_with(|| { + format!("failed reading signing key from file at path `{private_key_file}`") + })?; let sequencer_address = Address::from_verification_key(sequencer_key.verification_key()); @@ -78,3 +73,11 @@ impl Builder { )) } } + +fn read_signing_key_from_file>(path: P) -> eyre::Result { + let private_key_hex = fs::read_to_string(path)?; + let private_key_bytes: [u8; 32] = hex::decode(private_key_hex.trim())? + .try_into() + .map_err(|_| eyre!("invalid private key length; must be 32 bytes"))?; + Ok(SigningKey::from(private_key_bytes)) +} diff --git a/crates/astria-composer/src/executor/mod.rs b/crates/astria-composer/src/executor/mod.rs index a680e31fa..77bea7c4c 100644 --- a/crates/astria-composer/src/executor/mod.rs +++ b/crates/astria-composer/src/executor/mod.rs @@ -10,13 +10,16 @@ use std::{ time::Duration, }; -use astria_core::protocol::{ - abci::AbciErrorCode, - transaction::v1alpha1::{ - action::SequenceAction, - SignedTransaction, - TransactionParams, - UnsignedTransaction, +use astria_core::{ + crypto::SigningKey, + protocol::{ + abci::AbciErrorCode, + transaction::v1alpha1::{ + action::SequenceAction, + SignedTransaction, + TransactionParams, + UnsignedTransaction, + }, }, }; use astria_eyre::eyre::{ @@ -24,7 +27,6 @@ use astria_eyre::eyre::{ eyre, WrapErr as _, }; -use ed25519_consensus::SigningKey; use futures::{ future::{ self, @@ -37,7 +39,6 @@ use futures::{ }; use pin_project_lite::pin_project; use prost::Message as _; -use secrecy::Zeroize as _; use sequencer_client::{ tendermint_rpc::endpoint::broadcast::tx_sync, Address, @@ -146,12 +147,6 @@ impl Handle { } } -impl Drop for Executor { - fn drop(&mut self) { - self.sequencer_key.zeroize(); - } -} - #[derive(Debug)] pub(super) struct Status { is_connected: bool, @@ -537,12 +532,6 @@ pin_project! { state: SubmitState, bundle: SizedBundle, } - - impl PinnedDrop for SubmitFut { - fn drop(this: Pin<&mut Self>) { - this.project().signing_key.zeroize(); - } - } } pin_project! { diff --git a/crates/astria-composer/src/executor/tests.rs b/crates/astria-composer/src/executor/tests.rs index 9fb56fb97..3b7e8bebf 100644 --- a/crates/astria-composer/src/executor/tests.rs +++ b/crates/astria-composer/src/executor/tests.rs @@ -1,4 +1,7 @@ -use std::time::Duration; +use std::{ + io::Write, + time::Duration, +}; use astria_core::{ primitive::v1::{ @@ -14,6 +17,7 @@ use once_cell::sync::Lazy; use prost::Message; use sequencer_client::SignedTransaction; use serde_json::json; +use tempfile::NamedTempFile; use tendermint_rpc::{ endpoint::broadcast::tx_sync, request, @@ -62,7 +66,7 @@ static TELEMETRY: Lazy<()> = Lazy::new(|| { }); /// Start a mock sequencer server and mount a mock for the `accounts/nonce` query. -async fn setup() -> (MockServer, MockGuard, Config) { +async fn setup() -> (MockServer, MockGuard, Config, NamedTempFile) { use astria_core::generated::protocol::account::v1alpha1::NonceResponse; Lazy::force(&TELEMETRY); let server = MockServer::start().await; @@ -76,15 +80,18 @@ async fn setup() -> (MockServer, MockGuard, Config) { ) .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: "2bd806c97f0e00af1a1fc3328fa763a9269723c8db8fac4f93af71db186d6e90" - .to_string() - .into(), + private_key_file: keyfile.path().to_string_lossy().to_string(), block_time_ms: 2000, max_bytes_per_bundle: 1000, bundle_queue_capacity: 10, @@ -95,7 +102,7 @@ async fn setup() -> (MockServer, MockGuard, Config) { pretty_print: true, grpc_addr: "127.0.0.1:0".parse().unwrap(), }; - (server, startup_guard, cfg) + (server, startup_guard, cfg, keyfile) } /// Mount a mock for the `abci_query` endpoint. @@ -199,12 +206,12 @@ async fn wait_for_startup( #[tokio::test] async fn full_bundle() { // set up the executor, channel for writing seq actions, and the sequencer mock - let (sequencer, nonce_guard, cfg) = setup().await; + let (sequencer, nonce_guard, cfg, _keyfile) = setup().await; let shutdown_token = CancellationToken::new(); let (executor, executor_handle) = executor::Builder { sequencer_url: cfg.sequencer_url.clone(), sequencer_chain_id: cfg.sequencer_chain_id.clone(), - private_key: cfg.private_key.clone(), + private_key_file: cfg.private_key_file.clone(), block_time_ms: cfg.block_time_ms, max_bytes_per_bundle: cfg.max_bytes_per_bundle, bundle_queue_capacity: cfg.bundle_queue_capacity, @@ -290,12 +297,12 @@ async fn full_bundle() { #[tokio::test] async fn bundle_triggered_by_block_timer() { // set up the executor, channel for writing seq actions, and the sequencer mock - let (sequencer, nonce_guard, cfg) = setup().await; + let (sequencer, nonce_guard, cfg, _keyfile) = setup().await; let shutdown_token = CancellationToken::new(); let (executor, executor_handle) = executor::Builder { sequencer_url: cfg.sequencer_url.clone(), sequencer_chain_id: cfg.sequencer_chain_id.clone(), - private_key: cfg.private_key.clone(), + private_key_file: cfg.private_key_file.clone(), block_time_ms: cfg.block_time_ms, max_bytes_per_bundle: cfg.max_bytes_per_bundle, bundle_queue_capacity: cfg.bundle_queue_capacity, @@ -374,12 +381,12 @@ async fn bundle_triggered_by_block_timer() { #[tokio::test] async fn two_seq_actions_single_bundle() { // set up the executor, channel for writing seq actions, and the sequencer mock - let (sequencer, nonce_guard, cfg) = setup().await; + let (sequencer, nonce_guard, cfg, _keyfile) = setup().await; let shutdown_token = CancellationToken::new(); let (executor, executor_handle) = executor::Builder { sequencer_url: cfg.sequencer_url.clone(), sequencer_chain_id: cfg.sequencer_chain_id.clone(), - private_key: cfg.private_key.clone(), + private_key_file: cfg.private_key_file.clone(), block_time_ms: cfg.block_time_ms, max_bytes_per_bundle: cfg.max_bytes_per_bundle, bundle_queue_capacity: cfg.bundle_queue_capacity, diff --git a/crates/astria-composer/tests/blackbox/helper/mod.rs b/crates/astria-composer/tests/blackbox/helper/mod.rs index 593dfa514..fe09219ef 100644 --- a/crates/astria-composer/tests/blackbox/helper/mod.rs +++ b/crates/astria-composer/tests/blackbox/helper/mod.rs @@ -1,5 +1,6 @@ use std::{ collections::HashMap, + io::Write, net::SocketAddr, time::Duration, }; @@ -18,6 +19,7 @@ use astria_core::{ use astria_eyre::eyre; use ethers::prelude::Transaction; use once_cell::sync::Lazy; +use tempfile::NamedTempFile; use tendermint_rpc::{ endpoint::broadcast::tx_sync, request, @@ -82,15 +84,17 @@ pub async fn spawn_composer(rollup_ids: &[&str]) -> TestComposer { } let (sequencer, sequencer_setup_guard) = mock_sequencer::start().await; let sequencer_url = sequencer.uri(); + let keyfile = NamedTempFile::new().unwrap(); + (&keyfile) + .write_all("2bd806c97f0e00af1a1fc3328fa763a9269723c8db8fac4f93af71db186d6e90".as_bytes()) + .unwrap(); let config = Config { log: String::new(), api_listen_addr: "127.0.0.1:0".parse().unwrap(), sequencer_chain_id: "test-chain-1".to_string(), rollups, sequencer_url, - private_key: "2bd806c97f0e00af1a1fc3328fa763a9269723c8db8fac4f93af71db186d6e90" - .to_string() - .into(), + private_key_file: keyfile.path().to_string_lossy().to_string(), block_time_ms: 2000, max_bytes_per_bundle: 200_000, bundle_queue_capacity: 10, diff --git a/crates/astria-conductor/src/celestia/block_verifier.rs b/crates/astria-conductor/src/celestia/block_verifier.rs index aebf592b2..b945612f9 100644 --- a/crates/astria-conductor/src/celestia/block_verifier.rs +++ b/crates/astria-conductor/src/celestia/block_verifier.rs @@ -268,7 +268,7 @@ mod test { ) -> (validators::Response, account::Id, Commit) { use rand::rngs::OsRng; - let signing_key = ed25519_consensus::SigningKey::new(OsRng); + let signing_key = astria_core::crypto::SigningKey::new(OsRng); let pub_key = tendermint::public_key::PublicKey::from_raw_ed25519( signing_key.verification_key().as_ref(), ) diff --git a/crates/astria-conductor/tests/blackbox/helpers/mod.rs b/crates/astria-conductor/tests/blackbox/helpers/mod.rs index 661d4e857..e4faaff9a 100644 --- a/crates/astria-conductor/tests/blackbox/helpers/mod.rs +++ b/crates/astria-conductor/tests/blackbox/helpers/mod.rs @@ -510,12 +510,12 @@ pub fn make_blobs(heights: &[u32]) -> Blobs { } } -fn signing_key() -> ed25519_consensus::SigningKey { +fn signing_key() -> astria_core::crypto::SigningKey { use rand_chacha::{ rand_core::SeedableRng as _, ChaChaRng, }; - ed25519_consensus::SigningKey::new(ChaChaRng::seed_from_u64(0)) + astria_core::crypto::SigningKey::new(ChaChaRng::seed_from_u64(0)) } fn validator() -> tendermint::validator::Info { diff --git a/crates/astria-core/Cargo.toml b/crates/astria-core/Cargo.toml index f318c9fda..3a416eeeb 100644 --- a/crates/astria-core/Cargo.toml +++ b/crates/astria-core/Cargo.toml @@ -31,7 +31,7 @@ pbjson-types = { workspace = true } penumbra-ibc = { workspace = true } penumbra-proto = { workspace = true } prost = { workspace = true } -rand = { workspace = true, optional = true } +rand = { workspace = true } serde = { workspace = true, features = ["derive"], optional = true } sha2 = { workspace = true } tendermint = { workspace = true } @@ -41,13 +41,14 @@ tonic = { workspace = true, optional = true } tracing = { workspace = true } base64-serde = { workspace = true, optional = true } base64 = { workspace = true } +zeroize = { version = "1.7.0", features = ["zeroize_derive"] } [features] celestia = ["dep:celestia-types"] client = ["dep:tonic"] serde = ["dep:serde", "dep:pbjson", "dep:base64-serde"] server = ["dep:tonic"] -test-utils = ["dep:rand"] +test-utils = [] base64-serde = ["dep:base64-serde"] brotli = ["dep:brotli"] diff --git a/crates/astria-core/src/crypto.rs b/crates/astria-core/src/crypto.rs new file mode 100644 index 000000000..556fe68c2 --- /dev/null +++ b/crates/astria-core/src/crypto.rs @@ -0,0 +1,86 @@ +use std::fmt::{ + self, + Debug, + Formatter, +}; + +use ed25519_consensus::{ + Signature, + SigningKey as Ed25519SigningKey, + VerificationKey, +}; +use rand::{ + CryptoRng, + RngCore, +}; +use zeroize::{ + Zeroize, + ZeroizeOnDrop, +}; + +/// An Ed25519 signing key. +// *Implementation note*: this is currently a refinement type around +// ed25519_consensus::SigningKey overriding its Debug implementation +// to not accidentally leak it. +#[derive(Clone, Zeroize, ZeroizeOnDrop)] +pub struct SigningKey(Ed25519SigningKey); + +impl SigningKey { + /// Generates a new signing key. + pub fn new(rng: R) -> Self { + Self(Ed25519SigningKey::new(rng)) + } + + /// Creates a signature on `msg` using this key. + #[must_use] + pub fn sign(&self, msg: &[u8]) -> Signature { + self.0.sign(msg) + } + + /// Returns the byte encoding of the signing key. + #[must_use] + pub fn to_bytes(&self) -> [u8; 32] { + self.0.to_bytes() + } + + /// Returns the byte encoding of the signing key. + #[must_use] + pub fn as_bytes(&self) -> &[u8; 32] { + self.0.as_bytes() + } + + /// Returns the verification key associated with this signing key. + #[must_use] + pub fn verification_key(&self) -> VerificationKey { + self.0.verification_key() + } +} + +impl Debug for SigningKey { + fn fmt(&self, formatter: &mut Formatter) -> fmt::Result { + formatter + .debug_struct("SigningKey") + .field("verification_key", &self.0.verification_key()) + .finish_non_exhaustive() // avoids printing secret fields + } +} + +impl<'a> From<&'a SigningKey> for VerificationKey { + fn from(signing_key: &'a SigningKey) -> VerificationKey { + signing_key.verification_key() + } +} + +impl TryFrom<&[u8]> for SigningKey { + type Error = ed25519_consensus::Error; + + fn try_from(slice: &[u8]) -> Result { + Ok(Self(Ed25519SigningKey::try_from(slice)?)) + } +} + +impl From<[u8; 32]> for SigningKey { + fn from(seed: [u8; 32]) -> Self { + Self(Ed25519SigningKey::from(seed)) + } +} diff --git a/crates/astria-core/src/lib.rs b/crates/astria-core/src/lib.rs index c9b07569f..99c9a851f 100644 --- a/crates/astria-core/src/lib.rs +++ b/crates/astria-core/src/lib.rs @@ -6,6 +6,7 @@ compile_error!( #[rustfmt::skip] pub mod generated; +pub mod crypto; pub mod execution; pub mod primitive; pub mod protocol; diff --git a/crates/astria-core/src/protocol/test_utils.rs b/crates/astria-core/src/protocol/test_utils.rs index 541f5ac08..4fa099e20 100644 --- a/crates/astria-core/src/protocol/test_utils.rs +++ b/crates/astria-core/src/protocol/test_utils.rs @@ -13,6 +13,7 @@ use super::{ }, }; use crate::{ + crypto::SigningKey, primitive::v1::{ asset::default_native_asset_id, derive_merkle_tree_from_rollup_txs, @@ -49,7 +50,7 @@ pub struct ConfigureSequencerBlock { pub chain_id: Option, pub height: u32, pub proposer_address: Option, - pub signing_key: Option, + pub signing_key: Option, pub sequence_data: Vec<(RollupId, Vec)>, pub deposits: Vec, pub unix_timestamp: UnixTimeStamp, @@ -81,8 +82,7 @@ impl ConfigureSequencerBlock { let block_hash = block_hash.unwrap_or_default(); let chain_id = chain_id.unwrap_or_else(|| "test".to_string()); - let signing_key = - signing_key.unwrap_or_else(|| ed25519_consensus::SigningKey::new(rand::rngs::OsRng)); + let signing_key = signing_key.unwrap_or_else(|| SigningKey::new(rand::rngs::OsRng)); let proposer_address = proposer_address.unwrap_or_else(|| { let public_key: tendermint::crypto::ed25519::VerificationKey = diff --git a/crates/astria-core/src/protocol/transaction/v1alpha1/mod.rs b/crates/astria-core/src/protocol/transaction/v1alpha1/mod.rs index d8956ef55..78354b8bf 100644 --- a/crates/astria-core/src/protocol/transaction/v1alpha1/mod.rs +++ b/crates/astria-core/src/protocol/transaction/v1alpha1/mod.rs @@ -1,6 +1,5 @@ use ed25519_consensus::{ Signature, - SigningKey, VerificationKey, }; use prost::{ @@ -9,6 +8,7 @@ use prost::{ }; use super::raw; +use crate::crypto::SigningKey; pub mod action; pub use action::Action; diff --git a/crates/astria-sequencer-client/src/tests/http.rs b/crates/astria-sequencer-client/src/tests/http.rs index e4b69581c..34d2b6153 100644 --- a/crates/astria-sequencer-client/src/tests/http.rs +++ b/crates/astria-sequencer-client/src/tests/http.rs @@ -1,4 +1,5 @@ use astria_core::{ + crypto::SigningKey, primitive::v1::{ asset::default_native_asset_id, Address, @@ -10,7 +11,6 @@ use astria_core::{ UnsignedTransaction, }, }; -use ed25519_consensus::SigningKey; use hex_literal::hex; use serde_json::json; use tendermint::{ diff --git a/crates/astria-sequencer-relayer/Cargo.toml b/crates/astria-sequencer-relayer/Cargo.toml index 22f770eec..4213c7fe2 100644 --- a/crates/astria-sequencer-relayer/Cargo.toml +++ b/crates/astria-sequencer-relayer/Cargo.toml @@ -20,13 +20,11 @@ http = "0.2.9" k256 = "0.13.3" pin-project-lite = "0.2" serde_path_to_error = "0.1.13" -zeroize = { version = "1.6.0", features = ["zeroize_derive"] } axum = { workspace = true } base64 = { workspace = true } base64-serde = { workspace = true } celestia-types = { workspace = true } -ed25519-consensus = { workspace = true } futures = { workspace = true } hex = { workspace = true, features = ["serde"] } humantime = { workspace = true } diff --git a/crates/astria-sequencer-relayer/src/validator.rs b/crates/astria-sequencer-relayer/src/validator.rs index 2e1abe541..dc385735a 100644 --- a/crates/astria-sequencer-relayer/src/validator.rs +++ b/crates/astria-sequencer-relayer/src/validator.rs @@ -2,36 +2,19 @@ use std::path::Path; use astria_eyre::eyre::{ self, - bail, WrapErr as _, }; -use ed25519_consensus::{ - SigningKey, - VerificationKey, -}; use tendermint::account; use tendermint_config::PrivValidatorKey; use tracing::instrument; -use zeroize::{ - Zeroize, - ZeroizeOnDrop, -}; /// `Validator` holds the ed25519 keys to sign and verify tendermint /// messages. It also contains its address (`AccountId`) in the tendermint network. -#[derive(Clone, Debug, Zeroize, ZeroizeOnDrop)] +#[derive(Clone, Debug)] pub(crate) struct Validator { /// The tendermint validator account address; defined as /// Sha256(verification_key)[..20]. - #[zeroize(skip)] - pub(crate) address: account::Id, - - /// The ed25519 signing key of this validator. - pub(crate) signing_key: SigningKey, - - #[zeroize(skip)] - /// The ed25519 verification key of this validator. - pub(crate) verification_key: VerificationKey, + pub(super) address: account::Id, } impl Validator { @@ -43,32 +26,8 @@ impl Validator { pub(crate) fn from_path(path: impl AsRef) -> eyre::Result { let key = PrivValidatorKey::load_json_file(&path.as_ref()) .wrap_err("failed reading private validator key from file")?; - Self::from_priv_validator_key(key) - } - - pub(crate) fn from_priv_validator_key(key: PrivValidatorKey) -> eyre::Result { - let PrivValidatorKey { - address, - pub_key, - priv_key, - } = key; - let Some(tendermint_signing_key) = priv_key.ed25519_signing_key().cloned() else { - bail!("deserialized private key was not ed25519"); - }; - let signing_key = tendermint_signing_key.try_into().wrap_err( - "failed constructing ed25519 signing key from deserialized tendermint private key", - )?; - let Some(tendermint_verification_key) = pub_key.ed25519() else { - bail!("deserialized public key was not ed25519"); - }; - let verification_key = tendermint_verification_key.try_into().wrap_err( - "failed constructing ed25519 verification key from deserialized tendermint public key", - )?; - Ok(Self { - address, - signing_key, - verification_key, + address: key.address, }) } } diff --git a/crates/astria-sequencer-relayer/tests/blackbox/helpers/test_sequencer_relayer.rs b/crates/astria-sequencer-relayer/tests/blackbox/helpers/test_sequencer_relayer.rs index a3b4fadd9..dcba5ca0e 100644 --- a/crates/astria-sequencer-relayer/tests/blackbox/helpers/test_sequencer_relayer.rs +++ b/crates/astria-sequencer-relayer/tests/blackbox/helpers/test_sequencer_relayer.rs @@ -13,14 +13,16 @@ use std::{ }; use assert_json_diff::assert_json_include; -use astria_core::primitive::v1::RollupId; +use astria_core::{ + crypto::SigningKey, + primitive::v1::RollupId, +}; use astria_grpc_mock::MockGuard as GrpcMockGuard; use astria_sequencer_relayer::{ config::Config, SequencerRelayer, ShutdownHandle, }; -use ed25519_consensus::SigningKey; use futures::TryFutureExt; use itertools::Itertools; use once_cell::sync::Lazy; @@ -637,6 +639,7 @@ impl TestSequencerRelayerConfig { .ed25519_signing_key() .cloned() .unwrap() + .as_bytes() .try_into() .unwrap(); diff --git a/crates/astria-sequencer/src/app/test_utils.rs b/crates/astria-sequencer/src/app/test_utils.rs index 22d2dba2d..f2b618d5e 100644 --- a/crates/astria-sequencer/src/app/test_utils.rs +++ b/crates/astria-sequencer/src/app/test_utils.rs @@ -1,4 +1,5 @@ use astria_core::{ + crypto::SigningKey, primitive::v1::{ asset::DEFAULT_NATIVE_ASSET_DENOM, Address, @@ -13,7 +14,6 @@ use astria_core::{ }, }; use cnidarium::Storage; -use ed25519_consensus::SigningKey; use penumbra_ibc::params::IBCParameters; use crate::{ diff --git a/crates/astria-sequencer/src/app/tests_execute_transaction.rs b/crates/astria-sequencer/src/app/tests_execute_transaction.rs index 7bc739fdf..20b5bfe79 100644 --- a/crates/astria-sequencer/src/app/tests_execute_transaction.rs +++ b/crates/astria-sequencer/src/app/tests_execute_transaction.rs @@ -1,6 +1,7 @@ #[cfg(feature = "mint")] use astria_core::protocol::transaction::v1alpha1::action::MintAction; use astria_core::{ + crypto::SigningKey, primitive::v1::{ asset, asset::DEFAULT_NATIVE_ASSET_DENOM, @@ -23,7 +24,6 @@ use astria_core::{ sequencerblock::v1alpha1::block::Deposit, }; use cnidarium::StateDelta; -use ed25519_consensus::SigningKey; use penumbra_ibc::params::IBCParameters; use crate::{ diff --git a/crates/astria-sequencer/src/proposal/commitment.rs b/crates/astria-sequencer/src/proposal/commitment.rs index 3c67ea429..3d86a03ca 100644 --- a/crates/astria-sequencer/src/proposal/commitment.rs +++ b/crates/astria-sequencer/src/proposal/commitment.rs @@ -88,6 +88,7 @@ pub(crate) fn generate_rollup_datas_commitment( #[cfg(test)] mod test { use astria_core::{ + crypto::SigningKey, primitive::v1::{ asset::{ Denom, @@ -104,7 +105,6 @@ mod test { UnsignedTransaction, }, }; - use ed25519_consensus::SigningKey; use rand::rngs::OsRng; use super::*; diff --git a/crates/astria-sequencer/src/service/consensus.rs b/crates/astria-sequencer/src/service/consensus.rs index d3de2757e..91f06de2c 100644 --- a/crates/astria-sequencer/src/service/consensus.rs +++ b/crates/astria-sequencer/src/service/consensus.rs @@ -219,6 +219,7 @@ mod test { }; use astria_core::{ + crypto::SigningKey, primitive::v1::{ asset::DEFAULT_NATIVE_ASSET_DENOM, Address, @@ -231,10 +232,7 @@ mod test { }, }; use bytes::Bytes; - use ed25519_consensus::{ - SigningKey, - VerificationKey, - }; + use ed25519_consensus::VerificationKey; use prost::Message as _; use rand::rngs::OsRng; use tendermint::{