Skip to content

Commit

Permalink
fix: consider non-denominated coins in mixing progress calc (#249)
Browse files Browse the repository at this point in the history
* fix: consider non-denominated coins in mixing progress calc

* tests: add coinjoin-unmixed.wallet

* tests: fix context
  • Loading branch information
HashEngineering authored Apr 2, 2024
1 parent 5c96255 commit e8f0dae
Show file tree
Hide file tree
Showing 5 changed files with 56 additions and 6 deletions.
26 changes: 24 additions & 2 deletions core/src/main/java/org/bitcoinj/wallet/CoinJoinExtension.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -217,19 +218,29 @@ 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<Integer, List<TransactionOutput>> getOutputs() {
checkNotNull(wallet);
TreeMap<Integer, List<TransactionOutput>> 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());
if (getKeyChainGroup().findKeyFromPubKeyHash(pkh, Script.ScriptType.P2PKH) != null) {
int denom = CoinJoin.amountToDenomination(output.getValue());
List<TransactionOutput> listDenoms = outputs.get(denom);
listDenoms.add(output);
} else {
// non-denominated and non-collateral coins
outputs.get(-2).add(output);
}
}
return outputs;
Expand Down Expand Up @@ -593,18 +604,29 @@ 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);
if (rounds >= 0) {
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<Coin> 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));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,7 @@ public ImmutableList<ChildNumber> coinJoinDerivationPath(int account) {
.add(FEATURE_PURPOSE)
.add(coinType)
.add(new ChildNumber(4, true))
.add(ChildNumber.ZERO_HARDENED)
.add(new ChildNumber(account, true))
.build();
}

Expand Down
6 changes: 3 additions & 3 deletions core/src/main/java/org/bitcoinj/wallet/WalletEx.java
Original file line number Diff line number Diff line change
Expand Up @@ -396,15 +396,15 @@ 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);

log.info(COINJOIN_EXTRA, String.format("UPDATED %-70s %3d (non-denominated)", outpoint.toStringCpp(), roundsRef));
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;
Expand All @@ -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
Expand Down
28 changes: 28 additions & 0 deletions core/src/test/java/org/bitcoinj/wallet/CoinJoinExtensionTest.java
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
Binary file not shown.

0 comments on commit e8f0dae

Please sign in to comment.