Second part of supporting block chain re-orgs. Wallet now organizes transactions into four pools: unspent, spent, pending and inactive. Transactions track which blocks they have appeared in. BlockChain provides the Wallet with both segments of chain and it then moves transactions between the pools as necessary.

Activated the disabled tests in testForking, added a bunch more tests for this functionality.

Added various utility methods and made small fixes as required.

This changes the wallet format and invalidates existing wallets.
This commit is contained in:
Mike Hearn
2011-04-25 21:56:16 +00:00
parent 86d7b15f8c
commit c20118d871
9 changed files with 742 additions and 287 deletions

View File

@@ -36,6 +36,7 @@ public class BlockChainTest {
private BlockChain chain;
private Address coinbaseTo;
private NetworkParameters unitTestParams;
private Address someOtherGuy;
@Before
public void setUp() {
@@ -44,8 +45,10 @@ public class BlockChainTest {
unitTestParams = NetworkParameters.unitTests();
wallet = new Wallet(unitTestParams);
wallet.addKey(new ECKey());
coinbaseTo = wallet.keychain.get(0).toAddress(unitTestParams);
chain = new BlockChain(unitTestParams, wallet, new MemoryBlockStore(unitTestParams));
coinbaseTo = wallet.keychain.get(0).toAddress(unitTestParams);
someOtherGuy = new ECKey().toAddress(unitTestParams);
}
@Test
@@ -86,23 +89,26 @@ public class BlockChainTest {
}
@Test
public void testForking() throws Exception {
// Check that if the block chain forks, we end up using the right one.
// Start by building a couple of blocks on top of the genesis block.
final boolean[] flags = new boolean[1];
flags[0] = false;
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() {
flags[0] = true;
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(flags[0]);
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:
@@ -114,34 +120,106 @@ public class BlockChainTest {
// \-> b3
//
// Nothing should happen at this point. We saw b2 first so it takes priority.
Address someOtherGuy = new ECKey().toAddress(unitTestParams);
Block b3 = b1.createNextBlock(someOtherGuy);
assertTrue(chain.add(b3));
assertFalse(flags[0]); // No re-org took place.
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(flags[0]); // Re-org took place.
flags[0] = false;
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 balance should drop to 50 again.
if (false) {
// These tests do not pass currently, as wallet handling of re-orgs isn't implemented.
assertEquals("50.00", Utils.bitcoinValueToFriendlyString(wallet.getBalance()));
// ... and back to the first testNetChain
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
//
assertEquals("200.00", Utils.bitcoinValueToFriendlyString(wallet.getBalance()));
}
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.
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));
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 balance
// drops to zero again.
assertEquals(BigInteger.ZERO, wallet.getBalance());
// Not pending .... we don't know if our spend will EVER become active again (if there's an attack it may not).
assertEquals(0, wallet.getPendingTransactions().size());
}
@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

View File

@@ -26,38 +26,66 @@ import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
public class WalletTest {
static final NetworkParameters params = NetworkParameters.testNet();
static final NetworkParameters params = NetworkParameters.unitTests();
private Address myAddress;
private Wallet wallet;
private BlockStore blockStore;
@Before
public void setUp() {
public void setUp() throws Exception {
ECKey myKey = new ECKey();
myAddress = myKey.toAddress(params);
wallet = new Wallet(params);
wallet.addKey(myKey);
blockStore = new MemoryBlockStore(params);
}
private static byte fakeHashCounter = 0;
private Transaction createFakeTx(BigInteger nanocoins, Address to) {
Transaction t = new Transaction(params);
TransactionOutput o1 = new TransactionOutput(params, nanocoins, to);
TransactionOutput o1 = new TransactionOutput(params, nanocoins, to, t);
t.addOutput(o1);
// t1 is not a valid transaction - it has no inputs. Nonetheless, if we set it up with a fake hash it'll be
// valid enough for these tests.
byte[] hash = new byte[32];
for (byte i = 0; i < 32; i++) hash[i] = i;
t.setFakeHashForTesting(hash);
hash[0] = fakeHashCounter++;
t.setFakeHashForTesting(new Sha256Hash(hash));
return t;
}
class BlockPair {
StoredBlock storedBlock;
Block block;
}
// Emulates receiving a valid block that builds on top of the chain.
private BlockPair createFakeBlock(Transaction... transactions) {
try {
Block b = blockStore.getChainHead().getHeader().createNextBlock(new ECKey().toAddress(params));
for (Transaction tx : transactions)
b.addTransaction(tx);
b.solve();
BlockPair pair = new BlockPair();
pair.block = b;
pair.storedBlock = blockStore.getChainHead().build(b);
blockStore.put(pair.storedBlock);
blockStore.setChainHead(pair.storedBlock);
return pair;
} catch (VerificationException e) {
throw new RuntimeException(e); // Cannot happen.
} catch (BlockStoreException e) {
throw new RuntimeException(e); // Cannot happen.
}
}
@Test
public void testBasicSpending() throws Exception {
// We'll set up a wallet that receives a coin, then sends a coin of lesser value and keeps the change.
BigInteger v1 = Utils.toNanoCoins(1, 0);
Transaction t1 = createFakeTx(v1, myAddress);
wallet.receive(t1);
wallet.receive(t1, null, BlockChain.NewBlockType.BEST_CHAIN);
assertEquals(v1, wallet.getBalance());
ECKey k2 = new ECKey();
@@ -66,12 +94,27 @@ public class WalletTest {
// Do some basic sanity checks.
assertEquals(1, t2.inputs.size());
LOG(t2.inputs.get(0).getScriptSig().toString());
assertEquals(myAddress, t2.inputs.get(0).getScriptSig().getFromAddress());
// We have NOT proven that the signature is correct!
}
@Test
public void testSideChain() throws Exception {
// The wallet receives a coin on the main chain, then on a side chain. Only main chain counts towards balance.
BigInteger v1 = Utils.toNanoCoins(1, 0);
Transaction t1 = createFakeTx(v1, myAddress);
wallet.receive(t1, null, BlockChain.NewBlockType.BEST_CHAIN);
assertEquals(v1, wallet.getBalance());
BigInteger v2 = toNanoCoins(0, 50);
Transaction t2 = createFakeTx(v2, myAddress);
wallet.receive(t2, null, BlockChain.NewBlockType.SIDE_CHAIN);
assertEquals(v1, wallet.getBalance());
}
@Test
public void testListener() throws Exception {
final Transaction fakeTx = createFakeTx(Utils.toNanoCoins(1, 0), myAddress);
@@ -86,7 +129,7 @@ public class WalletTest {
}
};
wallet.addEventListener(listener);
wallet.receive(fakeTx);
wallet.receive(fakeTx, null, BlockChain.NewBlockType.BEST_CHAIN);
assertTrue(didRun[0]);
}
@@ -97,24 +140,29 @@ public class WalletTest {
BigInteger v2 = toNanoCoins(0, 50);
Transaction t1 = createFakeTx(v1, myAddress);
Transaction t2 = createFakeTx(v2, myAddress);
StoredBlock b1 = createFakeBlock(t1).storedBlock;
StoredBlock b2 = createFakeBlock(t2).storedBlock;
BigInteger expected = toNanoCoins(5, 50);
wallet.receive(t1);
wallet.receive(t2);
wallet.receive(t1, b1, BlockChain.NewBlockType.BEST_CHAIN);
wallet.receive(t2, b2, BlockChain.NewBlockType.BEST_CHAIN);
assertEquals(expected, wallet.getBalance());
// Now spend one coin.
BigInteger v3 = toNanoCoins(1, 0);
Transaction spend = wallet.createSend(new ECKey().toAddress(params), v3);
wallet.confirmSend(spend);
// We started with 5.50 so we should have 4.50 left.
// Balance should be 0.50 because the change output is pending confirmation by the network.
assertEquals(toNanoCoins(0, 50), wallet.getBalance());
// Now confirm the transaction by including it into a block.
StoredBlock b3 = createFakeBlock(spend).storedBlock;
wallet.receive(spend, b3, BlockChain.NewBlockType.BEST_CHAIN);
// Change is confirmed. We started with 5.50 so we should have 4.50 left.
BigInteger v4 = toNanoCoins(4, 50);
assertEquals(bitcoinValueToFriendlyString(v4),
bitcoinValueToFriendlyString(wallet.getBalance()));
// And spend another coin ...
wallet.confirmSend(wallet.createSend(new ECKey().toAddress(params), v3));
BigInteger v5 = toNanoCoins(3, 50);
assertEquals(bitcoinValueToFriendlyString(v5),
bitcoinValueToFriendlyString(wallet.getBalance()));
}
// Intuitively you'd expect to be able to create a transaction with identical inputs and outputs and get an
@@ -125,17 +173,22 @@ public class WalletTest {
@Test
public void testBlockChainCatchup() throws Exception {
Transaction tx1 = createFakeTx(Utils.toNanoCoins(1, 0), myAddress);
wallet.receive(tx1);
StoredBlock b1 = createFakeBlock(tx1).storedBlock;
wallet.receive(tx1, b1, BlockChain.NewBlockType.BEST_CHAIN);
// Send 0.10 to somebody else.
Transaction send1 = wallet.createSend(new ECKey().toAddress(params), toNanoCoins(0, 10), myAddress);
// Pretend it makes it into the block chain, our wallet state is cleared but we still have the keys, and we
// want to get back to our previous state.
wallet.receive(send1);
// want to get back to our previous state. We can do this by just not confirming the transaction as
// createSend is stateless.
StoredBlock b2 = createFakeBlock(send1).storedBlock;
wallet.receive(send1, b2, BlockChain.NewBlockType.BEST_CHAIN);
assertEquals(bitcoinValueToFriendlyString(wallet.getBalance()), "0.90");
// And we do it again after the catchup.
Transaction send2 = wallet.createSend(new ECKey().toAddress(params), toNanoCoins(0, 10), myAddress);
// What we'd really like to do is prove the official client would accept it .... no such luck unfortunately.
wallet.confirmSend(send2);
StoredBlock b3 = createFakeBlock(send2).storedBlock;
wallet.receive(send2, b3, BlockChain.NewBlockType.BEST_CHAIN);
assertEquals(bitcoinValueToFriendlyString(wallet.getBalance()), "0.80");
}
@@ -143,8 +196,8 @@ public class WalletTest {
public void testBalances() throws Exception {
BigInteger nanos = Utils.toNanoCoins(1, 0);
Transaction tx1 = createFakeTx(nanos, myAddress);
wallet.receive(tx1);
assertEquals(nanos, tx1.getValueSentToMe(wallet));
wallet.receive(tx1, null, BlockChain.NewBlockType.BEST_CHAIN);
assertEquals(nanos, tx1.getValueSentToMe(wallet, true));
// Send 0.10 to somebody else.
Transaction send1 = wallet.createSend(new ECKey().toAddress(params), toNanoCoins(0, 10), myAddress);
// Reserialize.