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
bors[bot] 4 years ago committed by GitHub
commit 485220929e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

14
Cargo.lock generated

@ -1945,6 +1945,8 @@ dependencies = [
"testcontainers",
"tokio",
"tracing",
"tracing-log",
"tracing-subscriber",
"url",
]
@ -2212,6 +2214,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"
@ -3293,6 +3306,7 @@ dependencies = [
"miniscript",
"monero",
"monero-harness",
"pem",
"port_check",
"prettytable-rs",
"rand 0.7.3",

@ -17,4 +17,6 @@ spectral = "0.6"
testcontainers = "0.11"
tokio = { version = "0.2", default-features = false, features = ["blocking", "macros", "rt-core", "time"] }
tracing = "0.1"
tracing-log = "0.1"
tracing-subscriber = { version = "0.2", default-features = false, features = ["fmt", "ansi", "env-filter"] }
url = "2"

@ -1,11 +1,16 @@
use crate::testutils::init_tracing;
use monero_harness::Monero;
use spectral::prelude::*;
use std::time::Duration;
use testcontainers::clients::Cli;
use tokio::time;
mod testutils;
#[tokio::test]
async fn init_miner_and_mine_to_miner_address() {
let _guard = init_tracing();
let tc = Cli::default();
let (monero, _monerod_container) = Monero::new(&tc, None, vec![]).await.unwrap();

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

@ -1,9 +1,14 @@
use crate::testutils::init_tracing;
use monero_harness::Monero;
use spectral::prelude::*;
use testcontainers::clients::Cli;
mod testutils;
#[tokio::test]
async fn fund_transfer_and_check_tx_key() {
let _guard = init_tracing();
let fund_alice: u64 = 1_000_000_000_000;
let fund_bob = 0;
let send_to_bob = 5_000_000_000;

@ -27,6 +27,7 @@ log = { version = "0.4", features = ["serde"] }
miniscript = { version = "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"] }

@ -7,8 +7,8 @@ use crate::{bitcoin, monero};
#[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,

@ -1,3 +1,5 @@
pub mod seed;
use crate::bitcoin::Timelock;
use conquer_once::Lazy;
use std::time::Duration;

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

@ -23,9 +23,11 @@ pub mod bitcoin;
pub mod cli;
pub mod config;
pub mod database;
pub mod fs;
pub mod monero;
pub mod network;
pub mod protocol;
pub mod seed;
pub mod trace;
pub type Never = std::convert::Infallible;

@ -24,9 +24,10 @@ use swap::{
cli::{Command, Options, Resume},
config::Config,
database::{Database, Swap},
monero,
monero, network,
network::transport::build,
protocol::{alice, alice::AliceState, bob, bob::BobState},
seed::Seed,
trace::init_tracing,
SwapAmounts,
};
@ -41,12 +42,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::Seed::from_file_or_generate(&data_dir)
.expect("Could not retrieve/initialize seed")
.into();
match opt.cmd {
Command::SellXmr {
@ -106,6 +114,7 @@ async fn main() -> Result<()> {
monero_wallet,
config,
db,
seed,
)
.await?;
}
@ -158,6 +167,7 @@ async fn main() -> Result<()> {
db,
alice_peer_id,
alice_addr,
seed,
)
.await?;
}
@ -201,6 +211,7 @@ async fn main() -> Result<()> {
monero_wallet,
config,
db,
seed,
)
.await?;
}
@ -233,6 +244,7 @@ async fn main() -> Result<()> {
db,
alice_peer_id,
alice_addr,
seed,
)
.await?;
}
@ -267,7 +279,7 @@ async fn setup_wallets(
Ok((bitcoin_wallet, monero_wallet))
}
#[allow(clippy::too_many_arguments)]
async fn alice_swap(
swap_id: Uuid,
state: AliceState,
@ -276,12 +288,11 @@ async fn alice_swap(
monero_wallet: Arc<swap::monero::Wallet>,
config: Config,
db: Database,
seed: Seed,
) -> Result<AliceState> {
let alice_behaviour = alice::Behaviour::default();
let alice_behaviour = alice::Behaviour::new(network::Seed::new(seed));
let alice_peer_id = alice_behaviour.peer_id();
info!("Own Peer-ID: {}", alice_peer_id);
let alice_transport = build(alice_behaviour.identity())?;
let (mut event_loop, handle) =
@ -310,8 +321,9 @@ async fn bob_swap(
db: Database,
alice_peer_id: PeerId,
alice_addr: Multiaddr,
seed: Seed,
) -> Result<BobState> {
let bob_behaviour = bob::Behaviour::default();
let bob_behaviour = bob::Behaviour::new(network::Seed::new(seed));
let bob_transport = build(bob_behaviour.identity())?;
let (event_loop, handle) =

@ -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: crate::seed::Seed) -> Self {
let mut engine = sha256::HashEngine::default();
engine.input(&seed.bytes());
engine.input(b"NETWORK");
let hash = sha256::Hash::from_engine(engine);
Self(hash.into_inner())
}
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())
}
}

@ -13,7 +13,7 @@ use crate::{
peer_tracker::{self, PeerTracker},
request_response::AliceToBob,
transport::SwapTransport,
TokioExecutor,
Seed, TokioExecutor,
},
protocol::bob,
SwapAmounts,
@ -145,6 +145,20 @@ pub struct Behaviour {
}
impl Behaviour {
pub fn new(seed: Seed) -> Self {
let identity = seed.derive_libp2p_identity();
Self {
pt: PeerTracker::default(),
amounts: Amounts::default(),
message0: message0::Behaviour::default(),
message1: message1::Behaviour::default(),
message2: message2::Behaviour::default(),
message3: message3::Behaviour::default(),
identity,
}
}
pub fn identity(&self) -> Keypair {
self.identity.clone()
}
@ -178,19 +192,3 @@ impl Behaviour {
debug!("Sent Message2");
}
}
impl Default for Behaviour {
fn default() -> Self {
let identity = Keypair::generate_ed25519();
Self {
pt: PeerTracker::default(),
amounts: Amounts::default(),
message0: message0::Behaviour::default(),
message1: message1::Behaviour::default(),
message2: message2::Behaviour::default(),
message3: message3::Behaviour::default(),
identity,
}
}
}

@ -12,7 +12,7 @@ use crate::{
network::{
peer_tracker::{self, PeerTracker},
transport::SwapTransport,
TokioExecutor,
Seed, TokioExecutor,
},
protocol::{alice, bob},
SwapAmounts,
@ -124,6 +124,20 @@ pub struct Behaviour {
}
impl Behaviour {
pub fn new(seed: Seed) -> Self {
let identity = seed.derive_libp2p_identity();
Self {
pt: PeerTracker::default(),
amounts: Amounts::default(),
message0: message0::Behaviour::default(),
message1: message1::Behaviour::default(),
message2: message2::Behaviour::default(),
message3: message3::Behaviour::default(),
identity,
}
}
pub fn identity(&self) -> Keypair {
self.identity.clone()
}

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

@ -11,6 +11,7 @@ use swap::{
config::Config,
monero,
protocol::{alice, bob},
seed::Seed,
};
use testcontainers::clients::Cli;
use testutils::init_tracing;
@ -65,6 +66,7 @@ async fn happy_path() {
xmr_alice,
alice_multiaddr.clone(),
config,
Seed::random().unwrap(),
)
.await;

@ -8,6 +8,7 @@ use swap::{
database::Database,
monero,
protocol::{alice, alice::AliceState, bob},
seed::Seed,
};
use tempfile::tempdir;
use testcontainers::clients::Cli;
@ -42,6 +43,7 @@ async fn given_alice_restarts_after_encsig_is_learned_resume_swap() {
let config = Config::regtest();
let alice_seed = Seed::random().unwrap();
let (
start_state,
mut alice_event_loop,
@ -57,6 +59,7 @@ async fn given_alice_restarts_after_encsig_is_learned_resume_swap() {
alice_xmr_starting_balance,
alice_multiaddr.clone(),
config,
alice_seed,
)
.await;
@ -125,7 +128,7 @@ async fn given_alice_restarts_after_encsig_is_learned_resume_swap() {
};
let (mut event_loop_after_restart, event_loop_handle_after_restart) =
testutils::init_alice_event_loop(alice_multiaddr);
testutils::init_alice_event_loop(alice_multiaddr, alice_seed);
tokio::spawn(async move { event_loop_after_restart.run().await });
let alice_state = alice::swap::swap(

@ -8,6 +8,7 @@ use swap::{
database::Database,
monero,
protocol::{alice, bob, bob::BobState},
seed::Seed,
};
use tempfile::tempdir;
use testcontainers::clients::Cli;
@ -57,6 +58,7 @@ async fn given_bob_restarts_after_encsig_is_sent_resume_swap() {
alice_xmr_starting_balance,
alice_multiaddr.clone(),
config,
Seed::random().unwrap(),
)
.await;

@ -8,6 +8,7 @@ use swap::{
database::Database,
monero,
protocol::{alice, alice::AliceState, bob, bob::BobState},
seed::Seed,
};
use tempfile::tempdir;
use testcontainers::clients::Cli;
@ -59,6 +60,7 @@ async fn given_bob_restarts_after_xmr_is_locked_resume_swap() {
alice_xmr_starting_balance,
alice_multiaddr.clone(),
Config::regtest(),
Seed::random().unwrap(),
)
.await;

@ -11,6 +11,7 @@ use swap::{
config::Config,
monero,
protocol::{alice, alice::AliceState, bob, bob::BobState},
seed::Seed,
};
use testcontainers::clients::Cli;
use testutils::init_tracing;
@ -63,6 +64,7 @@ async fn alice_punishes_if_bob_never_acts_after_fund() {
alice_xmr_starting_balance,
alice_multiaddr.clone(),
config,
Seed::random().unwrap(),
)
.await;

@ -9,6 +9,7 @@ use swap::{
database::Database,
monero,
protocol::{alice, alice::AliceState, bob, bob::BobState},
seed::Seed,
};
use tempfile::tempdir;
use testcontainers::clients::Cli;
@ -47,6 +48,7 @@ async fn given_alice_restarts_after_xmr_is_locked_abort_swap() {
.parse()
.expect("failed to parse Alice's address");
let alice_seed = Seed::random().unwrap();
let (
alice_state,
mut alice_event_loop_1,
@ -62,6 +64,7 @@ async fn given_alice_restarts_after_xmr_is_locked_abort_swap() {
alice_xmr_starting_balance,
alice_multiaddr.clone(),
Config::regtest(),
alice_seed,
)
.await;
@ -121,7 +124,7 @@ async fn given_alice_restarts_after_xmr_is_locked_abort_swap() {
};
let (mut alice_event_loop_2, alice_event_loop_handle_2) =
testutils::init_alice_event_loop(alice_multiaddr);
testutils::init_alice_event_loop(alice_multiaddr, alice_seed);
let alice_final_state = {
let alice_db = Database::open(alice_db_datadir.path()).unwrap();

@ -7,9 +7,10 @@ use swap::{
bitcoin,
config::Config,
database::Database,
monero,
monero, network,
network::transport::build,
protocol::{alice, alice::AliceState, bob, bob::BobState},
seed::Seed,
SwapAmounts,
};
use tempfile::tempdir;
@ -106,13 +107,13 @@ pub async fn init_alice_state(
pub fn init_alice_event_loop(
listen: Multiaddr,
seed: Seed,
) -> (
alice::event_loop::EventLoop,
alice::event_loop::EventLoopHandle,
) {
let alice_behaviour = alice::Behaviour::default();
let alice_behaviour = alice::Behaviour::new(network::Seed::new(seed));
let alice_transport = build(alice_behaviour.identity()).unwrap();
alice::event_loop::EventLoop::new(alice_transport, alice_behaviour, listen).unwrap()
}
@ -125,6 +126,7 @@ pub async fn init_alice(
xmr_starting_balance: monero::Amount,
listen: Multiaddr,
config: Config,
seed: Seed,
) -> (
AliceState,
alice::event_loop::EventLoop,
@ -146,7 +148,7 @@ pub async fn init_alice(
let alice_start_state =
init_alice_state(btc_to_swap, xmr_to_swap, alice_btc_wallet.clone(), config).await;
let (event_loop, event_loop_handle) = init_alice_event_loop(listen);
let (event_loop, event_loop_handle) = init_alice_event_loop(listen, seed);
let alice_db_datadir = tempdir().unwrap();
let alice_db = Database::open(alice_db_datadir.path()).unwrap();
@ -190,7 +192,8 @@ pub fn init_bob_event_loop(
alice_peer_id: PeerId,
alice_addr: Multiaddr,
) -> (bob::event_loop::EventLoop, bob::event_loop::EventLoopHandle) {
let bob_behaviour = bob::Behaviour::default();
let seed = Seed::random().unwrap();
let bob_behaviour = bob::Behaviour::new(network::Seed::new(seed));
let bob_transport = build(bob_behaviour.identity()).unwrap();
bob::event_loop::EventLoop::new(bob_transport, bob_behaviour, alice_peer_id, alice_addr)
.unwrap()

Loading…
Cancel
Save