diff --git a/core/src/main/java/org/bitcoinj/coinjoin/CoinJoin.java b/core/src/main/java/org/bitcoinj/coinjoin/CoinJoin.java index 9afd2512c..aafe8883c 100644 --- a/core/src/main/java/org/bitcoinj/coinjoin/CoinJoin.java +++ b/core/src/main/java/org/bitcoinj/coinjoin/CoinJoin.java @@ -378,4 +378,19 @@ public static String getStatusById(PoolStatus status) { public static boolean hasDSTX(Sha256Hash hash) { return mapDSTX.containsKey(hash); } + + public static String getRoundsString(int rounds) { + switch (rounds) { + case -4: + return "bad index"; + case -3: + return "collateral"; + case -2: + return "non-denominated"; + case -1: + return "no such tx"; + default: + return "coinjoin"; + } + } } diff --git a/core/src/main/java/org/bitcoinj/coinjoin/CoinJoinClientOptions.java b/core/src/main/java/org/bitcoinj/coinjoin/CoinJoinClientOptions.java index 37a79167b..d48f268bd 100644 --- a/core/src/main/java/org/bitcoinj/coinjoin/CoinJoinClientOptions.java +++ b/core/src/main/java/org/bitcoinj/coinjoin/CoinJoinClientOptions.java @@ -65,6 +65,8 @@ public static void setAmount(Coin amount) { public static boolean removeDenomination(Coin amount) { return CoinJoinClientOptions.get().allowedDenominations.get().remove(amount); } + public static boolean removeDenomination(Denomination denomination) { return CoinJoinClientOptions.get().allowedDenominations.get().remove(denomination.value); } + public static void resetDenominations() { CoinJoinClientOptions.get().allowedDenominations.set(CoinJoin.getStandardDenominations()); } private static CoinJoinClientOptions instance; private static boolean onceFlag; diff --git a/core/src/main/java/org/bitcoinj/coinjoin/Denomination.java b/core/src/main/java/org/bitcoinj/coinjoin/Denomination.java new file mode 100644 index 000000000..4d16257c7 --- /dev/null +++ b/core/src/main/java/org/bitcoinj/coinjoin/Denomination.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2023 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 org.bitcoinj.core.Coin; + +public enum Denomination { + TEN(Coin.COIN.multiply(10).add(Coin.valueOf(10000))), + ONE(Coin.COIN.add(Coin.valueOf(1000))), + TENTH(Coin.COIN.div(10).add(Coin.valueOf(100))), + HUNDREDTH(Coin.COIN.div(100).add(Coin.valueOf(10))), + THOUSANDTH(Coin.COIN.div(1000).add(Coin.valueOf(1))), + SMALLEST(THOUSANDTH.value); + + final Coin value; + Denomination(Coin value) { + this.value = value; + } +} diff --git a/core/src/main/java/org/bitcoinj/coinjoin/UnmixedZeroConfCoinSelector.java b/core/src/main/java/org/bitcoinj/coinjoin/UnmixedZeroConfCoinSelector.java index 0f06c3813..9a2e3bacd 100644 --- a/core/src/main/java/org/bitcoinj/coinjoin/UnmixedZeroConfCoinSelector.java +++ b/core/src/main/java/org/bitcoinj/coinjoin/UnmixedZeroConfCoinSelector.java @@ -26,17 +26,10 @@ import java.util.List; public class UnmixedZeroConfCoinSelector extends ZeroConfCoinSelector { - //private final Wallet wallet; - private boolean onlyConfirmed = false; - - private static final UnmixedZeroConfCoinSelector instance = new UnmixedZeroConfCoinSelector(); - - public static UnmixedZeroConfCoinSelector get() { - return instance; - } - - protected UnmixedZeroConfCoinSelector() { + private final Wallet wallet; + public UnmixedZeroConfCoinSelector(Wallet wallet) { super(); + this.wallet = wallet; } @Override @@ -58,10 +51,8 @@ public CoinSelection select(Coin target, List candidates) { protected boolean shouldSelect(Transaction tx) { if (tx != null) { for (TransactionOutput output : tx.getOutputs()) { - if (!output.isDenominated()) { + if (!output.isFullyMixed(wallet)) { TransactionConfidence confidence = tx.getConfidence(); - if (onlyConfirmed) - return confidence.getConfidenceType() == TransactionConfidence.ConfidenceType.BUILDING; return super.shouldSelect(tx); } } diff --git a/core/src/main/java/org/bitcoinj/coinjoin/utils/InputCoin.java b/core/src/main/java/org/bitcoinj/coinjoin/utils/InputCoin.java index c9c2a86af..a00d23e6d 100644 --- a/core/src/main/java/org/bitcoinj/coinjoin/utils/InputCoin.java +++ b/core/src/main/java/org/bitcoinj/coinjoin/utils/InputCoin.java @@ -22,15 +22,13 @@ import org.bitcoinj.core.TransactionOutPoint; import org.bitcoinj.core.TransactionOutput; -import javax.annotation.Nullable; - -public class InputCoin implements Comparable { +public class InputCoin implements Comparable { private final TransactionOutPoint outPoint; private final TransactionOutput output; private final Coin effectiveValue; private int inputBytes; - public InputCoin(@Nullable Transaction tx, int i) { + public InputCoin(Transaction tx, int i) { Preconditions.checkNotNull(tx, "transaction should not be null"); Preconditions.checkArgument(i < tx.getOutputs().size(), "The output index is out of range"); outPoint = new TransactionOutPoint(tx.getParams(), i, tx.getTxId()); @@ -38,15 +36,14 @@ public InputCoin(@Nullable Transaction tx, int i) { effectiveValue = output.getValue(); } - public InputCoin(@Nullable Transaction tx, int i, int inputBytes) { + public InputCoin(Transaction tx, int i, int inputBytes) { this(tx, i); this.inputBytes = inputBytes; } @Override - public int compareTo(Object o) { - InputCoin inputCoin = (InputCoin) o; + public int compareTo(InputCoin inputCoin) { return outPoint.getHash().compareTo(inputCoin.outPoint.getHash()); } diff --git a/core/src/main/java/org/bitcoinj/coinjoin/utils/TransactionBuilder.java b/core/src/main/java/org/bitcoinj/coinjoin/utils/TransactionBuilder.java index 3a0c8d358..07fe29d71 100644 --- a/core/src/main/java/org/bitcoinj/coinjoin/utils/TransactionBuilder.java +++ b/core/src/main/java/org/bitcoinj/coinjoin/utils/TransactionBuilder.java @@ -175,10 +175,12 @@ public boolean commit(StringBuilder strResult) { try { SendRequest request = SendRequest.forTx(req.tx); request.aesKey = wallet.getContext().coinJoinManager.requestKeyParameter(wallet); + request.coinControl = coinControl; wallet.sendCoins(request); transaction = request.tx; } catch (InsufficientMoneyException x) { - throw new RuntimeException(x); + strResult.append(x); + return false; } keepKeys = true; @@ -228,7 +230,6 @@ void clear() { @GuardedBy("lock") int getBytesTotal() { return bytesBase + vecOutputs.size() * bytesOutput + getSizeOfCompactSizeDiff(vecOutputs.size()); - } /// Helper to calculate static amount left by simply subtracting an used amount and a fee from a provided initial amount. static Coin getAmountLeft(Coin amountInitial, Coin amountUsed, Coin fee) { diff --git a/core/src/main/java/org/bitcoinj/core/TransactionBag.java b/core/src/main/java/org/bitcoinj/core/TransactionBag.java index f252c6091..c6c35a812 100644 --- a/core/src/main/java/org/bitcoinj/core/TransactionBag.java +++ b/core/src/main/java/org/bitcoinj/core/TransactionBag.java @@ -57,4 +57,7 @@ public interface TransactionBag { /** Returns transactions from a specific pool. */ Map getTransactionPool(WalletTransaction.Pool pool); + + /** Returns true if this output is fully mixed **/ + boolean isFullyMixed(TransactionOutput output); } diff --git a/core/src/main/java/org/bitcoinj/core/TransactionOutput.java b/core/src/main/java/org/bitcoinj/core/TransactionOutput.java index e8994233b..39ddf87f4 100644 --- a/core/src/main/java/org/bitcoinj/core/TransactionOutput.java +++ b/core/src/main/java/org/bitcoinj/core/TransactionOutput.java @@ -320,12 +320,14 @@ else if (ScriptPattern.isP2PKH(script)) } /** - * Returns true if this output is to a coinjoin related key + * Returns true if this output is to a coinjoin related key and is fully mixed */ public boolean isCoinJoin(TransactionBag transactionBag) { try { if(!isDenominated()) return false; + if(!transactionBag.isFullyMixed(this)) + return false; Script script = getScriptPubKey(); if (ScriptPattern.isP2PK(script)) @@ -445,4 +447,8 @@ public int hashCode() { public boolean equalsWithoutParent(TransactionOutput output) { return value == output.value && Arrays.equals(scriptBytes, output.scriptBytes); } + + public boolean isFullyMixed(TransactionBag transactionBag) { + return transactionBag.isFullyMixed(this); + } } diff --git a/core/src/main/java/org/bitcoinj/wallet/CoinControl.java b/core/src/main/java/org/bitcoinj/wallet/CoinControl.java index 741247123..d223d6e6e 100644 --- a/core/src/main/java/org/bitcoinj/wallet/CoinControl.java +++ b/core/src/main/java/org/bitcoinj/wallet/CoinControl.java @@ -24,6 +24,10 @@ import java.util.ArrayList; import java.util.HashSet; +/** + * CoinControl comes from Dash Core. Not all functions fields and functions are supported within the Wallet class + */ + public class CoinControl { private TransactionDestination destChange; //! If false, allows unselected inputs, but requires all selected inputs be used if fAllowOtherInputs is true (default) @@ -78,7 +82,7 @@ public void setNull(boolean fResetCoinType) { } public boolean hasSelected() { - return (setSelected.size() > 0); + return (!setSelected.isEmpty()); } public boolean isSelected(TransactionOutPoint output) { @@ -90,7 +94,7 @@ public void select(TransactionOutPoint output) { } public void unSelect(TransactionOutPoint output) { - setSelected.add(output); + setSelected.remove(output); } public void unSelectAll() { diff --git a/core/src/main/java/org/bitcoinj/wallet/CoinJoinExtension.java b/core/src/main/java/org/bitcoinj/wallet/CoinJoinExtension.java index 1e676c7fb..4e1bd31d4 100644 --- a/core/src/main/java/org/bitcoinj/wallet/CoinJoinExtension.java +++ b/core/src/main/java/org/bitcoinj/wallet/CoinJoinExtension.java @@ -22,8 +22,10 @@ import com.google.protobuf.CodedOutputStream; import org.bitcoinj.coinjoin.CoinJoin; import org.bitcoinj.coinjoin.CoinJoinClientOptions; +import org.bitcoinj.core.Address; import org.bitcoinj.core.Coin; import org.bitcoinj.core.Transaction; +import org.bitcoinj.core.TransactionOutPoint; import org.bitcoinj.core.TransactionOutput; import org.bitcoinj.crypto.ChildNumber; import org.bitcoinj.crypto.factory.ECKeyFactory; @@ -178,6 +180,7 @@ public void setRounds(int rounds) { } public TreeMap> getOutputs() { + checkNotNull(wallet); TreeMap> outputs = Maps.newTreeMap(); for (Coin amount : CoinJoin.getStandardDenominations()) { outputs.put(CoinJoin.amountToDenomination(amount), Lists.newArrayList()); @@ -205,8 +208,27 @@ public String toString(boolean includeLookahead, boolean includePrivateKeys, @Nu int denom = entry.getKey(); List outputs = entry.getValue(); Coin value = outputs.stream().map(TransactionOutput::getValue).reduce(Coin::add).orElse(Coin.ZERO); - builder.append(CoinJoin.denominationToString(denom)).append(" ").append(outputs.size()).append(" ") + builder.append(CoinJoin.denominationToString(denom)).append(" outputs:").append(outputs.size()).append(" total:") .append(value.toFriendlyString()).append("\n"); + outputs.forEach(output -> { + TransactionOutPoint outPoint = new TransactionOutPoint(output.getParams(), output.getIndex(), output.getParentTransactionHash()); + builder.append(" addr:") + .append(Address.fromPubKeyHash(output.getParams(), ScriptPattern.extractHashFromP2PKH(output.getScriptPubKey()))) + .append(" outpoint:") + .append(outPoint.toStringShort()) + .append(" "); + int rounds = ((WalletEx) wallet).getRealOutpointCoinJoinRounds(outPoint); + builder.append(CoinJoin.getRoundsString(rounds)); + if (rounds >= 0) { + builder.append(" ").append(rounds).append(" rounds"); + if (((WalletEx) wallet).isFullyMixed(outPoint)) { + builder.append(" (fully mixed)"); + } + } else { + builder.append(" ").append(output.getValue().toFriendlyString()); + } + builder.append("\n"); + }); } return builder.toString(); } diff --git a/core/src/main/java/org/bitcoinj/wallet/SendRequest.java b/core/src/main/java/org/bitcoinj/wallet/SendRequest.java index 90aef6cca..723c991a9 100644 --- a/core/src/main/java/org/bitcoinj/wallet/SendRequest.java +++ b/core/src/main/java/org/bitcoinj/wallet/SendRequest.java @@ -170,6 +170,8 @@ public class SendRequest { // Tracks if this has been passed to wallet.completeTx already: just a safety check. boolean completed; + public CoinControl coinControl = null; + protected SendRequest() {} /** @@ -313,4 +315,8 @@ public String toString() { //Dash Specific public boolean useInstantSend; + + public boolean hasCoinControl() { + return coinControl != null; + } } \ No newline at end of file diff --git a/core/src/main/java/org/bitcoinj/wallet/Wallet.java b/core/src/main/java/org/bitcoinj/wallet/Wallet.java index 046d5bc81..38b5e44d9 100644 --- a/core/src/main/java/org/bitcoinj/wallet/Wallet.java +++ b/core/src/main/java/org/bitcoinj/wallet/Wallet.java @@ -4444,6 +4444,9 @@ public void completeTx(SendRequest req) throws InsufficientMoneyException { // can customize coin selection policies. The call below will ignore immature coinbases and outputs // we don't have the keys for. List candidates = calculateAllSpendCandidates(true, req.missingSigsMode == MissingSigsMode.THROW); + if (req.hasCoinControl()) { + candidates.removeIf(output -> !req.coinControl.isSelected(output.getOutPointFor())); + } CoinSelection bestCoinSelection; TransactionOutput bestChangeOutput = null; @@ -6242,4 +6245,9 @@ public List
getIssuedSendAddresses(EvolutionContact contact) { addresses.add(Address.fromKey(getParams(), key)); return addresses; } + + @Override + public boolean isFullyMixed(TransactionOutput output) { + return false; + } } diff --git a/core/src/main/java/org/bitcoinj/wallet/WalletEx.java b/core/src/main/java/org/bitcoinj/wallet/WalletEx.java index fb5eaa53a..e39679128 100644 --- a/core/src/main/java/org/bitcoinj/wallet/WalletEx.java +++ b/core/src/main/java/org/bitcoinj/wallet/WalletEx.java @@ -212,8 +212,7 @@ public Coin getBalance(BalanceType balanceType) { List all = calculateAllSpendCandidates(true, balanceType == BalanceType.COINJOIN_SPENDABLE); Coin value = Coin.ZERO; for (TransactionOutput out : all) { - // exclude non coinjoin outputs if isCoinJoinOnly is true - // exclude coinjoin outputs when isCoinJoinOnly is false + // coinjoin outputs must be denominated, using coinjoin keys and fully mixed boolean isCoinJoin = out.isDenominated() && out.isCoinJoin(this) && isFullyMixed(out); if (isCoinJoin) @@ -312,7 +311,7 @@ public boolean isLockedCoin(TransactionOutPoint outPoint) { HashMap mapOutpointRoundsCache = new HashMap<>(); // Recursively determine the rounds of a given input (How deep is the CoinJoin chain for a given input) - int getRealOutpointCoinJoinRounds(TransactionOutPoint outPoint) { + public int getRealOutpointCoinJoinRounds(TransactionOutPoint outPoint) { return getRealOutpointCoinJoinRounds(outPoint, 0); } @@ -439,6 +438,7 @@ int getRealOutpointCoinJoinRounds(TransactionOutPoint outpoint, int rounds) { Sha256Hash coinJoinSalt = Sha256Hash.ZERO_HASH; + @Override public boolean isFullyMixed(TransactionOutput output) { return isFullyMixed(new TransactionOutPoint(params, output)); } diff --git a/core/src/test/java/org/bitcoinj/coinjoin/CoinJoinCoinSelectorTest.java b/core/src/test/java/org/bitcoinj/coinjoin/CoinJoinCoinSelectorTest.java index 06e3279eb..f1b99edcb 100644 --- a/core/src/test/java/org/bitcoinj/coinjoin/CoinJoinCoinSelectorTest.java +++ b/core/src/test/java/org/bitcoinj/coinjoin/CoinJoinCoinSelectorTest.java @@ -16,60 +16,20 @@ package org.bitcoinj.coinjoin; -import org.bitcoinj.core.Transaction; -import org.bitcoinj.core.TransactionConfidence; -import org.bitcoinj.crypto.DeterministicKey; -import org.bitcoinj.script.Script; -import org.bitcoinj.testing.TestWithWallet; -import org.bitcoinj.wallet.DerivationPathFactory; -import org.bitcoinj.wallet.WalletEx; -import org.junit.After; -import org.junit.Before; import org.junit.Test; -import static org.bitcoinj.core.Coin.COIN; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; -public class CoinJoinCoinSelectorTest extends TestWithWallet { +public class CoinJoinCoinSelectorTest extends TestWithCoinJoinWallet { - WalletEx walletEx; - @Before - @Override - public void setUp() throws Exception { - super.setUp(); - walletEx = WalletEx.fromSeed(wallet.getParams(), wallet.getKeyChainSeed(), Script.ScriptType.P2PKH); - } - - @After - @Override - public void tearDown() throws Exception { - super.tearDown(); - } @Test public void selectable() { - walletEx.initializeCoinJoin(); - DeterministicKey key = (DeterministicKey) walletEx.getCoinJoin().freshReceiveKey(); - CoinJoinCoinSelector coinSelector = new CoinJoinCoinSelector(walletEx); - Transaction txCoinJoin; - Transaction txDemonination = new Transaction(UNITTEST); - txDemonination.addOutput(CoinJoin.getSmallestDenomination(), (DeterministicKey) walletEx.getCoinJoin().freshReceiveKey()); - - txCoinJoin = new Transaction(UNITTEST); - txCoinJoin.addInput(txDemonination.getOutput(0)); - txCoinJoin.addOutput(CoinJoin.getSmallestDenomination(), (DeterministicKey) walletEx.getCoinJoin().freshReceiveKey()); - txCoinJoin.getConfidence().setConfidenceType(TransactionConfidence.ConfidenceType.BUILDING); - - assertTrue(coinSelector.shouldSelect(txCoinJoin)); - - Transaction txNotCoinJoin = new Transaction(UNITTEST); - txNotCoinJoin.addOutput(COIN, key); - txCoinJoin.getConfidence().setConfidenceType(TransactionConfidence.ConfidenceType.BUILDING); - - assertFalse(coinSelector.shouldSelect(txNotCoinJoin)); - + assertTrue(coinSelector.shouldSelect(lastTxCoinJoin)); + // txDenomination is mixed zero rounds, so it should not be selected + assertFalse(coinSelector.shouldSelect(txDenomination)); } } diff --git a/core/src/test/java/org/bitcoinj/coinjoin/CoinJoinTest.java b/core/src/test/java/org/bitcoinj/coinjoin/CoinJoinTest.java index 25fd3ce60..782ea9e99 100644 --- a/core/src/test/java/org/bitcoinj/coinjoin/CoinJoinTest.java +++ b/core/src/test/java/org/bitcoinj/coinjoin/CoinJoinTest.java @@ -16,6 +16,8 @@ package org.bitcoinj.coinjoin; +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; import org.bitcoinj.core.Coin; import org.bitcoinj.core.Context; import org.bitcoinj.core.NetworkParameters; @@ -26,10 +28,12 @@ import org.bitcoinj.params.UnitTestParams; import org.junit.Test; +import java.util.HashMap; import java.util.List; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; @@ -72,6 +76,11 @@ class DenomTest { Coin value = CoinJoin.getStandardDenominations().get(i); assertEquals(value, CoinJoin.denominationToAmount(CoinJoin.amountToDenomination(value))); } + + List denominationList = Lists.newArrayList(Denomination.values()); + denominationList.forEach(denomination -> { + assertNotEquals(-1, CoinJoin.getStandardDenominations().indexOf(denomination.value)); + }); } @Test public void collateralTests() { @@ -126,4 +135,16 @@ public void attemptToModifyStandardDenominationsTest() { assertThrows(UnsupportedOperationException.class, () -> denominations.add(Coin.COIN)); assertThrows(UnsupportedOperationException.class, () -> denominations.remove(0)); } + + @Test + public void roundsStringTest() { + HashMap map = Maps.newHashMap(); + map.put(0, "coinjoin"); + map.put(16, "coinjoin"); + map.put(-4, "bad index"); + map.put(-3, "collateral"); + map.put(-2, "non-denominated"); + map.put(-1, "no such tx"); + map.forEach((rounds, str) -> assertEquals(str, CoinJoin.getRoundsString(rounds))); + } } diff --git a/core/src/test/java/org/bitcoinj/coinjoin/DenominatedCoinSelectorTest.java b/core/src/test/java/org/bitcoinj/coinjoin/DenominatedCoinSelectorTest.java index 8364fa853..f74cfc92b 100644 --- a/core/src/test/java/org/bitcoinj/coinjoin/DenominatedCoinSelectorTest.java +++ b/core/src/test/java/org/bitcoinj/coinjoin/DenominatedCoinSelectorTest.java @@ -16,48 +16,20 @@ package org.bitcoinj.coinjoin; -import org.bitcoinj.core.ECKey; -import org.bitcoinj.core.Transaction; -import org.bitcoinj.core.TransactionConfidence; -import org.bitcoinj.testing.TestWithWallet; -import org.junit.After; -import org.junit.Before; import org.junit.Test; -import static org.bitcoinj.core.Coin.COIN; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; -public class DenominatedCoinSelectorTest extends TestWithWallet { - - @Before - @Override - public void setUp() throws Exception { - super.setUp(); - } - - @After - @Override - public void tearDown() throws Exception { - super.tearDown(); - } +public class DenominatedCoinSelectorTest extends TestWithCoinJoinWallet { @Test public void selectable() { DenominatedCoinSelector coinSelector = DenominatedCoinSelector.get(); - ECKey key = new ECKey(); - - Transaction txDenominated; - txDenominated = new Transaction(UNITTEST); - txDenominated.addOutput(CoinJoin.getSmallestDenomination(), key); - txDenominated.getConfidence().setConfidenceType(TransactionConfidence.ConfidenceType.BUILDING); - - assertTrue(coinSelector.shouldSelect(txDenominated)); - - Transaction txNotDenominated = new Transaction(UNITTEST); - txNotDenominated.addOutput(COIN, key); - - assertFalse(coinSelector.shouldSelect(txNotDenominated)); + assertTrue(coinSelector.shouldSelect(txDenomination)); + assertTrue(coinSelector.shouldSelect(lastTxCoinJoin)); + assertFalse(coinSelector.shouldSelect(txDeposit)); + assertFalse(coinSelector.shouldSelect(txReceiveZeroConf)); } } diff --git a/core/src/test/java/org/bitcoinj/coinjoin/TestWithCoinJoinWallet.java b/core/src/test/java/org/bitcoinj/coinjoin/TestWithCoinJoinWallet.java new file mode 100644 index 000000000..91c83d176 --- /dev/null +++ b/core/src/test/java/org/bitcoinj/coinjoin/TestWithCoinJoinWallet.java @@ -0,0 +1,108 @@ +/* + * Copyright 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 org.bitcoinj.core.Coin; +import org.bitcoinj.core.PeerAddress; +import org.bitcoinj.core.Sha256Hash; +import org.bitcoinj.core.Transaction; +import org.bitcoinj.core.TransactionConfidence; +import org.bitcoinj.crypto.DeterministicKey; +import org.bitcoinj.script.Script; +import org.bitcoinj.testing.TestWithWallet; +import org.bitcoinj.wallet.KeyChain; +import org.bitcoinj.wallet.WalletEx; +import org.bitcoinj.wallet.WalletTransaction; +import org.junit.After; +import org.junit.Before; + +import java.net.InetSocketAddress; + +import static org.bitcoinj.core.Coin.COIN; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +public class TestWithCoinJoinWallet extends TestWithWallet { + + WalletEx walletEx; + + Transaction txDeposit; + Transaction txDenomination; + Transaction lastTxCoinJoin; + Transaction txReceiveZeroConf; + @Before + @Override + public void setUp() throws Exception { + super.setUp(); + walletEx = WalletEx.fromSeed(wallet.getParams(), wallet.getKeyChainSeed(), Script.ScriptType.P2PKH); + CoinJoinClientOptions.setRounds(1); + setupWallet(); + } + + @After + @Override + public void tearDown() throws Exception { + CoinJoinClientOptions.setRounds(CoinJoinConstants.DEFAULT_COINJOIN_ROUNDS); + super.tearDown(); + } + + public void setupWallet() { + walletEx.initializeCoinJoin(); + DeterministicKey key = (DeterministicKey) walletEx.getCoinJoin().freshReceiveKey(); + + txDeposit = new Transaction(UNITTEST); + txDeposit.addInput(Sha256Hash.ZERO_HASH, 0, new Script(new byte[0])); + txDeposit.addOutput(COIN, wallet.freshAddress(KeyChain.KeyPurpose.RECEIVE_FUNDS)); + txDeposit.getConfidence().setConfidenceType(TransactionConfidence.ConfidenceType.BUILDING); + walletEx.addWalletTransaction(new WalletTransaction(WalletTransaction.Pool.SPENT, txDeposit)); + + Transaction txCoinJoin; + txDenomination = new Transaction(UNITTEST); + txDenomination.addInput(txDeposit.getOutput(0)); + txDenomination.addOutput(CoinJoin.getSmallestDenomination(), (DeterministicKey) walletEx.getCoinJoin().freshReceiveKey()); + txDenomination.getConfidence().setConfidenceType(TransactionConfidence.ConfidenceType.BUILDING); + walletEx.addWalletTransaction(new WalletTransaction(WalletTransaction.Pool.SPENT, txDenomination)); + + txCoinJoin = new Transaction(UNITTEST); + txCoinJoin.addInput(txDenomination.getOutput(0)); + txCoinJoin.addOutput(CoinJoin.getSmallestDenomination(), (DeterministicKey) walletEx.getCoinJoin().freshReceiveKey()); + txCoinJoin.getConfidence().setConfidenceType(TransactionConfidence.ConfidenceType.BUILDING); + walletEx.addWalletTransaction(new WalletTransaction(WalletTransaction.Pool.UNSPENT, txCoinJoin)); + + // mix for the max number of required rounds to make sure that the coinselecter chooses the transaction + int requiredRounds = CoinJoinClientOptions.getRounds() + CoinJoinClientOptions.getRandomRounds(); + lastTxCoinJoin = txCoinJoin; + for (int i = 1; i < requiredRounds; ++i) { + Transaction txNextCoinJoin = new Transaction(UNITTEST); + txNextCoinJoin.addInput(lastTxCoinJoin.getOutput(0)); + txNextCoinJoin.addOutput(CoinJoin.getSmallestDenomination(), (DeterministicKey) walletEx.getCoinJoin().freshReceiveKey()); + txNextCoinJoin.getConfidence().setConfidenceType(TransactionConfidence.ConfidenceType.BUILDING); + walletEx.addWalletTransaction(new WalletTransaction(WalletTransaction.Pool.UNSPENT, txNextCoinJoin)); + lastTxCoinJoin = txNextCoinJoin; + } + + // a transaction with no confirmations, but IS lock and seen by two peers + txReceiveZeroConf = new Transaction(UNITTEST); + txReceiveZeroConf.addInput(Sha256Hash.ZERO_HASH, 0, new Script(new byte[0])); + txReceiveZeroConf.addOutput(Coin.FIFTY_COINS, wallet.freshAddress(KeyChain.KeyPurpose.RECEIVE_FUNDS)); + txReceiveZeroConf.getConfidence().setConfidenceType(TransactionConfidence.ConfidenceType.PENDING); + txReceiveZeroConf.getConfidence().setIXType(TransactionConfidence.IXType.IX_LOCKED); + txReceiveZeroConf.getConfidence().markBroadcastBy(new PeerAddress(UNITTEST, new InetSocketAddress("127.0.0.1", 19999))); + txReceiveZeroConf.getConfidence().markBroadcastBy(new PeerAddress(UNITTEST, new InetSocketAddress("127.0.0.1", 19998))); + walletEx.addWalletTransaction(new WalletTransaction(WalletTransaction.Pool.SPENT, txReceiveZeroConf)); + } +} diff --git a/core/src/test/java/org/bitcoinj/coinjoin/UnmixedZeroConfCoinSelectorTest.java b/core/src/test/java/org/bitcoinj/coinjoin/UnmixedZeroConfCoinSelectorTest.java new file mode 100644 index 000000000..12d0b83d5 --- /dev/null +++ b/core/src/test/java/org/bitcoinj/coinjoin/UnmixedZeroConfCoinSelectorTest.java @@ -0,0 +1,38 @@ +/* + * Copyright 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 org.junit.Test; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +public class UnmixedZeroConfCoinSelectorTest extends TestWithCoinJoinWallet { + + + @Test + public void selectable() { + UnmixedZeroConfCoinSelector coinSelector = new UnmixedZeroConfCoinSelector(walletEx); + + // lastTxCoinJoin is fully mixed and should not be selected + assertFalse(coinSelector.shouldSelect(lastTxCoinJoin)); + // txDenomination, txDeposit is mixed zero rounds, so it should be selected + assertTrue(coinSelector.shouldSelect(txDenomination)); + assertTrue(coinSelector.shouldSelect(txDeposit)); + assertTrue(coinSelector.shouldSelect(txReceiveZeroConf)); + } +} diff --git a/core/src/test/java/org/bitcoinj/wallet/WalletTest.java b/core/src/test/java/org/bitcoinj/wallet/WalletTest.java index f78210e61..f6757075e 100644 --- a/core/src/test/java/org/bitcoinj/wallet/WalletTest.java +++ b/core/src/test/java/org/bitcoinj/wallet/WalletTest.java @@ -3605,4 +3605,11 @@ public void roundtripViaMnemonicCode() { assertEquals(wallet.freshReceiveAddress(Script.ScriptType.P2PKH), clone.freshReceiveAddress(Script.ScriptType.P2PKH)); } + + @Test + public void fullyMixedTest() throws Exception { + receiveATransaction(wallet, wallet.freshAddress(KeyPurpose.RECEIVE_FUNDS)); + Transaction tx = wallet.getWalletTransactions().iterator().next().getTransaction(); + assertFalse(wallet.isFullyMixed(tx.getOutput(0))); + } } diff --git a/tools/src/main/java/org/bitcoinj/tools/WalletTool.java b/tools/src/main/java/org/bitcoinj/tools/WalletTool.java index b7e4780a9..e23d713f9 100644 --- a/tools/src/main/java/org/bitcoinj/tools/WalletTool.java +++ b/tools/src/main/java/org/bitcoinj/tools/WalletTool.java @@ -22,6 +22,7 @@ import org.bitcoinj.coinjoin.CoinJoinClientManager; import org.bitcoinj.coinjoin.CoinJoinClientOptions; import org.bitcoinj.coinjoin.CoinJoinSendRequest; +import org.bitcoinj.coinjoin.Denomination; import org.bitcoinj.coinjoin.UnmixedZeroConfCoinSelector; import org.bitcoinj.coinjoin.utils.CoinJoinReporter; import org.bitcoinj.coinjoin.utils.ProTxToOutpoint; @@ -1638,7 +1639,8 @@ private static void mix() { CoinJoinClientOptions.setEnabled(true); CoinJoinClientOptions.setRounds(4); CoinJoinClientOptions.setSessions(1); - CoinJoinClientOptions.removeDenomination(CoinJoin.getSmallestDenomination()); + CoinJoinClientOptions.removeDenomination(Denomination.SMALLEST); + //CoinJoinClientOptions.removeDenomination(CoinJoinClientOptions.getDenominations().stream().min(Coin::compareTo).get()); Coin amountToMix = wallet.getBalance(); // set command line arguments