mirror of
https://github.com/bitcoinj/bitcoinj.git
synced 2024-11-20 02:09:29 +01:00
Check for double-spend of contract by force-adding it to wallet
This commit is contained in:
parent
f0be874815
commit
4b4405b7bc
@ -18,6 +18,7 @@ package com.google.bitcoin.protocols.channels;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
import com.google.bitcoin.core.*;
|
||||
@ -213,7 +214,7 @@ public class PaymentChannelServerState {
|
||||
* Note that if the network simply rejects the transaction, this future will never complete, a timeout should be used.
|
||||
* @throws VerificationException If the provided multisig contract is not well-formed or does not meet previously-specified parameters
|
||||
*/
|
||||
public synchronized ListenableFuture<PaymentChannelServerState> provideMultiSigContract(Transaction multisigContract) throws VerificationException {
|
||||
public synchronized ListenableFuture<PaymentChannelServerState> provideMultiSigContract(final Transaction multisigContract) throws VerificationException {
|
||||
checkNotNull(multisigContract);
|
||||
checkState(state == State.WAITING_FOR_MULTISIG_CONTRACT);
|
||||
try {
|
||||
@ -240,6 +241,13 @@ public class PaymentChannelServerState {
|
||||
Futures.addCallback(broadcaster.broadcastTransaction(multisigContract), new FutureCallback<Transaction>() {
|
||||
@Override public void onSuccess(Transaction transaction) {
|
||||
log.info("Successfully broadcast multisig contract {}. Channel now open.", transaction.getHashAsString());
|
||||
try {
|
||||
// Manually add the multisigContract to the wallet, overriding the isRelevant checks so we can track
|
||||
// it and check for double-spends later
|
||||
wallet.receivePending(multisigContract, Collections.EMPTY_LIST, true);
|
||||
} catch (VerificationException e) {
|
||||
throw new RuntimeException(e); // Cannot happen, we already called multisigContract.verify()
|
||||
}
|
||||
state = State.READY;
|
||||
future.set(PaymentChannelServerState.this);
|
||||
}
|
||||
@ -291,6 +299,21 @@ public class PaymentChannelServerState {
|
||||
if (newValueToMe.compareTo(bestValueToMe) < 0)
|
||||
return;
|
||||
|
||||
// Get the wallet's copy of the multisigContract (ie with confidence information), if this is null, the wallet
|
||||
// was not connected to the peergroup when the contract was broadcast (which may cause issues down the road, and
|
||||
// disables our double-spend check next)
|
||||
Transaction walletContract = wallet.getTransaction(multisigContract.getHash());
|
||||
checkState(walletContract != null, "Wallet did not contain multisig contract after state was marked READY");
|
||||
|
||||
// Note that we check for DEAD state here, but this test is essentially useless in production because we will
|
||||
// miss most double-spends due to bloom filtering right now anyway. This will eventually fixed by network-wide
|
||||
// double-spend notifications, so we just wait instead of attempting to add all dependant outpoints to our bloom
|
||||
// filters (and probably missing lots of edge-cases).
|
||||
if (walletContract.getConfidence().getConfidenceType() == TransactionConfidence.ConfidenceType.DEAD) {
|
||||
close();
|
||||
throw new VerificationException("Multisig contract was double-spent");
|
||||
}
|
||||
|
||||
Transaction.SigHash mode;
|
||||
// If the client doesn't want anything back, they shouldn't sign any outputs at all.
|
||||
if (fullyUsedUp)
|
||||
|
@ -27,6 +27,7 @@ import org.junit.Test;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.Iterator;
|
||||
import java.util.concurrent.BlockingQueue;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
@ -703,4 +704,88 @@ public class PaymentChannelStateTest extends TestWithWallet {
|
||||
pair.future.set(pair.tx);
|
||||
assertEquals(PaymentChannelServerState.State.CLOSED, serverState.getState());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void doubleSpendContractTest() throws Exception {
|
||||
// Tests that if the client double-spends the multisig contract after it is sent, no more payments are accepted
|
||||
|
||||
// Start with a copy of basic()....
|
||||
Utils.rollMockClock(0); // Use mock clock
|
||||
final long EXPIRE_TIME = Utils.now().getTime()/1000 + 60*60*24;
|
||||
|
||||
serverState = new PaymentChannelServerState(mockBroadcaster, serverWallet, serverKey, EXPIRE_TIME);
|
||||
assertEquals(PaymentChannelServerState.State.WAITING_FOR_REFUND_TRANSACTION, serverState.getState());
|
||||
|
||||
clientState = new PaymentChannelClientState(wallet, myKey, new ECKey(null, serverKey.getPubKey()), halfCoin, EXPIRE_TIME);
|
||||
assertEquals(PaymentChannelClientState.State.NEW, clientState.getState());
|
||||
clientState.initiate();
|
||||
assertEquals(PaymentChannelClientState.State.INITIATED, clientState.getState());
|
||||
|
||||
// Send the refund tx from client to server and get back the signature.
|
||||
Transaction refund = new Transaction(params, clientState.getIncompleteRefundTransaction().bitcoinSerialize());
|
||||
byte[] refundSig = serverState.provideRefundTransaction(refund, myKey.getPubKey());
|
||||
assertEquals(PaymentChannelServerState.State.WAITING_FOR_MULTISIG_CONTRACT, serverState.getState());
|
||||
// This verifies that the refund can spend the multi-sig output when run.
|
||||
clientState.provideRefundSignature(refundSig);
|
||||
assertEquals(PaymentChannelClientState.State.PROVIDE_MULTISIG_CONTRACT_TO_SERVER, clientState.getState());
|
||||
|
||||
// Validate the multisig contract looks right.
|
||||
Transaction multisigContract = new Transaction(params, clientState.getMultisigContract().bitcoinSerialize());
|
||||
assertEquals(PaymentChannelClientState.State.READY, clientState.getState());
|
||||
assertEquals(2, multisigContract.getOutputs().size()); // One multi-sig, one change.
|
||||
Script script = multisigContract.getOutput(0).getScriptPubKey();
|
||||
assertTrue(script.isSentToMultiSig());
|
||||
script = multisigContract.getOutput(1).getScriptPubKey();
|
||||
assertTrue(script.isSentToAddress());
|
||||
assertTrue(wallet.getPendingTransactions().contains(multisigContract));
|
||||
|
||||
// Provide the server with the multisig contract and simulate successful propagation/acceptance.
|
||||
serverState.provideMultiSigContract(multisigContract);
|
||||
assertEquals(PaymentChannelServerState.State.WAITING_FOR_MULTISIG_ACCEPTANCE, serverState.getState());
|
||||
final TxFuturePair pair = broadcasts.take();
|
||||
pair.future.set(pair.tx);
|
||||
assertEquals(PaymentChannelServerState.State.READY, serverState.getState());
|
||||
|
||||
// Make sure the refund transaction is not in the wallet and multisig contract's output is not connected to it
|
||||
assertEquals(2, wallet.getTransactions(false).size());
|
||||
Iterator<Transaction> walletTransactionIterator = wallet.getTransactions(false).iterator();
|
||||
Transaction clientWalletMultisigContract = walletTransactionIterator.next();
|
||||
assertFalse(clientWalletMultisigContract.getHash().equals(clientState.getCompletedRefundTransaction().getHash()));
|
||||
if (!clientWalletMultisigContract.getHash().equals(multisigContract.getHash())) {
|
||||
clientWalletMultisigContract = walletTransactionIterator.next();
|
||||
assertFalse(clientWalletMultisigContract.getHash().equals(clientState.getCompletedRefundTransaction().getHash()));
|
||||
} else
|
||||
assertFalse(walletTransactionIterator.next().getHash().equals(clientState.getCompletedRefundTransaction().getHash()));
|
||||
assertEquals(multisigContract.getHash(), clientWalletMultisigContract.getHash());
|
||||
assertFalse(clientWalletMultisigContract.getInput(0).getConnectedOutput().getSpentBy().getParentTransaction().getHash().equals(refund.getHash()));
|
||||
|
||||
// Both client and server are now in the ready state. Simulate a few micropayments of 0.005 bitcoins.
|
||||
BigInteger size = halfCoin.divide(BigInteger.TEN).divide(BigInteger.TEN);
|
||||
BigInteger totalPayment = BigInteger.ZERO;
|
||||
for (int i = 0; i < 5; i++) {
|
||||
byte[] signature = clientState.incrementPaymentBy(size);
|
||||
totalPayment = totalPayment.add(size);
|
||||
serverState.incrementPayment(halfCoin.subtract(totalPayment), signature);
|
||||
}
|
||||
|
||||
// Now create a double-spend and send it to the server
|
||||
Transaction doubleSpendContract = new Transaction(params);
|
||||
doubleSpendContract.addInput(new TransactionInput(params, doubleSpendContract, new byte[0],
|
||||
multisigContract.getInput(0).getOutpoint()));
|
||||
doubleSpendContract.addOutput(halfCoin, myKey);
|
||||
doubleSpendContract = new Transaction(params, doubleSpendContract.bitcoinSerialize());
|
||||
|
||||
StoredBlock block = new StoredBlock(params.getGenesisBlock().createNextBlock(myKey.toAddress(params)), BigInteger.TEN, 1);
|
||||
serverWallet.receiveFromBlock(doubleSpendContract, block, AbstractBlockChain.NewBlockType.BEST_CHAIN);
|
||||
|
||||
// Now if we try to spend again the server will reject it since it saw a double-spend
|
||||
try {
|
||||
byte[] signature = clientState.incrementPaymentBy(size);
|
||||
totalPayment = totalPayment.add(size);
|
||||
serverState.incrementPayment(halfCoin.subtract(totalPayment), signature);
|
||||
fail();
|
||||
} catch (VerificationException e) {
|
||||
assertTrue(e.getMessage().contains("double-spent"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user