When a wallet is added to a block chain that has a lower block height than the chain, try to repair.

Adds a "crash simulation" unit test.
This commit is contained in:
Andreas Schildbach
2014-08-16 17:45:16 +02:00
committed by Mike Hearn
parent d8944b922f
commit 483b3bbc9b
4 changed files with 90 additions and 3 deletions

View File

@@ -1,5 +1,6 @@
/*
* Copyright 2012 Google Inc.
* Copyright 2014 Andreas Schildbach
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -158,9 +159,23 @@ public abstract class AbstractBlockChain {
*/
public void addWallet(Wallet wallet) {
addListener(wallet, Threading.SAME_THREAD);
if (wallet.getLastBlockSeenHeight() != getBestChainHeight()) {
log.warn("Wallet/chain height mismatch: {} vs {}", wallet.getLastBlockSeenHeight(), getBestChainHeight());
int walletHeight = wallet.getLastBlockSeenHeight();
int chainHeight = getBestChainHeight();
if (walletHeight != chainHeight) {
log.warn("Wallet/chain height mismatch: {} vs {}", walletHeight, chainHeight);
log.warn("Hashes: {} vs {}", wallet.getLastBlockSeenHash(), getChainHead().getHeader().getHash());
// This special case happens when the VM crashes because of a transaction received. It causes the updated
// block store to persist, but not the wallet. In order to fix the issue, we roll back the block store to
// the wallet height to make it look like as if the block has never been received.
if (walletHeight < chainHeight && walletHeight > -1) {
try {
rollbackBlockStore(walletHeight);
log.info("Rolled back block store to height {}.", walletHeight);
} catch (BlockStoreException x) {
log.warn("Rollback of block store failed, continuing with mismatched heights.", x);
}
}
}
}
@@ -219,7 +234,15 @@ public abstract class AbstractBlockChain {
protected abstract StoredBlock addToBlockStore(StoredBlock storedPrev, Block header,
@Nullable TransactionOutputChanges txOutputChanges)
throws BlockStoreException, VerificationException;
/**
* Rollback the block store to a given height. This is currently only supported by {@link BlockChain} instances.
*
* @throws BlockStoreException
* if the operation fails or is unsupported.
*/
protected abstract void rollbackBlockStore(int height) throws BlockStoreException;
/**
* Called before setting chain head in memory.
* Should write the new head to block store and then commit any database transactions

View File

@@ -1,5 +1,6 @@
/**
* Copyright 2011 Google Inc.
* Copyright 2014 Andreas Schildbach
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -16,6 +17,8 @@
package com.google.bitcoin.core;
import static com.google.common.base.Preconditions.checkArgument;
import com.google.bitcoin.store.BlockStore;
import com.google.bitcoin.store.BlockStoreException;
@@ -80,6 +83,31 @@ public class BlockChain extends AbstractBlockChain {
return newBlock;
}
@Override
protected void rollbackBlockStore(int height) throws BlockStoreException {
lock.lock();
try {
int currentHeight = getBestChainHeight();
checkArgument(height >= 0 && height <= currentHeight, "Bad height: %s", height);
if (height == currentHeight)
return; // nothing to do
// Look for the block we want to be the new chain head
StoredBlock newChainHead = blockStore.getChainHead();
while (newChainHead.getHeight() > height) {
newChainHead = newChainHead.getPrev(blockStore);
if (newChainHead == null)
throw new BlockStoreException("Unreachable height");
}
// Modify store directly
blockStore.put(newChainHead);
this.setChainHead(newChainHead);
} finally {
lock.unlock();
}
}
@Override
protected boolean shouldVerifyTransactions() {
return false;

View File

@@ -1,5 +1,6 @@
/*
* Copyright 2012 Matt Corallo.
* Copyright 2014 Andreas Schildbach
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -93,6 +94,11 @@ public class FullPrunedBlockChain extends AbstractBlockChain {
return newBlock;
}
@Override
protected void rollbackBlockStore(int height) throws BlockStoreException {
throw new BlockStoreException("Unsupported");
}
@Override
protected boolean shouldVerifyTransactions() {
return true;

View File

@@ -1,5 +1,6 @@
/**
* Copyright 2011 Google Inc.
* Copyright 2014 Andreas Schildbach
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -424,4 +425,33 @@ public class BlockChainTest {
chain.trackFilteredTransactions(550);
assertEquals(rate1, chain.getFalsePositiveRate(), 1e-4);
}
@Test
public void rollbackBlockStore() throws Exception {
// This test simulates an issue on Android, that causes the VM to crash while receiving a block, so that the
// block store is persisted but the wallet is not.
Block b1 = unitTestParams.getGenesisBlock().createNextBlock(coinbaseTo);
Block b2 = b1.createNextBlock(coinbaseTo);
// Add block 1, no frills.
assertTrue(chain.add(b1));
assertEquals(b1.cloneAsHeader(), chain.getChainHead().getHeader());
assertEquals(1, chain.getBestChainHeight());
assertEquals(1, wallet.getLastBlockSeenHeight());
// Add block 2 while wallet is disconnected, to simulate crash.
chain.removeWallet(wallet);
assertTrue(chain.add(b2));
assertEquals(b2.cloneAsHeader(), chain.getChainHead().getHeader());
assertEquals(2, chain.getBestChainHeight());
assertEquals(1, wallet.getLastBlockSeenHeight());
// Add wallet back. This will detect the height mismatch and repair the damage done.
chain.addWallet(wallet);
assertEquals(b1.cloneAsHeader(), chain.getChainHead().getHeader());
assertEquals(1, chain.getBestChainHeight());
assertEquals(1, wallet.getLastBlockSeenHeight());
// Now add block 2 correctly.
assertTrue(chain.add(b2));
assertEquals(b2.cloneAsHeader(), chain.getChainHead().getHeader());
assertEquals(2, chain.getBestChainHeight());
assertEquals(2, wallet.getLastBlockSeenHeight());
}
}