Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance. Project price only 1 $
You can buy this project and download/modify it how often you want.
/*
* Copyright (c) 2022 Dash Core Group
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.bitcoinj.coinjoin;
import com.google.common.collect.Lists;
import org.bitcoinj.coinjoin.listeners.CoinJoinTransactionListener;
import org.bitcoinj.coinjoin.listeners.SessionCompleteListener;
import org.bitcoinj.coinjoin.listeners.SessionStartedListener;
import org.bitcoinj.coinjoin.utils.CoinJoinTransactionType;
import org.bitcoinj.coinjoin.utils.CompactTallyItem;
import org.bitcoinj.coinjoin.utils.KeyHolderStorage;
import org.bitcoinj.coinjoin.utils.MasternodeGroup;
import org.bitcoinj.coinjoin.utils.ReserveDestination;
import org.bitcoinj.coinjoin.utils.TransactionBuilder;
import org.bitcoinj.coinjoin.utils.TransactionBuilderOutput;
import org.bitcoinj.core.Coin;
import org.bitcoinj.core.ECKey;
import org.bitcoinj.core.KeyId;
import org.bitcoinj.core.MasternodeAddress;
import org.bitcoinj.core.MasternodeSync;
import org.bitcoinj.core.Message;
import org.bitcoinj.core.NetworkParameters;
import org.bitcoinj.core.Peer;
import org.bitcoinj.core.Sha256Hash;
import org.bitcoinj.core.Transaction;
import org.bitcoinj.core.TransactionDestination;
import org.bitcoinj.core.TransactionInput;
import org.bitcoinj.core.TransactionOutPoint;
import org.bitcoinj.core.TransactionOutput;
import org.bitcoinj.core.Utils;
import org.bitcoinj.evolution.Masternode;
import org.bitcoinj.evolution.SimplifiedMasternodeList;
import org.bitcoinj.script.Script;
import org.bitcoinj.script.ScriptBuilder;
import org.bitcoinj.script.ScriptException;
import org.bitcoinj.script.ScriptPattern;
import org.bitcoinj.signers.CoinJoinTransactionSigner;
import org.bitcoinj.signers.TransactionSigner;
import org.bitcoinj.utils.ListenerRegistration;
import org.bitcoinj.utils.Pair;
import org.bitcoinj.utils.Threading;
import org.bitcoinj.wallet.Balance;
import org.bitcoinj.wallet.CoinControl;
import org.bitcoinj.wallet.SendRequest;
import org.bitcoinj.wallet.WalletEx;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.annotation.concurrent.GuardedBy;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.Executor;
import java.util.concurrent.atomic.AtomicBoolean;
import static org.bitcoinj.coinjoin.CoinJoinConstants.COINJOIN_DENOM_OUTPUTS_THRESHOLD;
import static org.bitcoinj.coinjoin.CoinJoinConstants.COINJOIN_ENTRY_MAX_SIZE;
import static org.bitcoinj.coinjoin.CoinJoinConstants.COINJOIN_EXTRA;
import static org.bitcoinj.coinjoin.CoinJoinConstants.COINJOIN_QUEUE_TIMEOUT;
import static org.bitcoinj.coinjoin.CoinJoinConstants.COINJOIN_SIGNING_TIMEOUT;
import static org.bitcoinj.coinjoin.PoolMessage.ERR_CONNECTION_TIMEOUT;
import static org.bitcoinj.coinjoin.PoolMessage.ERR_SESSION;
import static org.bitcoinj.coinjoin.PoolMessage.ERR_TIMEOUT;
import static org.bitcoinj.coinjoin.PoolMessage.MSG_POOL_MAX;
import static org.bitcoinj.coinjoin.PoolMessage.MSG_POOL_MIN;
import static org.bitcoinj.coinjoin.PoolMessage.MSG_SUCCESS;
import static org.bitcoinj.coinjoin.PoolState.POOL_STATE_ACCEPTING_ENTRIES;
import static org.bitcoinj.coinjoin.PoolState.POOL_STATE_ERROR;
import static org.bitcoinj.coinjoin.PoolState.POOL_STATE_IDLE;
import static org.bitcoinj.coinjoin.PoolState.POOL_STATE_MAX;
import static org.bitcoinj.coinjoin.PoolState.POOL_STATE_MIN;
import static org.bitcoinj.coinjoin.PoolState.POOL_STATE_QUEUE;
import static org.bitcoinj.coinjoin.PoolState.POOL_STATE_SIGNING;
import static org.bitcoinj.script.ScriptOpCodes.OP_RETURN;
import static org.bitcoinj.wallet.CoinType.ONLY_COINJOIN_COLLATERAL;
public class CoinJoinClientSession extends CoinJoinBaseSession {
private static final Logger log = LoggerFactory.getLogger(CoinJoinClientSession.class);
private static final Random random = new Random();
private final ArrayList outPointLocked = Lists.newArrayList();
private String strLastMessage;
private String strAutoDenomResult;
private Boolean lastCreateDenominatedResult = true;
private Masternode mixingMasternode;
private boolean joined; // did we join a session (true), or start a session (false)
private Transaction txMyCollateral; // client side collateral
private boolean isMyCollateralValid = false;
private PendingDsaRequest pendingDsaRequest;
private final KeyHolderStorage keyHolderStorage; // storage for keys used in PrepareDenominate
private final WalletEx mixingWallet;
private final AtomicBoolean hasNothingToDo = new AtomicBoolean(false); // is mixing finished?
private final HashMap collateralSessionMap = new HashMap<>();
private final CopyOnWriteArrayList> sessionStartedListeners
= new CopyOnWriteArrayList<>();
private final CopyOnWriteArrayList> sessionCompleteListeners
= new CopyOnWriteArrayList<>();
private final CopyOnWriteArrayList> transactionListeners
= new CopyOnWriteArrayList<>();
/// Create denominations
private boolean createDenominated(Coin balanceToDenominate, boolean dryRun) {
if (!CoinJoinClientOptions.isEnabled()) return false;
// NOTE: We do not allow txes larger than 100 kB, so we have to limit number of inputs here.
// We still want to consume a lot of inputs to avoid creating only smaller denoms though.
// Knowing that each CTxIn is at least 148 B big, 400 inputs should take 400 x ~148 B = ~60 kB.
// This still leaves more than enough room for another data of typical CreateDenominated tx.
List vecTally = mixingWallet.selectCoinsGroupedByAddresses(true, true, true, 400);
if (vecTally.isEmpty()) {
log.info( "coinjoin: selectCoinsGroupedByAddresses can't find any inputs!");
return false;
}
// Start from the largest balances first to speed things up by creating txes with larger/largest denoms included
vecTally.sort((a, b) -> {
if (a.amount.isGreaterThan(b.amount))
return 1;
if (a.amount.equals(b.amount))
return 0;
return -1;
});
boolean fCreateMixingCollaterals = !mixingWallet.hasCollateralInputs();
for (CompactTallyItem item : vecTally) {
if (!createDenominated(balanceToDenominate, item, fCreateMixingCollaterals, dryRun)) continue;
return true;
}
log.info("coinjoin: createDenominated({}) failed! ", balanceToDenominate.toFriendlyString());
return false;
}
public void processTransaction(Transaction tx) {
log.info(COINJOIN_EXTRA, "processing tx of {}: {}", tx.getValue(mixingWallet), tx.getTxId());
if (collateralSessionMap.containsKey(tx.getTxId())) {
queueTransactionListeners(tx, collateralSessionMap.get(tx.getTxId()), CoinJoinTransactionType.MixingFee);
}
}
static class Result {
private T result;
public Result(T result) {
this.result = result;
}
public T get() {
return result;
}
public void set(T result) {
this.result = result;
}
}
interface NeedMoreOutputs {
boolean process(Coin balanceToDenominate, int outputs, Result addFinal);
}
interface CountPossibleOutputs {
int process(Coin amount);
}
private boolean createDenominated(Coin balanceToDenominate, CompactTallyItem tallyItem, boolean fCreateMixingCollaterals, boolean dryRun) {
if (!CoinJoinClientOptions.isEnabled())
return false;
// denominated input is always a single one, so we can check its amount directly and return early
if (tallyItem.inputCoins.size() == 1 && CoinJoin.isDenominatedAmount(tallyItem.amount)) {
return false;
}
try (TransactionBuilder txBuilder = new TransactionBuilder(mixingWallet, tallyItem, dryRun)) {
log.info("coinjoin: Start {}", txBuilder);
// ****** Add an output for mixing collaterals ************ /
if (fCreateMixingCollaterals && txBuilder.addOutput(CoinJoin.getMaxCollateralAmount()) == null) {
log.info("coinjoin: Failed to add collateral output");
return false;
}
// ****** Add outputs for denoms ************ /
final Result addFinal = new Result<>(true);
List denoms = CoinJoinClientOptions.getDenominations();
HashMap mapDenomCount = new HashMap<>();
for (Coin denomValue : denoms) {
mapDenomCount.put(denomValue, mixingWallet.countInputsWithAmount(denomValue));
}
// Will generate outputs for the createdenoms up to coinjoinmaxdenoms per denom
// This works in the way creating PS denoms has traditionally worked, assuming enough funds,
// it will start with the smallest denom then create 11 of those, then go up to the next biggest denom create 11
// and repeat. Previously, once the largest denom was reached, as many would be created were created as possible and
// then any remaining was put into a change address and denominations were created in the same manner a block later.
// Now, in this system, so long as we don't reach COINJOIN_DENOM_OUTPUTS_THRESHOLD outputs the process repeats in
// the same transaction, creating up to nCoinJoinDenomsHardCap per denomination in a single transaction.
while (txBuilder.couldAddOutput(CoinJoin.getSmallestDenomination()) && txBuilder.countOutputs() < COINJOIN_DENOM_OUTPUTS_THRESHOLD) {
for (int it = denoms.size() - 1; it >= 0; --it) {
Coin denomValue = denoms.get(it);
Integer currentDenomIt = mapDenomCount.get(denomValue);
int nOutputs = 0;
NeedMoreOutputs needMoreOutputs = new NeedMoreOutputs() {
@Override
public boolean process(Coin balanceToDenominate, int outputs, Result addFinal) {
if (txBuilder.couldAddOutput(denomValue)) {
if (addFinal.get() && balanceToDenominate.isPositive() && balanceToDenominate.isLessThan(denomValue)) {
addFinal.set(false); // add final denom only once, only the smalest possible one
log.info("coinjoin: 1 - FINAL - denomValue: {}, balanceToDenominate: {}, outputs: {}, {}",
denomValue.toFriendlyString(), balanceToDenominate.toFriendlyString(), outputs, txBuilder);
return true;
} else if (balanceToDenominate.isGreaterThanOrEqualTo(denomValue)) {
return true;
}
}
return false;
}
};
// add each output up to 11 times or until it can't be added again or until we reach nCoinJoinDenomsGoal
while (needMoreOutputs.process(balanceToDenominate, nOutputs, addFinal) && nOutputs <= 10 && currentDenomIt < CoinJoinClientOptions.getDenomsGoal()) {
// Add output and subtract denomination amount
if (txBuilder.addOutput(denomValue) != null) {
++nOutputs;
mapDenomCount.put(denomValue, ++currentDenomIt);
balanceToDenominate = balanceToDenominate.subtract(denomValue);
log.info("coinjoin: 1 - denomValue: {}, balanceToDenominate: {}, nOutputs: {}, {}",
denomValue.toFriendlyString(), balanceToDenominate.toFriendlyString(), nOutputs, txBuilder);
} else {
log.info("coinjoin: 1 - Error: AddOutput failed for denomValue: {}, balanceToDenominate: {}, nOutputs: {}, {}",
denomValue.toFriendlyString(), balanceToDenominate.toFriendlyString(), nOutputs, txBuilder);
return false;
}
}
if (txBuilder.getAmountLeft().isZero() || balanceToDenominate.isLessThanOrEqualTo(Coin.ZERO)) break;
}
boolean finished = true;
for (Map.Entry entry : mapDenomCount.entrySet()) {
// Check if this specific denom could use another loop, check that there aren't nCoinJoinDenomsGoal of this
// denom and that our nValueLeft/balanceToDenominate is enough to create one of these denoms, if so, loop again.
Coin denom = entry.getKey();
Integer count = entry.getValue();
if (count < CoinJoinClientOptions.getDenomsGoal() && txBuilder.couldAddOutput(denom) &&
balanceToDenominate.isGreaterThan(CoinJoin.getSmallestDenomination())) {
finished = false;
log.info("coinjoin: 1 - NOT finished - denomValue: {}, count: {}, balanceToDenominate: {}, {}",
entry.getKey().toFriendlyString(), entry.getValue(), balanceToDenominate.toFriendlyString(), txBuilder);
break;
}
log.info("coinjoin: 1 - FINISHED - denomValue: {}, count: {}, balanceToDenominate: {}, {}",
entry.getKey().toFriendlyString(), entry.getValue(), balanceToDenominate.toFriendlyString(), txBuilder);
}
if (finished) break;
}
// Now that nCoinJoinDenomsGoal worth of each denom have been created or the max number of denoms given the value of the input, do something with the remainder.
if (txBuilder.couldAddOutput(CoinJoin.getSmallestDenomination()) && balanceToDenominate.isGreaterThanOrEqualTo(CoinJoin.getSmallestDenomination()) && txBuilder.countOutputs() < COINJOIN_DENOM_OUTPUTS_THRESHOLD) {
Coin largestDenomValue = denoms.get(0);
log.info("coinjoin: 2 - Process remainder: {}", txBuilder);
CountPossibleOutputs countPossibleOutputs = new CountPossibleOutputs() {
@Override
public int process(Coin amount) {
ArrayList vecOutputs = new ArrayList<>();
while (true) {
// Create a potential output
vecOutputs.add(amount);
if (!txBuilder.couldAddOutputs(vecOutputs) || txBuilder.countOutputs() + vecOutputs.size() > COINJOIN_DENOM_OUTPUTS_THRESHOLD) {
// If it's not possible to add it due to insufficient amount left or total number of outputs exceeds
// COINJOIN_DENOM_OUTPUTS_THRESHOLD drop the output again and stop trying.
vecOutputs.remove(vecOutputs.size() - 1);
break;
}
}
return vecOutputs.size();
}
};
// Go big to small
for (Coin denomValue : denoms) {
if (balanceToDenominate.isLessThanOrEqualTo(Coin.ZERO)) break;
int nOutputs = 0;
// Number of denoms we can create given our denom and the amount of funds we have left
int denomsToCreateValue = countPossibleOutputs.process(denomValue);
// Prefer overshooting the target balance by larger denoms (hence `+1`) instead of a more
// accurate approximation by many smaller denoms. This is ok because when we get here we
// should have nCoinJoinDenomsGoal of each smaller denom already. Also, without `+1`
// we can end up in a situation when there is already nCoinJoinDenomsHardCap of smaller
// denoms, yet we can't mix the remaining balanceToDenominate because it's smaller than
// denomValue (and thus denomsToCreateBal == 0), so the target would never get reached
// even when there is enough funds for that.
int denomsToCreateBal = (int) (balanceToDenominate.value / denomValue.value + 1);
// Use the smaller value
int denomsToCreate = Math.min(denomsToCreateValue, denomsToCreateBal);
log.info("coinjoin: 2 - balanceToDenominate: {}, denomValue: {}, denomsToCreateValue: {}, denomsToCreateBal: {}",
balanceToDenominate.toFriendlyString(), denomValue.toFriendlyString(), denomsToCreateValue, denomsToCreateBal);
Integer it = mapDenomCount.get(denomValue);
for (int i = 0; i < denomsToCreate; ++i) {
// Never go above the cap unless it's the largest denom
if (denomValue != largestDenomValue && it >= CoinJoinClientOptions.getDenomsHardCap()) break;
// Increment helpers, add output and subtract denomination amount
if (txBuilder.addOutput(denomValue) != null) {
nOutputs++;
mapDenomCount.put(denomValue, ++it);
balanceToDenominate = balanceToDenominate.subtract(denomValue);
} else {
log.info("coinjoin: 2 - Error: AddOutput failed at {}/{}, {}", i + 1, denomsToCreate, txBuilder);
break;
}
log.info("coinjoin: 2 - denomValue: {}, balanceToDenominate: {}, nOutputs: {}, {}",
denomValue.toFriendlyString(), balanceToDenominate.toFriendlyString(), nOutputs, txBuilder);
if (txBuilder.countOutputs() >= COINJOIN_DENOM_OUTPUTS_THRESHOLD) break;
}
if (txBuilder.countOutputs() >= COINJOIN_DENOM_OUTPUTS_THRESHOLD) break;
}
}
log.info("coinjoin: 3 - balanceToDenominate: {}, {}", balanceToDenominate.toFriendlyString(), txBuilder);
for (Map.Entry it : mapDenomCount.entrySet()) {
log.info("coinjoin: 3 - DONE - denomValue: {}, count: {}", it.getKey().toFriendlyString(), it.getValue());
}
// No reasons to create mixing collaterals if we can't create denoms to mix
if ((fCreateMixingCollaterals && txBuilder.countOutputs() == 1) || txBuilder.countOutputs() == 0) {
return false;
}
if (!dryRun) {
StringBuilder strResult = new StringBuilder();
if (!txBuilder.commit(strResult)) {
log.info("coinjoin: Commit failed: {}", strResult);
return false;
}
// use the same nCachedLastSuccessBlock as for DS mixing to prevent race
mixingWallet.getContext().coinJoinManager.coinJoinClientManagers.get(mixingWallet.getDescription()).updatedSuccessBlock();
log.info("coinjoin: txid: {}", strResult);
queueTransactionListeners(txBuilder.getTransaction(), CoinJoinTransactionType.CreateDenomination);
}
}
return true;
}
/// Split up large inputs or make fee sized inputs
private boolean makeCollateralAmounts() {
if (!CoinJoinClientOptions.isEnabled()) return false;
// NOTE: We do not allow txes larger than 100 kB, so we have to limit number of inputs here.
// We still want to consume a lot of inputs to avoid creating only smaller denoms though.
// Knowing that each CTxIn is at least 148 B big, 400 inputs should take 400 x ~148 B = ~60 kB.
// This still leaves more than enough room for another data of typical MakeCollateralAmounts tx.
List vecTally = mixingWallet.selectCoinsGroupedByAddresses(false, false, true, 400);
if (vecTally.isEmpty()) {
log.info("coinjoin: selectCoinsGroupedByAddresses can't find any inputs!\n");
return false;
}
// Start from the smallest balances first to consume tiny amounts and cleanup UTXO a bit
Collections.sort(vecTally, new Comparator() {
@Override
public int compare(CompactTallyItem a, CompactTallyItem b) {
return a.amount.compareTo(b.amount);
}
});
// First try to use only non-denominated funds
for (CompactTallyItem item : vecTally) {
if (!makeCollateralAmounts(item, false)) continue;
return true;
}
// There should be at least some denominated funds we should be able to break in pieces to continue mixing
for (CompactTallyItem item : vecTally) {
if (!makeCollateralAmounts(item, true)) continue;
return true;
}
// If we got here then something is terribly broken actually
log.error("coinjoin: ERROR: Can't make collaterals!\n");
return false;
}
private boolean makeCollateralAmounts(CompactTallyItem tallyItem, boolean fTryDenominated) {
if (!CoinJoinClientOptions.isEnabled()) return false;
// Denominated input is always a single one, so we can check its amount directly and return early
if (!fTryDenominated && tallyItem.inputCoins.size() == 1 && CoinJoin.isDenominatedAmount(tallyItem.amount)) {
return false;
}
// Skip single inputs that can be used as collaterals already
if (tallyItem.inputCoins.size() == 1 && CoinJoin.isCollateralAmount(tallyItem.amount)) {
return false;
}
//const auto pwallet = GetWallet(mixingWallet.GetName());
final WalletEx wallet = mixingWallet;
if (wallet == null) {
log.info("coinjoin: Couldn't get wallet pointer");
return false;
}
try (TransactionBuilder txBuilder = new TransactionBuilder(wallet, tallyItem, false)) {
log.info("coinjoin: Start {}", txBuilder);
// Skip way too tiny amounts. Smallest we want is minimum collateral amount in a one output tx
if (!txBuilder.couldAddOutput(CoinJoin.getCollateralAmount())) {
return false;
}
int nCase; // Just for debug logs
if (txBuilder.couldAddOutputs(Arrays.asList(CoinJoin.getMaxCollateralAmount(), CoinJoin.getCollateralAmount()))) {
nCase = 1;
// , see TransactionRecord::decomposeTransaction
// Out1 == CoinJoin.GetMaxCollateralAmount()
// Out2 >= CoinJoin.GetCollateralAmount()
txBuilder.addOutput(CoinJoin.getMaxCollateralAmount());
// Note, here we first add a zero amount output to get the remainder after all fees and then assign it
TransactionBuilderOutput out = txBuilder.addOutput();
Coin nAmountLeft = txBuilder.getAmountLeft();
// If remainder is denominated add one duff to the fee
out.updateAmount(CoinJoin.isDenominatedAmount(nAmountLeft) ? nAmountLeft.subtract(Coin.valueOf(1, 0)) : nAmountLeft);
} else if (txBuilder.couldAddOutputs(Arrays.asList(CoinJoin.getCollateralAmount(), CoinJoin.getCollateralAmount()))) {
nCase = 2;
// , see TransactionRecord::decomposeTransaction
// Out1 CoinJoin.isCollateralAmount()
// Out2 CoinJoin.isCollateralAmount()
// First add two outputs to get the available value after all fees
TransactionBuilderOutput out1 = txBuilder.addOutput();
TransactionBuilderOutput out2 = txBuilder.addOutput();
// Create two equal outputs from the available value. This adds one duff to the fee if txBuilder.GetAmountLeft() is odd.
Coin nAmountOutputs = txBuilder.getAmountLeft().div(2);
assert (CoinJoin.isCollateralAmount(nAmountOutputs));
out1.updateAmount(nAmountOutputs);
out2.updateAmount(nAmountOutputs);
} else { // still at least possible to add one CoinJoin.GetCollateralAmount() output
nCase = 3;
// , see TransactionRecord::decomposeTransaction
// Out1 CoinJoin.isCollateralAmount()
// Out2 Skipped
TransactionBuilderOutput out = txBuilder.addOutput();
out.updateAmount(txBuilder.getAmountLeft());
assert (CoinJoin.isCollateralAmount(out.getAmount()));
}
log.info("coinjoin: Done with case {}: {}", nCase, txBuilder);
assert (txBuilder.isDust(txBuilder.getAmountLeft()));
StringBuilder strResult = new StringBuilder();
if (!txBuilder.commit(strResult)) {
log.info("coinjoin: Commit failed: {}", strResult);
return false;
}
mixingWallet.getContext().coinJoinManager.coinJoinClientManagers.get(mixingWallet.getDescription()).updatedSuccessBlock();
log.info("coinjoin: txid: {}", strResult);
queueTransactionListeners(txBuilder.getTransaction(), CoinJoinTransactionType.MakeCollateralInputs);
return true;
}
}
private boolean createCollateralTransaction(Transaction txCollateral, StringBuilder strReason){
ArrayList vCoins = new ArrayList<>();
CoinControl coin_control = new CoinControl();
coin_control.setCoinType(ONLY_COINJOIN_COLLATERAL);
mixingWallet.availableCoins(vCoins, true, coin_control);
if (vCoins.isEmpty()) {
strReason.append("CoinJoin requires a collateral transaction and could not locate an acceptable input!");
return false;
}
TransactionOutput output = vCoins.get(random.nextInt(vCoins.size()));
final TransactionOutput txout = output.getParentTransaction().getOutput(output.getIndex());
txCollateral.clearInputs();
txCollateral.addInput(output);
txCollateral.clearOutputs();
// pay collateral charge in fees
// NOTE: no need for protobump patch here,
// CoinJoin.isCollateralAmount in GetCollateralTxDSIn should already take care of this
if (txout.getValue().isGreaterThanOrEqualTo(CoinJoin.getCollateralAmount().multiply(2))) {
// make our change address
Script scriptChange;
ReserveDestination reserveDest = new ReserveDestination(mixingWallet);
TransactionDestination dest = reserveDest.getReservedDestination(true);
scriptChange = dest.getScript();
reserveDest.keepDestination();
// return change
txCollateral.addOutput(txout.getValue().subtract(CoinJoin.getCollateralAmount()), scriptChange);
} else { // txout.nValue < CoinJoin.GetCollateralAmount() * 2
// create dummy data output only and pay everything as a fee
txCollateral.addOutput(Coin.ZERO, new ScriptBuilder().op(OP_RETURN).build());
}
try {
// don't set the source to TransactionConfidence.Source.SELF on these collateral transactions
// otherwise when the masternodes do submit these transactions to the network, they will be
// ignored by DashJ and when they are received
SendRequest req = SendRequest.forTx(txCollateral);
req.aesKey = context.coinJoinManager.requestKeyParameter(mixingWallet);
mixingWallet.signTransaction(req);
} catch (ScriptException x) {
strReason.append("Unable to sign collateral transaction!");
return false;
}
isMyCollateralValid = true;
if (!collateralSessionMap.containsKey(txCollateral.getTxId())) {
collateralSessionMap.put(txCollateral.getTxId(), 0);
}
return true;
}
private boolean joinExistingQueue(Coin balanceNeedsAnonymized) {
if (!CoinJoinClientOptions.isEnabled()) return false;
if (mixingWallet.getContext().coinJoinManager.getCoinJoinClientQueueManager() == null) return false;
SimplifiedMasternodeList mnList = mixingWallet.getContext().masternodeListManager.getListAtChainTip();
// Dash Core checks for recent winners, but we cannot do that
// Look through the queues and see if anything matches
CoinJoinQueue dsq;
while ((dsq = mixingWallet.getContext().coinJoinManager.getCoinJoinClientQueueManager().getQueueItemAndTry()) != null) {
Masternode dmn = mnList.getMN(dsq.getProTxHash());
if (dmn == null) {
log.info("coinjoin: dsq masternode is not in masternode list, masternode={}", dsq.getProTxHash());
continue;
}
// mixing rate limit i.e. nLastDsq check should already pass in DSQUEUE ProcessMessage
// in order for dsq to get into veCoinJoinQueue, so we should be safe to mix already,
// no need for additional verification here
log.info("coinjoin: trying existing queue: {}", dsq);
ArrayList vecTxDSInTmp = new ArrayList<>();
// Try to match their denominations if possible, select exact number of denominations
if (!mixingWallet.selectTxDSInsByDenomination(dsq.getDenomination(), balanceNeedsAnonymized, vecTxDSInTmp)) {
log.info("coinjoin: couldn't match denomination {} ({})", dsq.getDenomination(), CoinJoin.denominationToString(dsq.getDenomination()));
continue;
}
mixingWallet.getContext().coinJoinManager.coinJoinClientManagers.get(mixingWallet.getDescription()).addUsedMasternode(dsq.getProTxHash());
if (mixingWallet.getContext().coinJoinManager.isMasternodeOrDisconnectRequested(dmn.getService())) {
log.info("coinjoin: skipping masternode connection, addr={}", dmn.getService());
continue;
}
sessionDenom = dsq.getDenomination();
mixingMasternode = dmn;
pendingDsaRequest = new PendingDsaRequest(dmn.getService(), new CoinJoinAccept(mixingWallet.getContext().getParams(), sessionDenom, txMyCollateral));
mixingWallet.getContext().coinJoinManager.addPendingMasternode(this);
setState(POOL_STATE_QUEUE);
timeLastSuccessfulStep.set(Utils.currentTimeSeconds());
log.info("coinjoin: join existing queue -> pending connection, denom: {} ({}), addr={}",
sessionDenom, CoinJoin.denominationToString(sessionDenom), dmn.getService());
setStatus(PoolStatus.CONNECTING);
joined = true;
return true;
}
setStatus(PoolStatus.WARN_NO_MIXING_QUEUES);
return false;
}
private boolean startNewQueue(Coin balanceNeedsAnonymized) {
if (!CoinJoinClientOptions.isEnabled())
return false;
if (!balanceNeedsAnonymized.isPositive())
return false;
int nTries = 0;
SimplifiedMasternodeList mnList = context.masternodeListManager.getListAtChainTip();
int nMnCount = mnList.getValidMNsCount();
// find available denominated amounts
HashSet setAmounts = new HashSet<>();
if (!mixingWallet.selectDenominatedAmounts(balanceNeedsAnonymized, setAmounts)) {
// this should never happen according to Dash Core
// but this does happen if there is are coins that can be denominated, but cannot be cause of the denom goals
// so if the last denom result was false and there are no inputs, lets trigger the end.
if (!lastCreateDenominatedResult)
setStatus(PoolStatus.ERR_NO_INPUTS);
return false;
}
// otherwise, try one randomly
while (nTries < 10) {
Masternode dmn = context.coinJoinManager.coinJoinClientManagers.get(mixingWallet.getDescription()).getRandomNotUsedMasternode();
if (dmn == null) {
setStatus(PoolStatus.ERR_MASTERNODE_NOT_FOUND);
return false;
}
context.coinJoinManager.coinJoinClientManagers.get(mixingWallet.getDescription()).addUsedMasternode(dmn.getProTxHash());
long nLastDsq = context.masternodeMetaDataManager.getMetaInfo(dmn.getProTxHash()).getLastDsq();
long nDsqThreshold = context.masternodeMetaDataManager.getDsqThreshold(dmn.getProTxHash(), nMnCount);
if (nLastDsq != 0 && nDsqThreshold > context.masternodeMetaDataManager.getDsqCount()) {
log.info("coinjoin: warning: Too early to mix on this masternode!" + /* Continued */
" masternode={} addr={} nLastDsq={} nDsqThreshold={} nDsqCount={}",
dmn.getProTxHash(), dmn.getService(), nLastDsq,
nDsqThreshold, context.masternodeMetaDataManager.getDsqCount());
nTries++;
continue;
}
if (context.coinJoinManager.isMasternodeOrDisconnectRequested(dmn.getService())) {
log.info("coinjoin: warning: skipping masternode connection, addr={}", dmn.getService());
nTries++;
continue;
}
log.info("coinjoin: attempt {} connection to mn {}, protx: {}", nTries + 1, dmn.getService(), dmn.getProTxHash().toString().substring(0, 16));
// try to get a single random denom out of setAmounts
while (sessionDenom == 0) {
for (Coin it : setAmounts) {
if (setAmounts.size() > 1 && random.nextInt(2) != 0)
continue;
sessionDenom = CoinJoin.amountToDenomination(it);
break;
}
}
mixingMasternode = dmn;
context.coinJoinManager.addPendingMasternode(this);
pendingDsaRequest = new PendingDsaRequest(dmn.getService(), new CoinJoinAccept(context.getParams(), sessionDenom, txMyCollateral));
setState(POOL_STATE_QUEUE);
timeLastSuccessfulStep.set(Utils.currentTimeSeconds());
log.info("coinjoin: start new queue -> pending connection, nSessionDenom: {} ({}), addr={}",
sessionDenom, CoinJoin.denominationToString(sessionDenom), dmn.getService());
context.coinJoinManager.startAsync();
setStatus(PoolStatus.CONNECTING);
joined = false;
return true;
}
strAutoDenomResult = "Failed to start a new mixing queue";
return false;
}
/// step 0: select denominated inputs and txouts
private boolean selectDenominate(StringBuilder strErrorRet, List vecTxDSInRet) {
if (!CoinJoinClientOptions.isEnabled()) return false;
if (mixingWallet.isEncrypted() && context.coinJoinManager.requestKeyParameter(mixingWallet) == null) {
strErrorRet.append("Wallet locked, unable to create transaction!");
return false;
}
if (getEntriesCount() > 0) {
strErrorRet.append("Already have pending entries in the CoinJoin pool");
return false;
}
vecTxDSInRet.clear();
boolean fSelected = mixingWallet.selectTxDSInsByDenomination(sessionDenom, CoinJoin.getMaxPoolAmount(), vecTxDSInRet);
if (!fSelected) {
strErrorRet.append("Can't select current denominated inputs: ").append(CoinJoin.denominationToAmount(sessionDenom).toFriendlyString()).append(" for session ").append(sessionID.get());
vecTxDSInRet.forEach(input -> strErrorRet.append("\n").append(input));
return false;
}
return true;
}
/// step 1: prepare denominated inputs and outputs
private boolean prepareDenominate(int minRounds, int maxRounds, StringBuilder strErrorRet, List vecTxDSIn, List> vecPSInOutPairsRet) {
return prepareDenominate(minRounds, maxRounds, strErrorRet, vecTxDSIn, vecPSInOutPairsRet, false);
}
private boolean prepareDenominate(int minRounds, int maxRounds, StringBuilder strErrorRet, List vecTxDSIn, List> vecPSInOutPairsRet, boolean fDryRun) {
if (!CoinJoin.isValidDenomination(sessionDenom)) {
strErrorRet.append("Incorrect session denom");
return false;
}
Coin nDenomAmount = CoinJoin.denominationToAmount(sessionDenom);
// NOTE: No need to randomize order of inputs because they were
// initially shuffled in CWallet::SelectTxDSInsByDenomination already.
int nSteps = 0;
vecPSInOutPairsRet.clear();
// Try to add up to COINJOIN_ENTRY_MAX_SIZE of every needed denomination
for (CoinJoinTransactionInput entry : vecTxDSIn) {
if (nSteps >= COINJOIN_ENTRY_MAX_SIZE) break;
if (entry.getRounds() < minRounds || entry.getRounds() > maxRounds) continue;
Script scriptDenom;
if (fDryRun) {
scriptDenom = new Script(new byte[0]);
} else {
// randomly skip some inputs when we have at least one of the same denom already
// TODO: make it adjustable via options/cmd-line params
if (nSteps >= 1 && random.nextInt(5) == 0) {
// still count it as a step to randomize number of inputs
// if we have more than (or exactly) COINJOIN_ENTRY_MAX_SIZE of them
++nSteps;
continue;
}
scriptDenom = keyHolderStorage.addKey(mixingWallet);
}
vecPSInOutPairsRet.add(new Pair<>(entry, new TransactionOutput(context.getParams(), null, nDenomAmount, scriptDenom.getProgram())));
// step is complete
++nSteps;
}
if (vecPSInOutPairsRet.isEmpty()) {
keyHolderStorage.returnAll();
if (strErrorRet.length() == 0)
strErrorRet.append("Can't prepare current denominated outputs");
return false;
}
if (fDryRun) {
return true;
}
for (Pair pair : vecPSInOutPairsRet) {
mixingWallet.lockCoin(pair.getFirst().getOutpoint());
outPointLocked.add(pair.getFirst().getOutpoint());
}
return true;
}
/// step 2: send denominated inputs and outputs prepared in step 1
private boolean sendDenominate(List> vecPSInOutPairs) {
if (txMyCollateral.getInputs().isEmpty()) {
log.info("coinjoin: CoinJoin collateral not set");
return false;
}
// we should already be connected to a Masternode
if (sessionID.get() == 0) {
log.info("coinjoin: No Masternode has been selected yet.");
unlockCoins();
keyHolderStorage.returnAll();
lock.lock();
try {
setNull();
} finally {
lock.unlock();
}
return false;
}
setState(POOL_STATE_ACCEPTING_ENTRIES);
strLastMessage = "";
log.info("coinjoin: Added transaction to pool.");
Transaction tx = new Transaction(context.getParams()); // for debug purposes only
ArrayList vecTxDSInTmp = Lists.newArrayList();
ArrayList vecTxOutTmp = Lists.newArrayList();
for (Pair pair : vecPSInOutPairs) {
vecTxDSInTmp.add(pair.getFirst());
vecTxOutTmp.add(pair.getSecond());
tx.addInput(pair.getFirst());
tx.addOutput(pair.getSecond());
}
log.info(COINJOIN_EXTRA, "coinjoin: Submitting partial tx {} to session {}", tx, sessionID.get()); /* Continued */
// store our entry for later use
lock.lock();
try {
CoinJoinEntry entry = new CoinJoinEntry(context.getParams(), vecTxDSInTmp, vecTxOutTmp, txMyCollateral);
entries.add(entry);
relay(entry);
timeLastSuccessfulStep.set(Utils.currentTimeSeconds());
} finally {
lock.unlock();
}
return true;
}
private void relay(CoinJoinEntry entry) {
if (mixingMasternode != null) {
log.info("Sending {} to {}", entry.toString(true), sessionID);
if(!context.coinJoinManager.forPeer(mixingMasternode.getService(), new MasternodeGroup.ForPeer() {
@Override
public boolean process(Peer peer) {
peer.sendMessage(entry);
return true;
}
}, true)) {
log.info("coinjoin: failed to send to {} CoinJoinEntry: {}", mixingMasternode.getService().getSocketAddress(), entry);
}
}
}
/// Process Masternode updates about the progress of mixing
private void processPoolStateUpdate(Peer peer, CoinJoinStatusUpdate statusUpdate) {
log.info("status update received: {} from {}", statusUpdate, peer.getAddress().getSocketAddress());
// do not update state when mixing client state is one of these
if (state.get() == POOL_STATE_IDLE || state.get() == POOL_STATE_ERROR) return;
if (statusUpdate.getState().value < POOL_STATE_MIN.value || statusUpdate.getState().value > POOL_STATE_MAX.value) {
log.info("coinjoin session: statusUpdate.state is out of bounds: {}", statusUpdate.getState());
return;
}
if (statusUpdate.getMessageID().value < MSG_POOL_MIN.value || statusUpdate.getMessageID().value > MSG_POOL_MAX.value) {
log.info("coinjoin session: statusUpdate.nMessageID is out of bounds: {}", statusUpdate.getMessageID());
return;
}
String strMessageTmp = CoinJoin.getMessageByID(statusUpdate.getMessageID());
strAutoDenomResult = "Masternode:" + " " + strMessageTmp;
switch (statusUpdate.getStatusUpdate()) {
case STATUS_REJECTED: {
log.error("coinjoin session: rejected by Masternode {}: {}", peer.getAddress().getSocketAddress(), strMessageTmp);
setState(POOL_STATE_ERROR);
unlockCoins();
keyHolderStorage.returnAll();
switch (statusUpdate.getMessageID()) {
case ERR_INVALID_COLLATERAL:
log.error("coinjoin: collateral invalid: {}", CoinJoin.isCollateralValid(txMyCollateral));
isMyCollateralValid = false;
setNull(); // for now lets disconnect. TODO: Why is the collateral invalid?
break;
default:
log.warn("coinjoin: rejected for other reasons");
break;
}
timeLastSuccessfulStep.set(Utils.currentTimeSeconds());
strLastMessage = strMessageTmp;
break;
}
case STATUS_ACCEPTED: {
if (state.get() == statusUpdate.getState() && statusUpdate.getState() == POOL_STATE_QUEUE && sessionID.get() == 0 && statusUpdate.getSessionID() != 0) {
// new session id should be set only in POOL_STATE_QUEUE state
sessionID.set(statusUpdate.getSessionID());
collateralSessionMap.put(txMyCollateral.getTxId(), statusUpdate.getSessionID());
timeLastSuccessfulStep.set(Utils.currentTimeSeconds());
strMessageTmp = strMessageTmp + " Set SID to " + sessionID.get();
queueSessionStartedListeners(MSG_SUCCESS);
}
log.info("coinjoin session: accepted by Masternode: {}", strMessageTmp);
break;
}
default: {
log.info("coinjoin session: statusUpdate.statusUpdate is out of bounds: {}", statusUpdate.getStatusUpdate());
break;
}
}
}
// Set the 'state' value, with some logging and capturing when the state changed
private void setState(PoolState state) {
this.state.set(state);
}
private void completedTransaction(PoolMessage messageID) {
if (messageID == MSG_SUCCESS) {
log.info("coinjoin: completedTransaction -- success");
queueSessionCompleteListeners(getState(), MSG_SUCCESS);
mixingWallet.getContext().coinJoinManager.coinJoinClientManagers.get(mixingWallet.getDescription()).updatedSuccessBlock();
keyHolderStorage.keepAll();
} else {
log.info("coinjoin: completedTransaction -- error");
keyHolderStorage.returnAll();
}
unlockCoins();
lock.lock();
try {
setNull(); // this will also disconnect the masternode
setStatus(PoolStatus.COMPLETE);
} finally {
lock.unlock();
}
strLastMessage = CoinJoin.getMessageByID(messageID);
}
/// As a client, check and sign the final transaction
private boolean signFinalTransaction(Transaction finalTransactionNew, Peer node) {
if (!CoinJoinClientOptions.isEnabled())
return false;
if (mixingMasternode == null)
return false;
lock.lock();
try {
finalMutableTransaction = new Transaction(finalTransactionNew.getParams());//finalTransactionNew;
finalMutableTransaction.setVersionAndType(finalTransactionNew.getVersionShort(), finalTransactionNew.getType());
finalMutableTransaction.setLockTime(finalTransactionNew.getLockTime());
// we need to connect inputs
for (TransactionInput input : finalTransactionNew.getInputs()) {
Transaction tx = mixingWallet.getTransaction(input.getOutpoint().getHash());
if (tx != null) {
// this is our input, so connect to our transaction
TransactionOutPoint outPoint = new TransactionOutPoint(input.getParams(), input.getOutpoint().getIndex(), tx);
byte[] pubKeyHash = ScriptPattern.extractHashFromP2PKH(tx.getOutput(outPoint.getIndex()).getScriptPubKey());
ECKey key = mixingWallet.findKeyFromPubKeyHash(pubKeyHash, Script.ScriptType.P2PKH);
if (key != null) {
Script inputScript = ScriptBuilder.createInputScript(null, key);
TransactionInput connectedInput = new TransactionInput(input.getParams(), finalMutableTransaction, inputScript.getProgram(), outPoint);
finalMutableTransaction.addInput(connectedInput);
continue; // go to the next input
}
}
// this is not our input
finalMutableTransaction.addInput(input);
}
for (TransactionOutput output : finalTransactionNew.getOutputs()) {
finalMutableTransaction.addOutput(output);
}
log.info(COINJOIN_EXTRA, "coinjoin: finalMutableTx={}", finalMutableTransaction); /* Continued */
// STEP 1: check final transaction general rules
// Make sure it's BIP69 compliant
// TODO: do these actually work?, do we have a unit test?
finalMutableTransaction.sortInputs();
finalMutableTransaction.sortOutputs();
if (!finalMutableTransaction.getTxId().equals(finalTransactionNew.getTxId())) {
log.info(COINJOIN_EXTRA, "coinjoin: ERROR! Masternode {} is not BIP69 compliant!", mixingMasternode.getProTxHash());
//unlockCoins();
//keyHolderStorage.returnAll();
//setNull();
//return false;
}
// Make sure all inputs/outputs are valid
ValidInOuts validResult = isValidInOuts(finalMutableTransaction.getInputs(), finalMutableTransaction.getOutputs());
if (!validResult.result) {
log.info("coinjoin: ERROR! IsValidInOuts() failed: {}", CoinJoin.getMessageByID(validResult.messageId));
unlockCoins();
keyHolderStorage.returnAll();
setNull();
return false;
}
// STEP 2: make sure our own inputs/outputs are present, otherwise refuse to sign
ArrayList sigs = Lists.newArrayList();
for (CoinJoinEntry entry: entries){
// Check that the final transaction has all our outputs
for (TransactionOutput txout : entry.getMixingOutputs()) {
boolean found = false;
for (TransactionOutput output : finalMutableTransaction.getOutputs()) {
found = txout.getValue().equals(output.getValue())
&& Arrays.equals(txout.getScriptBytes(), output.getScriptBytes());
if (found) {
break;
}
}
if (!found) {
// Something went wrong and we'll refuse to sign. It's possible we'll be charged collateral. But that's
// better than signing if the transaction doesn't look like what we wanted.
log.info("coinjoin: an output is missing, refusing to sign! txout={}", txout);
unlockCoins();
keyHolderStorage.returnAll();
setNull();
return false;
}
}
for (CoinJoinTransactionInput txdsin : entry.getMixingInputs()){
/* Sign my transaction and all outputs */
int nMyInputIndex = -1;
//Script prevPubKey = null;
for (int i = 0; i < finalMutableTransaction.getInputs().size(); ++i){
// cppcheck-suppress useStlAlgorithm
if (finalMutableTransaction.getInput(i).equalsWithoutParent(txdsin)) {
nMyInputIndex = i;
//prevPubKey = txdsin.getPrevPubKey();
break;
}
}
if (nMyInputIndex == -1) {
// Can't find one of my own inputs, refuse to sign. It's possible we'll be charged collateral. But that's
// better than signing if the transaction doesn't look like what we wanted.
log.info("coinjoin: missing input! txdsin={}\n", txdsin);
unlockCoins();
keyHolderStorage.returnAll();
setNull();
return false;
}
sigs.add(finalMutableTransaction.getInput(nMyInputIndex));
log.info("coinjoin: myInputIndex: {}, sigs.size: {}, scriptSig={}",
nMyInputIndex, sigs.size(), finalMutableTransaction.getInput(nMyInputIndex).getScriptSig());
}
}
TransactionSigner.ProposedTransaction proposal = new TransactionSigner.ProposedTransaction(finalMutableTransaction);
CoinJoinTransactionSigner signer = new CoinJoinTransactionSigner(sigs, true);
if (!signer.signInputs(proposal, mixingWallet))
log.info("{} returned false for the tx", signer.getClass().getName());
if (sigs.isEmpty()) {
log.info("coinjoin: can't sign anything!");
unlockCoins();
keyHolderStorage.returnAll();
setNull();
return false;
}
// push all of our signatures to the Masternode
log.info("coinjoin: pushing sigs to the masternode, finalMutableTx={}", finalMutableTransaction); /* Continued */
node.sendMessage(new CoinJoinSignedInputs(mixingWallet.getContext().getParams(), sigs));
setState(POOL_STATE_SIGNING);
timeLastSuccessfulStep.set(Utils.currentTimeSeconds());
return true;
} finally {
lock.unlock();
}
}
@GuardedBy("lock")
protected void setNull() {
// Client side
if (mixingMasternode != null) {
if (context.coinJoinManager.isMasternodeOrDisconnectRequested(mixingMasternode.getService())) {
if (!context.coinJoinManager.disconnectMasternode(mixingMasternode)) {
log.info("not closing existing masternode: {}", mixingMasternode.getService().getSocketAddress());
}
} else {
log.info("not closing masternode since it is not found: {}", mixingMasternode.getService().getSocketAddress());
}
}
mixingMasternode = null;
pendingDsaRequest = null;
log.info("session zeroed out {}; {}", state, status);
super.setNull();
}
// internal session id
static int nextId = 0;
private final int id;
public int getId() {
return id;
}
public CoinJoinClientSession(WalletEx mixingWallet) {
super(mixingWallet.getContext());
this.mixingWallet = mixingWallet;
this.keyHolderStorage = new KeyHolderStorage();
this.txMyCollateral = new Transaction(context.getParams());
this.id = nextId++;
}
public void processMessage(Peer peer, Message message, boolean enable_bip61) {
if (message instanceof CoinJoinStatusUpdate) {
processStatusUpdate(peer, (CoinJoinStatusUpdate) message);
} else if (message instanceof CoinJoinFinalTransaction) {
processFinalTransaction(peer, (CoinJoinFinalTransaction) message);
} else if (message instanceof CoinJoinComplete) {
processComplete(peer, (CoinJoinComplete) message);
}
}
private void processStatusUpdate(Peer peer, CoinJoinStatusUpdate statusUpdate) {
if (mixingMasternode == null) {
log.info(COINJOIN_EXTRA, "mixingMaster node is null, ignoring status update");
return;
}
if (!mixingMasternode.getService().getAddr().equals(peer.getAddress().getAddr()))
return;
processPoolStateUpdate(peer, statusUpdate);
}
private void processFinalTransaction(Peer peer, CoinJoinFinalTransaction finalTransaction) {
if (mixingMasternode == null)
return;
if (!mixingMasternode.getService().getAddr().equals(peer.getAddress().getAddr()))
return;
if (sessionID.get() != finalTransaction.getMsgSessionID()) {
log.info("DSFINALTX: message doesn't match current CoinJoin session: sessionID: {} msgSessionID: {}",
sessionID, finalTransaction.getMsgSessionID());
return;
}
log.info(COINJOIN_EXTRA, "DSFINALTX: txNew {}", finalTransaction.getTransaction()); /* Continued */
// check to see if input is spent already? (and probably not confirmed)
signFinalTransaction(finalTransaction.getTransaction(), peer);
}
private void processComplete(Peer peer, CoinJoinComplete completeMessage) {
if (mixingMasternode == null)
return;
if (!mixingMasternode.getService().getAddr().equals(peer.getAddress().getAddr()))
return;
if (completeMessage.getMsgMessageID().value < MSG_POOL_MIN.value || completeMessage.getMsgMessageID().value > MSG_POOL_MAX.value) {
log.info("DSCOMPLETE: msgID is out of bounds: {}", completeMessage.getMsgMessageID());
return;
}
if (sessionID.get() != completeMessage.getMsgSessionID()) {
log.info("DSCOMPLETE: message doesn't match current CoinJoin session: SID: {} msgID: {} ({})",
sessionID.get(), completeMessage.getMsgSessionID(),
CoinJoin.getMessageByID(completeMessage.getMsgMessageID()));
return;
}
log.info("DSCOMPLETE: msgSID {} msg {} ({})", completeMessage.getMsgSessionID(),
completeMessage.getMsgMessageID(), CoinJoin.getMessageByID(completeMessage.getMsgMessageID()));
completedTransaction(completeMessage.getMsgMessageID());
}
public void unlockCoins() {
if (!CoinJoinClientOptions.isEnabled())
return;
// TODO: should we wait here? check Dash Core code
for (TransactionOutPoint outpoint : outPointLocked)
mixingWallet.unlockCoin(outpoint);
outPointLocked.clear();
}
public void resetPool() {
txMyCollateral = new Transaction(mixingWallet.getParams());
unlockCoins();
keyHolderStorage.returnAll();
setNull();
}
private static int nStatusMessageProgress = 0;
public String getStatus(boolean fWaitForBlock) {
nStatusMessageProgress += 10;
String strSuffix;
if (fWaitForBlock || (mixingWallet.getContext().masternodeSync.hasSyncFlag(MasternodeSync.SYNC_FLAGS.SYNC_GOVERNANCE) && !mixingWallet.getContext().masternodeSync.isBlockchainSynced())) {
return strAutoDenomResult;
}
switch (state.get()) {
case POOL_STATE_IDLE:
return "CoinJoin is idle.";
case POOL_STATE_QUEUE:
if (nStatusMessageProgress % 70 <= 30)
strSuffix = ".";
else if (nStatusMessageProgress % 70 <= 50)
strSuffix = "..";
else
strSuffix = "...";
return String.format("Submitted to masternode, waiting in queue %s", strSuffix);
case POOL_STATE_ACCEPTING_ENTRIES:
return strAutoDenomResult;
case POOL_STATE_SIGNING:
if (nStatusMessageProgress % 70 <= 40)
return "Found enough users, signing ...";
else if (nStatusMessageProgress % 70 <= 50)
strSuffix = ".";
else if (nStatusMessageProgress % 70 <= 60)
strSuffix = "..";
else
strSuffix = "...";
return String.format("Found enough users, signing ( waiting %s )", strSuffix);
case POOL_STATE_ERROR:
return "CoinJoin request incomplete: " + strLastMessage + " Will retry...";
default:
return String.format("Unknown state: id = %s", state.get());
}
}
public Masternode getMixingMasternodeInfo() {
return mixingMasternode;
}
/// Passively run mixing in the background according to the configuration in settings
public boolean doAutomaticDenominating() {
return doAutomaticDenominating(false);
}
public boolean doAutomaticDenominating(boolean fDryRun) {
if (state.get() != POOL_STATE_IDLE) return false;
if (mixingWallet.getContext().masternodeSync.hasSyncFlag(MasternodeSync.SYNC_FLAGS.SYNC_GOVERNANCE) && !mixingWallet.getContext().masternodeSync.isBlockchainSynced()) {
strAutoDenomResult = "Can't mix while sync in progress.";
return false;
}
if (!CoinJoinClientOptions.isEnabled()) return false;
Coin balanceNeedsAnonymized;
if (!fDryRun && mixingWallet.isEncrypted() && context.coinJoinManager.requestKeyParameter(mixingWallet) == null) {
setStatus(PoolStatus.ERR_WALLET_LOCKED);
return false;
}
if (getEntriesCount() > 0) {
if (!fDryRun)
setStatus(PoolStatus.MIXING);
return false;
}
boolean hasLock = lock.tryLock();
if (!hasLock) {
strAutoDenomResult = "Lock is already in place.";
return false;
}
try {
if (!fDryRun && mixingWallet.getContext().masternodeListManager.getListAtChainTip().getValidMNsCount() == 0 &&
!mixingWallet.getContext().getParams().getId().equals(NetworkParameters.ID_REGTEST)) {
strAutoDenomResult = "No Masternodes detected.";
log.info("coinjoin: {}", strAutoDenomResult);
//status.set(PoolStatus.ERR_NO_MASTERNODES_DETECTED);
//hasNothingToDo.set(true);
setStatus(PoolStatus.ERR_NO_MASTERNODES_DETECTED);
queueSessionCompleteListeners(getState(), ERR_SESSION);
return false;
}
Balance bal = mixingWallet.getBalanceInfo();
// check if there is anything left to do
Coin nBalanceAnonymized = bal.getAnonymized();
balanceNeedsAnonymized = CoinJoinClientOptions.getAmount().subtract(nBalanceAnonymized);
if (balanceNeedsAnonymized.isLessThanOrEqualTo(Coin.ZERO)) {
log.info("coinjoin: Nothing to do {}", fDryRun);
// nothing to do, just keep it in idle mode
//status.set(PoolStatus.FINISHED);
//hasNothingToDo.set(true);
if (!fDryRun) {
setStatus(PoolStatus.FINISHED);
}
return false;
}
hasNothingToDo.set(false);
Coin nValueMin = CoinJoin.getSmallestDenomination();
// if there are no confirmed DS collateral inputs yet
if (!mixingWallet.hasCollateralInputs()) {
// should have some additional amount for them
nValueMin = nValueMin.add(CoinJoin.getMaxCollateralAmount());
}
// including denoms but applying some restrictions
Coin nBalanceAnonymizable = mixingWallet.getAnonymizableBalance();
// mixable balance is way too small
if (nBalanceAnonymizable.isLessThan(nValueMin)) {
//TODO: let us leave the previous code in this block for now
// we don't yet have a way to precalculate if we have funds that can be anonomized
// reliably
//setStatus(PoolStatus.ERR_NOT_ENOUGH_FUNDS);
//queueSessionCompleteListeners(getState(), ERR_SESSION);
//return false;
Coin balanceLeftToMix = mixingWallet.getAnonymizableBalance(false, false);
if (!fDryRun && !balanceLeftToMix.isGreaterThanOrEqualTo(nValueMin)) {
setStatus(PoolStatus.ERR_NOT_ENOUGH_FUNDS);
queueSessionCompleteListeners(getState(), ERR_SESSION);
}
log.info("coinjoin: balance too low: dryRun: {}", fDryRun);
return false;
}
// excluding denoms
Coin nBalanceAnonimizableNonDenom = mixingWallet.getAnonymizableBalance(true);
// denoms
Coin nBalanceDenominatedConf = bal.getDenominatedTrusted();
Coin nBalanceDenominatedUnconf = bal.getDenominatedUntrustedPending();
Coin nBalanceDenominated = nBalanceDenominatedConf.add(nBalanceDenominatedUnconf);
Coin nBalanceToDenominate = CoinJoinClientOptions.getAmount().subtract(nBalanceDenominated);
// adjust balanceNeedsAnonymized to consume final denom
if (nBalanceDenominated.subtract(nBalanceAnonymized).isGreaterThan(balanceNeedsAnonymized)) {
List denoms = CoinJoinClientOptions.getDenominations();
Coin nAdditionalDenom = Coin.ZERO;
for (Coin denom :denoms){
if (balanceNeedsAnonymized.isLessThan(denom)) {
nAdditionalDenom = denom;
} else {
break;
}
}
balanceNeedsAnonymized = balanceNeedsAnonymized.add(nAdditionalDenom);
}
log.info(COINJOIN_EXTRA, "coinjoin: wallet stats:\n{}", bal);
log.info(COINJOIN_EXTRA, "coinjoin: current stats:\n" +
" min: {}\n" +
" myTrusted: {}\n" +
" balanceAnonymizable: {}\n" +
" balanceAnonymized: {}\n" +
" balanceNeedsAnonymized: {}\n" +
" balanceAnonimizableNonDenom: {}\n" +
" balanceDenominatedConf: {}\n" +
" balanceDenominatedUnconf: {}\n" +
" balanceDenominated: {}\n" +
" balanceToDenominate: {}\n",
nValueMin.toFriendlyString(),
bal.getMyTrusted().toFriendlyString(),
nBalanceAnonymizable.toFriendlyString(),
nBalanceAnonymized.toFriendlyString(),
balanceNeedsAnonymized.toFriendlyString(),
nBalanceAnonimizableNonDenom.toFriendlyString(),
nBalanceDenominatedConf.toFriendlyString(),
nBalanceDenominatedUnconf.toFriendlyString(),
nBalanceDenominated.toFriendlyString(),
nBalanceToDenominate.toFriendlyString()
);
// Check if we have should create more denominated inputs i.e.
// there are funds to denominate and denominated balance does not exceed
// max amount to mix yet.
lastCreateDenominatedResult = true;
if (nBalanceAnonimizableNonDenom.isGreaterThanOrEqualTo(nValueMin.add(CoinJoin.getCollateralAmount())) && nBalanceToDenominate.isGreaterThan(Coin.ZERO)) {
lastCreateDenominatedResult = createDenominated(nBalanceToDenominate, fDryRun);
}
if (fDryRun) {
log.info("coinjoin: create denominations {}, {}", lastCreateDenominatedResult, fDryRun);
return lastCreateDenominatedResult;
}
//check if we have the collateral sized inputs
if (!mixingWallet.hasCollateralInputs()) {
return !mixingWallet.hasCollateralInputs(false) && makeCollateralAmounts();
}
if (sessionID.get() != 0) {
setStatus(PoolStatus.MIXING);
return false;
}
// Initial phase, find a Masternode
// Clean if there is anything left from previous session
unlockCoins();
keyHolderStorage.returnAll();
setNull();
// should be no unconfirmed denoms in non-multi-session mode
if (!CoinJoinClientOptions.isMultiSessionEnabled() && nBalanceDenominatedUnconf.isGreaterThan(Coin.ZERO)){
strAutoDenomResult = "Found unconfirmed denominated outputs, will wait till they confirm to continue.";
log.info("coinjoin: {}", strAutoDenomResult);
return false;
}
//check our collateral and create new if needed
StringBuilder strReason = new StringBuilder();
if (txMyCollateral.isEmpty()) {
if (!createCollateralTransaction(txMyCollateral, strReason)) {
log.info("coinjoin: create collateral error: {}", strReason);
return false;
}
} else {
if (!isMyCollateralValid || !CoinJoin.isCollateralValid(txMyCollateral)) {
log.info("coinjoin: invalid collateral, recreating... [id: {}] ", id);
TransactionOutput output = txMyCollateral.getOutput(0);
if (ScriptPattern.isP2PKH(output.getScriptPubKey())) {
mixingWallet.getCoinJoin().addUnusedKey(KeyId.fromBytes(ScriptPattern.extractHashFromP2PKH(output.getScriptPubKey()), false));
}
if (!createCollateralTransaction(txMyCollateral, strReason)) {
log.info("coinjoin: create collateral error: {}", strReason);
return false;
}
}
}
// lock the funds we're going to use for our collateral
for (TransactionInput txin :txMyCollateral.getInputs()){
mixingWallet.lockCoin(txin.getOutpoint());
outPointLocked.add(txin.getOutpoint());
}
} finally {
lock.unlock();
}
// Always attempt to join an existing queue
if (joinExistingQueue(balanceNeedsAnonymized)) {
return true;
}
// If we were unable to find/join an existing queue then start a new one.
if (startNewQueue(balanceNeedsAnonymized)) {
return true;
}
setStatus(PoolStatus.WARN_NO_COMPATIBLE_MASTERNODE);
return false;
}
private void setStatus(PoolStatus poolStatus) {
strAutoDenomResult = CoinJoin.getStatusById(poolStatus);
if (poolStatus.isError())
log.error("coinjoin: {}", strAutoDenomResult);
else if (poolStatus.isWarning())
log.warn("coinjoin: {}", strAutoDenomResult);
else
log.info("coinjoin: {}", strAutoDenomResult);
status.set(poolStatus);
if (poolStatus.shouldStop()) {
log.info("Session has nothing to do: {}", poolStatus);
if (poolStatus.isError())
log.error("Session has an error: {}", poolStatus);
hasNothingToDo.set(true);
}
}
/// As a client, submit part of a future mixing transaction to a Masternode to start the process
public boolean submitDenominate() {
StringBuilder strError = new StringBuilder();
ArrayList vecTxDSIn = Lists.newArrayList();
ArrayList> vecPSInOutPairsTmp = Lists.newArrayList();
if (!selectDenominate(strError, vecTxDSIn)) {
log.info("coinjoin: SelectDenominate failed, error: {}", strError);
return false;
}
ArrayList> vecInputsByRounds = Lists.newArrayList();
for (int i = 0; i < CoinJoinClientOptions.getRounds() + CoinJoinClientOptions.getRandomRounds(); ++i) {
if (prepareDenominate(i, i, strError, vecTxDSIn, vecPSInOutPairsTmp, true)) {
log.info("coinjoin: Running CoinJoin denominate for {} rounds, success", i);
vecInputsByRounds.add(new Pair<>(i, vecPSInOutPairsTmp.size()));
} else {
log.info(COINJOIN_EXTRA, "coinjoin: Running CoinJoin denominate for {} rounds, error: {}", i, strError);
}
}
// more inputs first, for equal input count prefer the one with fewer rounds
vecInputsByRounds.sort((a, b) -> {
int inputCompare = Integer.compare(b.getSecond(), a.getSecond());
return inputCompare != 0 ? inputCompare : Integer.compare(a.getFirst(), b.getFirst());
});
log.info("coinjoin: vecInputsByRounds(size={}) for denom {}", vecInputsByRounds.size(), sessionDenom);
for (Pair pair : vecInputsByRounds) {
log.info(COINJOIN_EXTRA, "coinjoin: vecInputsByRounds: rounds: {}, inputs: {}", pair.getFirst(), pair.getSecond());
}
int nRounds = vecInputsByRounds.get(0).getFirst();
if (prepareDenominate(nRounds, nRounds, strError, vecTxDSIn, vecPSInOutPairsTmp)) {
log.info("coinjoin: Running CoinJoin denominate for {} rounds, success", nRounds);
return sendDenominate(vecPSInOutPairsTmp);
}
// We failed? That's strange but let's just make final attempt and try to mix everything
if (prepareDenominate(0, CoinJoinClientOptions.getRounds() - 1, strError, vecTxDSIn, vecPSInOutPairsTmp)) {
log.info("coinjoin: Running CoinJoin denominate for all rounds, success\n");
return sendDenominate(vecPSInOutPairsTmp);
}
// Should never actually get here but just in case
log.info("coinjoin: Running CoinJoin denominate for all rounds, error: {}", strError);
strAutoDenomResult = strError.toString();
return false;
}
public boolean processPendingDsaRequest() {
if (pendingDsaRequest == null)
return false;
// should be masternode peers
boolean sentMessage = mixingWallet.getContext().coinJoinManager.forPeer(pendingDsaRequest.getAddress(), new MasternodeGroup.ForPeer() {
@Override
public boolean process(Peer peer) {
timeLastSuccessfulStep.set(Utils.currentTimeSeconds());
peer.sendMessage(pendingDsaRequest.getDsa());
log.info(COINJOIN_EXTRA, "coinjoin: valid collateral before sending: {}", CoinJoin.isCollateralValid(pendingDsaRequest.getDsa().getTxCollateral()));
log.info("sending {} to {}", pendingDsaRequest.getDsa(), peer);
return true;
}
}, false);
if (sentMessage) {
pendingDsaRequest = null;
} else if (pendingDsaRequest.isExpired()) {
log.info("coinjoin: failed to connect to {}; reason: cannot find peer", pendingDsaRequest.getAddress());
setStatus(PoolStatus.CONNECTION_TIMEOUT);
queueSessionCompleteListeners(getState(), ERR_CONNECTION_TIMEOUT);
setNull();
}
return sentMessage;
}
public boolean checkTimeout() {
if (state.get() == POOL_STATE_IDLE)
return false;
if (state.get() == POOL_STATE_ERROR) {
if (Utils.currentTimeSeconds() - timeLastSuccessfulStep.get() >= 10) {
// reset after being in POOL_STATE_ERROR for 10 or more seconds
log.info("coinjoin: resetting session {}", sessionID);
setNull();
}
return false;
}
int nLagTime = 10; // give the server a few extra seconds before resetting.
int nTimeout = (state.get() == POOL_STATE_SIGNING) ? COINJOIN_SIGNING_TIMEOUT : COINJOIN_QUEUE_TIMEOUT;
boolean fTimeout = Utils.currentTimeSeconds() - timeLastSuccessfulStep.get() >= nTimeout + nLagTime;
if (!fTimeout)
return false;
log.info("coinjoin: {} {} timed out ({})", (state.get() == POOL_STATE_SIGNING) ? "Signing at session" : "Session", sessionID.get(), nTimeout);
queueSessionCompleteListeners(getState(), ERR_TIMEOUT);
setState(POOL_STATE_ERROR);
setStatus(PoolStatus.TIMEOUT);
unlockCoins();
keyHolderStorage.returnAll();
timeLastSuccessfulStep.set(Utils.currentTimeSeconds());
strLastMessage = CoinJoin.getMessageByID(ERR_SESSION);
return true;
}
@Override
public String toString() {
return "CoinJoinClientSession{id=" + id +
", mixer=" + (mixingMasternode != null ? mixingMasternode.getService().getSocketAddress() : "none") +
", joined=" + joined +
", msg='" + strLastMessage + '\'' +
", dsa=" + pendingDsaRequest +
", entries=" + entries.size() +
", state=" + state +
", SID=" + sessionID +
", denom=" + CoinJoin.denominationToString(sessionDenom) + "[" + sessionDenom + "]" +
'}';
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
CoinJoinClientSession that = (CoinJoinClientSession) o;
return id == that.id;
}
@Override
public int hashCode() {
return id;
}
public boolean hasNothingToDo() {
return hasNothingToDo.get();
}
/**
* Adds an event listener object. Methods on this object are called when something interesting happens,
* like receiving money. Runs the listener methods in the user thread.
*/
public void addSessionStartedListener(SessionStartedListener listener) {
addSessionStartedListener(Threading.USER_THREAD, listener);
}
/**
* Adds an event listener object. Methods on this object are called when something interesting happens,
* like receiving money. The listener is executed by the given executor.
*/
public void addSessionStartedListener(Executor executor, SessionStartedListener listener) {
// This is thread safe, so we don't need to take the lock.
sessionStartedListeners.add(new ListenerRegistration<>(listener, executor));
}
/**
* Removes the given event listener object. Returns true if the listener was removed, false if that listener
* was never added.
*/
public boolean removeSessionStartedListener(SessionStartedListener listener) {
return ListenerRegistration.removeFromList(listener, sessionStartedListeners);
}
protected void queueSessionStartedListeners(PoolMessage message) {
//checkState(lock.isHeldByCurrentThread());
for (final ListenerRegistration registration : sessionStartedListeners) {
registration.executor.execute(new Runnable() {
@Override
public void run() {
registration.listener.onSessionStarted(mixingWallet, getSessionID(), getSessionDenom(), message);
}
});
}
}
/**
* Adds an event listener object. Methods on this object are called when something interesting happens,
* like receiving money. Runs the listener methods in the user thread.
*/
public void addSessionCompleteListener(SessionCompleteListener listener) {
addSessionCompleteListener(Threading.USER_THREAD, listener);
}
/**
* Adds an event listener object. Methods on this object are called when something interesting happens,
* like receiving money. The listener is executed by the given executor.
*/
public void addSessionCompleteListener(Executor executor, SessionCompleteListener listener) {
// This is thread safe, so we don't need to take the lock.
sessionCompleteListeners.add(new ListenerRegistration<>(listener, executor));
}
/**
* Removes the given event listener object. Returns true if the listener was removed, false if that listener
* was never added.
*/
public boolean removeSessionCompleteListener(SessionCompleteListener listener) {
return ListenerRegistration.removeFromList(listener, sessionCompleteListeners);
}
protected void queueSessionCompleteListeners(PoolState state, PoolMessage message) {
//checkState(lock.isHeldByCurrentThread());
for (final ListenerRegistration registration : sessionCompleteListeners) {
registration.executor.execute(() -> {
MasternodeAddress address = mixingMasternode != null ? mixingMasternode.getService() : null;
registration.listener.onSessionComplete(mixingWallet, getSessionID(), getSessionDenom(), state, message, address, joined);
});
}
}
public void addTransationListener(CoinJoinTransactionListener listener) {
addTransationListener (Threading.USER_THREAD, listener);
}
/**
* Adds an event listener object. Methods on this object are called when something interesting happens,
* like receiving money. The listener is executed by the given executor.
*/
public void addTransationListener(Executor executor, CoinJoinTransactionListener listener) {
// This is thread safe, so we don't need to take the lock.
transactionListeners.add(new ListenerRegistration<>(listener, executor));
}
/**
* Removes the given event listener object. Returns true if the listener was removed, false if that listener
* was never added.
*/
public boolean removeTransactionListener(CoinJoinTransactionListener listener) {
return ListenerRegistration.removeFromList(listener, transactionListeners);
}
protected void queueTransactionListeners(Transaction tx, CoinJoinTransactionType type) {
//checkState(lock.isHeldByCurrentThread());
for (final ListenerRegistration registration : transactionListeners) {
registration.executor.execute(() -> registration.listener.onTransactionProcessed(tx, type, getSessionID()));
}
}
/**
*
* @param tx Transaction used for some purpose
* @param sessionId the session ID when the tx was used
* @param type CoinJoinTransactionType of tx
*/
protected void queueTransactionListeners(Transaction tx, int sessionId, CoinJoinTransactionType type) {
//checkState(lock.isHeldByCurrentThread());
for (final ListenerRegistration registration : transactionListeners) {
registration.executor.execute(() -> registration.listener.onTransactionProcessed(tx, type, sessionId));
}
}
}