mirror of
https://github.com/Qortal/altcoinj.git
synced 2025-02-07 14:54:15 +00: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.math.BigInteger;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
import java.util.Collections;
|
||||||
import javax.annotation.Nullable;
|
import javax.annotation.Nullable;
|
||||||
|
|
||||||
import com.google.bitcoin.core.*;
|
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.
|
* 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
|
* @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);
|
checkNotNull(multisigContract);
|
||||||
checkState(state == State.WAITING_FOR_MULTISIG_CONTRACT);
|
checkState(state == State.WAITING_FOR_MULTISIG_CONTRACT);
|
||||||
try {
|
try {
|
||||||
@ -240,6 +241,13 @@ public class PaymentChannelServerState {
|
|||||||
Futures.addCallback(broadcaster.broadcastTransaction(multisigContract), new FutureCallback<Transaction>() {
|
Futures.addCallback(broadcaster.broadcastTransaction(multisigContract), new FutureCallback<Transaction>() {
|
||||||
@Override public void onSuccess(Transaction transaction) {
|
@Override public void onSuccess(Transaction transaction) {
|
||||||
log.info("Successfully broadcast multisig contract {}. Channel now open.", transaction.getHashAsString());
|
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;
|
state = State.READY;
|
||||||
future.set(PaymentChannelServerState.this);
|
future.set(PaymentChannelServerState.this);
|
||||||
}
|
}
|
||||||
@ -291,6 +299,21 @@ public class PaymentChannelServerState {
|
|||||||
if (newValueToMe.compareTo(bestValueToMe) < 0)
|
if (newValueToMe.compareTo(bestValueToMe) < 0)
|
||||||
return;
|
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;
|
Transaction.SigHash mode;
|
||||||
// If the client doesn't want anything back, they shouldn't sign any outputs at all.
|
// If the client doesn't want anything back, they shouldn't sign any outputs at all.
|
||||||
if (fullyUsedUp)
|
if (fullyUsedUp)
|
||||||
|
@ -27,6 +27,7 @@ import org.junit.Test;
|
|||||||
|
|
||||||
import java.math.BigInteger;
|
import java.math.BigInteger;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.Iterator;
|
import java.util.Iterator;
|
||||||
import java.util.concurrent.BlockingQueue;
|
import java.util.concurrent.BlockingQueue;
|
||||||
import java.util.concurrent.ExecutionException;
|
import java.util.concurrent.ExecutionException;
|
||||||
@ -703,4 +704,88 @@ public class PaymentChannelStateTest extends TestWithWallet {
|
|||||||
pair.future.set(pair.tx);
|
pair.future.set(pair.tx);
|
||||||
assertEquals(PaymentChannelServerState.State.CLOSED, serverState.getState());
|
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…
x
Reference in New Issue
Block a user