/** * Called when the connection terminates. Notifies the {@link StoredServerChannel} object that we * can attempt to resume this channel in the future and stops generating messages for the client. * * <p>Note that this <b>MUST</b> still be called even after either {@link * ServerConnection#destroyConnection(CloseReason)} or {@link PaymentChannelServer#close()} is * called to actually handle the connection close logic. */ public void connectionClosed() { lock.lock(); try { log.info("Server channel closed."); connectionOpen = false; try { if (state != null && state.getMultisigContract() != null) { StoredPaymentChannelServerStates channels = (StoredPaymentChannelServerStates) wallet.getExtensions().get(StoredPaymentChannelServerStates.EXTENSION_ID); if (channels != null) { StoredServerChannel storedServerChannel = channels.getChannel(state.getMultisigContract().getHash()); if (storedServerChannel != null) { storedServerChannel.clearConnectedHandler(); } } } } catch (IllegalStateException e) { // Expected when we call getMultisigContract() sometimes } } finally { lock.unlock(); } }
private synchronized void updateChannelInWallet() { if (storedServerChannel != null) { storedServerChannel.updateValueToMe(bestValueToMe, bestValueSignature); StoredPaymentChannelServerStates channels = (StoredPaymentChannelServerStates) wallet.getExtensions().get(StoredPaymentChannelServerStates.EXTENSION_ID); channels.updatedChannel(storedServerChannel); } }
/** * Stores this channel's state in the wallet as a part of a {@link * StoredPaymentChannelServerStates} wallet extension and keeps it up-to-date each time payment is * incremented. This will be automatically removed when a call to {@link * PaymentChannelServerState#close()} completes successfully. A channel may only be stored after * it has fully opened (ie state == State.READY). * * @param connectedHandler Optional {@link PaymentChannelServer} object that manages this object. * This will set the appropriate pointer in the newly created {@link StoredServerChannel} * before it is committed to wallet. If set, closing the state object will propagate the close * to the handler which can then do a TCP disconnect. */ public synchronized void storeChannelInWallet(@Nullable PaymentChannelServer connectedHandler) { checkState(state == State.READY); if (storedServerChannel != null) return; log.info("Storing state with contract hash {}.", multisigContract.getHash()); StoredPaymentChannelServerStates channels = (StoredPaymentChannelServerStates) wallet.addOrGetExistingExtension( new StoredPaymentChannelServerStates(wallet, broadcaster)); storedServerChannel = new StoredServerChannel( this, multisigContract, clientOutput, refundTransactionUnlockTimeSecs, serverKey, bestValueToMe, bestValueSignature); if (connectedHandler != null) checkState( storedServerChannel.setConnectedHandler(connectedHandler, false) == connectedHandler); channels.putChannel(storedServerChannel); }
/** * Closes this channel and broadcasts the highest value payment transaction on the network. * * <p>This will set the state to {@link State#CLOSED} if the transaction is successfully broadcast * on the network. If we fail to broadcast for some reason, the state is set to {@link * State#ERROR}. * * <p>If the current state is before {@link State#READY} (ie we have not finished initializing the * channel), we simply set the state to {@link State#CLOSED} and let the client handle getting its * refund transaction confirmed. * * @return a future which completes when the provided multisig contract successfully broadcasts, * or throws if the broadcast fails for some reason. Note that if the network simply rejects * the transaction, this future will never complete, a timeout should be used. * @throws InsufficientMoneyException If the payment tx would have cost more in fees to spend than * it is worth. */ public synchronized ListenableFuture<Transaction> close() throws InsufficientMoneyException { if (storedServerChannel != null) { StoredServerChannel temp = storedServerChannel; storedServerChannel = null; StoredPaymentChannelServerStates channels = (StoredPaymentChannelServerStates) wallet.getExtensions().get(StoredPaymentChannelServerStates.EXTENSION_ID); channels.closeChannel( temp); // May call this method again for us (if it wasn't the original caller) if (state.compareTo(State.CLOSING) >= 0) return closedFuture; } if (state.ordinal() < State.READY.ordinal()) { log.error("Attempt to settle channel in state " + state); state = State.CLOSED; closedFuture.set(null); return closedFuture; } if (state != State.READY) { // TODO: What is this codepath for? log.warn("Failed attempt to settle a channel in state " + state); return closedFuture; } Transaction tx = null; try { Wallet.SendRequest req = makeUnsignedChannelContract(bestValueToMe); tx = req.tx; // Provide a throwaway signature so that completeTx won't complain out about unsigned inputs // it doesn't // know how to sign. Note that this signature does actually have to be valid, so we can't use // a dummy // signature to save time, because otherwise completeTx will try to re-sign it to make it // valid and then // die. We could probably add features to the SendRequest API to make this a bit more // efficient. signMultisigInput(tx, Transaction.SigHash.NONE, true); // Let wallet handle adding additional inputs/fee as necessary. req.shuffleOutputs = false; req.missingSigsMode = Wallet.MissingSigsMode.USE_DUMMY_SIG; wallet.completeTx(req); // TODO: Fix things so shuffling is usable. feePaidForPayment = req.tx.getFee(); log.info("Calculated fee is {}", feePaidForPayment); if (feePaidForPayment.compareTo(bestValueToMe) > 0) { final String msg = String.format( Locale.US, "Had to pay more in fees (%s) than the channel was worth (%s)", feePaidForPayment, bestValueToMe); throw new InsufficientMoneyException(feePaidForPayment.subtract(bestValueToMe), msg); } // Now really sign the multisig input. signMultisigInput(tx, Transaction.SigHash.ALL, false); // Some checks that shouldn't be necessary but it can't hurt to check. tx.verify(); // Sanity check syntax. for (TransactionInput input : tx.getInputs()) input.verify(); // Run scripts and ensure it is valid. } catch (InsufficientMoneyException e) { throw e; // Don't fall through. } catch (Exception e) { log.error( "Could not verify self-built tx\nMULTISIG {}\nCLOSE {}", multisigContract, tx != null ? tx : ""); throw new RuntimeException(e); // Should never happen. } state = State.CLOSING; log.info("Closing channel, broadcasting tx {}", tx); // The act of broadcasting the transaction will add it to the wallet. ListenableFuture<Transaction> future = broadcaster.broadcastTransaction(tx).future(); Futures.addCallback( future, new FutureCallback<Transaction>() { @Override public void onSuccess(Transaction transaction) { log.info("TX {} propagated, channel successfully closed.", transaction.getHash()); state = State.CLOSED; closedFuture.set(transaction); } @Override public void onFailure(Throwable throwable) { log.error("Failed to settle channel, could not broadcast", throwable); state = State.ERROR; closedFuture.setException(throwable); } }); return closedFuture; }
@GuardedBy("lock") private void receiveVersionMessage(Protos.TwoWayChannelMessage msg) throws VerificationException { checkState(step == InitStep.WAITING_ON_CLIENT_VERSION && msg.hasClientVersion()); final Protos.ClientVersion clientVersion = msg.getClientVersion(); final int major = clientVersion.getMajor(); if (major != SERVER_MAJOR_VERSION) { error( "This server needs protocol version " + SERVER_MAJOR_VERSION + " , client offered " + major, Protos.Error.ErrorCode.NO_ACCEPTABLE_VERSION, CloseReason.NO_ACCEPTABLE_VERSION); return; } Protos.ServerVersion.Builder versionNegotiationBuilder = Protos.ServerVersion.newBuilder() .setMajor(SERVER_MAJOR_VERSION) .setMinor(SERVER_MINOR_VERSION); conn.sendToClient( Protos.TwoWayChannelMessage.newBuilder() .setType(Protos.TwoWayChannelMessage.MessageType.SERVER_VERSION) .setServerVersion(versionNegotiationBuilder) .build()); ByteString reopenChannelContractHash = clientVersion.getPreviousChannelContractHash(); if (reopenChannelContractHash != null && reopenChannelContractHash.size() == 32) { Sha256Hash contractHash = new Sha256Hash(reopenChannelContractHash.toByteArray()); log.info("New client that wants to resume {}", contractHash); StoredPaymentChannelServerStates channels = (StoredPaymentChannelServerStates) wallet.getExtensions().get(StoredPaymentChannelServerStates.EXTENSION_ID); if (channels != null) { StoredServerChannel storedServerChannel = channels.getChannel(contractHash); if (storedServerChannel != null) { final PaymentChannelServer existingHandler = storedServerChannel.setConnectedHandler(this, false); if (existingHandler != this) { log.warn(" ... and that channel is already in use, disconnecting other user."); existingHandler.close(); storedServerChannel.setConnectedHandler(this, true); } log.info("Got resume version message, responding with VERSIONS and CHANNEL_OPEN"); state = storedServerChannel.getOrCreateState(wallet, broadcaster); step = InitStep.CHANNEL_OPEN; conn.sendToClient( Protos.TwoWayChannelMessage.newBuilder() .setType(Protos.TwoWayChannelMessage.MessageType.CHANNEL_OPEN) .build()); conn.channelOpen(contractHash); return; } else { log.error(" ... but we do not have any record of that contract! Resume failed."); } } else { log.error(" ... but we do not have any stored channels! Resume failed."); } } log.info( "Got initial version message, responding with VERSIONS and INITIATE: min value={}", minAcceptedChannelSize.value); myKey = new ECKey(); wallet.freshReceiveKey(); expireTime = Utils.currentTimeSeconds() + truncateTimeWindow(clientVersion.getTimeWindowSecs()); step = InitStep.WAITING_ON_UNSIGNED_REFUND; Protos.Initiate.Builder initiateBuilder = Protos.Initiate.newBuilder() .setMultisigKey(ByteString.copyFrom(myKey.getPubKey())) .setExpireTimeSecs(expireTime) .setMinAcceptedChannelSize(minAcceptedChannelSize.value) .setMinPayment(Transaction.REFERENCE_DEFAULT_MIN_TX_FEE.value); conn.sendToClient( Protos.TwoWayChannelMessage.newBuilder() .setInitiate(initiateBuilder) .setType(Protos.TwoWayChannelMessage.MessageType.INITIATE) .build()); }