Deterministic peer id from seed for alice

This includes the introduction of the --data-dir parameter instead of the --database.
Both the seed file and the database are stored in the data-dir, the database in sub-folder `database`.
This commit is contained in:
Daniel Karzel 2021-01-08 12:04:48 +11:00
parent 903469f62a
commit 0e09c08af5
12 changed files with 353 additions and 17 deletions

13
Cargo.lock generated
View File

@ -2242,6 +2242,17 @@ dependencies = [
"winapi 0.3.9",
]
[[package]]
name = "pem"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f4c220d01f863d13d96ca82359d1e81e64a7c6bf0637bcde7b2349630addf0c6"
dependencies = [
"base64 0.13.0",
"once_cell",
"regex",
]
[[package]]
name = "percent-encoding"
version = "2.1.0"
@ -3346,6 +3357,7 @@ dependencies = [
"log",
"monero",
"monero-harness",
"pem",
"port_check",
"prettytable-rs",
"rand 0.7.3",
@ -3361,6 +3373,7 @@ dependencies = [
"strum",
"tempfile",
"testcontainers",
"thiserror",
"time",
"tokio",
"tracing",

View File

@ -24,6 +24,7 @@ libp2p-tokio-socks5 = "0.4"
log = { version = "0.4", features = ["serde"] }
monero = { version = "0.9", features = ["serde_support"] }
monero-harness = { path = "../monero-harness" }
pem = "0.8"
prettytable-rs = "0.8"
rand = "0.7"
reqwest = { version = "0.10", default-features = false, features = ["socks"] }
@ -36,6 +37,7 @@ sled = "0.34"
structopt = "0.3"
strum = { version = "0.20", features = ["derive"] }
tempfile = "3"
thiserror = "1"
time = "0.2"
tokio = { version = "0.2", features = ["rt-threaded", "time", "macros", "sync"] }
tracing = { version = "0.1", features = ["attributes"] }

View File

@ -6,7 +6,7 @@ use crate::{
peer_tracker::{self, PeerTracker},
request_response::AliceToBob,
transport::SwapTransport,
TokioExecutor,
Seed, TokioExecutor,
},
SwapAmounts,
};
@ -142,8 +142,8 @@ pub struct Behaviour {
}
impl Behaviour {
pub fn identity(&self) -> Keypair {
self.identity.clone()
pub fn identity(&self, seed: Seed) -> Keypair {
seed.derive_libp2p_identity()
}
pub fn peer_id(&self) -> PeerId {

View File

@ -5,8 +5,8 @@ use uuid::Uuid;
#[derive(structopt::StructOpt, Debug)]
pub struct Options {
// TODO: Default value should points to proper configuration folder in home folder
#[structopt(long = "database", default_value = "./.swap-db/")]
pub db_path: String,
#[structopt(long = "data-dir", default_value = "./.swap-data/")]
pub data_dir: String,
#[structopt(subcommand)]
pub cmd: Command,

3
swap/src/config.rs Normal file
View File

@ -0,0 +1,3 @@
mod seed;
pub use self::seed::Seed;

196
swap/src/config/seed.rs Normal file
View File

@ -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);
}
}

14
swap/src/fs.rs Normal file
View File

@ -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(())
}

View File

@ -20,9 +20,12 @@ pub mod alice;
pub mod bitcoin;
pub mod bob;
pub mod cli;
pub mod config;
pub mod database;
pub mod fs;
pub mod monero;
pub mod network;
pub mod seed;
pub mod trace;
pub type Never = std::convert::Infallible;

View File

@ -26,8 +26,9 @@ use swap::{
bob::swap::BobState,
cli::{Command, Options, Resume},
database::{Database, Swap},
monero,
monero, network,
network::transport::build,
seed::Seed,
trace::init_tracing,
SwapAmounts,
};
@ -43,12 +44,19 @@ async fn main() -> Result<()> {
init_tracing(LevelFilter::Info).expect("initialize tracing");
let opt = Options::from_args();
let config = Config::testnet();
info!("Database: {}", opt.db_path);
let db = Database::open(std::path::Path::new(opt.db_path.as_str()))
.context("Could not open database")?;
info!(
"Database and Seed will be stored in directory: {}",
opt.data_dir
);
let data_dir = std::path::Path::new(opt.data_dir.as_str()).to_path_buf();
let db =
Database::open(data_dir.join("database").as_path()).context("Could not open database")?;
let seed = swap::config::Seed::from_file_or_generate(&data_dir)
.expect("Could not retrieve/initialize seed")
.into();
match opt.cmd {
Command::SellXmr {
@ -108,6 +116,7 @@ async fn main() -> Result<()> {
monero_wallet,
config,
db,
&seed,
)
.await?;
}
@ -204,6 +213,7 @@ async fn main() -> Result<()> {
monero_wallet,
config,
db,
&seed,
)
.await?;
}
@ -270,7 +280,7 @@ async fn setup_wallets(
Ok((bitcoin_wallet, monero_wallet))
}
#[allow(clippy::too_many_arguments)]
async fn alice_swap(
swap_id: Uuid,
state: AliceState,
@ -279,13 +289,14 @@ async fn alice_swap(
monero_wallet: Arc<monero::Wallet>,
config: Config,
db: Database,
seed: &Seed,
) -> Result<AliceState> {
let alice_behaviour = alice::Behaviour::default();
let alice_peer_id = alice_behaviour.peer_id();
let identity = alice_behaviour.identity(network::Seed::new(seed.bytes()));
let alice_peer_id = PeerId::from(identity.public());
info!("Own Peer-ID: {}", alice_peer_id);
let alice_transport = build(alice_behaviour.identity())?;
let alice_transport = build(identity)?;
let (mut event_loop, handle) =
alice::event_loop::EventLoop::new(alice_transport, alice_behaviour, listen_addr)?;

View File

@ -1,5 +1,7 @@
use crate::seed::SEED_LENGTH;
use bitcoin::hashes::{sha256, Hash, HashEngine};
use futures::prelude::*;
use libp2p::core::Executor;
use libp2p::{core::Executor, identity::ed25519};
use std::pin::Pin;
use tokio::runtime::Handle;
@ -17,3 +19,35 @@ impl Executor for TokioExecutor {
let _ = self.handle.spawn(future);
}
}
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub struct Seed([u8; SEED_LENGTH]);
impl Seed {
/// prefix "NETWORK" to the provided seed and apply sha256
pub fn new(seed: [u8; crate::seed::SEED_LENGTH]) -> Self {
let mut engine = sha256::HashEngine::default();
engine.input(&seed);
engine.input(b"NETWORK");
let hash = sha256::Hash::from_engine(engine);
Self(hash.into_inner())
}
pub fn bytes(&self) -> [u8; SEED_LENGTH] {
self.0
}
pub fn derive_libp2p_identity(&self) -> libp2p::identity::Keypair {
let mut engine = sha256::HashEngine::default();
engine.input(&self.bytes());
engine.input(b"LIBP2P_IDENTITY");
let hash = sha256::Hash::from_engine(engine);
let key =
ed25519::SecretKey::from_bytes(hash.into_inner()).expect("we always pass 32 bytes");
libp2p::identity::Keypair::Ed25519(key.into())
}
}

58
swap/src/seed.rs Normal file
View File

@ -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();
}
}

View File

@ -5,7 +5,7 @@ use rand::rngs::OsRng;
use std::sync::Arc;
use swap::{
alice, alice::swap::AliceState, bitcoin, bob, bob::swap::BobState, database::Database, monero,
network::transport::build, SwapAmounts,
network, network::transport::build, seed::Seed, SwapAmounts,
};
use tempfile::tempdir;
use testcontainers::{clients::Cli, Container};
@ -106,8 +106,10 @@ pub fn init_alice_event_loop(
alice::event_loop::EventLoop,
alice::event_loop::EventLoopHandle,
) {
let seed = Seed::random().unwrap();
let alice_behaviour = alice::Behaviour::default();
let alice_transport = build(alice_behaviour.identity()).unwrap();
let alice_transport =
build(alice_behaviour.identity(network::Seed::new(seed.bytes()))).unwrap();
alice::event_loop::EventLoop::new(alice_transport, alice_behaviour, listen).unwrap()
}