Skip to content

Commit

Permalink
Prevent creating double spends when swiping a paper wallet twice.
Browse files Browse the repository at this point in the history
  • Loading branch information
Andreas Schildbach committed Jan 15, 2017
1 parent 096791a commit a2eaaa5
Show file tree
Hide file tree
Showing 2 changed files with 92 additions and 68 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -17,22 +17,19 @@

package de.schildbach.wallet.ui.send;

import static com.google.common.base.Preconditions.checkState;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.Set;
import java.util.concurrent.TimeUnit;

import javax.net.SocketFactory;
Expand All @@ -44,11 +41,9 @@

import org.bitcoinj.core.Address;
import org.bitcoinj.core.Coin;
import org.bitcoinj.core.NetworkParameters;
import org.bitcoinj.core.Sha256Hash;
import org.bitcoinj.core.Transaction;
import org.bitcoinj.core.TransactionConfidence.ConfidenceType;
import org.bitcoinj.core.TransactionOutput;
import org.bitcoinj.core.UTXO;
import org.bitcoinj.script.Script;
import org.bitcoinj.script.ScriptBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
Expand Down Expand Up @@ -80,7 +75,7 @@ public final class RequestWalletBalanceTask {
private static final Logger log = LoggerFactory.getLogger(RequestWalletBalanceTask.class);

public interface ResultCallback {
void onResult(Collection<Transaction> transactions);
void onResult(Set<UTXO> utxos);

void onFail(int messageResId, Object... messageArgs);
}
Expand Down Expand Up @@ -160,46 +155,19 @@ public void run() {
final JsonAdapter<JsonRpcResponse> responseAdapter = moshi.adapter(JsonRpcResponse.class);
final JsonRpcResponse response = responseAdapter.fromJson(source);
if (response.id == request.id) {
final Map<Sha256Hash, Transaction> transactions = new HashMap<>();
for (final JsonRpcResponse.Utxo utxo : response.result) {
if (utxo.height > 0) {
final Sha256Hash utxoHash = Sha256Hash.wrap(utxo.tx_hash);
final int utxoIndex = utxo.tx_pos;
final Coin utxoValue = Coin.valueOf(utxo.value);

Transaction tx = transactions.get(utxoHash);
if (tx == null) {
tx = new FakeTransaction(Constants.NETWORK_PARAMETERS, utxoHash);
tx.getConfidence().setConfidenceType(ConfidenceType.BUILDING);
transactions.put(utxoHash, tx);
}

final TransactionOutput output = new TransactionOutput(Constants.NETWORK_PARAMETERS, tx,
utxoValue, ScriptBuilder.createOutputScript(address).getProgram());
if (tx.getOutputs().size() > utxoIndex) {
// Work around not being able to replace outputs on transactions
final List<TransactionOutput> outputs = new ArrayList<TransactionOutput>(
tx.getOutputs());
final TransactionOutput dummy = outputs.set(utxoIndex, output);
checkState(dummy.getValue().equals(Coin.NEGATIVE_SATOSHI),
"Index %s must be dummy output", utxoIndex);
// Remove and re-add all outputs
tx.clearOutputs();
for (final TransactionOutput o : outputs)
tx.addOutput(o);
} else {
// Fill with dummies as needed
while (tx.getOutputs().size() < utxoIndex)
tx.addOutput(new TransactionOutput(Constants.NETWORK_PARAMETERS, tx,
Coin.NEGATIVE_SATOSHI, new byte[] {}));
// Add the real output
tx.addOutput(output);
}
}
final Set<UTXO> utxos = new HashSet<>();
for (final JsonRpcResponse.Utxo responseUtxo : response.result) {
final Sha256Hash utxoHash = Sha256Hash.wrap(responseUtxo.tx_hash);
final int utxoIndex = responseUtxo.tx_pos;
final Coin utxoValue = Coin.valueOf(responseUtxo.value);
final Script script = ScriptBuilder.createOutputScript(address);
final UTXO utxo = new UTXO(utxoHash, utxoIndex, utxoValue, responseUtxo.height, false,
script);
utxos.add(utxo);
}

log.info("fetched {} unspent outputs from {}", response.result.length, socketAddress);
onResult(transactions.values());
onResult(utxos);
} else {
log.info("id mismatch response:{} vs request:{}", response.id, request.id);
onFail(R.string.error_parse, socketAddress.toString());
Expand All @@ -215,11 +183,11 @@ public void run() {
});
}

protected void onResult(final Collection<Transaction> transactions) {
protected void onResult(final Set<UTXO> utxos) {
callbackHandler.post(new Runnable() {
@Override
public void run() {
resultCallback.onResult(transactions);
resultCallback.onResult(utxos);
}
});
}
Expand All @@ -233,20 +201,6 @@ public void run() {
});
}

private static class FakeTransaction extends Transaction {
private final Sha256Hash hash;

public FakeTransaction(final NetworkParameters params, final Sha256Hash hash) {
super(params);
this.hash = hash;
}

@Override
public Sha256Hash getHash() {
return hash;
}
}

private static Map<InetSocketAddress, String> loadElectrumServers(final InputStream is) throws IOException {
final Splitter splitter = Splitter.on(':');
final Map<InetSocketAddress, String> addresses = new HashMap<>();
Expand Down
80 changes: 75 additions & 5 deletions wallet/src/de/schildbach/wallet/ui/send/SweepWalletFragment.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,18 +19,27 @@

import static com.google.common.base.Preconditions.checkState;

import java.util.Collection;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;

import javax.annotation.Nullable;

import org.bitcoinj.core.Address;
import org.bitcoinj.core.Coin;
import org.bitcoinj.core.DumpedPrivateKey;
import org.bitcoinj.core.ECKey;
import org.bitcoinj.core.NetworkParameters;
import org.bitcoinj.core.Sha256Hash;
import org.bitcoinj.core.Transaction;
import org.bitcoinj.core.TransactionConfidence;
import org.bitcoinj.core.TransactionConfidence.ConfidenceType;
import org.bitcoinj.core.TransactionInput;
import org.bitcoinj.core.TransactionOutPoint;
import org.bitcoinj.core.TransactionOutput;
import org.bitcoinj.core.UTXO;
import org.bitcoinj.core.VerificationException;
import org.bitcoinj.core.VersionedChecksummedBytes;
import org.bitcoinj.crypto.BIP38PrivateKey;
Expand All @@ -40,10 +49,11 @@
import org.bitcoinj.wallet.Wallet;
import org.bitcoinj.wallet.Wallet.BalanceType;
import org.bitcoinj.wallet.WalletTransaction;
import org.bitcoinj.wallet.WalletTransaction.Pool;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.common.collect.ComparisonChain;

import de.schildbach.wallet.Configuration;
import de.schildbach.wallet.Constants;
import de.schildbach.wallet.WalletApplication;
Expand Down Expand Up @@ -467,22 +477,68 @@ public void run() {
}
};

private static final Comparator<UTXO> UTXO_COMPARATOR = new Comparator<UTXO>() {
@Override
public int compare(final UTXO lhs, final UTXO rhs) {
return ComparisonChain.start().compare(lhs.getHash(), rhs.getHash()).compare(lhs.getIndex(), rhs.getIndex())
.result();
}
};

private void requestWalletBalance() {
ProgressDialogFragment.showProgress(fragmentManager,
getString(R.string.sweep_wallet_fragment_request_wallet_balance_progress));

final RequestWalletBalanceTask.ResultCallback callback = new RequestWalletBalanceTask.ResultCallback() {
@Override
public void onResult(final Collection<Transaction> transactions) {
public void onResult(final Set<UTXO> utxos) {
ProgressDialogFragment.dismissProgress(fragmentManager);

// Filter UTXOs we've already spent and sort the rest.
final Set<Transaction> walletTxns = application.getWallet().getTransactions(false);
final Set<UTXO> sortedUtxos = new TreeSet<>(UTXO_COMPARATOR);
for (final UTXO utxo : utxos)
if (!utxoSpentBy(walletTxns, utxo))
sortedUtxos.add(utxo);

// Fake transaction funding the wallet to sweep.
final Map<Sha256Hash, Transaction> fakeTxns = new HashMap<>();
for (final UTXO utxo : sortedUtxos) {
Transaction fakeTx = fakeTxns.get(utxo.getHash());
if (fakeTx == null) {
fakeTx = new FakeTransaction(Constants.NETWORK_PARAMETERS, utxo.getHash());
fakeTx.getConfidence().setConfidenceType(ConfidenceType.BUILDING);
fakeTxns.put(fakeTx.getHash(), fakeTx);
}
final TransactionOutput fakeOutput = new TransactionOutput(Constants.NETWORK_PARAMETERS, fakeTx,
utxo.getValue(), utxo.getScript().getProgram());
// Fill with output dummies as needed.
while (fakeTx.getOutputs().size() < utxo.getIndex())
fakeTx.addOutput(new TransactionOutput(Constants.NETWORK_PARAMETERS, fakeTx,
Coin.NEGATIVE_SATOSHI, new byte[] {}));
// Add the actual output we will spend later.
fakeTx.addOutput(fakeOutput);
}

walletToSweep.clearTransactions(0);
for (final Transaction transaction : transactions)
walletToSweep.addWalletTransaction(new WalletTransaction(Pool.UNSPENT, transaction));
for (final Transaction tx : fakeTxns.values())
walletToSweep.addWalletTransaction(new WalletTransaction(WalletTransaction.Pool.UNSPENT, tx));
log.info("built wallet to sweep:\n{}", walletToSweep.toString(false, true, false, null));

updateView();
}

private boolean utxoSpentBy(final Set<Transaction> transactions, final UTXO utxo) {
for (final Transaction tx : transactions) {
for (final TransactionInput input : tx.getInputs()) {
final TransactionOutPoint outpoint = input.getOutpoint();
if (outpoint.getHash().equals(utxo.getHash()) && outpoint.getIndex() == utxo.getIndex())
return true;
}
}
return false;
}

@Override
public void onFail(final int messageResId, final Object... messageArgs) {
ProgressDialogFragment.dismissProgress(fragmentManager);
Expand Down Expand Up @@ -648,4 +704,18 @@ private void showInsufficientMoneyDialog() {
}
}.sendCoinsOffline(sendRequest); // send asynchronously
}

private static class FakeTransaction extends Transaction {
private final Sha256Hash hash;

public FakeTransaction(final NetworkParameters params, final Sha256Hash hash) {
super(params);
this.hash = hash;
}

@Override
public Sha256Hash getHash() {
return hash;
}
}
}

0 comments on commit a2eaaa5

Please sign in to comment.