1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
//! Farm identity

use parity_scale_codec::{Decode, Encode};
use schnorrkel::context::SigningContext;
use schnorrkel::{ExpansionMode, Keypair, PublicKey, SecretKey, Signature};
use std::ops::Deref;
use std::path::Path;
use std::{fmt, fs, io};
use subspace_core_primitives::REWARD_SIGNING_CONTEXT;
use substrate_bip39::mini_secret_from_entropy;
use thiserror::Error;
use tracing::debug;
use zeroize::Zeroizing;

/// Entropy used for identity generation.
const ENTROPY_LENGTH: usize = 32;

#[derive(Debug, Encode, Decode)]
struct IdentityFileContents {
    entropy: Vec<u8>,
}

fn keypair_from_entropy(entropy: &[u8]) -> Keypair {
    mini_secret_from_entropy(entropy, "")
        .expect("32 bytes can always build a key; qed")
        .expand_to_keypair(ExpansionMode::Ed25519)
}

/// Errors happening when trying to create/open single disk farm
#[derive(Debug, Error)]
pub enum IdentityError {
    /// I/O error occurred
    #[error("Identity I/O error: {0}")]
    Io(#[from] io::Error),
    /// Decoding error
    #[error("Decoding error: {0}")]
    Decoding(#[from] parity_scale_codec::Error),
}

/// `Identity` struct is an abstraction of public & secret key related operations.
///
/// It is basically a wrapper of the keypair (which holds public & secret keys)
/// and a context that will be used for signing.
#[derive(Clone)]
pub struct Identity {
    keypair: Zeroizing<Keypair>,
    entropy: Zeroizing<Vec<u8>>,
    substrate_ctx: SigningContext,
}

impl fmt::Debug for Identity {
    #[inline]
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_struct("Identity")
            .field("keypair", &self.keypair)
            .finish_non_exhaustive()
    }
}

impl Deref for Identity {
    type Target = Keypair;

    #[inline]
    fn deref(&self) -> &Self::Target {
        &self.keypair
    }
}

impl Identity {
    pub(crate) const FILE_NAME: &'static str = "identity.bin";

    /// Size of the identity file on disk
    pub fn file_size() -> usize {
        IdentityFileContents {
            entropy: vec![0; ENTROPY_LENGTH],
        }
        .encoded_size()
    }

    /// Opens the existing identity, or creates a new one.
    pub fn open_or_create<B: AsRef<Path>>(base_directory: B) -> Result<Self, IdentityError> {
        if let Some(identity) = Self::open(base_directory.as_ref())? {
            Ok(identity)
        } else {
            Self::create(base_directory)
        }
    }

    /// Opens the existing identity, returns `Ok(None)` if it doesn't exist.
    pub fn open<B: AsRef<Path>>(base_directory: B) -> Result<Option<Self>, IdentityError> {
        let identity_file = base_directory.as_ref().join(Self::FILE_NAME);
        if identity_file.exists() {
            debug!("Opening existing keypair");
            let bytes = Zeroizing::new(fs::read(identity_file)?);
            let IdentityFileContents { entropy } =
                IdentityFileContents::decode(&mut bytes.as_ref())?;

            Ok(Some(Self {
                keypair: Zeroizing::new(keypair_from_entropy(&entropy)),
                entropy: Zeroizing::new(entropy),
                substrate_ctx: schnorrkel::context::signing_context(REWARD_SIGNING_CONTEXT),
            }))
        } else {
            debug!("Existing keypair not found");
            Ok(None)
        }
    }

    /// Creates new identity, overrides identity that might already exist.
    pub fn create<B: AsRef<Path>>(base_directory: B) -> Result<Self, IdentityError> {
        let identity_file = base_directory.as_ref().join(Self::FILE_NAME);
        debug!("Generating new keypair");
        let entropy = rand::random::<[u8; ENTROPY_LENGTH]>().to_vec();

        let identity_file_contents = IdentityFileContents { entropy };
        fs::write(identity_file, identity_file_contents.encode())?;

        let IdentityFileContents { entropy } = identity_file_contents;

        Ok(Self {
            keypair: Zeroizing::new(keypair_from_entropy(&entropy)),
            entropy: Zeroizing::new(entropy),
            substrate_ctx: schnorrkel::context::signing_context(REWARD_SIGNING_CONTEXT),
        })
    }

    /// Create identity from given entropy, overrides identity that might already exist.
    ///
    /// Primarily used for testing.
    #[doc(hidden)]
    pub fn from_entropy<B: AsRef<Path>>(
        base_directory: B,
        entropy: Vec<u8>,
    ) -> Result<Self, IdentityError> {
        let identity_file = base_directory.as_ref().join(Self::FILE_NAME);
        debug!("Creating identity from provided entropy");

        let identity_file_contents = IdentityFileContents { entropy };
        fs::write(identity_file, identity_file_contents.encode())?;

        let IdentityFileContents { entropy } = identity_file_contents;

        Ok(Self {
            keypair: Zeroizing::new(keypair_from_entropy(&entropy)),
            entropy: Zeroizing::new(entropy),
            substrate_ctx: schnorrkel::context::signing_context(REWARD_SIGNING_CONTEXT),
        })
    }

    /// Returns the public key of the identity.
    pub fn public_key(&self) -> &PublicKey {
        &self.keypair.public
    }

    /// Returns the secret key of the identity.
    pub fn secret_key(&self) -> &SecretKey {
        &self.keypair.secret
    }

    /// Returns entropy used to generate keypair.
    pub fn entropy(&self) -> &[u8] {
        &self.entropy
    }

    /// Sign reward hash.
    pub fn sign_reward_hash(&self, header_hash: &[u8]) -> Signature {
        self.keypair.sign(self.substrate_ctx.bytes(header_hash))
    }
}