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#[derive(Clone, Derivative, Parser)]
22#[derivative(Debug)]
23pub struct KeySetOptions {
24 #[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 #[clap(long, env = "ESPRESSO_SEQUENCER_KEY_INDEX", requires = "MNEMONIC")]
36 pub index: Option<u64>,
37
38 #[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 #[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 #[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 #[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 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 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}