/**
* Copyright 2011 Google Inc.
*
* 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 com.google.bitcoin.core;
import org.junit.Before;
import org.junit.Test;
import java.math.BigInteger;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public class ChainSplitTests {
private NetworkParameters unitTestParams;
private Wallet wallet;
private BlockChain chain;
private Address coinbaseTo;
private Address someOtherGuy;
@Before
public void setUp() {
unitTestParams = NetworkParameters.unitTests();
wallet = new Wallet(unitTestParams);
wallet.addKey(new ECKey());
chain = new BlockChain(unitTestParams, wallet, new MemoryBlockStore(unitTestParams));
coinbaseTo = wallet.keychain.get(0).toAddress(unitTestParams);
someOtherGuy = new ECKey().toAddress(unitTestParams);
}
@Test
public void testForking1() throws Exception {
// Check that if the block chain forks, we end up using the right chain. Only tests inbound transactions
// (receiving coins). Checking that we understand reversed spends is in testForking2.
// TODO: Change this test to not use coinbase transactions as they are special (maturity rules).
final boolean[] reorgHappened = new boolean[1];
reorgHappened[0] = false;
wallet.addEventListener(new WalletEventListener() {
@Override
public void onReorganize() {
reorgHappened[0] = true;
}
});
// Start by building a couple of blocks on top of the genesis block.
Block b1 = unitTestParams.genesisBlock.createNextBlock(coinbaseTo);
Block b2 = b1.createNextBlock(coinbaseTo);
assertTrue(chain.add(b1));
assertTrue(chain.add(b2));
assertFalse(reorgHappened[0]);
// We got two blocks which generated 50 coins each, to us.
assertEquals("100.00", Utils.bitcoinValueToFriendlyString(wallet.getBalance()));
// We now have the following chain:
// genesis -> b1 -> b2
//
// so fork like this:
//
// genesis -> b1 -> b2
// \-> b3
//
// Nothing should happen at this point. We saw b2 first so it takes priority.
Block b3 = b1.createNextBlock(someOtherGuy);
assertTrue(chain.add(b3));
assertFalse(reorgHappened[0]); // No re-org took place.
assertEquals("100.00", Utils.bitcoinValueToFriendlyString(wallet.getBalance()));
// Now we add another block to make the alternative chain longer.
assertTrue(chain.add(b3.createNextBlock(someOtherGuy)));
assertTrue(reorgHappened[0]); // Re-org took place.
reorgHappened[0] = false;
//
// genesis -> b1 -> b2
// \-> b3 -> b4
//
// We lost some coins! b2 is no longer a part of the best chain so our available balance should drop to 50.
assertEquals("50.00", Utils.bitcoinValueToFriendlyString(wallet.getBalance()));
// ... and back to the first chain.
Block b5 = b2.createNextBlock(coinbaseTo);
Block b6 = b5.createNextBlock(coinbaseTo);
assertTrue(chain.add(b5));
assertTrue(chain.add(b6));
//
// genesis -> b1 -> b2 -> b5 -> b6
// \-> b3 -> b4
//
assertTrue(reorgHappened[0]);
assertEquals("200.00", Utils.bitcoinValueToFriendlyString(wallet.getBalance()));
}
@Test
public void testForking2() throws Exception {
// Check that if the chain forks and new coins are received in the alternate chain our balance goes up
// after the re-org takes place.
Block b1 = unitTestParams.genesisBlock.createNextBlock(someOtherGuy);
Block b2 = b1.createNextBlock(someOtherGuy);
assertTrue(chain.add(b1));
assertTrue(chain.add(b2));
// genesis -> b1 -> b2
// \-> b3 -> b4
assertEquals(BigInteger.ZERO, wallet.getBalance());
Block b3 = b1.createNextBlock(coinbaseTo);
Block b4 = b3.createNextBlock(someOtherGuy);
assertTrue(chain.add(b3));
assertEquals(BigInteger.ZERO, wallet.getBalance());
assertTrue(chain.add(b4));
assertEquals("50.00", Utils.bitcoinValueToFriendlyString(wallet.getBalance()));
}
@Test
public void testForking3() throws Exception {
// Check that we can handle our own spends being rolled back by a fork.
Block b1 = unitTestParams.genesisBlock.createNextBlock(coinbaseTo);
chain.add(b1);
assertEquals("50.00", Utils.bitcoinValueToFriendlyString(wallet.getBalance()));
Address dest = new ECKey().toAddress(unitTestParams);
Transaction spend = wallet.createSend(dest, Utils.toNanoCoins(10, 0));
wallet.confirmSend(spend);
// Waiting for confirmation ...
assertEquals(BigInteger.ZERO, wallet.getBalance());
Block b2 = b1.createNextBlock(someOtherGuy);
b2.addTransaction(spend);
b2.solve();
chain.add(b2);
assertEquals(Utils.toNanoCoins(40, 0), wallet.getBalance());
// genesis -> b1 (receive coins) -> b2 (spend coins)
// \-> b3 -> b4
Block b3 = b1.createNextBlock(someOtherGuy);
Block b4 = b3.createNextBlock(someOtherGuy);
chain.add(b3);
chain.add(b4);
// b4 causes a re-org that should make our spend go inactive. Because the inputs are already spent our
// available balance drops to zero again.
assertEquals(BigInteger.ZERO, wallet.getBalance(Wallet.BalanceType.AVAILABLE));
// We estimate that it'll make it back into the block chain (we know we won't double spend).
// assertEquals(Utils.toNanoCoins(40, 0), wallet.getBalance(Wallet.BalanceType.ESTIMATED));
}
@Test
public void testForking4() throws Exception {
// Check that we can handle external spends on an inactive chain becoming active. An external spend is where
// we see a transaction that spends our own coins but we did not broadcast it ourselves. This happens when
// keys are being shared between wallets.
Block b1 = unitTestParams.genesisBlock.createNextBlock(coinbaseTo);
chain.add(b1);
assertEquals("50.00", Utils.bitcoinValueToFriendlyString(wallet.getBalance()));
Address dest = new ECKey().toAddress(unitTestParams);
Transaction spend = wallet.createSend(dest, Utils.toNanoCoins(50, 0));
// We do NOT confirm the spend here. That means it's not considered to be pending because createSend is
// stateless. For our purposes it is as if some other program with our keys created the tx.
//
// genesis -> b1 (receive 50) --> b2
// \-> b3 (external spend) -> b4
Block b2 = b1.createNextBlock(someOtherGuy);
chain.add(b2);
Block b3 = b1.createNextBlock(someOtherGuy);
b3.addTransaction(spend);
b3.solve();
chain.add(b3);
// The external spend is not active yet.
assertEquals(Utils.toNanoCoins(50, 0), wallet.getBalance());
Block b4 = b3.createNextBlock(someOtherGuy);
chain.add(b4);
// The external spend is now active.
assertEquals(Utils.toNanoCoins(0, 0), wallet.getBalance());
}
@Test
public void testDoubleSpendOnFork() throws Exception {
// Check what happens when a re-org happens and one of our confirmed transactions becomes invalidated by a
// double spend on the new best chain.
final boolean[] eventCalled = new boolean[1];
wallet.addEventListener(new WalletEventListener() {
@Override
public void onDeadTransaction(Transaction deadTx, Transaction replacementTx) {
eventCalled[0] = true;
}
});
Block b1 = unitTestParams.genesisBlock.createNextBlock(coinbaseTo);
chain.add(b1);
Transaction t1 = wallet.createSend(someOtherGuy, Utils.toNanoCoins(10, 0));
Address yetAnotherGuy = new ECKey().toAddress(unitTestParams);
Transaction t2 = wallet.createSend(yetAnotherGuy, Utils.toNanoCoins(20, 0));
wallet.confirmSend(t1);
// Receive t1 as confirmed by the network.
Block b2 = b1.createNextBlock(new ECKey().toAddress(unitTestParams));
b2.addTransaction(t1);
b2.solve();
chain.add(b2);
// Now we make a double spend become active after a re-org.
Block b3 = b1.createNextBlock(new ECKey().toAddress(unitTestParams));
b3.addTransaction(t2);
b3.solve();
chain.add(b3); // Side chain.
Block b4 = b3.createNextBlock(new ECKey().toAddress(unitTestParams));
chain.add(b4); // New best chain.
// Should have seen a double spend.
assertTrue(eventCalled[0]);
assertEquals(Utils.toNanoCoins(30, 0), wallet.getBalance());
}
@Test
public void testDoubleSpendOnForkPending() throws Exception {
// Check what happens when a re-org happens and one of our UNconfirmed transactions becomes invalidated by a
// double spend on the new best chain.
final Transaction[] eventDead = new Transaction[1];
final Transaction[] eventReplacement = new Transaction[1];
wallet.addEventListener(new WalletEventListener() {
@Override
public void onDeadTransaction(Transaction deadTx, Transaction replacementTx) {
eventDead[0] = deadTx;
eventReplacement[0] = replacementTx;
}
});
// Start with 50 coins.
Block b1 = unitTestParams.genesisBlock.createNextBlock(coinbaseTo);
chain.add(b1);
Transaction t1 = wallet.createSend(someOtherGuy, Utils.toNanoCoins(10, 0));
Address yetAnotherGuy = new ECKey().toAddress(unitTestParams);
Transaction t2 = wallet.createSend(yetAnotherGuy, Utils.toNanoCoins(20, 0));
wallet.confirmSend(t1);
// t1 is still pending ...
Block b2 = b1.createNextBlock(new ECKey().toAddress(unitTestParams));
chain.add(b2);
assertEquals(Utils.toNanoCoins(0, 0), wallet.getBalance());
assertEquals(Utils.toNanoCoins(40, 0), wallet.getBalance(Wallet.BalanceType.ESTIMATED));
// Now we make a double spend become active after a re-org.
// genesis -> b1 -> b2 [t1 pending]
// \-> b3 (t2) -> b4
Block b3 = b1.createNextBlock(new ECKey().toAddress(unitTestParams));
b3.addTransaction(t2);
b3.solve();
chain.add(b3); // Side chain.
Block b4 = b3.createNextBlock(new ECKey().toAddress(unitTestParams));
chain.add(b4); // New best chain.
// Should have seen a double spend against the pending pool.
assertEquals(t1, eventDead[0]);
assertEquals(t2, eventReplacement[0]);
assertEquals(Utils.toNanoCoins(30, 0), wallet.getBalance());
// ... and back to our own parallel universe.
Block b5 = b2.createNextBlock(new ECKey().toAddress(unitTestParams));
chain.add(b5);
Block b6 = b5.createNextBlock(new ECKey().toAddress(unitTestParams));
chain.add(b6);
// genesis -> b1 -> b2 -> b5 -> b6 [t1 pending]
// \-> b3 [t2 inactive] -> b4
assertEquals(Utils.toNanoCoins(0, 0), wallet.getBalance());
assertEquals(Utils.toNanoCoins(40, 0), wallet.getBalance(Wallet.BalanceType.ESTIMATED));
}
}