First part of block chain handling rework.

- Store the block chain using a get/put interface keyed by hash, 
  so we can add disk storage later.
  
- Add unit tests for difficulty transitions. Move some stuff into 
  NetworkParameters to make that easier.
  
- Track the best chain using total work done. Inform the wallet
  when a re-org takes place. Wallet currently doesn't do anything
  with this beyond informing the event listeners.

With this patch we're getting closer to a correct SPV implementation.
This commit is contained in:
Mike Hearn
2011-03-24 09:14:32 +00:00
parent 30327cd888
commit dbab159551
11 changed files with 506 additions and 111 deletions

View File

@@ -18,28 +18,35 @@ package com.google.bitcoin.core;
import com.google.bitcoin.bouncycastle.util.encoders.Hex;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
import java.math.BigInteger;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.junit.Assert.*;
// Tests still to write:
// - Rest of checkDifficultyTransitions: verify we don't accept invalid transitions.
// - Fragmented chains can be joined together.
// - Longest chain is selected based on total difficulty not length.
// - Longest testNetChain is selected based on total difficulty not length.
// - Many more ...
public class BlockChainTest {
private static final NetworkParameters params = NetworkParameters.testNet();
private static final NetworkParameters testNet = NetworkParameters.testNet();
private BlockChain testNetChain;
private Wallet wallet;
private BlockChain chain;
private Address coinbaseTo;
private NetworkParameters unitTestParams;
@Before
public void setUp() {
Wallet wallet = new Wallet(params);
chain = new BlockChain(params, wallet);
testNetChain = new BlockChain(testNet, new Wallet(testNet));
unitTestParams = NetworkParameters.unitTests();
wallet = new Wallet(unitTestParams);
wallet.addKey(new ECKey());
coinbaseTo = wallet.keychain.get(0).toAddress(unitTestParams);
chain = new BlockChain(unitTestParams, wallet);
}
@Test
@@ -47,7 +54,7 @@ public class BlockChainTest {
// Check that we can plug a few blocks together.
// Block 1 from the testnet.
Block b1 = getBlock1();
assertTrue(chain.add(b1));
assertTrue(testNetChain.add(b1));
// Block 2 from the testnet.
Block b2 = getBlock2();
@@ -55,37 +62,60 @@ public class BlockChainTest {
long n = b2.getNonce();
try {
b2.setNonce(12345);
chain.add(b2);
testNetChain.add(b2);
fail();
} catch (VerificationException e) {
b2.setNonce(n);
}
assertTrue(chain.add(b2));
// Now it works because we reset the nonce.
assertTrue(testNetChain.add(b2));
}
private Block createNextBlock(Address to, Block prev) throws VerificationException {
return createNextBlock(to, prev, Block.EASIEST_DIFFICULTY_TARGET, System.currentTimeMillis() / 1000);
}
private Block createNextBlock(Address to, Block prev, long difficultyTarget,
long time) throws VerificationException {
Block b = new Block(prev.params);
b.setDifficultyTarget(Block.EASIEST_DIFFICULTY_TARGET);
b.setDifficultyTarget(difficultyTarget);
b.addCoinbaseTransaction(to);
b.setPrevBlockHash(prev.getHash());
b.setTime(time);
b.solve();
b.verify();
return b;
}
@Test @Ignore
public void testForking() throws Exception {
// Check that if the block chain forks, we end up using the right one.
NetworkParameters unitTestParams = NetworkParameters.unitTests();
Wallet wallet = new Wallet(unitTestParams);
wallet.addKey(new ECKey());
Address coinbaseTo = wallet.keychain.get(0).toAddress(unitTestParams);
// Start by building a couple of blocks on top of the genesis block.
@Test
public void testUnconnectedBlocks() throws Exception {
Block b1 = createNextBlock(coinbaseTo, unitTestParams.genesisBlock);
Block b2 = createNextBlock(coinbaseTo, b1);
chain = new BlockChain(unitTestParams, wallet);
chain.add(b1);
chain.add(b2);
Block b3 = createNextBlock(coinbaseTo, b2);
// Connected.
assertTrue(chain.add(b1));
// Unconnected.
assertFalse(chain.add(b3));
}
@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;
wallet.addEventListener(new WalletEventListener() {
@Override
public void onReorganize() {
flags[0] = true;
}
});
Block b1 = createNextBlock(coinbaseTo, unitTestParams.genesisBlock);
Block b2 = createNextBlock(coinbaseTo, b1);
assertTrue(chain.add(b1));
assertTrue(chain.add(b2));
assertFalse(flags[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:
@@ -99,33 +129,65 @@ public class BlockChainTest {
// Nothing should happen at this point. We saw b2 first so it takes priority.
Address someOtherGuy = new ECKey().toAddress(unitTestParams);
Block b3 = createNextBlock(someOtherGuy, b1);
chain.add(b3);
assertTrue(chain.add(b3));
assertFalse(flags[0]); // No re-org took place.
assertEquals("100.00", Utils.bitcoinValueToFriendlyString(wallet.getBalance()));
// Now we add another block to make the alternative chain longer.
chain.add(createNextBlock(someOtherGuy, b3));
assertTrue(chain.add(createNextBlock(someOtherGuy, b3)));
assertTrue(flags[0]); // Re-org took place.
flags[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.
assertEquals("50.00", Utils.bitcoinValueToFriendlyString(wallet.getBalance()));
// ... and back to the first chain
Block b5 = createNextBlock(coinbaseTo, b2);
Block b6 = createNextBlock(coinbaseTo, b5);
chain.add(b5);
chain.add(b6);
//
// genesis -> b1 -> b2 -> b5 -> b6
// \-> b3 -> b4
//
assertEquals("200.00", Utils.bitcoinValueToFriendlyString(wallet.getBalance()));
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 = createNextBlock(coinbaseTo, b2);
Block b6 = createNextBlock(coinbaseTo, b5);
assertTrue(chain.add(b5));
assertTrue(chain.add(b6));
//
// genesis -> b1 -> b2 -> b5 -> b6
// \-> b3 -> b4
//
assertEquals("200.00", Utils.bitcoinValueToFriendlyString(wallet.getBalance()));
}
}
@Test
public void testDifficultyTransitions() throws Exception {
// Add a bunch of blocks in a loop until we reach a difficulty transition point. The unit test params have an
// artificially shortened period.
Block prev = unitTestParams.genesisBlock;
Block.fakeClock = System.currentTimeMillis() / 1000;
for (int i = 0; i < unitTestParams.interval - 1; i++) {
Block newBlock = createNextBlock(coinbaseTo, prev, Block.EASIEST_DIFFICULTY_TARGET, Block.fakeClock);
assertTrue(chain.add(newBlock));
prev = newBlock;
// The fake chain should seem to be "fast" for the purposes of difficulty calculations.
Block.fakeClock += 2;
}
// Now add another block that has no difficulty adjustment, it should be rejected.
try {
chain.add(createNextBlock(coinbaseTo, prev));
fail();
} catch (VerificationException e) {
}
// Create a new block with the right difficulty target given our blistering speed relative to the huge amount
// of time it's supposed to take (set in the unit test network parameters).
Block b = createNextBlock(coinbaseTo, prev, 0x201fFFFFL, Block.fakeClock);
assertTrue(chain.add(b));
// Successfully traversed a difficulty transition period.
}
@Test
public void testBadDifficulty() throws Exception {
assertTrue(chain.add(getBlock1()));
assertTrue(testNetChain.add(getBlock1()));
Block b2 = getBlock2();
assertTrue(chain.add(b2));
assertTrue(testNetChain.add(b2));
NetworkParameters params2 = NetworkParameters.testNet();
Block bad = new Block(params2);
// Merkle root can be anything here, doesn't matter.
@@ -139,7 +201,7 @@ public class BlockChainTest {
// solutions.
bad.setDifficultyTarget(Block.EASIEST_DIFFICULTY_TARGET);
try {
chain.add(bad);
testNetChain.add(bad);
// The difficulty target above should be rejected on the grounds of being easier than the networks
// allowable difficulty.
fail();
@@ -151,7 +213,7 @@ public class BlockChainTest {
params2.proofOfWorkLimit = new BigInteger
("00ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", 16);
try {
chain.add(bad);
testNetChain.add(bad);
// We should not get here as the difficulty target should not be changing at this point.
fail();
} catch (VerificationException e) {
@@ -161,8 +223,9 @@ public class BlockChainTest {
// TODO: Test difficulty change is not out of range when a transition period becomes valid.
}
// Some blocks from the test net.
private Block getBlock2() throws Exception {
Block b2 = new Block(params);
Block b2 = new Block(testNet);
b2.setMerkleRoot(Hex.decode("addc858a17e21e68350f968ccd384d6439b64aafa6c193c8b9dd66320470838b"));
b2.setNonce(2642058077L);
b2.setTime(1296734343L);
@@ -173,7 +236,7 @@ public class BlockChainTest {
}
private Block getBlock1() throws Exception {
Block b1 = new Block(params);
Block b1 = new Block(testNet);
b1.setMerkleRoot(Hex.decode("0e8e58ecdacaa7b3c6304a35ae4ffff964816d2b80b62b58558866ce4e648c10"));
b1.setNonce(236038445);
b1.setTime(1296734340);