diff --git a/core/src/main/java/com/google/bitcoin/core/Transaction.java b/core/src/main/java/com/google/bitcoin/core/Transaction.java index fe863cc9..51846008 100644 --- a/core/src/main/java/com/google/bitcoin/core/Transaction.java +++ b/core/src/main/java/com/google/bitcoin/core/Transaction.java @@ -21,6 +21,7 @@ import com.google.bitcoin.crypto.TransactionSignature; import com.google.bitcoin.script.Script; import com.google.bitcoin.script.ScriptBuilder; import com.google.bitcoin.script.ScriptOpCodes; +import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.primitives.Ints; import com.google.common.primitives.Longs; @@ -1129,6 +1130,12 @@ public class Transaction extends ChildMessage implements Serializable { return Collections.unmodifiableList(outputs); } + /** Randomly re-orders the transaction outputs: good for privacy */ + public void shuffleOutputs() { + maybeParse(); + Collections.shuffle(outputs); + } + /** @return the given transaction: same as getInputs().get(index). */ public TransactionInput getInput(int index) { maybeParse(); diff --git a/core/src/main/java/com/google/bitcoin/core/Wallet.java b/core/src/main/java/com/google/bitcoin/core/Wallet.java index ab84d303..2de64bbe 100644 --- a/core/src/main/java/com/google/bitcoin/core/Wallet.java +++ b/core/src/main/java/com/google/bitcoin/core/Wallet.java @@ -21,6 +21,7 @@ import com.google.bitcoin.core.TransactionConfidence.ConfidenceType; import com.google.bitcoin.crypto.KeyCrypter; import com.google.bitcoin.crypto.KeyCrypterException; import com.google.bitcoin.crypto.KeyCrypterScrypt; +import com.google.bitcoin.params.UnitTestParams; import com.google.bitcoin.script.Script; import com.google.bitcoin.script.ScriptBuilder; import com.google.bitcoin.script.ScriptChunk; @@ -1655,6 +1656,13 @@ public class Wallet implements Serializable, BlockChainListener, PeerFilterProvi */ public CoinSelector coinSelector = null; + /** + * If true (the default), the outputs will be shuffled during completion to randomize the location of the change + * output, if any. This is normally what you want for privacy reasons but in unit tests it can be annoying + * so it can be disabled here. + */ + public boolean shuffleOutputs = true; + // Tracks if this has been passed to wallet.completeTx already: just a safety check. private boolean completed; @@ -1738,6 +1746,8 @@ public class Wallet implements Serializable, BlockChainListener, PeerFilterProvi */ public Transaction createSend(Address address, BigInteger nanocoins) throws InsufficientMoneyException { SendRequest req = SendRequest.to(address, nanocoins); + if (params == UnitTestParams.get()) + req.shuffleOutputs = false; completeTx(req); return req.tx; } @@ -1952,6 +1962,10 @@ public class Wallet implements Serializable, BlockChainListener, PeerFilterProvi log.info(" with a fee of {}", bitcoinValueToFriendlyString(calculatedFee)); } + // Now shuffle the outputs to obfuscate which is the change. + if (req.shuffleOutputs) + req.tx.shuffleOutputs(); + // Now sign the inputs, thus proving that we are entitled to redeem the connected outputs. req.tx.signInputs(Transaction.SigHash.ALL, this, req.aesKey); diff --git a/core/src/main/java/com/google/bitcoin/protocols/channels/PaymentChannelClientState.java b/core/src/main/java/com/google/bitcoin/protocols/channels/PaymentChannelClientState.java index 9f6e56e7..51495c8f 100644 --- a/core/src/main/java/com/google/bitcoin/protocols/channels/PaymentChannelClientState.java +++ b/core/src/main/java/com/google/bitcoin/protocols/channels/PaymentChannelClientState.java @@ -253,6 +253,7 @@ public class PaymentChannelClientState { Wallet.SendRequest req = Wallet.SendRequest.forTx(template); req.coinSelector = AllowUnconfirmedCoinSelector.get(); editContractSendRequest(req); + req.shuffleOutputs = false; // TODO: Fix things so shuffling is usable. wallet.completeTx(req); BigInteger multisigFee = req.fee; multisigContract = req.tx; diff --git a/core/src/main/java/com/google/bitcoin/protocols/channels/PaymentChannelServerState.java b/core/src/main/java/com/google/bitcoin/protocols/channels/PaymentChannelServerState.java index 5d72a106..23856906 100644 --- a/core/src/main/java/com/google/bitcoin/protocols/channels/PaymentChannelServerState.java +++ b/core/src/main/java/com/google/bitcoin/protocols/channels/PaymentChannelServerState.java @@ -394,7 +394,8 @@ public class PaymentChannelServerState { // die. We could probably add features to the SendRequest API to make this a bit more efficient. signMultisigInput(tx, Transaction.SigHash.NONE, true); // Let wallet handle adding additional inputs/fee as necessary. - wallet.completeTx(req); + req.shuffleOutputs = false; + wallet.completeTx(req); // TODO: Fix things so shuffling is usable. feePaidForPayment = req.fee; log.info("Calculated fee is {}", feePaidForPayment); if (feePaidForPayment.compareTo(bestValueToMe) >= 0) { diff --git a/core/src/test/java/com/google/bitcoin/core/WalletTest.java b/core/src/test/java/com/google/bitcoin/core/WalletTest.java index 9fd14edb..27c11f3d 100644 --- a/core/src/test/java/com/google/bitcoin/core/WalletTest.java +++ b/core/src/test/java/com/google/bitcoin/core/WalletTest.java @@ -283,6 +283,7 @@ public class WalletTest extends TestWithWallet { } // Complete the transaction successfully. + req.shuffleOutputs = false; wallet.completeTx(req); Transaction t2 = req.tx; @@ -371,6 +372,7 @@ public class WalletTest extends TestWithWallet { req.aesKey = aesKey; Address a = req.changeAddress = new ECKey().toAddress(params); req.ensureMinRequiredFee = false; + req.shuffleOutputs = false; wallet.completeTx(req); Transaction t3 = req.tx; assertEquals(a, t3.getOutput(1).getScriptPubKey().getToAddress(params)); @@ -1760,6 +1762,7 @@ public class WalletTest extends TestWithWallet { request19.tx.clearInputs(); request19 = SendRequest.forTx(request19.tx); request19.feePerKb = BigInteger.ONE; + request19.shuffleOutputs = false; wallet.completeTx(request19); assertEquals(Transaction.REFERENCE_DEFAULT_MIN_TX_FEE, request19.fee); assertEquals(2, request19.tx.getInputs().size()); @@ -1767,7 +1770,6 @@ public class WalletTest extends TestWithWallet { for (TransactionOutput out : request19.tx.getOutputs()) outValue19 = outValue19.add(out.getValue()); // But now our change output is CENT-minfee, so we have to pay min fee - // Change this assert when we eventually randomize output order assertEquals(request19.tx.getOutput(request19.tx.getOutputs().size() - 1).getValue(), CENT.subtract(Transaction.REFERENCE_DEFAULT_MIN_TX_FEE)); assertEquals(outValue19, Utils.COIN.add(CENT).subtract(Transaction.REFERENCE_DEFAULT_MIN_TX_FEE)); @@ -1827,6 +1829,7 @@ public class WalletTest extends TestWithWallet { request25 = SendRequest.forTx(request25.tx); request25.feePerKb = CENT.divide(BigInteger.valueOf(3)); request25.ensureMinRequiredFee = false; + request25.shuffleOutputs = false; wallet.completeTx(request25); assertEquals(CENT.subtract(BigInteger.ONE), request25.fee); assertEquals(2, request25.tx.getInputs().size()); @@ -1834,7 +1837,6 @@ public class WalletTest extends TestWithWallet { for (TransactionOutput out : request25.tx.getOutputs()) outValue25 = outValue25.add(out.getValue()); // Our change output should be one satoshi - // Change this assert when we eventually randomize output order assertEquals(BigInteger.ONE, request25.tx.getOutput(request25.tx.getOutputs().size() - 1).getValue()); // and our fee should be CENT-1 satoshi assertEquals(outValue25, Utils.COIN.add(BigInteger.ONE)); @@ -2044,6 +2046,7 @@ public class WalletTest extends TestWithWallet { SendRequest request1 = SendRequest.to(notMyAddr, CENT); // If we just complete as-is, we will use one of the COIN outputs to get higher priority, // resulting in a change output + request1.shuffleOutputs = false; wallet.completeTx(request1); assertEquals(1, request1.tx.getInputs().size()); assertEquals(2, request1.tx.getOutputs().size()); @@ -2066,6 +2069,7 @@ public class WalletTest extends TestWithWallet { request3.tx.addInput(new TransactionInput(params, request3.tx, new byte[]{}, new TransactionOutPoint(params, 0, tx3.getHash()))); // Now completeTx will result in two inputs, two outputs and a fee of a CENT // Note that it is simply assumed that the inputs are correctly signed, though in fact the first is not + request3.shuffleOutputs = false; wallet.completeTx(request3); assertEquals(2, request3.tx.getInputs().size()); assertEquals(2, request3.tx.getOutputs().size());