espresso_node/
keyset.rs

1use std::{collections::HashMap, path::PathBuf};
2
3use alloy::signers::local::coins_bip39::{English, Mnemonic};
4use anyhow::{Context, bail};
5use clap::Parser;
6use derivative::Derivative;
7use hotshot::types::{BLSPrivKey, BLSPubKey, SignatureKey};
8use hotshot_types::{
9    light_client::{StateKeyPair, StateSignKey},
10    x25519,
11};
12use tagged_base64::TaggedBase64;
13
14/// Keys can be specified in one of three ways:
15/// * A mnemonic phrase
16/// * A path to a key file
17/// * Individual private keys set via environment variable
18///
19/// Note that the third option always takes precedence, but if not all keys are specified explicitly
20/// in this way, one of the first two options may be used to generate the remaining keys.
21#[derive(Clone, Derivative, Parser)]
22#[derivative(Debug)]
23pub struct KeySetOptions {
24    /// Mnemonic phrase used to generate keys.
25    #[clap(
26        long,
27        name = "MNEMONIC",
28        env = "ESPRESSO_SEQUENCER_KEY_MNEMONIC",
29        conflicts_with = "KEY_FILE"
30    )]
31    #[derivative(Debug = "ignore")]
32    pub mnemonic: Option<Mnemonic<English>>,
33
34    /// Optional index to enable generating multiple keysets from the same mnemonic.
35    #[clap(long, env = "ESPRESSO_SEQUENCER_KEY_INDEX", requires = "MNEMONIC")]
36    pub index: Option<u64>,
37
38    /// Path to file containing private keys.
39    ///
40    /// The file should follow the .env format, with two keys:
41    /// * ESPRESSO_SEQUENCER_PRIVATE_STAKING_KEY
42    /// * ESPRESSO_SEQUENCER_PRIVATE_STATE_KEY
43    ///
44    /// Appropriate key files can be generated with the `keygen` utility program.
45    #[clap(
46        long,
47        name = "KEY_FILE",
48        env = "ESPRESSO_SEQUENCER_KEY_FILE",
49        conflicts_with = "MNEMONIC"
50    )]
51    pub key_file: Option<PathBuf>,
52
53    /// Private staking key.
54    ///
55    /// This can be used as an alternative to MNEMONIC or KEY_FILE.
56    #[clap(
57        long,
58        env = "ESPRESSO_SEQUENCER_PRIVATE_STAKING_KEY",
59        conflicts_with = "KEY_FILE"
60    )]
61    #[derivative(Debug = "ignore")]
62    pub private_staking_key: Option<TaggedBase64>,
63
64    /// Private state signing key.
65    ///
66    /// This can be used as an alternative to MNEMONIC or KEY_FILE.
67    #[clap(
68        long,
69        env = "ESPRESSO_SEQUENCER_PRIVATE_STATE_KEY",
70        conflicts_with = "KEY_FILE"
71    )]
72    #[derivative(Debug = "ignore")]
73    pub private_state_key: Option<TaggedBase64>,
74
75    /// Private x25519 key.
76    ///
77    /// This can be used as an alternative to MNEMONIC or KEY_FILE.
78    #[clap(
79        long,
80        env = "ESPRESSO_SEQUENCER_PRIVATE_X25519_KEY",
81        conflicts_with = "KEY_FILE"
82    )]
83    #[derivative(Debug = "ignore")]
84    pub private_x25519_key: Option<TaggedBase64>,
85}
86
87#[derive(Clone, Derivative)]
88#[derivative(Debug)]
89pub struct KeySet {
90    #[derivative(Debug = "ignore")]
91    pub staking: BLSPrivKey,
92    #[derivative(Debug = "ignore")]
93    pub state: StateSignKey,
94    #[derivative(Debug = "ignore")]
95    pub x25519: x25519::SecretKey,
96}
97
98impl TryFrom<KeySetOptions> for KeySet {
99    type Error = anyhow::Error;
100
101    fn try_from(opt: KeySetOptions) -> Result<Self, Self::Error> {
102        // If any of the keys are set explicitly, those take precedence.
103        let mut staking = opt
104            .private_staking_key
105            .map(BLSPrivKey::try_from)
106            .transpose()
107            .context("parsing private staking key")?;
108        let mut state = opt
109            .private_state_key
110            .map(StateSignKey::try_from)
111            .transpose()
112            .context("parsing private state key")?;
113        let mut x25519 = opt
114            .private_x25519_key
115            .map(x25519::SecretKey::try_from)
116            .transpose()
117            .context("parsing private x25519 key")?;
118
119        // If provided, a mnemonic or key file can be used to fill in missing keys.
120        if let Some(mnemonic) = opt.mnemonic {
121            let entropy = mnemonic.to_seed(None).context("invalid mnemonic")?;
122            let index = opt.index.unwrap_or_default();
123            if staking.is_none() {
124                let seed = blake3::derive_key("espresso staking key", &entropy);
125                staking = Some(BLSPubKey::generated_from_seed_indexed(seed, index).1);
126            }
127            if state.is_none() {
128                let seed = blake3::derive_key("espresso state key", &entropy);
129                state = Some(
130                    StateKeyPair::generate_from_seed_indexed(seed, index)
131                        .0
132                        .sign_key(),
133                );
134            }
135            if x25519.is_none() {
136                let seed = blake3::derive_key("espresso x25519 key", &entropy);
137                x25519 = Some(
138                    x25519::Keypair::generated_from_seed_indexed(seed, index)
139                        .context("generating x25519 key from mnemonic")?
140                        .secret_key(),
141                );
142            }
143        } else if let Some(path) = &opt.key_file {
144            let vars = dotenvy::from_path_iter(path)
145                .context("reading key file")?
146                .collect::<Result<HashMap<_, _>, _>>()
147                .context("reading key file")?;
148            if staking.is_none() {
149                staking = Some(read_from_key_file(
150                    &vars,
151                    "ESPRESSO_SEQUENCER_PRIVATE_STAKING_KEY",
152                )?);
153            }
154            if state.is_none() {
155                state = Some(read_from_key_file(
156                    &vars,
157                    "ESPRESSO_SEQUENCER_PRIVATE_STATE_KEY",
158                )?);
159            }
160            if x25519.is_none() {
161                x25519 = Some(read_from_key_file(
162                    &vars,
163                    "ESPRESSO_SEQUENCER_PRIVATE_X25519_KEY",
164                )?);
165            }
166        }
167
168        let (Some(staking), Some(state), Some(x25519)) = (staking, state, x25519) else {
169            bail!("neither mnemonic, key file nor full set of private keys was provided")
170        };
171        Ok(Self {
172            staking,
173            state,
174            x25519,
175        })
176    }
177}
178
179fn read_from_key_file<
180    T: TryFrom<TaggedBase64, Error: Send + Sync + std::error::Error + 'static>,
181>(
182    vars: &HashMap<String, String>,
183    env: &str,
184) -> anyhow::Result<T> {
185    TaggedBase64::parse(vars.get(env).context(format!("key file missing {env}"))?)
186        .context(format!("key file has malformed {env}"))?
187        .try_into()
188        .context(format!("key file has malformed {env}"))
189}