subspace_farmer/single_disk_farm/
identity.rs1use parity_scale_codec::{Decode, Encode};
4use schnorrkel::context::SigningContext;
5use schnorrkel::{ExpansionMode, Keypair, PublicKey, SecretKey, Signature};
6use std::ops::Deref;
7use std::path::Path;
8use std::{fmt, fs, io};
9use subspace_core_primitives::REWARD_SIGNING_CONTEXT;
10use substrate_bip39::mini_secret_from_entropy;
11use thiserror::Error;
12use tracing::debug;
13use zeroize::Zeroizing;
14
15const ENTROPY_LENGTH: usize = 32;
17
18#[derive(Debug, Encode, Decode)]
19struct IdentityFileContents {
20 entropy: Vec<u8>,
21}
22
23fn keypair_from_entropy(entropy: &[u8]) -> Keypair {
24 mini_secret_from_entropy(entropy, "")
25 .expect("32 bytes can always build a key; qed")
26 .expand_to_keypair(ExpansionMode::Ed25519)
27}
28
29#[derive(Debug, Error)]
31pub enum IdentityError {
32 #[error("Identity I/O error: {0}")]
34 Io(#[from] io::Error),
35 #[error("Invalid contents")]
37 InvalidContents,
38 #[error("Decoding error: {0}")]
40 Decoding(#[from] parity_scale_codec::Error),
41}
42
43#[derive(Clone)]
48pub struct Identity {
49 keypair: Zeroizing<Keypair>,
50 entropy: Zeroizing<Vec<u8>>,
51 substrate_ctx: SigningContext,
52}
53
54impl fmt::Debug for Identity {
55 #[inline]
56 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
57 f.debug_struct("Identity")
58 .field("keypair", &self.keypair)
59 .finish_non_exhaustive()
60 }
61}
62
63impl Deref for Identity {
64 type Target = Keypair;
65
66 #[inline]
67 fn deref(&self) -> &Self::Target {
68 &self.keypair
69 }
70}
71
72impl Identity {
73 pub(crate) const FILE_NAME: &'static str = "identity.bin";
74
75 pub fn file_size() -> usize {
77 IdentityFileContents {
78 entropy: vec![0; ENTROPY_LENGTH],
79 }
80 .encoded_size()
81 }
82
83 pub fn open_or_create<B: AsRef<Path>>(base_directory: B) -> Result<Self, IdentityError> {
85 if let Some(identity) = Self::open(base_directory.as_ref())? {
86 Ok(identity)
87 } else {
88 Self::create(base_directory)
89 }
90 }
91
92 pub fn open<B: AsRef<Path>>(base_directory: B) -> Result<Option<Self>, IdentityError> {
94 let identity_file = base_directory.as_ref().join(Self::FILE_NAME);
95 if identity_file.exists() {
96 debug!("Opening existing keypair");
97 let bytes = Zeroizing::new(fs::read(identity_file)?);
98 let IdentityFileContents { entropy } =
99 IdentityFileContents::decode(&mut bytes.as_ref())?;
100
101 if entropy.len() != ENTROPY_LENGTH {
102 return Err(IdentityError::InvalidContents);
103 }
104
105 Ok(Some(Self {
106 keypair: Zeroizing::new(keypair_from_entropy(&entropy)),
107 entropy: Zeroizing::new(entropy),
108 substrate_ctx: schnorrkel::context::signing_context(REWARD_SIGNING_CONTEXT),
109 }))
110 } else {
111 debug!("Existing keypair not found");
112 Ok(None)
113 }
114 }
115
116 pub fn create<B: AsRef<Path>>(base_directory: B) -> Result<Self, IdentityError> {
118 let identity_file = base_directory.as_ref().join(Self::FILE_NAME);
119 debug!("Generating new keypair");
120 let entropy = rand::random::<[u8; ENTROPY_LENGTH]>().to_vec();
121
122 let identity_file_contents = IdentityFileContents { entropy };
123 fs::write(identity_file, identity_file_contents.encode())?;
124
125 let IdentityFileContents { entropy } = identity_file_contents;
126
127 Ok(Self {
128 keypair: Zeroizing::new(keypair_from_entropy(&entropy)),
129 entropy: Zeroizing::new(entropy),
130 substrate_ctx: schnorrkel::context::signing_context(REWARD_SIGNING_CONTEXT),
131 })
132 }
133
134 pub fn public_key(&self) -> &PublicKey {
136 &self.keypair.public
137 }
138
139 pub fn secret_key(&self) -> &SecretKey {
141 &self.keypair.secret
142 }
143
144 pub fn entropy(&self) -> &[u8] {
146 &self.entropy
147 }
148
149 pub fn sign_reward_hash(&self, header_hash: &[u8]) -> Signature {
151 self.keypair.sign(self.substrate_ctx.bytes(header_hash))
152 }
153}