diff --git a/core/src/main/java/org/bitcoinj/wallet/CoinJoinExtension.java b/core/src/main/java/org/bitcoinj/wallet/CoinJoinExtension.java index 897e84ab2..f0d97f16a 100644 --- a/core/src/main/java/org/bitcoinj/wallet/CoinJoinExtension.java +++ b/core/src/main/java/org/bitcoinj/wallet/CoinJoinExtension.java @@ -55,6 +55,7 @@ import java.util.Set; import java.util.TreeMap; import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.locks.ReentrantLock; import java.util.stream.Stream; @@ -217,12 +218,19 @@ public Coin getUnmixableTotal() { return sum; } + /** returns a tree associating denominations with their outputs + * Denomination of -1 are collaterals + * Denomination of -2 are other undenominated outputs + * + * @return + */ public TreeMap> getOutputs() { checkNotNull(wallet); TreeMap> outputs = Maps.newTreeMap(); for (Coin amount : CoinJoin.getStandardDenominations()) { outputs.put(CoinJoin.amountToDenomination(amount), Lists.newArrayList()); } + outputs.put(-2, Lists.newArrayList()); outputs.put(0, Lists.newArrayList()); for (TransactionOutput output : wallet.getUnspents()) { byte [] pkh = ScriptPattern.extractHashFromP2PKH(output.getScriptPubKey()); @@ -230,6 +238,9 @@ public TreeMap> getOutputs() { int denom = CoinJoin.amountToDenomination(output.getValue()); List listDenoms = outputs.get(denom); listDenoms.add(output); + } else { + // non-denominated and non-collateral coins + outputs.get(-2).add(output); } } return outputs; @@ -593,7 +604,7 @@ public double getMixingProgress() { getOutputs().forEach((denom, outputs) -> { outputs.forEach(output -> { // do not count mixing collateral for fees - if (denom != -1) { + if (denom >= 0) { // getOutputs has a bug where non-denominated items are marked as denominated TransactionOutPoint outPoint = new TransactionOutPoint(output.getParams(), output.getIndex(), output.getParentTransactionHash()); int rounds = ((WalletEx) wallet).getRealOutpointCoinJoinRounds(outPoint); @@ -601,10 +612,21 @@ public double getMixingProgress() { totalInputs.addAndGet(1); totalRounds.addAndGet(rounds); } + } else if (denom == -2) { + // estimate what the denominations would be: use greedy algorithm + AtomicInteger unmixedInputs = new AtomicInteger(0); + AtomicReference outputValue = new AtomicReference<>(output.getValue().subtract(CoinJoin.getCollateralAmount())); + CoinJoinClientOptions.getDenominations().forEach(coin -> { + while (outputValue.get().subtract(coin).isGreaterThan(Coin.ZERO)) { + unmixedInputs.getAndIncrement(); + outputValue.set(outputValue.get().subtract(coin)); + } + }); + totalInputs.set(totalInputs.get() + unmixedInputs.get()); } }); }); - double progress = totalRounds.get() / (requiredRounds * totalInputs.get()); + double progress = totalInputs.get() != 0 ? totalRounds.get() / (requiredRounds * totalInputs.get()) : 0.0; log.info("getMixingProgress: {} = {} / ({} * {})", progress, totalRounds.get(), requiredRounds, totalInputs.get()); return Math.max(0.0, Math.min(progress, 1.0)); } diff --git a/core/src/main/java/org/bitcoinj/wallet/DerivationPathFactory.java b/core/src/main/java/org/bitcoinj/wallet/DerivationPathFactory.java index ef1841a37..782483519 100644 --- a/core/src/main/java/org/bitcoinj/wallet/DerivationPathFactory.java +++ b/core/src/main/java/org/bitcoinj/wallet/DerivationPathFactory.java @@ -193,7 +193,7 @@ public ImmutableList coinJoinDerivationPath(int account) { .add(FEATURE_PURPOSE) .add(coinType) .add(new ChildNumber(4, true)) - .add(ChildNumber.ZERO_HARDENED) + .add(new ChildNumber(account, true)) .build(); } diff --git a/core/src/main/java/org/bitcoinj/wallet/WalletEx.java b/core/src/main/java/org/bitcoinj/wallet/WalletEx.java index 0f650a1e2..9c931a666 100644 --- a/core/src/main/java/org/bitcoinj/wallet/WalletEx.java +++ b/core/src/main/java/org/bitcoinj/wallet/WalletEx.java @@ -396,7 +396,7 @@ int getRealOutpointCoinJoinRounds(TransactionOutPoint outpoint, int rounds) { } // make sure the final output is non-denominate - if (!CoinJoin.isDenominatedAmount (txOut.getValue())){ //NOT DENOM + if (!CoinJoin.isDenominatedAmount (txOut.getValue())) { //NOT DENOM roundsRef = -2; mapOutpointRoundsCache.put(outpoint, roundsRef); @@ -404,7 +404,7 @@ int getRealOutpointCoinJoinRounds(TransactionOutPoint outpoint, int rounds) { return roundsRef; } - for (TransactionOutput out :wtx.getTransaction().getOutputs()){ + for (TransactionOutput out :wtx.getTransaction().getOutputs()) { if (!CoinJoin.isDenominatedAmount (out.getValue())){ // this one is denominated but there is another non-denominated output found in the same tx roundsRef = 0; @@ -418,7 +418,7 @@ int getRealOutpointCoinJoinRounds(TransactionOutPoint outpoint, int rounds) { int nShortest = -10; // an initial value, should be no way to get this by calculations boolean fDenomFound = false; // only denoms here so let's look up - for (TransactionInput txinNext :wtx.getTransaction().getInputs()){ + for (TransactionInput txinNext :wtx.getTransaction().getInputs()) { if (isMine(txinNext)) { int n = getRealOutpointCoinJoinRounds(txinNext.getOutpoint(), rounds + 1); // denom found, find the shortest chain or initially assign nShortest with the first found value diff --git a/core/src/test/java/org/bitcoinj/wallet/CoinJoinExtensionTest.java b/core/src/test/java/org/bitcoinj/wallet/CoinJoinExtensionTest.java new file mode 100644 index 000000000..e2c188492 --- /dev/null +++ b/core/src/test/java/org/bitcoinj/wallet/CoinJoinExtensionTest.java @@ -0,0 +1,28 @@ +package org.bitcoinj.wallet; + +import org.bitcoinj.core.Coin; +import org.bitcoinj.core.Context; +import org.bitcoinj.params.TestNet3Params; +import org.junit.Test; + +import java.io.IOException; +import java.io.InputStream; + +import static org.junit.Assert.assertEquals; + +public class CoinJoinExtensionTest { + + @Test public void emptyWalletProgressTest() { + new Context(TestNet3Params.get()); + try (InputStream is = getClass().getResourceAsStream("coinjoin-unmixed.wallet")) { + WalletEx wallet = (WalletEx) new WalletProtobufSerializer().readWallet(is); + assertEquals(Coin.valueOf(99999628), wallet.getBalance(Wallet.BalanceType.ESTIMATED)); + + assertEquals(0.00, wallet.getCoinJoin().getMixingProgress(), 0.001); + } catch (IOException e) { + throw new RuntimeException(e); + } catch (UnreadableWalletException e) { + throw new RuntimeException(e); + } + } +} diff --git a/core/src/test/resources/org/bitcoinj/wallet/coinjoin-unmixed.wallet b/core/src/test/resources/org/bitcoinj/wallet/coinjoin-unmixed.wallet new file mode 100644 index 000000000..5a31b652c Binary files /dev/null and b/core/src/test/resources/org/bitcoinj/wallet/coinjoin-unmixed.wallet differ