Payment channels: bug fixes and improved close behaviour.

The client now has a new CLOSED state, which is entered once a CLOSE has been sent and the close transaction (final contract) has been broadcast onto the P2P network and entered the wallet. Once received, the hash of the close tx is stored in the wallet - the tx is itself already in the wallets spent pool because it connects to the output of the multisig tx. After seeing three confirmations of the close TX the state is deleted from the client wallet for good.

 Together these changes resolve a bug/design issue in which if a channel was opened, then closed, then another channel was opened but not closed, then a third attempt to connect to the server was made, the client would try to resume the first closed channel. That would fail because the server already deleted its state object and result in new channels being created even though the second could have been resumed. By tracking the fact that the channel was closed, it can be skipped when considering what channel to resume.
This commit is contained in:
Mike Hearn 2013-09-30 14:33:30 +02:00
parent 38dadf4667
commit 02416c97fa
5 changed files with 352 additions and 20 deletions

View File

@ -764,6 +764,28 @@ public final class ClientState {
* <code>required uint64 refundFees = 6;</code>
*/
long getRefundFees();
// optional bytes closeTransactionHash = 7;
/**
* <code>optional bytes closeTransactionHash = 7;</code>
*
* <pre>
* When set, the hash of the transaction that was presented by the server for closure of the channel.
* It spends the contractTransaction and is expected to be broadcast to the network by the server.
* It's supposed to be in the wallet already.
* </pre>
*/
boolean hasCloseTransactionHash();
/**
* <code>optional bytes closeTransactionHash = 7;</code>
*
* <pre>
* When set, the hash of the transaction that was presented by the server for closure of the channel.
* It spends the contractTransaction and is expected to be broadcast to the network by the server.
* It's supposed to be in the wallet already.
* </pre>
*/
com.google.protobuf.ByteString getCloseTransactionHash();
}
/**
* Protobuf type {@code paymentchannels.StoredClientPaymentChannel}
@ -851,6 +873,11 @@ public final class ClientState {
refundFees_ = input.readUInt64();
break;
}
case 58: {
bitField0_ |= 0x00000040;
closeTransactionHash_ = input.readBytes();
break;
}
}
}
} catch (com.google.protobuf.InvalidProtocolBufferException e) {
@ -987,6 +1014,34 @@ public final class ClientState {
return refundFees_;
}
// optional bytes closeTransactionHash = 7;
public static final int CLOSETRANSACTIONHASH_FIELD_NUMBER = 7;
private com.google.protobuf.ByteString closeTransactionHash_;
/**
* <code>optional bytes closeTransactionHash = 7;</code>
*
* <pre>
* When set, the hash of the transaction that was presented by the server for closure of the channel.
* It spends the contractTransaction and is expected to be broadcast to the network by the server.
* It's supposed to be in the wallet already.
* </pre>
*/
public boolean hasCloseTransactionHash() {
return ((bitField0_ & 0x00000040) == 0x00000040);
}
/**
* <code>optional bytes closeTransactionHash = 7;</code>
*
* <pre>
* When set, the hash of the transaction that was presented by the server for closure of the channel.
* It spends the contractTransaction and is expected to be broadcast to the network by the server.
* It's supposed to be in the wallet already.
* </pre>
*/
public com.google.protobuf.ByteString getCloseTransactionHash() {
return closeTransactionHash_;
}
private void initFields() {
id_ = com.google.protobuf.ByteString.EMPTY;
contractTransaction_ = com.google.protobuf.ByteString.EMPTY;
@ -994,6 +1049,7 @@ public final class ClientState {
myKey_ = com.google.protobuf.ByteString.EMPTY;
valueToMe_ = 0L;
refundFees_ = 0L;
closeTransactionHash_ = com.google.protobuf.ByteString.EMPTY;
}
private byte memoizedIsInitialized = -1;
public final boolean isInitialized() {
@ -1049,6 +1105,9 @@ public final class ClientState {
if (((bitField0_ & 0x00000020) == 0x00000020)) {
output.writeUInt64(6, refundFees_);
}
if (((bitField0_ & 0x00000040) == 0x00000040)) {
output.writeBytes(7, closeTransactionHash_);
}
getUnknownFields().writeTo(output);
}
@ -1082,6 +1141,10 @@ public final class ClientState {
size += com.google.protobuf.CodedOutputStream
.computeUInt64Size(6, refundFees_);
}
if (((bitField0_ & 0x00000040) == 0x00000040)) {
size += com.google.protobuf.CodedOutputStream
.computeBytesSize(7, closeTransactionHash_);
}
size += getUnknownFields().getSerializedSize();
memoizedSerializedSize = size;
return size;
@ -1215,6 +1278,8 @@ public final class ClientState {
bitField0_ = (bitField0_ & ~0x00000010);
refundFees_ = 0L;
bitField0_ = (bitField0_ & ~0x00000020);
closeTransactionHash_ = com.google.protobuf.ByteString.EMPTY;
bitField0_ = (bitField0_ & ~0x00000040);
return this;
}
@ -1267,6 +1332,10 @@ public final class ClientState {
to_bitField0_ |= 0x00000020;
}
result.refundFees_ = refundFees_;
if (((from_bitField0_ & 0x00000040) == 0x00000040)) {
to_bitField0_ |= 0x00000040;
}
result.closeTransactionHash_ = closeTransactionHash_;
result.bitField0_ = to_bitField0_;
onBuilt();
return result;
@ -1301,6 +1370,9 @@ public final class ClientState {
if (other.hasRefundFees()) {
setRefundFees(other.getRefundFees());
}
if (other.hasCloseTransactionHash()) {
setCloseTransactionHash(other.getCloseTransactionHash());
}
this.mergeUnknownFields(other.getUnknownFields());
return this;
}
@ -1562,6 +1634,66 @@ public final class ClientState {
return this;
}
// optional bytes closeTransactionHash = 7;
private com.google.protobuf.ByteString closeTransactionHash_ = com.google.protobuf.ByteString.EMPTY;
/**
* <code>optional bytes closeTransactionHash = 7;</code>
*
* <pre>
* When set, the hash of the transaction that was presented by the server for closure of the channel.
* It spends the contractTransaction and is expected to be broadcast to the network by the server.
* It's supposed to be in the wallet already.
* </pre>
*/
public boolean hasCloseTransactionHash() {
return ((bitField0_ & 0x00000040) == 0x00000040);
}
/**
* <code>optional bytes closeTransactionHash = 7;</code>
*
* <pre>
* When set, the hash of the transaction that was presented by the server for closure of the channel.
* It spends the contractTransaction and is expected to be broadcast to the network by the server.
* It's supposed to be in the wallet already.
* </pre>
*/
public com.google.protobuf.ByteString getCloseTransactionHash() {
return closeTransactionHash_;
}
/**
* <code>optional bytes closeTransactionHash = 7;</code>
*
* <pre>
* When set, the hash of the transaction that was presented by the server for closure of the channel.
* It spends the contractTransaction and is expected to be broadcast to the network by the server.
* It's supposed to be in the wallet already.
* </pre>
*/
public Builder setCloseTransactionHash(com.google.protobuf.ByteString value) {
if (value == null) {
throw new NullPointerException();
}
bitField0_ |= 0x00000040;
closeTransactionHash_ = value;
onChanged();
return this;
}
/**
* <code>optional bytes closeTransactionHash = 7;</code>
*
* <pre>
* When set, the hash of the transaction that was presented by the server for closure of the channel.
* It spends the contractTransaction and is expected to be broadcast to the network by the server.
* It's supposed to be in the wallet already.
* </pre>
*/
public Builder clearCloseTransactionHash() {
bitField0_ = (bitField0_ & ~0x00000040);
closeTransactionHash_ = getDefaultInstance().getCloseTransactionHash();
onChanged();
return this;
}
// @@protoc_insertion_point(builder_scope:paymentchannels.StoredClientPaymentChannel)
}
@ -1595,12 +1727,13 @@ public final class ClientState {
"\n storedclientpaymentchannel.proto\022\017paym" +
"entchannels\"\\\n\033StoredClientPaymentChanne" +
"ls\022=\n\010channels\030\001 \003(\0132+.paymentchannels.S" +
"toredClientPaymentChannel\"\226\001\n\032StoredClie" +
"toredClientPaymentChannel\"\264\001\n\032StoredClie" +
"ntPaymentChannel\022\n\n\002id\030\001 \002(\014\022\033\n\023contract" +
"Transaction\030\002 \002(\014\022\031\n\021refundTransaction\030\003" +
" \002(\014\022\r\n\005myKey\030\004 \002(\014\022\021\n\tvalueToMe\030\005 \002(\004\022\022" +
"\n\nrefundFees\030\006 \002(\004B4\n%com.google.bitcoin" +
".protocols.channelsB\013ClientState"
"\n\nrefundFees\030\006 \002(\004\022\034\n\024closeTransactionHa" +
"sh\030\007 \001(\014B4\n%com.google.bitcoin.protocols" +
".channelsB\013ClientState"
};
com.google.protobuf.Descriptors.FileDescriptor.InternalDescriptorAssigner assigner =
new com.google.protobuf.Descriptors.FileDescriptor.InternalDescriptorAssigner() {
@ -1618,7 +1751,7 @@ public final class ClientState {
internal_static_paymentchannels_StoredClientPaymentChannel_fieldAccessorTable = new
com.google.protobuf.GeneratedMessage.FieldAccessorTable(
internal_static_paymentchannels_StoredClientPaymentChannel_descriptor,
new java.lang.String[] { "Id", "ContractTransaction", "RefundTransaction", "MyKey", "ValueToMe", "RefundFees", });
new java.lang.String[] { "Id", "ContractTransaction", "RefundTransaction", "MyKey", "ValueToMe", "RefundFees", "CloseTransactionHash", });
return null;
}
};

View File

@ -20,6 +20,7 @@ import com.google.bitcoin.core.*;
import com.google.bitcoin.crypto.TransactionSignature;
import com.google.bitcoin.script.Script;
import com.google.bitcoin.script.ScriptBuilder;
import com.google.bitcoin.utils.Threading;
import com.google.bitcoin.wallet.AllowUnconfirmedCoinSelector;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableList;
@ -64,6 +65,7 @@ import static com.google.common.base.Preconditions.*;
*/
public class PaymentChannelClientState {
private static final Logger log = LoggerFactory.getLogger(PaymentChannelClientState.class);
private static final int CONFIRMATIONS_FOR_DELETE = 3;
private final Wallet wallet;
// Both sides need a key (private in our case, public for the server) in order to manage the multisig contract
@ -97,7 +99,8 @@ public class PaymentChannelClientState {
SAVE_STATE_IN_WALLET,
PROVIDE_MULTISIG_CONTRACT_TO_SERVER,
READY,
EXPIRED
EXPIRED,
CLOSED
}
private State state;
@ -118,6 +121,16 @@ public class PaymentChannelClientState {
this.valueToMe = checkNotNull(storedClientChannel.valueToMe);
this.storedChannel = storedClientChannel;
this.state = State.READY;
initWalletListeners();
}
private boolean isCloseTransaction(Transaction tx) {
try {
tx.getInput(0).verify(multisigContract.getOutput(0));
return true;
} catch (VerificationException e) {
return false;
}
}
/**
@ -139,6 +152,7 @@ public class PaymentChannelClientState {
BigInteger value, long expiryTimeInSeconds) throws VerificationException {
checkArgument(value.compareTo(BigInteger.ZERO) > 0);
this.wallet = checkNotNull(wallet);
initWalletListeners();
this.serverMultisigKey = checkNotNull(serverMultisigKey);
if (!myKey.isPubKeyCanonical() || !serverMultisigKey.isPubKeyCanonical())
throw new VerificationException("Pubkey was not canonical (ie non-standard)");
@ -148,6 +162,51 @@ public class PaymentChannelClientState {
this.state = State.NEW;
}
private synchronized void initWalletListeners() {
// Register a listener that watches out for the server closing the channel.
if (storedChannel != null && storedChannel.close != null) {
watchCloseConfirmations();
}
wallet.addEventListener(new AbstractWalletEventListener() {
@Override
public void onCoinsReceived(Wallet wallet, Transaction tx, BigInteger prevBalance, BigInteger newBalance) {
synchronized (PaymentChannelClientState.this) {
if (multisigContract == null) return;
if (isCloseTransaction(tx)) {
log.info("Close: transaction {} closed contract {}", tx.getHash(), multisigContract.getHash());
// Record the fact that it was closed along with the transaction that closed it.
state = State.CLOSED;
if (storedChannel == null) return;
storedChannel.close = tx;
updateChannelInWallet();
watchCloseConfirmations();
}
}
}
}, Threading.SAME_THREAD);
}
private void watchCloseConfirmations() {
// When we see the close transaction get a few confirmations, we can just delete the record
// of this channel along with the refund tx from the wallet, because we're not going to need
// any of that any more.
storedChannel.close.getConfidence().getDepthFuture(CONFIRMATIONS_FOR_DELETE).addListener(new Runnable() {
@Override
public void run() {
deleteChannelFromWallet();
}
}, Threading.SAME_THREAD);
}
private synchronized void deleteChannelFromWallet() {
log.info("Close tx has confirmed, deleting channel from wallet: {}", storedChannel);
StoredPaymentChannelClientStates channels = (StoredPaymentChannelClientStates)
wallet.getExtensions().get(StoredPaymentChannelClientStates.EXTENSION_ID);
channels.removeChannel(storedChannel);
wallet.addOrUpdateExtension(channels);
storedChannel = null;
}
/**
* This object implements a state machine, and this accessor returns which state it's currently in.
*/
@ -359,7 +418,6 @@ public class PaymentChannelClientState {
synchronized (storedChannel) {
storedChannel.active = false;
}
storedChannel = null;
}
/**

View File

@ -22,6 +22,8 @@ import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.HashMultimap;
import com.google.protobuf.ByteString;
import net.jcip.annotations.GuardedBy;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.annotation.Nullable;
import java.math.BigInteger;
@ -39,6 +41,7 @@ import static com.google.common.base.Preconditions.checkState;
* and broadcasting the refund transaction over the given {@link TransactionBroadcaster}.
*/
public class StoredPaymentChannelClientStates implements WalletExtension {
private static final Logger log = LoggerFactory.getLogger(StoredPaymentChannelClientStates.class);
static final String EXTENSION_ID = StoredPaymentChannelClientStates.class.getName();
@GuardedBy("lock") @VisibleForTesting final HashMultimap<Sha256Hash, StoredClientChannel> mapChannels = HashMultimap.create();
@ -59,6 +62,30 @@ public class StoredPaymentChannelClientStates implements WalletExtension {
this.containingWallet = checkNotNull(containingWallet);
}
/** Returns this extension from the given wallet, or null if no such extension was added. */
@Nullable
public static StoredPaymentChannelClientStates getFromWallet(Wallet wallet) {
return (StoredPaymentChannelClientStates) wallet.getExtensions().get(EXTENSION_ID);
}
/** Returns the outstanding amount of money sent back to us for all channels to this server added together. */
public BigInteger getBalanceForServer(Sha256Hash id) {
BigInteger balance = BigInteger.ZERO;
lock.lock();
try {
Set<StoredClientChannel> setChannels = mapChannels.get(id);
for (StoredClientChannel channel : setChannels) {
synchronized (channel) {
if (channel.close != null) continue;
balance = balance.add(channel.valueToMe);
}
}
return balance;
} finally {
lock.unlock();
}
}
/**
* Finds an inactive channel with the given id and returns it, or returns null.
*/
@ -70,12 +97,17 @@ public class StoredPaymentChannelClientStates implements WalletExtension {
for (StoredClientChannel channel : setChannels) {
synchronized (channel) {
// Check if the channel is usable (has money, inactive) and if so, activate it.
if (channel.valueToMe.equals(BigInteger.ZERO))
log.info("Considering channel {} contract {}", channel.hashCode(), channel.contract.getHash());
if (channel.close != null || channel.valueToMe.equals(BigInteger.ZERO)) {
log.info(" ... but is closed or empty");
continue;
}
if (!channel.active) {
log.info(" ... activating");
channel.active = true;
return channel;
}
log.info(" ... but is already active");
}
}
} finally {
@ -169,13 +201,16 @@ public class StoredPaymentChannelClientStates implements WalletExtension {
checkState(channel.refundFees.compareTo(BigInteger.ZERO) >= 0 && channel.refundFees.compareTo(NetworkParameters.MAX_MONEY) < 0);
checkNotNull(channel.myKey.getPrivKeyBytes());
checkState(channel.refund.getConfidence().getSource() == TransactionConfidence.Source.SELF);
builder.addChannels(ClientState.StoredClientPaymentChannel.newBuilder()
final ClientState.StoredClientPaymentChannel.Builder value = ClientState.StoredClientPaymentChannel.newBuilder()
.setId(ByteString.copyFrom(channel.id.getBytes()))
.setContractTransaction(ByteString.copyFrom(channel.contract.bitcoinSerialize()))
.setRefundTransaction(ByteString.copyFrom(channel.refund.bitcoinSerialize()))
.setMyKey(ByteString.copyFrom(channel.myKey.getPrivKeyBytes()))
.setValueToMe(channel.valueToMe.longValue())
.setRefundFees(channel.refundFees.longValue()));
.setRefundFees(channel.refundFees.longValue());
if (channel.close != null)
value.setCloseTransactionHash(ByteString.copyFrom(channel.close.getHash().getBytes()));
builder.addChannels(value);
}
return builder.build().toByteArray();
} finally {
@ -200,6 +235,8 @@ public class StoredPaymentChannelClientStates implements WalletExtension {
new ECKey(new BigInteger(1, storedState.getMyKey().toByteArray()), null, true),
BigInteger.valueOf(storedState.getValueToMe()),
BigInteger.valueOf(storedState.getRefundFees()), false);
if (storedState.hasCloseTransactionHash())
channel.close = containingWallet.getTransaction(new Sha256Hash(storedState.toByteArray()));
putChannel(channel, false);
}
} finally {
@ -229,6 +266,8 @@ public class StoredPaymentChannelClientStates implements WalletExtension {
class StoredClientChannel {
Sha256Hash id;
Transaction contract, refund;
// The transaction that closed the channel (generated by the server)
Transaction close;
ECKey myKey;
BigInteger valueToMe, refundFees;
@ -249,14 +288,17 @@ class StoredClientChannel {
@Override
public String toString() {
final String newline = String.format("%n");
final String closeStr = close == null ? "still open" : close.toString().replaceAll(newline, newline + " ");
return String.format("Stored client channel for server ID %s (%s)%n" +
" Key: %s%n" +
" Value left: %d%n" +
" Refund fees: %d%n" +
" Contract: %s" +
"Refund: %s",
" Key: %s%n" +
" Value left: %d%n" +
" Refund fees: %d%n" +
" Contract: %s" +
"Refund: %s" +
"Close: %s",
id, active ? "active" : "inactive", myKey, valueToMe, refundFees,
contract.toString().replaceAll(newline, newline + " "),
refund.toString().replaceAll(newline, newline + " "));
refund.toString().replaceAll(newline, newline + " "),
closeStr);
}
}

View File

@ -42,4 +42,8 @@ message StoredClientPaymentChannel {
required bytes myKey = 4;
required uint64 valueToMe = 5;
required uint64 refundFees = 6;
// When set, the hash of the transaction that was presented by the server for closure of the channel.
// It spends the contractTransaction and is expected to be broadcast to the network by the server.
// It's supposed to be in the wallet already.
optional bytes closeTransactionHash = 7;
}

View File

@ -41,11 +41,13 @@ import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import static com.google.bitcoin.protocols.channels.PaymentChannelCloseException.CloseReason;
import static com.google.bitcoin.utils.TestUtils.createFakeBlock;
import static org.bitcoin.paymentchannel.Protos.TwoWayChannelMessage.MessageType;
import static org.junit.Assert.*;
public class ChannelConnectionTest extends TestWithWallet {
private Wallet serverWallet;
private BlockChain serverChain;
private AtomicBoolean fail;
private BlockingQueue<Transaction> broadcasts;
private TransactionBroadcaster mockBroadcaster;
@ -65,11 +67,10 @@ public class ChannelConnectionTest extends TestWithWallet {
sendMoneyToWallet(Utils.COIN, AbstractBlockChain.NewBlockType.BEST_CHAIN);
sendMoneyToWallet(Utils.COIN, AbstractBlockChain.NewBlockType.BEST_CHAIN);
wallet.addExtension(new StoredPaymentChannelClientStates(wallet, failBroadcaster));
chain = new BlockChain(params, wallet, blockStore); // Recreate chain as sendMoneyToWallet will confuse it
serverWallet = new Wallet(params);
serverWallet.addExtension(new StoredPaymentChannelServerStates(serverWallet, failBroadcaster));
serverWallet.addKey(new ECKey());
chain.addWallet(serverWallet);
serverChain = new BlockChain(params, serverWallet, blockStore);
// Use an atomic boolean to indicate failure because fail()/assert*() dont work in network threads
fail = new AtomicBoolean(false);
@ -181,16 +182,27 @@ public class ChannelConnectionTest extends TestWithWallet {
client.close();
broadcastTxPause.release();
broadcasts.take();
Transaction closeTx = broadcasts.take();
assertEquals(PaymentChannelServerState.State.CLOSED, serverState.getState());
if (!serverState.getBestValueToMe().equals(Utils.CENT.multiply(BigInteger.valueOf(3))) || !serverState.getFeePaid().equals(BigInteger.ZERO))
fail();
assertTrue(channels.mapChannels.isEmpty());
// Send the close TX to the client wallet.
sendMoneyToWallet(closeTx, AbstractBlockChain.NewBlockType.BEST_CHAIN);
assertEquals(PaymentChannelClientState.State.CLOSED, client.state().getState());
server.close();
server.close();
// Now confirm the close TX and see if the channel deletes itself from the wallet.
assertEquals(1, StoredPaymentChannelClientStates.getFromWallet(wallet).mapChannels.size());
wallet.notifyNewBestBlock(createFakeBlock(blockStore).storedBlock);
assertEquals(1, StoredPaymentChannelClientStates.getFromWallet(wallet).mapChannels.size());
wallet.notifyNewBestBlock(createFakeBlock(blockStore).storedBlock);
assertEquals(1, StoredPaymentChannelClientStates.getFromWallet(wallet).mapChannels.size());
wallet.notifyNewBestBlock(createFakeBlock(blockStore).storedBlock);
assertEquals(0, StoredPaymentChannelClientStates.getFromWallet(wallet).mapChannels.size());
}
@Test
@ -602,4 +614,87 @@ public class ChannelConnectionTest extends TestWithWallet {
Protos.TwoWayChannelMessage msg = pair.clientRecorder.checkNextMsg(MessageType.CLIENT_VERSION);
assertFalse(msg.getClientVersion().hasPreviousChannelContractHash());
}
@Test
public void repeatedChannels() throws Exception {
// Ensures we're selecting channels correctly. Covers a bug in which we'd always try and fail to resume
// the first channel due to lack of proper closing behaviour.
// Open up a normal channel, but don't spend all of it, then close it.
{
Sha256Hash someServerId = Sha256Hash.ZERO_HASH;
ChannelTestUtils.RecordingPair pair = ChannelTestUtils.makeRecorders(serverWallet, mockBroadcaster);
pair.server.connectionOpen();
PaymentChannelClient client = new PaymentChannelClient(wallet, myKey, Utils.COIN, someServerId, pair.clientRecorder);
PaymentChannelServer server = pair.server;
client.connectionOpen();
server.receiveMessage(pair.clientRecorder.checkNextMsg(MessageType.CLIENT_VERSION));
client.receiveMessage(pair.serverRecorder.checkNextMsg(MessageType.SERVER_VERSION));
client.receiveMessage(pair.serverRecorder.checkNextMsg(MessageType.INITIATE));
server.receiveMessage(pair.clientRecorder.checkNextMsg(MessageType.PROVIDE_REFUND));
client.receiveMessage(pair.serverRecorder.checkNextMsg(MessageType.RETURN_REFUND));
broadcastTxPause.release();
server.receiveMessage(pair.clientRecorder.checkNextMsg(MessageType.PROVIDE_CONTRACT));
broadcasts.take();
client.receiveMessage(pair.serverRecorder.checkNextMsg(MessageType.CHANNEL_OPEN));
Sha256Hash contractHash = (Sha256Hash) pair.serverRecorder.q.take();
pair.clientRecorder.checkOpened();
assertNull(pair.serverRecorder.q.poll());
assertNull(pair.clientRecorder.q.poll());
client.incrementPayment(Utils.CENT);
client.incrementPayment(Utils.CENT);
client.incrementPayment(Utils.CENT);
server.receiveMessage(pair.clientRecorder.checkNextMsg(MessageType.UPDATE_PAYMENT));
server.receiveMessage(pair.clientRecorder.checkNextMsg(MessageType.UPDATE_PAYMENT));
server.receiveMessage(pair.clientRecorder.checkNextMsg(MessageType.UPDATE_PAYMENT));
// Close it and verify it's considered to be closed.
broadcastTxPause.release();
client.close();
server.receiveMessage(pair.clientRecorder.checkNextMsg(MessageType.CLOSE));
Transaction close = broadcasts.take();
sendMoneyToWallet(close, AbstractBlockChain.NewBlockType.BEST_CHAIN);
client.connectionClosed();
server.connectionClosed();
}
// Now open a second channel and don't spend all of it/don't close it.
{
Sha256Hash someServerId = Sha256Hash.ZERO_HASH;
ChannelTestUtils.RecordingPair pair = ChannelTestUtils.makeRecorders(serverWallet, mockBroadcaster);
pair.server.connectionOpen();
PaymentChannelClient client = new PaymentChannelClient(wallet, myKey, Utils.COIN, someServerId, pair.clientRecorder);
PaymentChannelServer server = pair.server;
client.connectionOpen();
final Protos.TwoWayChannelMessage msg = pair.clientRecorder.checkNextMsg(MessageType.CLIENT_VERSION);
assertFalse(msg.getClientVersion().hasPreviousChannelContractHash());
server.receiveMessage(msg);
client.receiveMessage(pair.serverRecorder.checkNextMsg(MessageType.SERVER_VERSION));
client.receiveMessage(pair.serverRecorder.checkNextMsg(MessageType.INITIATE));
server.receiveMessage(pair.clientRecorder.checkNextMsg(MessageType.PROVIDE_REFUND));
client.receiveMessage(pair.serverRecorder.checkNextMsg(MessageType.RETURN_REFUND));
broadcastTxPause.release();
server.receiveMessage(pair.clientRecorder.checkNextMsg(MessageType.PROVIDE_CONTRACT));
broadcasts.take();
client.receiveMessage(pair.serverRecorder.checkNextMsg(MessageType.CHANNEL_OPEN));
Sha256Hash contractHash = (Sha256Hash) pair.serverRecorder.q.take();
pair.clientRecorder.checkOpened();
assertNull(pair.serverRecorder.q.poll());
assertNull(pair.clientRecorder.q.poll());
client.incrementPayment(Utils.CENT);
server.receiveMessage(pair.clientRecorder.checkNextMsg(MessageType.UPDATE_PAYMENT));
client.connectionClosed();
server.connectionClosed();
}
// Now connect again and check we resume the second channel.
{
Sha256Hash someServerId = Sha256Hash.ZERO_HASH;
ChannelTestUtils.RecordingPair pair = ChannelTestUtils.makeRecorders(serverWallet, mockBroadcaster);
pair.server.connectionOpen();
PaymentChannelClient client = new PaymentChannelClient(wallet, myKey, Utils.COIN, someServerId, pair.clientRecorder);
PaymentChannelServer server = pair.server;
client.connectionOpen();
server.receiveMessage(pair.clientRecorder.checkNextMsg(MessageType.CLIENT_VERSION));
client.receiveMessage(pair.serverRecorder.checkNextMsg(MessageType.SERVER_VERSION));
client.receiveMessage(pair.serverRecorder.checkNextMsg(MessageType.CHANNEL_OPEN));
}
assertEquals(2, StoredPaymentChannelClientStates.getFromWallet(wallet).mapChannels.size());
}
}