package com.greenaddress.greenbits.spv; import android.annotation.TargetApi; import android.app.NotificationManager; import android.app.PendingIntent; import android.content.Context; import android.content.Intent; import android.net.ConnectivityManager; import android.net.NetworkInfo; import android.os.Build; import android.support.v4.app.NotificationCompat; import android.support.v4.app.NotificationCompat.Builder; import android.util.Log; import android.util.Pair; import android.util.SparseArray; import com.google.common.base.Function; import com.google.common.collect.Lists; import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.greenaddress.greenapi.JSONMap; import com.greenaddress.greenapi.Network; import com.greenaddress.greenapi.PreparedTransaction; import com.greenaddress.greenbits.GaService; import com.greenaddress.greenbits.ui.CB; import com.greenaddress.greenbits.ui.R; import com.greenaddress.greenbits.ui.TabbedMainActivity; import org.bitcoinj.core.Address; import org.bitcoinj.core.AddressFormatException; import org.bitcoinj.core.Block; import org.bitcoinj.core.BlockChain; import org.bitcoinj.core.BloomFilter; import org.bitcoinj.core.CheckpointManager; import org.bitcoinj.core.Coin; import org.bitcoinj.core.FilteredBlock; import org.bitcoinj.core.Peer; import org.bitcoinj.core.PeerAddress; import org.bitcoinj.core.PeerGroup; import org.bitcoinj.core.Sha256Hash; import org.bitcoinj.core.StoredBlock; import org.bitcoinj.core.Transaction; import org.bitcoinj.core.TransactionOutPoint; import org.bitcoinj.core.VerificationException; import org.bitcoinj.core.listeners.DownloadProgressTracker; import org.bitcoinj.core.listeners.TransactionReceivedInBlockListener; import org.bitcoinj.net.BlockingClientManager; import org.bitcoinj.net.discovery.DnsDiscovery; import org.bitcoinj.params.RegTestParams; import org.bitcoinj.store.BlockStore; import org.bitcoinj.store.BlockStoreException; import org.bitcoinj.store.SPVBlockStore; import org.bitcoinj.wallet.Wallet; import java.io.IOException; import java.io.InputStream; import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.URI; import java.net.URISyntaxException; import java.net.UnknownHostException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class SPV { private final static String TAG = SPV.class.getSimpleName(); private final Map<TransactionOutPoint, Coin> mCountedUtxoValues = new HashMap<>(); private final static String VERIFIED = "verified_utxo_"; private final static String SPENDABLE = "verified_utxo_spendable_value_"; static class AccountInfo extends Pair<Integer, Integer> { public AccountInfo(final Integer subAccount, final Integer pointer) { super(subAccount, pointer); } public Integer getSubAccount() { return first; } public Integer getPointer() { return second; } } // We use a single threaded executor to serialise config changes // without forcing callers to block. private final ExecutorService mExecutor = Executors.newSingleThreadExecutor(); private final SparseArray<Coin> mVerifiedCoinBalances = new SparseArray<>(); private final Map<Sha256Hash, List<Integer>> mUnspentOutpoints = new HashMap<>(); private final Map<TransactionOutPoint, AccountInfo> mUnspentDetails = new HashMap<>(); private final GaService mService; private int mBlocksRemaining = Integer.MAX_VALUE; private BlockStore mBlockStore; private BlockChain mBlockChain; private PeerGroup mPeerGroup; private final PeerFilterProvider mPeerFilter = new PeerFilterProvider(this); private NotificationManager mNotifyManager; private Builder mNotificationBuilder; private final static int mNotificationId = 1; private int mNetWorkType; private final Object mStateLock = new Object(); public SPV(final GaService service) { mService = service; mNetWorkType = ConnectivityManager.TYPE_DUMMY; } public GaService getService() { return mService; } private <T> String Var(final String name, final T value) { return name + " => " + value.toString() + ' '; } public boolean isEnabled() { return !mService.isWatchOnly() && mService.cfg("SPV").getBoolean("enabled", true) && !GaService.IS_ELEMENTS; } public void setEnabledAsync(final boolean enabled) { mExecutor.execute(new Runnable() { public void run() { setEnabled(enabled); } }); } private void setEnabled(final boolean enabled) { synchronized (mStateLock) { final boolean current = isEnabled(); Log.d(TAG, "setEnabled: " + Var("enabled", enabled) + Var("current", current)); if (enabled == current) return; mService.cfgEdit("SPV").putBoolean("enabled", enabled).apply(); // FIXME: Should we delete unspent here? reset(false /* deleteAllData */, false /* deleteUnspent */); } } public boolean isSyncOnMobileEnabled() { return mService.cfg("SPV").getBoolean("mobileSyncEnabled", false); } public void setSyncOnMobileEnabledAsync(final boolean enabled) { mExecutor.execute(new Runnable() { public void run() { setSyncOnMobileEnabled(enabled); } }); } private void setSyncOnMobileEnabled(final boolean enabled) { synchronized (mStateLock) { final boolean current = isSyncOnMobileEnabled(); final boolean currentlyEnabled = isEnabled(); Log.d(TAG, "setSyncOnMobileEnabled: " + Var("enabled", enabled) + Var("current", current)); if (enabled == current) return; // Setting hasn't changed mService.cfgEdit("SPV").putBoolean("mobileSyncEnabled", enabled).apply(); if (getNetworkType() != ConnectivityManager.TYPE_MOBILE) return; // Any change doesn't affect us since we aren't currently on mobile if (enabled && currentlyEnabled) { if (mPeerGroup == null) setup(); startSync(); } else stopSync(); } } public String getTrustedPeers() { return mService.cfg("TRUSTED").getString("address", mService.cfg().getString("trusted_peer", "")).trim(); } public void setTrustedPeersAsync(final String peers) { mExecutor.execute(new Runnable() { public void run() { setTrustedPeers(peers); } }); } private void setTrustedPeers(final String peers) { synchronized (mStateLock) { // FIXME: We should check if the peers differ here, instead of in the caller final String current = getTrustedPeers(); Log.d(TAG, "setTrustedPeers: " + Var("peers", peers) + Var("current", current)); mService.cfgEdit("TRUSTED").putString("address", peers).apply(); mService.setUserConfig("trusted_peer_addr", peers, true); reset(false /* deleteAllData */, false /* deleteUnspent */); } } public PeerGroup getPeerGroup(){ return mPeerGroup; } public boolean isVerified(final Sha256Hash txHash) { return mService.cfgIn(VERIFIED).getBoolean(txHash.toString(), false); } public void startAsync() { mExecutor.execute(new Runnable() { public void run() { start(); } }); } private void start() { synchronized (mStateLock) { Log.d(TAG, "start"); reset(false /* deleteAllData */, true /* deleteUnspent */); } } public Coin getVerifiedBalance(final int subAccount) { return mVerifiedCoinBalances.get(subAccount); } private boolean isUnspentOutpoint(final Sha256Hash txHash) { return mUnspentOutpoints.containsKey(txHash); } private TransactionOutPoint createOutPoint(final Integer index, final Sha256Hash txHash) { return new TransactionOutPoint(Network.NETWORK, index, txHash); } public ListenableFuture<Void> updateUnspentOutputs() { final boolean currentlyEnabled = isEnabled(); Log.d(TAG, "updateUnspentOutputs: " + Var("currentlyEnabled", currentlyEnabled)); if (!currentlyEnabled) return Futures.immediateFuture(null); final boolean filterAsset = true; // TODO: Elements doesn't support SPV yet return Futures.transform(mService.getAllUnspentOutputs(0, null, filterAsset), new Function<List<JSONMap>, Void>() { @Override public Void apply(final List<JSONMap> utxos) { updateUnspentOutputs(utxos); return null; } }, mService.getExecutor()); } private void updateUnspentOutputs(final List<JSONMap> utxos) { final Set<TransactionOutPoint> newUtxos = new HashSet<>(); boolean recalculateBloom = false; Log.d(TAG, Var("number of utxos", utxos.size())); for (final JSONMap utxo : utxos) { final Integer prevIndex = utxo.getInt("pt_idx"); final Integer subaccount = utxo.getInt("subaccount"); final Integer pointer = utxo.getInt("pointer"); final Sha256Hash txHash = utxo.getHash("txhash"); if (isVerified(txHash)) { addToUtxo(txHash, prevIndex, subaccount, pointer); addUtxoToValues(txHash, false /* updateVerified */); } else { recalculateBloom = true; addToBloomFilter(utxo.getInt("block_height"), txHash, prevIndex, subaccount, pointer); } newUtxos.add(createOutPoint(prevIndex, txHash)); } final List<Integer> changedSubaccounts = new ArrayList<>(); for (final TransactionOutPoint oldUtxo : new HashSet<>(mCountedUtxoValues.keySet())) { if (!newUtxos.contains(oldUtxo)) { recalculateBloom = true; final int subAccount = mUnspentDetails.get(oldUtxo).getSubAccount(); final Coin verifiedBalance = getVerifiedBalance(subAccount); mVerifiedCoinBalances.put(subAccount, verifiedBalance.subtract(mCountedUtxoValues.get(oldUtxo))); changedSubaccounts.add(subAccount); mCountedUtxoValues.remove(oldUtxo); mUnspentDetails.remove(oldUtxo); mUnspentOutpoints.get(oldUtxo.getHash()).remove(((int) oldUtxo.getIndex())); } } if (recalculateBloom && mPeerGroup != null) mPeerGroup.recalculateFastCatchupAndFilter(PeerGroup.FilterRecalculateMode.SEND_IF_CHANGED); fireBalanceChanged(changedSubaccounts); } private void fireBalanceChanged(final List<Integer> subAccounts) { for (final int subAccount : subAccounts) mService.fireBalanceChanged(subAccount); } private void updateBalance(final TransactionOutPoint txOutpoint, final int subAccount, final Coin addValue) { if (mCountedUtxoValues.containsKey(txOutpoint)) return; mCountedUtxoValues.put(txOutpoint, addValue); final Coin verifiedBalance = getVerifiedBalance(subAccount); if (verifiedBalance == null) mVerifiedCoinBalances.put(subAccount, addValue); else mVerifiedCoinBalances.put(subAccount, verifiedBalance.add(addValue)); } public void addUtxoToValues(final Sha256Hash txHash, final boolean updateVerified) { final String txHashHex = txHash.toString(); if (updateVerified) mService.cfgInEdit(VERIFIED).putBoolean(txHashHex, true).apply(); final List<Integer> changedSubaccounts = new ArrayList<>(); boolean missing = false; for (final Integer outpoint : mUnspentOutpoints.get(txHash)) { final String key = txHashHex + ':' + outpoint; final long value = mService.cfgIn(SPENDABLE).getLong(key, -1); if (value == -1) { missing = true; continue; } final TransactionOutPoint txOutpoint = createOutPoint(outpoint, txHash); final int subAccount = mUnspentDetails.get(txOutpoint).getSubAccount(); if (!mCountedUtxoValues.containsKey(txOutpoint)) changedSubaccounts.add(subAccount); updateBalance(txOutpoint, subAccount, Coin.valueOf(value)); } fireBalanceChanged(changedSubaccounts); if (!missing) return; CB.after(mService.getRawUnspentOutput(txHash), new CB.Op<Transaction>() { @Override public void onSuccess(final Transaction result) { final List<Integer> changedSubaccounts = new ArrayList<>(); final List<ListenableFuture<Boolean>> futuresList = new ArrayList<>(); if (!result.getHash().equals(txHash)) { Log.e(TAG, "txHash mismatch: expected " + txHashHex + ", got " + result.getHash().toString()); return; } for (final Integer outpoint : mUnspentOutpoints.get(txHash)) { final TransactionOutPoint txOutpoint = createOutPoint(outpoint, txHash); if (mCountedUtxoValues.containsKey(txOutpoint)) continue; final AccountInfo accountInfo = mUnspentDetails.get(txOutpoint); final int subAccount = accountInfo.getSubAccount(); final int pointer = accountInfo.getPointer(); final ListenableFuture<Boolean> verifyFn; verifyFn = mService.verifySpendableBy(result.getOutput(outpoint), subAccount, pointer); futuresList.add(Futures.transform(verifyFn, new Function<Boolean, Boolean>() { @Override public Boolean apply(final Boolean input) { final String key = txHashHex + ':' + outpoint; if (!input) Log.e(TAG, "txHash " + key + " not spendable!"); else { final Coin value = result.getOutput(outpoint).getValue(); updateBalance(txOutpoint, subAccount, value); changedSubaccounts.add(subAccount); mService.cfgInEdit(SPENDABLE).putLong(key, value.longValue()).apply(); } return input; } })); } CB.after(Futures.allAsList(futuresList), new CB.Op<List<Boolean>>() { @Override public void onSuccess(final List<Boolean> result) { fireBalanceChanged(changedSubaccounts); } }); } }); } public int getBloomFilterElementCount() { final int count = mUnspentOutpoints.size(); return count == 0 ? 1 : count; } public BloomFilter getBloomFilter(final int size, final double falsePositiveRate, final long nTweak) { final Set<Sha256Hash> keys = mUnspentOutpoints.keySet(); Log.d(TAG, "getBloomFilter returning " + keys.size() + " items"); final BloomFilter filter = new BloomFilter(size, falsePositiveRate, nTweak); for (final Sha256Hash hash : keys) filter.insert(hash.getReversedBytes()); if (keys.isEmpty()) { // Add a fake entry to avoid downloading blocks when filter is empty, // as empty bloom filters are ignored by bitcoinj. // FIXME: This results in a constant filter that peers can use to identify // us as a GreenBits client. That is undesirable. filter.insert(new byte[]{(byte) 0xde, (byte) 0xad, (byte) 0xbe, (byte) 0xef}); } return filter; } public void onNewBlock(final int blockHeight) { Log.d(TAG, "onNewBlock: " + Var("blockHeight", blockHeight) + Var("isEnabled", isEnabled())); if (isEnabled()) addToBloomFilter(blockHeight, null, -1, -1, -1); } private void addToBloomFilter(final Integer blockHeight, final Sha256Hash txHash, final int prevIndex, final int subAccount, final int pointer) { if (mBlockChain == null) return; // can happen before login (onNewBlock) if (txHash != null) addToUtxo(txHash, prevIndex, subAccount, pointer); if (blockHeight != null && blockHeight <= mBlockChain.getBestChainHeight() && (txHash == null || !mUnspentOutpoints.containsKey(txHash))) { // new tx or block notification with blockHeight <= current blockHeight means we might've [1] // synced the height already while we haven't seen the tx, so we need to re-sync to be able // to verify it. // [1] - "might've" in case of txHash == null (block height notification), // because it depends on the order of notifications // - "must've" in case of txHash != null, because this means the tx arrived only after // requesting it manually and we already had higher blockHeight // // We do it using the special case in bitcoinj for VM crashed because of // a transaction received. try { Log.d(TAG, "Creating fake wallet for re-sync"); final Wallet fakeWallet = new Wallet(Network.NETWORK) { @Override public int getLastBlockSeenHeight() { return blockHeight - 1; } }; mBlockChain.addWallet(fakeWallet); mBlockChain.removeWallet(fakeWallet); // can be removed, because the call above // should rollback already } catch (final Exception e) { e.printStackTrace(); Log.w(TAG, "fakeWallet exception: " + e.toString()); } } } private void addToUtxo(final Sha256Hash txHash, final Integer prevIndex, final int subAccount, final int pointer) { mUnspentDetails.put(createOutPoint(prevIndex, txHash), new AccountInfo(subAccount, pointer)); if (mUnspentOutpoints.get(txHash) == null) mUnspentOutpoints.put(txHash, Lists.newArrayList(prevIndex)); else mUnspentOutpoints.get(txHash).add(prevIndex); } private ListenableFuture<Boolean> verifyOutputSpendable(final PreparedTransaction ptx, final int index) { return mService.verifySpendableBy(ptx.mDecoded.getOutputs().get(index), ptx.mSubAccount, ptx.mChangePointer); } public ListenableFuture<Coin> validateTx(final PreparedTransaction ptx, final String recipientStr, final Coin amount) { Address recipient = null; try { recipient = Address.fromBase58(Network.NETWORK, recipientStr); } catch (final AddressFormatException e) { } // 1. Find the change output: ListenableFuture<List<Boolean>> changeFn = Futures.immediateFuture(null); if (ptx.mDecoded.getOutputs().size() == 2) { changeFn = Futures.allAsList(Lists.newArrayList(verifyOutputSpendable(ptx, 0), verifyOutputSpendable(ptx, 1))); } else if (ptx.mDecoded.getOutputs().size() > 2) throw new IllegalArgumentException("Verification: Wrong number of transaction outputs."); // 2. Verify the main output value and address, if available: final Address recipientAddr = recipient; return Futures.transform(changeFn, new Function<List<Boolean>, Coin>() { @Override public Coin apply(final List<Boolean> input) { return Verifier.verify(mService, mCountedUtxoValues, ptx, recipientAddr, amount, input); } }); } public int getSPVBlocksRemaining() { if (isEnabled()) return mBlocksRemaining; return 0; } public int getSPVHeight() { if (mBlockChain != null && isEnabled()) return mBlockChain.getBestChainHeight(); return 0; } @TargetApi(Build.VERSION_CODES.M) private PendingIntent getNotificationIntent() { final Context service = getService(); final Intent intent = new Intent(service, TabbedMainActivity.class); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); return PendingIntent.getActivity(service, 0, intent, PendingIntent.FLAG_IMMUTABLE); } private void startSync() { synchronized (mStateLock) { final boolean isRunning = mPeerGroup != null && mPeerGroup.isRunning(); Log.d(TAG, "startSync: " + Var("isRunning", isRunning)); if (isRunning) return; // Already started to sync if (mPeerGroup == null) { // FIXME: Thi should not be possible but it happens in the wild. Log.d(TAG, "startSync: mPeerGroup is null"); return; } if (mNotifyManager == null) { mNotifyManager = (NotificationManager) mService.getSystemService(Context.NOTIFICATION_SERVICE); mNotificationBuilder = new NotificationCompat.Builder(mService); mNotificationBuilder.setContentTitle("GreenBits SPV Sync") .setSmallIcon(R.drawable.ic_sync_black_24dp); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) mNotificationBuilder.setContentIntent(getNotificationIntent()); } mNotificationBuilder.setContentText("Connecting to peer(s)..."); updateNotification(0, 0); CB.after(mPeerGroup.startAsync(), new FutureCallback<Object>() { @Override public void onSuccess(final Object result) { mPeerGroup.startBlockChainDownload(new DownloadProgressTracker() { @Override public void onChainDownloadStarted(final Peer peer, final int blocksLeft) { // Note that this method may be called multiple times if syncing // switches peers while downloading. Log.d(TAG, "onChainDownloadStarted: " + Var("blocksLeft", blocksLeft)); mBlocksRemaining = blocksLeft; super.onChainDownloadStarted(peer, blocksLeft); } @Override public void onBlocksDownloaded(final Peer peer, final Block block, final FilteredBlock filteredBlock, final int blocksLeft) { //Log.d(TAG, "onBlocksDownloaded: " + Var("blocksLeft", blocksLeft)); mBlocksRemaining = blocksLeft; super.onBlocksDownloaded(peer, block, filteredBlock, blocksLeft); } @Override protected void startDownload(final int blocks) { Log.d(TAG, "startDownload"); updateNotification(100, 0); } @Override protected void progress(final double percent, final int blocksSoFar, final Date date) { //Log.d(TAG, "progress: " + Var("percent", percent)); mNotificationBuilder.setContentText("Sync in progress..."); updateNotification(100, (int) percent); } @Override protected void doneDownload() { Log.d(TAG, "doneDownLoad"); mNotifyManager.cancel(mNotificationId); } }); } @Override public void onFailure(final Throwable t) { t.printStackTrace(); mNotifyManager.cancel(mNotificationId); } }); } } private void updateNotification(final int total, final int soFar) { mNotificationBuilder.setProgress(total, soFar, false); mNotifyManager.notify(mNotificationId, mNotificationBuilder.build()); } private PeerAddress getPeerAddress(final String address) throws URISyntaxException, UnknownHostException { final URI uri = new URI("btc://" + address); final String host = uri.getHost(); if (host == null) throw new UnknownHostException(address); final int port = uri.getPort() == -1? Network.NETWORK.getPort() : uri.getPort(); if (!mService.isProxyEnabled()) return new PeerAddress(Network.NETWORK, InetAddress.getByName(host), port); return new PeerAddress(Network.NETWORK, host, port) { @Override public InetSocketAddress toSocketAddress() { return InetSocketAddress.createUnresolved(host, port); } @Override public String toString() { return String.format("%s:%s", host, port); } @Override public int hashCode() { return uri.hashCode(); } }; } private void addPeer(final String address) throws URISyntaxException { if (address.isEmpty() && !mService.isProxyEnabled()) { // Blank w/o proxy: Use the built in resolving via DNS mPeerGroup.addPeerDiscovery(new DnsDiscovery(Network.NETWORK)); return; } try { mPeerGroup.addAddress(getPeerAddress(address)); } catch (final UnknownHostException e) { // FIXME: Should report this error: one the host here couldn't be resolved e.printStackTrace(); } } private final TransactionReceivedInBlockListener mTxListner = new TransactionReceivedInBlockListener() { @Override public void receiveFromBlock(final Transaction tx, final StoredBlock block, final BlockChain.NewBlockType blockType, final int relativityOffset) throws VerificationException { getService().notifyObservers(tx.getHash()); } @Override public boolean notifyTransactionIsInBlock(final Sha256Hash txHash, final StoredBlock block, final BlockChain.NewBlockType blockType, final int relativityOffset) throws VerificationException { getService().notifyObservers(txHash); return isUnspentOutpoint(txHash); } }; private void setPingInterval(final long interval) { synchronized (mStateLock) { if (mPeerGroup != null) mPeerGroup.setPingIntervalMsec(interval); } } public void enablePingMonitoring() { setPingInterval(PeerGroup.DEFAULT_PING_INTERVAL_MSEC); } public void disablePingMonitoring() { setPingInterval(-1); } private void setup(){ synchronized (mStateLock) { Log.d(TAG, "setup: " + Var("mPeerGroup != null", mPeerGroup != null)); if (mPeerGroup != null) { // FIXME: Make sure this can never happen Log.e(TAG, "Must stop and tear down SPV before setting up again!"); return; } try { Log.d(TAG, "Creating block store"); mBlockStore = new SPVBlockStore(Network.NETWORK, mService.getSPVChainFile()); final StoredBlock storedBlock = mBlockStore.getChainHead(); // detect corruptions as early as possible if (storedBlock.getHeight() == 0 && Network.NETWORK != RegTestParams.get()) { InputStream is = null; try { is = mService.getAssets().open("checkpoints"); final int keyTime = mService.getLoginData().get("earliest_key_creation_time"); CheckpointManager.checkpoint(Network.NETWORK, is, mBlockStore, keyTime); } catch (final IOException e) { // couldn't load checkpoints, log & skip e.printStackTrace(); } finally { try { if (is != null) is.close(); } catch (final IOException e) { // do nothing } } } Log.d(TAG, "Creating block chain"); mBlockChain = new BlockChain(Network.NETWORK, mBlockStore); mBlockChain.addTransactionReceivedListener(mTxListner); System.setProperty("user.home", mService.getFilesDir().toString()); Log.d(TAG, "Creating peer group"); if (!mService.isProxyEnabled()) mPeerGroup = new PeerGroup(Network.NETWORK, mBlockChain); else { final String proxyHost = mService.getProxyHost(); final String proxyPort = mService.getProxyPort(); final Socks5SocketFactory sf = new Socks5SocketFactory(proxyHost, proxyPort); final BlockingClientManager bcm = new BlockingClientManager(sf); bcm.setConnectTimeoutMillis(60000); mPeerGroup = new PeerGroup(Network.NETWORK, mBlockChain, bcm); mPeerGroup.setConnectTimeoutMillis(60000); } disablePingMonitoring(); try { updateUnspentOutputs().get(); } catch(final ExecutionException | InterruptedException e) { e.printStackTrace(); } mPeerGroup.addPeerFilterProvider(mPeerFilter); Log.d(TAG, "Adding peers"); final String peers = getTrustedPeers(); final ArrayList<String> addresses; if (peers.isEmpty()) { // DEFAULT_PEER is only set for regtest. For other networks // it is empty and so will cause us to use DNS discovery. addresses = new ArrayList<>(Collections.singletonList(Network.DEFAULT_PEER)); } else addresses = new ArrayList<>(Arrays.asList(peers.split(","))); for (final String address: addresses) addPeer(address); } catch (final BlockStoreException | UnknownHostException | URISyntaxException e) { e.printStackTrace(); } } } public void stopSyncAsync() { mExecutor.execute(new Runnable() { public void run() { stopSync(); } }); } private void stopSync() { synchronized (mStateLock) { Log.d(TAG, "stopSync: " + Var("isEnabled", isEnabled())); if (mPeerGroup != null && mPeerGroup.isRunning()) { Log.d(TAG, "Stopping peer group"); final Intent i = new Intent("PEERGROUP_UPDATED"); i.putExtra("peergroup", "stopSPVSync"); mService.sendBroadcast(i); mPeerGroup.stop(); } if (mNotifyManager != null) mNotifyManager.cancel(mNotificationId); if (mBlockChain != null) { Log.d(TAG, "Disposing of block chain"); mBlockChain.removeTransactionReceivedListener(mTxListner); mBlockChain = null; } if (mPeerGroup != null) { Log.d(TAG, "Deleting peer group"); mPeerGroup.removePeerFilterProvider(mPeerFilter); mPeerGroup = null; } if (mBlockStore != null) { Log.d(TAG, "Closing block store"); try { mBlockStore.close(); mBlockStore = null; } catch (final BlockStoreException x) { throw new RuntimeException(x); } } } } // We only care about mobile vs non-mobile so treat others as ethernet private int getNetworkType(final NetworkInfo info) { if (info == null) return ConnectivityManager.TYPE_DUMMY; final int type = info.getType(); return type == ConnectivityManager.TYPE_MOBILE ? type : ConnectivityManager.TYPE_ETHERNET; } private int getNetworkType() { return getNetworkType(mService.getNetworkInfo()); } // Handle changes to network connectivity. // Note that this only handles mobile/non-mobile transitions public void onNetConnectivityChangedAsync(final NetworkInfo info) { mExecutor.execute(new Runnable() { public void run() { onNetConnectivityChanged(info); } }); } private void onNetConnectivityChanged(final NetworkInfo info) { synchronized (mStateLock) { final int oldType = mNetWorkType; final int newType = getNetworkType(info); mNetWorkType = newType; if (!isEnabled() || newType == oldType) return; // No change Log.d(TAG, "onNetConnectivityChanged: " + Var("newType", newType) + Var("oldType", oldType) + Var("isSyncOnMobileEnabled", isSyncOnMobileEnabled())); // FIXME: - It seems network connectivity changes can happen when // mPeerGroup is null (i.e. setup hasn't been called), // but its not clear what path leads to this happening. if (newType == ConnectivityManager.TYPE_MOBILE) { if (!isSyncOnMobileEnabled()) stopSync(); // Mobile network and we have sync mobile disabled } else if (oldType == ConnectivityManager.TYPE_MOBILE) { if (isSyncOnMobileEnabled()) startSync(); // Non-Mobile network and we have sync mobile enabled } } } public void resetAsync() { mExecutor.execute(new Runnable() { public void run() { reset(true /* deleteAllData */, true /* deleteUnspent */); } }); } private void reset(final boolean deleteAllData, final boolean deleteUnspent) { synchronized (mStateLock) { Log.d(TAG, "reset: " + Var("deleteAllData", deleteAllData) + Var("deleteUnspent", deleteUnspent)); stopSync(); if (deleteAllData) { Log.d(TAG, "Deleting chain file"); mService.getSPVChainFile().delete(); try { Log.d(TAG, "Clearing verified and spendable transactions"); mService.cfgInEdit(SPENDABLE).clear().commit(); mService.cfgInEdit(VERIFIED).clear().commit(); } catch (final NullPointerException e) { // ignore } } if (deleteUnspent) { Log.d(TAG, "Resetting unspent outputs"); mUnspentDetails.clear(); mUnspentOutpoints.clear(); mCountedUtxoValues.clear(); mVerifiedCoinBalances.clear(); } if (isEnabled()) { setup(); // We might race with our network callbacks, so fetch the network type // if its unknown. if (mNetWorkType == ConnectivityManager.TYPE_DUMMY) mNetWorkType = getNetworkType(); if (isSyncOnMobileEnabled() || mNetWorkType != ConnectivityManager.TYPE_MOBILE) startSync(); } Log.d(TAG, "Finished reset"); } } }