/** * Copyright 2013 multibit.org * * Licensed under the MIT license (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://opensource.org/licenses/mit-license.php * * 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 org.multibit.network; import com.google.bitcoin.core.CheckpointManager; import com.google.bitcoin.core.PeerGroup; import com.google.bitcoin.core.StoredBlock; import com.google.bitcoin.store.BlockStoreException; import org.multibit.controller.bitcoin.BitcoinController; import org.multibit.message.Message; import org.multibit.message.MessageManager; import org.multibit.model.bitcoin.WalletData; import org.multibit.viewsystem.swing.view.panels.SendBitcoinPanel; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.swing.*; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.text.DateFormat; import java.util.*; import java.util.Timer; /** * ReplayManager is responsible for updating Wallets that are not updated to * MultiBit's main BlockStore. This happens when: 1) The user imports some * private keys 2) They do a 'Reset blockchain and transactions' 3) An out of * date wallet is opened 4) Encrypted wallets are opened when the user has used * an older version of MultiBit that does not understand them (they then get out * of date). */ public enum ReplayManager { INSTANCE; private static final Logger log = LoggerFactory.getLogger(ReplayManager.class); private ReplayManagerTimerTask replayManagerTimerTask; private Timer replayManagerTimer; /** * The actual chain height prior to any replay * (as opposed to the current height whilst replaying). */ private int actualLastChainHeight; private static final int REPLAY_MANAGER_DELAY_TIME = 0; // ms private static final int REPLAY_MANAGER_REPEAT_TIME = 333; // ms private BitcoinController controller; private final Queue<ReplayTask> replayTaskQueue = new LinkedList<ReplayTask>(); private static boolean regularDownloadIsRunning = false; public void initialise(BitcoinController controller, boolean clearQueue) { this.controller = controller; if (clearQueue) { replayTaskQueue.clear(); } replayManagerTimerTask = new ReplayManagerTimerTask(controller, replayTaskQueue); replayManagerTimer = new Timer(); replayManagerTimer.scheduleAtFixedRate(replayManagerTimerTask, REPLAY_MANAGER_DELAY_TIME, REPLAY_MANAGER_REPEAT_TIME); } /** * Synchronise one or more wallets with the blockchain. */ public void syncWallet(final ReplayTask replayTask) throws IOException, BlockStoreException { log.info("Starting replay task : " + replayTask.toString()); // Remember the chain height. if (controller.getMultiBitService().getChain() != null) { actualLastChainHeight = controller.getMultiBitService().getChain().getBestChainHeight(); } // Mark the wallets as busy and set the replay task uuid into the model List<WalletData> perWalletModelDataList = replayTask.getPerWalletModelDataToReplay(); if (perWalletModelDataList != null) { for (WalletData perWalletModelData : perWalletModelDataList) { perWalletModelData.setBusy(true); perWalletModelData.setBusyTaskKey("multiBitDownloadListener.downloadingText"); perWalletModelData.setBusyTaskVerbKey("multiBitDownloadListener.downloadingTextShort"); perWalletModelData.setReplayTaskUUID(replayTask.getUuid()); } controller.fireWalletBusyChange(true); } Date dateToReplayFrom = replayTask.getStartDate(); MessageManager.INSTANCE.addMessage(new Message(controller.getLocaliser().getString( "resetTransactionsSubmitAction.startReplay"))); log.debug("Starting replay of blockchain from date = '" + dateToReplayFrom); // Reset UI to zero peers. controller.getPeerEventListener().onPeerDisconnected(null, 0); // Restart peerGroup and download rest of blockchain. Message message; if (dateToReplayFrom != null) { message = new Message(controller.getLocaliser().getString( "resetTransactionSubmitAction.replayingBlockchain", new Object[]{DateFormat.getDateInstance(DateFormat.MEDIUM, controller.getLocaliser().getLocale()).format( dateToReplayFrom)}), false); } else { message = new Message(controller.getLocaliser().getString( "resetTransactionSubmitAction.replayingBlockchain", new Object[]{DateFormat.getDateInstance(DateFormat.MEDIUM, controller.getLocaliser().getLocale()).format( MultiBitService.genesisBlockCreationDate)}), false); } MessageManager.INSTANCE.addMessage(message); log.debug("About to restart PeerGroup."); message = new Message(controller.getLocaliser().getString("multiBitService.stoppingBitcoinNetworkConnection"), false, 0); MessageManager.INSTANCE.addMessage(message); controller.getMultiBitService().getPeerGroup().stopAndWait(); log.debug("PeerGroup is now stopped."); // Reset UI to zero peers. controller.getPeerEventListener().onPeerDisconnected(null, 0); // Close the blockstore and recreate a new one. int newChainHeightAfterTruncate = controller.getMultiBitService().createNewBlockStoreForReplay(dateToReplayFrom); log.debug("dateToReplayFrom = " + dateToReplayFrom + ", newChainHeightAfterTruncate = " + newChainHeightAfterTruncate); replayTask.setStartHeight(newChainHeightAfterTruncate); // Create a new PeerGroup. controller.getMultiBitService().createNewPeerGroup(); log.debug("Recreated PeerGroup."); // Hook up the download listeners. addDownloadListeners(perWalletModelDataList); // Start up the PeerGroup. PeerGroup peerGroup = controller.getMultiBitService().getPeerGroup(); peerGroup.start(); log.debug("Restarted PeerGroup = " + peerGroup.toString()); log.debug("About to start blockchain download."); controller.getMultiBitService().getPeerGroup().downloadBlockChain(); log.debug("Blockchain download started."); } public void addDownloadListeners(List<WalletData> perWalletModelDataList) { PeerGroup peerGroup = controller.getMultiBitService().getPeerGroup(); if (peerGroup instanceof MultiBitPeerGroup) { if (perWalletModelDataList != null) { for (WalletData perWalletModelData : perWalletModelDataList) { if (perWalletModelData.getSingleWalletDownloadListener() != null) { ((MultiBitPeerGroup) peerGroup).getMultiBitDownloadListener().addSingleWalletPanelDownloadListener( perWalletModelData.getSingleWalletDownloadListener()); } } } } } public void removeDownloadListeners(List<WalletData> perWalletModelDataList) { PeerGroup peerGroup = controller.getMultiBitService().getPeerGroup(); if (peerGroup instanceof MultiBitPeerGroup) { if (perWalletModelDataList != null) { for (WalletData perWalletModelData : perWalletModelDataList) { if (perWalletModelData.getSingleWalletDownloadListener() != null) { ((MultiBitPeerGroup) peerGroup).getMultiBitDownloadListener().removeDownloadListener( perWalletModelData.getSingleWalletDownloadListener()); } } } } } /** * Add a ReplayTask to the ReplayManager's list of tasks to do. * * @param replayTask */ public boolean offerReplayTask(ReplayTask replayTask) { if (replayTask == null) { return false; } log.debug("Received ReplayTask of " + replayTask.toString()); // Work out for this replay task where the blockchain will be truncated to. int startHeight = replayTask.getStartHeight(); if (startHeight == ReplayTask.UNKNOWN_START_HEIGHT) { File checkpointsFile = new File(controller.getMultiBitService().getCheckpointsFilename()); System.out.println("ReplayManager#offerReplayTask checkpointsFile = " + checkpointsFile.getAbsolutePath()); if (checkpointsFile.exists() && replayTask.getStartDate() != null) { FileInputStream stream = null; try { stream = new FileInputStream(checkpointsFile); CheckpointManager checkpointManager = new CheckpointManager(controller.getModel().getNetworkParameters(), stream); StoredBlock checkpoint = checkpointManager.getCheckpointBefore(replayTask.getStartDate().getTime() / 1000); System.out.println("ReplayManager#offerReplayTask checkpoint = " + checkpoint); if (checkpoint != null) { startHeight = checkpoint.getHeight(); System.out.println("ReplayManager#offerReplayTask startHeight = " + startHeight); // Store it in the replay task as it will be used for percents. replayTask.setStartHeight(startHeight); } } catch (IOException e) { log.error(e.getClass().getName() + " " + e.getMessage()); } finally { if (stream != null) { try { stream.close(); } catch (IOException e) { e.printStackTrace(); } } } } } log.debug("Actual replayTask offered = " + replayTask.toString()); synchronized (replayTaskQueue) { replayTaskQueue.offer(replayTask); String waitingText = "singleWalletPanel.waiting.text"; String waitingVerb = "singleWalletPanel.waiting.verb"; for (WalletData perWalletModelData : replayTask.getPerWalletModelDataToReplay()) { if (perWalletModelData != null) { perWalletModelData.setBusy(true); perWalletModelData.setBusyTaskVerbKey(waitingVerb); perWalletModelData.setBusyTaskKey(waitingText); // Set the height on the wallet to be the startHeight. // This means that if the user shuts down MultBit replay on start up // will be from the required startHeight. perWalletModelData.getWallet().setLastBlockSeenHeight(startHeight); perWalletModelData.getWallet().setLastBlockSeenHash(null); perWalletModelData.setDirty(true); } } } return true; } /** * Called by the downloadlistener when the synchronise completes. * * @param replayTaskUUID TODO */ public void taskHasCompleted(UUID replayTaskUUID) { log.debug("ReplayTask with UUID " + replayTaskUUID + " has completed."); // Check the UUID matches the current task. ReplayTask currentTask = replayTaskQueue.peek(); if (currentTask == null) { return; } else { // Not relevant - ignore. if (!currentTask.getUuid().equals(replayTaskUUID)) { return; } } // Tell the ReplayTimerTask that we are cleaning up. replayManagerTimerTask.currentTaskIsTidyingUp(true); try { if (currentTask != null) { // This task is complete. Inform the UI. List<WalletData> perWalletModelDataList = currentTask.getPerWalletModelDataToReplay(); if (perWalletModelDataList != null) { for (WalletData perWalletModelData : perWalletModelDataList) { perWalletModelData.setBusyTaskVerbKey(null); perWalletModelData.setBusyTaskKey(null); perWalletModelData.setBusy(false); perWalletModelData.setReplayTaskUUID(null); } } // TODO - does not look quite right. controller.fireWalletBusyChange(false); } } finally { // No longer tidying up. replayManagerTimerTask.currentTaskIsTidyingUp(false); // Everything is completed - clear to start the next task. replayManagerTimerTask.currentTaskHasCompleted(); } } public ReplayTask getCurrentReplayTask() { synchronized (replayTaskQueue) { if (replayTaskQueue.isEmpty()) { return null; } else { return replayTaskQueue.peek(); } } } /** * See if there is a waiting replay task for a perWalletModelData * * @param perWalletModelData * @return the waiting ReplayTask or null if there is not one. */ @SuppressWarnings("unchecked") public ReplayTask getWaitingReplayTask(WalletData perWalletModelData) { synchronized (replayTaskQueue) { if (replayTaskQueue.isEmpty()) { return null; } else { for (ReplayTask replayTask : (List<ReplayTask>) replayTaskQueue) { List<WalletData> list = replayTask.getPerWalletModelDataToReplay(); if (list != null) { for (WalletData item : list) { if (perWalletModelData.getWalletFilename().equals(item.getWalletFilename())) { return replayTask; } } } } return null; } } } /** * Download the block chain. * This does not use a ReplayTask. */ public void downloadBlockChain() { @SuppressWarnings("rawtypes") SwingWorker worker = new SwingWorker() { @Override protected Object doInBackground() throws Exception { if (controller.getMultiBitService().getPeerGroup() != null) { regularDownloadIsRunning = true; SendBitcoinPanel.setEnableSendButton(false); log.debug("Downloading blockchain - regularDownloadIsRunning = " + regularDownloadIsRunning); controller.getMultiBitService().getPeerGroup().downloadBlockChain(); } else { log.error("Cannot download blockchain as there is no PeerGroup"); } return null; // return not used } }; worker.execute(); } /** * Method called back by BitcoinPeerEventListener to indicate a block chain download has completed */ public void downloadHasCompleted() { regularDownloadIsRunning = false; SendBitcoinPanel.setEnableSendButton(true); log.debug("Download has completed (in ReplayManager) - regularDownloadIsRunning = " + regularDownloadIsRunning); } /** * The actual height of the block chain just prior to a replay being started. * * @return */ public int getActualLastChainHeight() { return actualLastChainHeight; } }