Merge #127
127: Deterministic peer id for alice and bob r=da-kami a=da-kami The first commit introduces a seed file similar to Nectar. Note, that the parameter --database was changed to --data-dir where the database is stored in the sub-folder database inside that data directory. This is breaking change, run commands will have to be adapted. I opted for keeping the structure of generating an overall seed and then deriving the network seed from it (as in Nectar, where we use the seed for wallet creation as well). I feel this is cleaner than just using the seed for the network only. The second commit applies the deterministic peer id to Bob as well. We don't have to do this because in the current setup Bob can have a new identity every time. I would still harmonize this to avoid confusion in the future. I don't see a reason why Bob's setup should be different from Alice here. Co-authored-by: Daniel Karzel <daniel@comit.network>pull/119/head
commit
485220929e
@ -0,0 +1,27 @@
|
||||
use tracing::subscriber::DefaultGuard;
|
||||
use tracing_log::LogTracer;
|
||||
|
||||
/// Utility function to initialize logging in the test environment.
|
||||
/// Note that you have to keep the `_guard` in scope after calling in test:
|
||||
///
|
||||
/// ```rust
|
||||
/// let _guard = init_tracing();
|
||||
/// ```
|
||||
pub fn init_tracing() -> DefaultGuard {
|
||||
// converts all log records into tracing events
|
||||
// Note: Make sure to initialize without unwrapping, otherwise this causes
|
||||
// trouble when running multiple tests.
|
||||
let _ = LogTracer::init();
|
||||
|
||||
let global_filter = tracing::Level::WARN;
|
||||
let test_filter = tracing::Level::DEBUG;
|
||||
let monero_harness_filter = tracing::Level::INFO;
|
||||
|
||||
use tracing_subscriber::util::SubscriberInitExt as _;
|
||||
tracing_subscriber::fmt()
|
||||
.with_env_filter(format!(
|
||||
"{},test={},monero_harness={}",
|
||||
global_filter, test_filter, monero_harness_filter,
|
||||
))
|
||||
.set_default()
|
||||
}
|
@ -0,0 +1,196 @@
|
||||
use crate::{fs::ensure_directory_exists, seed};
|
||||
use pem::{encode, Pem};
|
||||
use seed::SEED_LENGTH;
|
||||
use std::{
|
||||
ffi::OsStr,
|
||||
fmt,
|
||||
fs::{self, File},
|
||||
io::{self, Write},
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
#[derive(Clone, Copy, PartialEq)]
|
||||
pub struct Seed(seed::Seed);
|
||||
|
||||
impl Seed {
|
||||
pub fn random() -> Result<Self, Error> {
|
||||
Ok(Seed(seed::Seed::random()?))
|
||||
}
|
||||
|
||||
pub fn from_file_or_generate(data_dir: &PathBuf) -> Result<Self, Error> {
|
||||
let file_path_buf = data_dir.join("seed.pem");
|
||||
let file_path = Path::new(&file_path_buf);
|
||||
|
||||
if file_path.exists() {
|
||||
return Self::from_file(&file_path);
|
||||
}
|
||||
|
||||
tracing::info!("No seed file found, creating at: {}", file_path.display());
|
||||
|
||||
let random_seed = Seed::random()?;
|
||||
random_seed.write_to(file_path.to_path_buf())?;
|
||||
|
||||
Ok(random_seed)
|
||||
}
|
||||
|
||||
fn from_file<D>(seed_file: D) -> Result<Self, Error>
|
||||
where
|
||||
D: AsRef<OsStr>,
|
||||
{
|
||||
let file = Path::new(&seed_file);
|
||||
let contents = fs::read_to_string(file)?;
|
||||
let pem = pem::parse(contents)?;
|
||||
|
||||
tracing::info!("Read in seed from file: {}", file.display());
|
||||
|
||||
Self::from_pem(pem)
|
||||
}
|
||||
|
||||
fn from_pem(pem: pem::Pem) -> Result<Self, Error> {
|
||||
if pem.contents.len() != SEED_LENGTH {
|
||||
Err(Error::IncorrectLength(pem.contents.len()))
|
||||
} else {
|
||||
let mut array = [0; SEED_LENGTH];
|
||||
for (i, b) in pem.contents.iter().enumerate() {
|
||||
array[i] = *b;
|
||||
}
|
||||
|
||||
Ok(Self::from(array))
|
||||
}
|
||||
}
|
||||
|
||||
fn write_to(&self, seed_file: PathBuf) -> Result<(), Error> {
|
||||
ensure_directory_exists(&seed_file)?;
|
||||
|
||||
let data = (self.0).bytes();
|
||||
let pem = Pem {
|
||||
tag: String::from("SEED"),
|
||||
contents: data.to_vec(),
|
||||
};
|
||||
|
||||
let pem_string = encode(&pem);
|
||||
|
||||
let mut file = File::create(seed_file)?;
|
||||
file.write_all(pem_string.as_bytes())?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for Seed {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "Seed([*****])")
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Seed {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{:?}", self)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<[u8; SEED_LENGTH]> for Seed {
|
||||
fn from(bytes: [u8; 32]) -> Self {
|
||||
Seed(seed::Seed::from(bytes))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Seed> for seed::Seed {
|
||||
fn from(seed: Seed) -> Self {
|
||||
seed.0
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum Error {
|
||||
#[error("Seed generation: ")]
|
||||
SeedGeneration(#[from] crate::seed::Error),
|
||||
#[error("io: ")]
|
||||
Io(#[from] io::Error),
|
||||
#[error("PEM parse: ")]
|
||||
PemParse(#[from] pem::PemError),
|
||||
#[error("expected 32 bytes of base64 encode, got {0} bytes")]
|
||||
IncorrectLength(usize),
|
||||
#[error("RNG: ")]
|
||||
Rand(#[from] rand::Error),
|
||||
#[error("no default path")]
|
||||
NoDefaultPath,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::env::temp_dir;
|
||||
|
||||
#[test]
|
||||
fn seed_byte_string_must_be_32_bytes_long() {
|
||||
let _seed = Seed::from(*b"this string is exactly 32 bytes!");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn seed_from_pem_works() {
|
||||
let payload: &str = "syl9wSYaruvgxg9P5Q1qkZaq5YkM6GvXkxe+VYrL/XM=";
|
||||
|
||||
// 32 bytes base64 encoded.
|
||||
let pem_string: &str = "-----BEGIN SEED-----
|
||||
syl9wSYaruvgxg9P5Q1qkZaq5YkM6GvXkxe+VYrL/XM=
|
||||
-----END SEED-----
|
||||
";
|
||||
|
||||
let want = base64::decode(payload).unwrap();
|
||||
let pem = pem::parse(pem_string).unwrap();
|
||||
let got = Seed::from_pem(pem).unwrap();
|
||||
|
||||
assert_eq!((got.0).bytes(), *want);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn seed_from_pem_fails_for_short_seed() {
|
||||
let short = "-----BEGIN SEED-----
|
||||
VnZUNFZ4dlY=
|
||||
-----END SEED-----
|
||||
";
|
||||
let pem = pem::parse(short).unwrap();
|
||||
match Seed::from_pem(pem) {
|
||||
Ok(_) => panic!("should fail for short payload"),
|
||||
Err(e) => {
|
||||
match e {
|
||||
Error::IncorrectLength(_) => {} // pass
|
||||
_ => panic!("should fail with IncorrectLength error"),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn seed_from_pem_fails_for_long_seed() {
|
||||
let long = "-----BEGIN SEED-----
|
||||
mbKANv2qKGmNVg1qtquj6Hx1pFPelpqOfE2JaJJAMEg1FlFhNRNlFlE=
|
||||
mbKANv2qKGmNVg1qtquj6Hx1pFPelpqOfE2JaJJAMEg1FlFhNRNlFlE=
|
||||
-----END SEED-----
|
||||
";
|
||||
let pem = pem::parse(long).unwrap();
|
||||
match Seed::from_pem(pem) {
|
||||
Ok(_) => panic!("should fail for long payload"),
|
||||
Err(e) => {
|
||||
match e {
|
||||
Error::IncorrectLength(_) => {} // pass
|
||||
_ => panic!("should fail with IncorrectLength error"),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn round_trip_through_file_write_read() {
|
||||
let tmpfile = temp_dir().join("seed.pem");
|
||||
|
||||
let seed = Seed::random().unwrap();
|
||||
seed.write_to(tmpfile.clone())
|
||||
.expect("Write seed to temp file");
|
||||
|
||||
let rinsed = Seed::from_file(tmpfile).expect("Read from temp file");
|
||||
assert_eq!(seed.0, rinsed.0);
|
||||
}
|
||||
}
|
@ -0,0 +1,14 @@
|
||||
use std::path::Path;
|
||||
|
||||
pub fn ensure_directory_exists(file: &Path) -> Result<(), std::io::Error> {
|
||||
if let Some(path) = file.parent() {
|
||||
if !path.exists() {
|
||||
tracing::info!(
|
||||
"Parent directory does not exist, creating recursively: {}",
|
||||
file.display()
|
||||
);
|
||||
return std::fs::create_dir_all(path);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
@ -0,0 +1,58 @@
|
||||
use ::bitcoin::secp256k1::{self, constants::SECRET_KEY_SIZE, SecretKey};
|
||||
use rand::prelude::*;
|
||||
use std::fmt;
|
||||
|
||||
pub const SEED_LENGTH: usize = 32;
|
||||
|
||||
#[derive(Clone, Copy, Eq, PartialEq)]
|
||||
pub struct Seed([u8; SEED_LENGTH]);
|
||||
|
||||
impl Seed {
|
||||
pub fn random() -> Result<Self, Error> {
|
||||
let mut bytes = [0u8; SECRET_KEY_SIZE];
|
||||
rand::thread_rng().fill_bytes(&mut bytes);
|
||||
|
||||
// If it succeeds once, it'll always succeed
|
||||
let _ = SecretKey::from_slice(&bytes)?;
|
||||
|
||||
Ok(Seed(bytes))
|
||||
}
|
||||
|
||||
pub fn bytes(&self) -> [u8; SEED_LENGTH] {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for Seed {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "Seed([*****])")
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Seed {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{:?}", self)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<[u8; SEED_LENGTH]> for Seed {
|
||||
fn from(bytes: [u8; SEED_LENGTH]) -> Self {
|
||||
Seed(bytes)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, thiserror::Error)]
|
||||
pub enum Error {
|
||||
#[error("Secp256k1: ")]
|
||||
Secp256k1(#[from] secp256k1::Error),
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn generate_random_seed() {
|
||||
let _ = Seed::random().unwrap();
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue