2021-02-10 17:57:17 -05:00
|
|
|
use crate::fs::ensure_directory_exists;
|
2021-03-03 19:28:58 -05:00
|
|
|
use ::bitcoin::secp256k1::constants::SECRET_KEY_SIZE;
|
|
|
|
use ::bitcoin::secp256k1::{self, SecretKey};
|
2021-03-04 01:40:51 -05:00
|
|
|
use anyhow::{Context, Result};
|
2021-02-09 01:23:13 -05:00
|
|
|
use bdk::bitcoin::util::bip32::ExtendedPrivKey;
|
2021-03-02 21:26:12 -05:00
|
|
|
use bitcoin::hashes::{sha256, Hash, HashEngine};
|
|
|
|
use libp2p::identity;
|
2021-02-10 17:57:17 -05:00
|
|
|
use pem::{encode, Pem};
|
2021-01-07 20:04:48 -05:00
|
|
|
use rand::prelude::*;
|
2021-03-03 19:28:58 -05:00
|
|
|
use std::ffi::OsStr;
|
|
|
|
use std::fmt;
|
|
|
|
use std::fs::{self, File};
|
|
|
|
use std::io::{self, Write};
|
|
|
|
use std::path::{Path, PathBuf};
|
2021-04-22 02:03:12 -04:00
|
|
|
use torut::onion::TorSecretKeyV3;
|
2021-01-07 20:04:48 -05:00
|
|
|
|
|
|
|
pub const SEED_LENGTH: usize = 32;
|
|
|
|
|
RPC server for API Interface (#1276)
* saving: implementing internal api shared by cli and rpc server
* writing async rpc methods and using arc for shared struct references
* cleaning up, renamed Init to Context
* saving: cleaning up and initial work for tests
* Respond with bitcoin withdraw txid
* Print RPC server address
* Cleanup, formatting, add `get_seller`, `get_swap_start_date` RPC endpoints
* fixing tests in cli module
* uncommenting and fixing more tests
* split api module and propagate errors with rpc server
* moving methods to api and validating addresses for rpc
* add broadcast channel to handle shutdowns gracefully and prepare for RPC server test
* added files
* Update rpc.rs
* adding new unfinished RPC tests
* updating rpc-server tests
* fixing warnings
* fixing formatting and cargo clippy warnings
* fix missing import in test
* fix: add data_dir to config to make config command work
* set server listen address manually and return file locations in JSON on Config
* Add called api method and swap_id to tracing for context, reduced boilerplate
* Pass server_address properly to RpcServer
* Update Cargo.lock
* dprint fmt
* Add cancel_refund RPC endpoint
* Combine Cmd and Params
* Disallow concurrent swaps
* Use RwLock instead of Mutex to allow for parallel reads and add get_current_swap endpoint
* Return wallet descriptor to RPC API caller
* Append all cli logs to single log file
After careful consideration, I've concluded that it's not practical/possible to ensure that the previous behaviour (one log file per swap) is preserved due to limitations of the tracing-subscriber crate and a big in the built in JSON formatter
* Add get_swap_expired_timelock timelock, other small refactoring
- Add get_swap_expired_timelock endpoint to return expired timelock if one exists. Fails if bitcoin lock tx has not yet published or if swap is already finished.
- Rename current_epoch to expired_timelock to enforce consistent method names
- Add blocks left until current expired timelock expires (next timelock expires) to ExpiredTimelock struct
- Change .expect() to .unwrap() in rpc server method register because those will only fail if we register the same method twice which will never happen
* initiating swaps in a separate task and handling shutdown signals with broadcast queues
* Replace get_swap_start_date, get_seller, get_expired_timelock with one get_swap_info rpc method
* WIP: Struct for concurrent swaps manager
* Ensure correct tracing spans
* Add note regarding Request, Method structs
* Update request.rs
* Add tracing span attribute log_reference_id to logs caused by rpc call
* Sync bitcoin wallet before initial max_giveable call
* use Span::current() to pass down to tracing span to spawned tasks
* Remove unused shutdown channel
* Add `get_monero_recovery_info` RPC endpoint
- Add `get_monero_recovery_info` RPC endpoint
- format PrivateViewKey using Display
* Rename `Method::RawHistory` to `Method::GetRawStates`
* Wait for swap to be suspended after sending signal
* Remove notes
* Add tracing span attribute log_reference_id to logs caused by rpc call
* Sync bitcoin wallet before initial max_giveable call
* use Span::current() to pass down to tracing span to spawned tasks
* Remove unused shutdown channel
* Add `get_monero_recovery_info` RPC endpoint
- Add `get_monero_recovery_info` RPC endpoint
- format PrivateViewKey using Display
* Rename `Method::RawHistory` to `Method::GetRawStates`
* Wait for swap to be suspended after sending signal
* Return additonal info on GetSwapInfo
* Update wallet.rs
* fix compile issues for tests and use serial_test crate
* fix rpc tests, only check for RPC errors and not returned values
* Rename `get_raw_history` tp `get_raw_states`
* Fix typo in rpc server stopped tracing log
* Remove unnecessary success property on suspend_current_swap response
* fixing test_cli_arguments and other tests
* WIP: RPC server integration tests
* WIP: Integration tests for RPC server
* Update rpc tests
* fix compile and warnings in tests/rpc.rs
* test: fix assert
* clippy --fix
* remove otp file
* cargo clippy fixes
* move resume swap initialization code out of spawned task
* Use `in_current_span` to pass down tracing span to spawned tasks
* moving buy_xmr initialization code out of spawned tasks
* cargo fmt
* Moving swap initialization code inside tokio select block to handle swap lock release logic
* Remove unnecessary swap suspension listener from determine_btc_to_swap call in BuyXmr
* Spawn event loop before requesting quote
* Release swap lock after receiving shutdown signal
* Remove inner tokio::select in BuyXmr and Resume
* Improve debug text for swap resume
* Return error to API caller if bid quote request fails
* Print error if one occurs during process invoked by API call
* Return bid quote to API caller
* Use type safe query! macro for database retrieval of states
* Return tx_lock_fee to API caller on GetSwapInfo call
Update request.rs
* Allow API caller to retrieve last synced bitcoin balane and avoid costly sync
* Return restore height on MoneroRecovery command to API Caller
* Include entire error cause-chain in API response
* Add span to bitcoin wallet logs
* Log event loop connection properties as tracing fields
* Wait for background tasks to complete before exiting CLI
* clippy
* specify sqlx patch version explicitly
* remove mem::forget and replace with _guard
* ci: add rpc test job
* test: wrap rpc test in #[cfg(test)]
* add missing tokio::test attribute
* fix and merge rpc tests, parse uuuid and multiaddr from serde_json value
* default Tor socks port to 9050, Cargo fmt
* Update swap/sqlite_dev_setup.sh: add version
Co-authored-by: Byron Hambly <byron@hambly.dev>
* ci: free up space on ubuntu test job
* Update swap/src/bitcoin/wallet.rs
Co-authored-by: Byron Hambly <byron@hambly.dev>
* Update swap/src/bitcoin/wallet.rs
Co-authored-by: Byron Hambly <byron@hambly.dev>
* fmt
---------
Co-authored-by: binarybaron <86064887+binarybaron@users.noreply.github.com>
Co-authored-by: Byron Hambly <byron@hambly.dev>
2024-05-22 09:12:58 -04:00
|
|
|
#[derive(Clone, Eq, PartialEq)]
|
2021-01-07 20:04:48 -05:00
|
|
|
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))
|
|
|
|
}
|
|
|
|
|
2021-03-02 21:26:12 -05:00
|
|
|
pub fn derive_extended_private_key(
|
|
|
|
&self,
|
|
|
|
network: bitcoin::Network,
|
|
|
|
) -> Result<ExtendedPrivKey> {
|
|
|
|
let seed = self.derive(b"BITCOIN_EXTENDED_PRIVATE_KEY").bytes();
|
2021-03-04 01:40:51 -05:00
|
|
|
let private_key = ExtendedPrivKey::new_master(network, &seed)
|
|
|
|
.context("Failed to create new master extended private key")?;
|
2021-03-02 21:26:12 -05:00
|
|
|
|
2021-02-09 01:23:13 -05:00
|
|
|
Ok(private_key)
|
|
|
|
}
|
|
|
|
|
2021-03-02 21:26:12 -05:00
|
|
|
pub fn derive_libp2p_identity(&self) -> identity::Keypair {
|
|
|
|
let bytes = self.derive(b"NETWORK").derive(b"LIBP2P_IDENTITY").bytes();
|
|
|
|
let key = identity::ed25519::SecretKey::from_bytes(bytes).expect("we always pass 32 bytes");
|
|
|
|
|
|
|
|
identity::Keypair::Ed25519(key.into())
|
2021-01-07 20:04:48 -05:00
|
|
|
}
|
2021-02-10 17:57:17 -05:00
|
|
|
|
2021-04-22 02:03:12 -04:00
|
|
|
pub fn derive_torv3_key(&self) -> TorSecretKeyV3 {
|
|
|
|
let bytes = self.derive(b"TOR").bytes();
|
|
|
|
let sk = ed25519_dalek::SecretKey::from_bytes(&bytes)
|
|
|
|
.expect("Failed to create a new extended secret key for Tor.");
|
|
|
|
let esk = ed25519_dalek::ExpandedSecretKey::from(&sk);
|
|
|
|
esk.to_bytes().into()
|
|
|
|
}
|
|
|
|
|
2021-02-10 17:57:17 -05:00
|
|
|
pub fn from_file_or_generate(data_dir: &Path) -> Result<Self, Error> {
|
|
|
|
let file_path_buf = data_dir.join("seed.pem");
|
|
|
|
let file_path = Path::new(&file_path_buf);
|
|
|
|
|
|
|
|
if file_path.exists() {
|
2023-07-31 04:32:15 -04:00
|
|
|
return Self::from_file(file_path);
|
2021-02-10 17:57:17 -05:00
|
|
|
}
|
|
|
|
|
2021-07-06 02:42:05 -04:00
|
|
|
tracing::debug!("No seed file found, creating at {}", file_path.display());
|
2021-02-10 17:57:17 -05:00
|
|
|
|
|
|
|
let random_seed = Seed::random()?;
|
|
|
|
random_seed.write_to(file_path.to_path_buf())?;
|
|
|
|
|
|
|
|
Ok(random_seed)
|
|
|
|
}
|
|
|
|
|
2021-03-02 21:26:12 -05:00
|
|
|
/// Derive a new seed using the given scope.
|
|
|
|
///
|
|
|
|
/// This function is purposely kept private because it is only a helper
|
|
|
|
/// function for deriving specific secret material from the root seed
|
|
|
|
/// like the libp2p identity or the seed for the Bitcoin wallet.
|
|
|
|
fn derive(&self, scope: &[u8]) -> Self {
|
|
|
|
let mut engine = sha256::HashEngine::default();
|
|
|
|
|
|
|
|
engine.input(&self.bytes());
|
|
|
|
engine.input(scope);
|
|
|
|
|
|
|
|
let hash = sha256::Hash::from_engine(engine);
|
|
|
|
|
|
|
|
Self(hash.into_inner())
|
|
|
|
}
|
|
|
|
|
|
|
|
fn bytes(&self) -> [u8; SEED_LENGTH] {
|
|
|
|
self.0
|
|
|
|
}
|
|
|
|
|
2021-02-10 17:57:17 -05:00
|
|
|
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)?;
|
|
|
|
|
2021-03-04 00:52:29 -05:00
|
|
|
tracing::debug!("Reading in seed from {}", file.display());
|
2021-02-10 17:57:17 -05:00
|
|
|
|
|
|
|
Self::from_pem(pem)
|
|
|
|
}
|
|
|
|
|
|
|
|
fn from_pem(pem: pem::Pem) -> Result<Self, Error> {
|
2023-10-23 05:21:04 -04:00
|
|
|
let contents = pem.contents();
|
|
|
|
if contents.len() != SEED_LENGTH {
|
|
|
|
Err(Error::IncorrectLength(contents.len()))
|
2021-02-10 17:57:17 -05:00
|
|
|
} else {
|
|
|
|
let mut array = [0; SEED_LENGTH];
|
2023-10-23 05:21:04 -04:00
|
|
|
for (i, b) in contents.iter().enumerate() {
|
2021-02-10 17:57:17 -05:00
|
|
|
array[i] = *b;
|
|
|
|
}
|
|
|
|
|
|
|
|
Ok(Self::from(array))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fn write_to(&self, seed_file: PathBuf) -> Result<(), Error> {
|
|
|
|
ensure_directory_exists(&seed_file)?;
|
|
|
|
|
|
|
|
let data = self.bytes();
|
2023-10-23 05:21:04 -04:00
|
|
|
let pem = Pem::new("SEED", data);
|
2021-02-10 17:57:17 -05:00
|
|
|
|
|
|
|
let pem_string = encode(&pem);
|
|
|
|
|
|
|
|
let mut file = File::create(seed_file)?;
|
|
|
|
file.write_all(pem_string.as_bytes())?;
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
}
|
2021-01-07 20:04:48 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
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)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-02-10 17:57:17 -05:00
|
|
|
#[derive(Debug, thiserror::Error)]
|
2021-01-07 20:04:48 -05:00
|
|
|
pub enum Error {
|
|
|
|
#[error("Secp256k1: ")]
|
|
|
|
Secp256k1(#[from] secp256k1::Error),
|
2021-02-10 17:57:17 -05:00
|
|
|
#[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,
|
2021-01-07 20:04:48 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
mod tests {
|
|
|
|
use super::*;
|
2021-02-10 17:57:17 -05:00
|
|
|
use std::env::temp_dir;
|
2021-01-07 20:04:48 -05:00
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn generate_random_seed() {
|
|
|
|
let _ = Seed::random().unwrap();
|
|
|
|
}
|
2021-02-10 17:57:17 -05:00
|
|
|
|
|
|
|
#[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() {
|
2023-01-09 08:54:23 -05:00
|
|
|
use base64::engine::general_purpose;
|
|
|
|
use base64::Engine;
|
|
|
|
|
2021-02-10 17:57:17 -05:00
|
|
|
let payload: &str = "syl9wSYaruvgxg9P5Q1qkZaq5YkM6GvXkxe+VYrL/XM=";
|
|
|
|
|
|
|
|
// 32 bytes base64 encoded.
|
|
|
|
let pem_string: &str = "-----BEGIN SEED-----
|
|
|
|
syl9wSYaruvgxg9P5Q1qkZaq5YkM6GvXkxe+VYrL/XM=
|
|
|
|
-----END SEED-----
|
|
|
|
";
|
|
|
|
|
2023-01-09 08:54:23 -05:00
|
|
|
let want = general_purpose::STANDARD.decode(payload).unwrap();
|
2021-02-10 17:57:17 -05:00
|
|
|
let pem = pem::parse(pem_string).unwrap();
|
|
|
|
let got = Seed::from_pem(pem).unwrap();
|
|
|
|
|
|
|
|
assert_eq!(got.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]
|
|
|
|
fn seed_from_pem_fails_for_long_seed() {
|
|
|
|
let long = "-----BEGIN SEED-----
|
2023-10-23 05:21:04 -04:00
|
|
|
MIIBPQIBAAJBAOsfi5AGYhdRs/x6q5H7kScxA0Kzzqe6WI6gf6+tc6IvKQJo5rQc
|
|
|
|
dWWSQ0nRGt2hOPDO+35NKhQEjBQxPh/v7n0CAwEAAQJBAOGaBAyuw0ICyENy5NsO
|
2021-02-10 17:57:17 -05:00
|
|
|
-----END SEED-----
|
|
|
|
";
|
|
|
|
let pem = pem::parse(long).unwrap();
|
2023-10-23 05:21:04 -04:00
|
|
|
assert_eq!(pem.contents().len(), 96);
|
|
|
|
|
2021-02-10 17:57:17 -05:00
|
|
|
match Seed::from_pem(pem) {
|
|
|
|
Ok(_) => panic!("should fail for long payload"),
|
|
|
|
Err(e) => {
|
|
|
|
match e {
|
2023-10-23 05:21:04 -04:00
|
|
|
Error::IncorrectLength(len) => assert_eq!(len, 96), // pass
|
2021-02-10 17:57:17 -05:00
|
|
|
_ => 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);
|
|
|
|
}
|
2021-01-07 20:04:48 -05:00
|
|
|
}
|