Payment channel extension should be able to be initialized in two steps:

* A constructor that only takes the wallet as an argument

* A setTransactionBroadcaster() which should be called when the Bitcoin network is ready

Motivation: Some wallets (MultiBitHD) does not use WalletAppKit, and starts with reading the wallet before initializing the bitcoin network.
Now these wallets can create the wallet (and the wallet file is read), and call the setter after the bitcoin network is up.
This commit is contained in:
ollekullberg 2014-08-11 12:23:36 +02:00 committed by Mike Hearn
parent 90492b61f7
commit fe91dc9110
4 changed files with 160 additions and 44 deletions

View File

@ -19,6 +19,8 @@ package com.google.bitcoin.kits;
import com.google.bitcoin.core.*;
import com.google.bitcoin.net.discovery.DnsDiscovery;
import com.google.bitcoin.protocols.channels.StoredPaymentChannelClientStates;
import com.google.bitcoin.protocols.channels.StoredPaymentChannelServerStates;
import com.google.bitcoin.store.BlockStoreException;
import com.google.bitcoin.store.SPVBlockStore;
import com.google.bitcoin.store.WalletProtobufSerializer;
@ -243,6 +245,9 @@ public class WalletAppKit extends AbstractIdleService {
boolean chainFileExists = chainFile.exists();
vWalletFile = new File(directory, filePrefix + ".wallet");
boolean shouldReplayWallet = (vWalletFile.exists() && !chainFileExists) || restoreFromSeed != null;
vWallet = createOrLoadWallet(shouldReplayWallet);
// Initiate Bitcoin network objects (block store, blockchain and peer group)
vStore = new SPVBlockStore(params, chainFile);
if ((!chainFileExists || restoreFromSeed != null) && checkpoints != null) {
// Initialize the chain file with a checkpoint to speed up first-run sync.
@ -257,16 +262,7 @@ public class WalletAppKit extends AbstractIdleService {
vStore = new SPVBlockStore(params, chainFile);
}
} else {
// Ugly hack! We have to create the wallet once here to learn the earliest key time, and then throw it
// away. The reason is that wallet extensions might need access to peergroups/chains/etc so we have to
// create the wallet later, but we need to know the time early here before we create the BlockChain
// object.
if (vWalletFile.exists()) {
FileInputStream stream = new FileInputStream(vWalletFile);
final WalletProtobufSerializer serializer = new WalletProtobufSerializer();
final Wallet wallet = serializer.readWallet(params, null, WalletProtobufSerializer.parseToProto(stream));
time = wallet.getEarliestKeyCreationTime();
}
time = vWallet.getEarliestKeyCreationTime();
}
CheckpointManager.checkpoint(params, checkpoints, vStore, time);
}
@ -275,35 +271,6 @@ public class WalletAppKit extends AbstractIdleService {
if (this.userAgent != null)
vPeerGroup.setUserAgent(userAgent, version);
maybeMoveOldWalletOutOfTheWay();
if (vWalletFile.exists()) {
FileInputStream walletStream = new FileInputStream(vWalletFile);
try {
List<WalletExtension> extensions = provideWalletExtensions();
vWallet = new Wallet(params);
WalletExtension[] extArray = extensions.toArray(new WalletExtension[extensions.size()]);
Protos.Wallet proto = WalletProtobufSerializer.parseToProto(walletStream);
final WalletProtobufSerializer serializer;
if (walletFactory != null)
serializer = new WalletProtobufSerializer(walletFactory);
else
serializer = new WalletProtobufSerializer();
vWallet = serializer.readWallet(params, extArray, proto);
if (shouldReplayWallet)
vWallet.clearTransactions(0);
} finally {
walletStream.close();
}
} else {
vWallet = createWallet();
vWallet.freshReceiveKey();
for (WalletExtension e : provideWalletExtensions()) {
vWallet.addExtension(e);
}
vWallet.saveToFile(vWalletFile);
}
if (useAutoSave) vWallet.autosaveToFile(vWalletFile, 200, TimeUnit.MILLISECONDS, null);
// Set up peer addresses or discovery first, so if wallet extensions try to broadcast a transaction
// before we're actually connected the broadcast waits for an appropriate number of connections.
if (peerAddresses != null) {
@ -322,6 +289,7 @@ public class WalletAppKit extends AbstractIdleService {
vPeerGroup.awaitRunning();
// Make sure we shut down cleanly.
installShutdownHook();
completeExtensionInitiations(vPeerGroup);
// TODO: Be able to use the provided download listener when doing a blocking startup.
final DownloadListener listener = new DownloadListener();
@ -332,6 +300,7 @@ public class WalletAppKit extends AbstractIdleService {
vPeerGroup.addListener(new Service.Listener() {
@Override
public void running() {
completeExtensionInitiations(vPeerGroup);
final PeerEventListener l = downloadListener == null ? new DownloadListener() : downloadListener;
vPeerGroup.startBlockChainDownload(l);
}
@ -347,6 +316,49 @@ public class WalletAppKit extends AbstractIdleService {
}
}
private Wallet createOrLoadWallet(boolean shouldReplayWallet) throws Exception {
Wallet wallet;
maybeMoveOldWalletOutOfTheWay();
if (vWalletFile.exists()) {
wallet = loadWallet(shouldReplayWallet);
} else {
wallet = createWallet();
wallet.freshReceiveKey();
for (WalletExtension e : provideWalletExtensions()) {
wallet.addExtension(e);
}
wallet.saveToFile(vWalletFile);
}
if (useAutoSave) wallet.autosaveToFile(vWalletFile, 200, TimeUnit.MILLISECONDS, null);
return wallet;
}
private Wallet loadWallet(boolean shouldReplayWallet) throws Exception {
Wallet wallet;
FileInputStream walletStream = new FileInputStream(vWalletFile);
try {
List<WalletExtension> extensions = provideWalletExtensions();
wallet = new Wallet(params);
WalletExtension[] extArray = extensions.toArray(new WalletExtension[extensions.size()]);
Protos.Wallet proto = WalletProtobufSerializer.parseToProto(walletStream);
final WalletProtobufSerializer serializer;
if (walletFactory != null)
serializer = new WalletProtobufSerializer(walletFactory);
else
serializer = new WalletProtobufSerializer();
wallet = serializer.readWallet(params, extArray, proto);
if (shouldReplayWallet)
wallet.clearTransactions(0);
} finally {
walletStream.close();
}
return wallet;
}
protected Wallet createWallet() {
KeyChainGroup kcg;
if (restoreFromSeed != null)
@ -376,6 +388,24 @@ public class WalletAppKit extends AbstractIdleService {
}
}
/*
* As soon as the transaction broadcaster han been created we will pass it to the
* payment channel extensions
*/
private void completeExtensionInitiations(TransactionBroadcaster transactionBroadcaster) {
StoredPaymentChannelClientStates clientStoredChannels = (StoredPaymentChannelClientStates)
vWallet.getExtensions().get(StoredPaymentChannelClientStates.class.getName());
if(clientStoredChannels != null) {
clientStoredChannels.setTransactionBroadcaster(transactionBroadcaster);
}
StoredPaymentChannelServerStates serverStoredChannels = (StoredPaymentChannelServerStates)
vWallet.getExtensions().get(StoredPaymentChannelServerStates.class.getName());
if(serverStoredChannels != null) {
serverStoredChannels.setTransactionBroadcaster(transactionBroadcaster);
}
}
protected PeerGroup createPeerGroup() throws TimeoutException {
if (useTor) {
TorClient torClient = new TorClient();

View File

@ -20,6 +20,7 @@ import com.google.bitcoin.core.*;
import com.google.bitcoin.utils.Threading;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.HashMultimap;
import com.google.common.util.concurrent.SettableFuture;
import com.google.protobuf.ByteString;
import net.jcip.annotations.GuardedBy;
import org.slf4j.Logger;
@ -30,6 +31,9 @@ import java.util.Date;
import java.util.Set;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.locks.ReentrantLock;
import static com.google.common.base.Preconditions.checkNotNull;
@ -42,12 +46,13 @@ import static com.google.common.base.Preconditions.checkState;
public class StoredPaymentChannelClientStates implements WalletExtension {
private static final Logger log = LoggerFactory.getLogger(StoredPaymentChannelClientStates.class);
static final String EXTENSION_ID = StoredPaymentChannelClientStates.class.getName();
static final int MAX_SECONDS_TO_WAIT_FOR_BROADCASTER_TO_BE_SET = 10;
@GuardedBy("lock") @VisibleForTesting final HashMultimap<Sha256Hash, StoredClientChannel> mapChannels = HashMultimap.create();
@VisibleForTesting final Timer channelTimeoutHandler = new Timer(true);
private Wallet containingWallet;
private final TransactionBroadcaster announcePeerGroup;
private final SettableFuture<TransactionBroadcaster> announcePeerGroupFuture = SettableFuture.create();
protected final ReentrantLock lock = Threading.lock("StoredPaymentChannelClientStates");
@ -57,10 +62,29 @@ public class StoredPaymentChannelClientStates implements WalletExtension {
* transactions.
*/
public StoredPaymentChannelClientStates(@Nullable Wallet containingWallet, TransactionBroadcaster announcePeerGroup) {
this.announcePeerGroup = checkNotNull(announcePeerGroup);
setTransactionBroadcaster(announcePeerGroup);
this.containingWallet = containingWallet;
}
/**
* Creates a new StoredPaymentChannelClientStates and associates it with the given {@link Wallet}
*
* Use this constructor if you use WalletAppKit, it will provide the broadcaster for you (no need to call the setter)
*/
public StoredPaymentChannelClientStates(@Nullable Wallet containingWallet) {
this.containingWallet = containingWallet;
}
/**
* Use this setter if the broadcaster is not available during instantiation and you're not using WalletAppKit.
* This setter will let you delay the setting of the broadcaster until the Bitcoin network is ready.
*
* @param transactionBroadcaster which is used to complete and announce contract and refund transactions.
*/
public void setTransactionBroadcaster(TransactionBroadcaster transactionBroadcaster) {
this.announcePeerGroupFuture.set(checkNotNull(transactionBroadcaster));
}
/** Returns this extension from the given wallet, or null if no such extension was added. */
@Nullable
public static StoredPaymentChannelClientStates getFromWallet(Wallet wallet) {
@ -171,6 +195,7 @@ public class StoredPaymentChannelClientStates implements WalletExtension {
channelTimeoutHandler.schedule(new TimerTask() {
@Override
public void run() {
TransactionBroadcaster announcePeerGroup = getAnnouncePeerGroup();
removeChannel(channel);
announcePeerGroup.broadcastTransaction(channel.contract);
announcePeerGroup.broadcastTransaction(channel.refund);
@ -184,6 +209,24 @@ public class StoredPaymentChannelClientStates implements WalletExtension {
containingWallet.addOrUpdateExtension(this);
}
/**
* If the peer group has not been set for MAX_SECONDS_TO_WAIT_FOR_BROADCASTER_TO_BE_SET seconds, then
* the programmer probably forgot to set it and we should throw exception.
*/
private TransactionBroadcaster getAnnouncePeerGroup() {
try {
return announcePeerGroupFuture.get(MAX_SECONDS_TO_WAIT_FOR_BROADCASTER_TO_BE_SET, TimeUnit.SECONDS);
} catch (InterruptedException e) {
throw new RuntimeException(e);
} catch (ExecutionException e) {
throw new RuntimeException(e);
} catch (TimeoutException e) {
String err = "Transaction broadcaster not set";
log.error(err);
throw new RuntimeException(err, e);
}
}
/**
* <p>Removes the channel with the given id from this set of stored states and notifies the wallet of an update to
* this wallet extension.</p>

View File

@ -19,12 +19,16 @@ package com.google.bitcoin.protocols.channels;
import com.google.bitcoin.core.*;
import com.google.bitcoin.utils.Threading;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.util.concurrent.SettableFuture;
import com.google.protobuf.ByteString;
import net.jcip.annotations.GuardedBy;
import org.slf4j.LoggerFactory;
import javax.annotation.Nullable;
import java.util.*;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.locks.ReentrantLock;
import static com.google.common.base.Preconditions.*;
@ -37,10 +41,11 @@ public class StoredPaymentChannelServerStates implements WalletExtension {
private static final org.slf4j.Logger log = LoggerFactory.getLogger(StoredPaymentChannelServerStates.class);
static final String EXTENSION_ID = StoredPaymentChannelServerStates.class.getName();
static final int MAX_SECONDS_TO_WAIT_FOR_BROADCASTER_TO_BE_SET = 10;
@GuardedBy("lock") @VisibleForTesting final Map<Sha256Hash, StoredServerChannel> mapChannels = new HashMap<Sha256Hash, StoredServerChannel>();
private Wallet wallet;
private final TransactionBroadcaster broadcaster;
private final SettableFuture<TransactionBroadcaster> broadcasterFuture = SettableFuture.create();
private final Timer channelTimeoutHandler = new Timer(true);
@ -60,8 +65,27 @@ public class StoredPaymentChannelServerStates implements WalletExtension {
* {@link TransactionBroadcaster} which are used to complete and announce payment transactions.
*/
public StoredPaymentChannelServerStates(@Nullable Wallet wallet, TransactionBroadcaster broadcaster) {
setTransactionBroadcaster(broadcaster);
this.wallet = wallet;
this.broadcaster = checkNotNull(broadcaster);
}
/**
* Creates a new PaymentChannelServerStateManager and associates it with the given {@link Wallet}
*
* Use this constructor if you use WalletAppKit, it will provide the broadcaster for you (no need to call the setter)
*/
public StoredPaymentChannelServerStates(@Nullable Wallet wallet) {
this.wallet = wallet;
}
/**
* Use this setter if the broadcaster is not available during instantiation and you're not using WalletAppKit.
* This setter will let you delay the setting of the broadcaster until the Bitcoin network is ready.
*
* @param broadcaster Used when the payment channels are closed
*/
public void setTransactionBroadcaster(TransactionBroadcaster broadcaster) {
this.broadcasterFuture.set(checkNotNull(broadcaster));
}
/**
@ -83,6 +107,7 @@ public class StoredPaymentChannelServerStates implements WalletExtension {
synchronized (channel) {
channel.closeConnectedHandler();
try {
TransactionBroadcaster broadcaster = getBroadcaster();
channel.getOrCreateState(wallet, broadcaster).close();
} catch (InsufficientMoneyException e) {
e.printStackTrace();
@ -94,6 +119,24 @@ public class StoredPaymentChannelServerStates implements WalletExtension {
wallet.addOrUpdateExtension(this);
}
/**
* If the broadcaster has not been set for MAX_SECONDS_TO_WAIT_FOR_BROADCASTER_TO_BE_SET seconds, then
* the programmer probably forgot to set it and we should throw exception.
*/
private TransactionBroadcaster getBroadcaster() {
try {
return broadcasterFuture.get(MAX_SECONDS_TO_WAIT_FOR_BROADCASTER_TO_BE_SET, TimeUnit.SECONDS);
} catch (InterruptedException e) {
throw new RuntimeException(e);
} catch (ExecutionException e) {
throw new RuntimeException(e);
} catch (TimeoutException e) {
String err = "Transaction broadcaster not set";
log.error(err);
throw new RuntimeException(err,e);
}
}
/**
* Gets the {@link StoredServerChannel} with the given channel id (ie contract transaction hash).
*/

View File

@ -61,7 +61,7 @@ public class ExamplePaymentChannelServer implements PaymentChannelServerListener
// The StoredPaymentChannelClientStates object is responsible for, amongst other things, broadcasting
// the refund transaction if its lock time has expired. It also persists channels so we can resume them
// after a restart.
return ImmutableList.<WalletExtension>of(new StoredPaymentChannelServerStates(null, peerGroup()));
return ImmutableList.<WalletExtension>of(new StoredPaymentChannelServerStates(null));
}
};
appKit.connectToLocalHost();