Skip to content

Commit bcf8f39

Browse files
committed
joinstr: implem Joinstr::restart() in order to restart a pool execution
1 parent 2999e05 commit bcf8f39

File tree

4 files changed

+171
-4
lines changed

4 files changed

+171
-4
lines changed

rust/joinstr/src/electrum.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -646,6 +646,14 @@ impl Client {
646646
pub fn url(&self) -> String {
647647
self.url.clone()
648648
}
649+
650+
/// Returns the port of the electrum client.
651+
///
652+
/// # Returns
653+
/// A `u16` containing the port of the electrum server.
654+
pub fn port(&self) -> u16 {
655+
self.port
656+
}
649657
}
650658

651659
impl BitcoinBackend for Client {

rust/joinstr/src/joinstr/error.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,9 @@ pub enum Error {
4343
AlreadyHaveOutput,
4444
WrongRole,
4545
WrongConfig,
46+
PoolKey,
47+
InputParsing,
48+
PoolCorrupted,
4649
}
4750

4851
impl From<crate::coinjoin::Error> for Error {

rust/joinstr/src/joinstr/mod.rs

Lines changed: 159 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ pub struct Joinstr<'a> {
3636
pub inner: Arc<Mutex<JoinstrInner<'a>>>,
3737
}
3838

39-
#[derive(Debug, Default, Clone, Copy)]
39+
#[derive(Debug, Default, Clone, Copy, Serialize, Deserialize)]
4040
pub enum Role {
4141
#[default]
4242
Unknown,
@@ -95,9 +95,10 @@ pub struct JoinstrInner<'a> {
9595

9696
#[derive(Debug, Clone, Serialize, Deserialize)]
9797
pub struct State {
98+
role: Role,
9899
pool_secret_key: String, /* nostr::Keys*/
99100
relay: String,
100-
electrum: Option<String>,
101+
electrum: Option<(String, u16)>,
101102
pool: Pool,
102103
input: Option<Coin>,
103104
output: Option<Address<NetworkUnchecked>>,
@@ -827,6 +828,160 @@ impl Joinstr<'_> {
827828

828829
Ok(())
829830
}
831+
832+
pub fn restart<S>(state: State, name: &str, signer: S) -> Result<Self, Error>
833+
where
834+
S: JoinstrSigner + Sync + Clone + Send + 'static,
835+
{
836+
let State {
837+
role,
838+
pool_secret_key,
839+
relay,
840+
electrum,
841+
pool,
842+
input,
843+
output,
844+
network,
845+
peers,
846+
outputs,
847+
inputs,
848+
final_tx,
849+
} = state;
850+
let secret_key = nostr::SecretKey::from_hex(pool_secret_key).map_err(|_| Error::PoolKey)?;
851+
let keys = Keys::new(secret_key);
852+
let mut j = Joinstr::new(keys, relay, name)?.network(network);
853+
let mut inner = j.inner.lock().expect("poisoned");
854+
inner.role = role;
855+
inner.pool = Some(pool);
856+
if let Some((url, port)) = electrum {
857+
inner.electrum_client = Some(crate::electrum::Client::new(&url, port)?)
858+
}
859+
inner.input = input;
860+
if let Some(addr) = output {
861+
if addr.is_valid_for_network(network) {
862+
inner.output = Some(addr.assume_checked().clone());
863+
} else {
864+
return Err(Error::WrongAddressNetwork);
865+
}
866+
}
867+
inner.network = network;
868+
inner.peers = peers;
869+
let mut outs = vec![];
870+
for o in outputs {
871+
if !o.is_valid_for_network(network) {
872+
return Err(Error::WrongAddressNetwork);
873+
}
874+
outs.push(o.assume_checked().clone());
875+
}
876+
inner.outputs = outs;
877+
let mut inps = vec![];
878+
for i in inputs {
879+
inps.push(serde_json::from_value(i).map_err(|_| Error::InputParsing)?);
880+
}
881+
inner.inputs = inps;
882+
883+
// pool state is already finalized
884+
if let Some(tx) = final_tx {
885+
inner.final_tx = Some(tx);
886+
drop(inner);
887+
return Ok(j);
888+
}
889+
890+
let expected_peers = inner
891+
.pool
892+
.as_ref()
893+
.expect("always have a pool")
894+
.payload
895+
.as_ref()
896+
.expect("have a payload")
897+
.peers;
898+
899+
let mut recv_peers = vec![];
900+
let mut recv_outputs = vec![];
901+
let mut recv_inputs = vec![];
902+
903+
// NOTE: here it's tricky to restart if we have a non finalized coinjoin:
904+
// - we cannot trust the timestamp of messages
905+
// - an external actor can have posted a join request w/ a fake timestamp
906+
// - a malicious peer can have posted a message w/ a fake timestamp
907+
// - a malicious peer can have posted a fake message
908+
// - a message can have been deleted
909+
//
910+
// FIXME: it's then easy to be DOS by a malicious actor, we should have a way to sign
911+
// with the pool key & post a backup state
912+
913+
// get all already received pool messages
914+
while let Ok(Some(msg)) = inner.client.try_receive_pool_msg() {
915+
match msg {
916+
PoolMessage::Input(input) => {
917+
recv_inputs.push(input);
918+
}
919+
PoolMessage::Output(address) => {
920+
if address.is_valid_for_network(network) {
921+
recv_outputs.push(address.assume_checked());
922+
}
923+
}
924+
PoolMessage::Psbt(psbt) => {
925+
if let Ok(input) = psbt.try_into() {
926+
recv_inputs.push(input);
927+
}
928+
}
929+
PoolMessage::Join(Some(public_key)) => {
930+
recv_peers.push(public_key);
931+
}
932+
_ => {}
933+
}
934+
}
935+
936+
let total_peers = inner.peers.len() + recv_peers.len();
937+
let total_outputs = inner.outputs.len() + recv_outputs.len();
938+
let total_inputs = inner.inputs.len() + recv_inputs.len();
939+
940+
if total_peers > expected_peers {
941+
return Err(Error::PoolCorrupted);
942+
}
943+
if total_outputs > total_peers {
944+
return Err(Error::PoolCorrupted);
945+
}
946+
if total_inputs > total_outputs {
947+
return Err(Error::PoolCorrupted);
948+
}
949+
if total_inputs > total_peers {
950+
return Err(Error::PoolCorrupted);
951+
}
952+
953+
inner.peers.append(&mut recv_peers);
954+
inner.outputs.append(&mut recv_outputs);
955+
inner.inputs.append(&mut recv_inputs);
956+
957+
let joined = inner.peers.len() >= expected_peers;
958+
let output_registered = inner.outputs.len() >= expected_peers;
959+
let inputs_registered = inner.inputs.len() >= expected_peers;
960+
961+
drop(inner);
962+
963+
if !joined || !output_registered {
964+
j.register_outputs()?;
965+
}
966+
967+
if !inputs_registered {
968+
j.inner.lock().expect("poisoned").generate_unsigned_tx()?;
969+
970+
rand_delay();
971+
972+
let mut inner = j.inner.lock().expect("poisoned");
973+
if inner.input.is_some() {
974+
inner.register_input(&signer)?;
975+
}
976+
drop(inner);
977+
978+
j.register_inputs()?;
979+
980+
j.inner.lock().expect("poisoned").broadcast_tx()?;
981+
}
982+
983+
Ok(j)
984+
}
830985
}
831986

832987
impl<'a> JoinstrInner<'a> {
@@ -1278,13 +1433,14 @@ impl<'a> JoinstrInner<'a> {
12781433
} else {
12791434
return None;
12801435
};
1281-
let electrum = self.electrum_client.as_ref().map(|c| c.url());
1436+
let electrum = self.electrum_client.as_ref().map(|c| (c.url(), c.port()));
12821437
let pool = if let Some(pool) = &self.pool {
12831438
pool.clone()
12841439
} else {
12851440
return None;
12861441
};
12871442
Some(State {
1443+
role: self.role,
12881444
pool_secret_key: keys.secret_key().to_secret_hex(),
12891445
relay,
12901446
electrum,

rust/joinstr/src/nostr/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ use std::{
2424
time::{Duration, SystemTime, UNIX_EPOCH},
2525
};
2626

27-
#[derive(Debug, Clone, PartialEq)]
27+
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2828
pub struct InputDataSigned {
2929
pub txin: TxIn,
3030
pub amount: Option<Amount>,

0 commit comments

Comments
 (0)