private long truncateTimeWindow(long timeWindow) { if (timeWindow < minTimeWindow) { log.info( "client requested time window {} s to short, offering {} s", timeWindow, minTimeWindow); return minTimeWindow; } if (timeWindow > maxTimeWindow) { log.info( "client requested time window {} s to long, offering {} s", timeWindow, minTimeWindow); return maxTimeWindow; } return timeWindow; }
@GuardedBy("lock") private void receiveUpdatePaymentMessage(Protos.UpdatePayment msg, boolean sendAck) throws VerificationException, ValueOutOfRangeException, InsufficientMoneyException { log.info("Got a payment update"); Coin lastBestPayment = state.getBestValueToMe(); final Coin refundSize = Coin.valueOf(msg.getClientChangeValue()); boolean stillUsable = state.incrementPayment(refundSize, msg.getSignature().toByteArray()); Coin bestPaymentChange = state.getBestValueToMe().subtract(lastBestPayment); ListenableFuture<ByteString> ackInfoFuture = null; if (bestPaymentChange.signum() > 0) { ByteString info = (msg.hasInfo()) ? msg.getInfo() : null; ackInfoFuture = conn.paymentIncrease(bestPaymentChange, state.getBestValueToMe(), info); } if (sendAck) { final Protos.TwoWayChannelMessage.Builder ack = Protos.TwoWayChannelMessage.newBuilder(); ack.setType(Protos.TwoWayChannelMessage.MessageType.PAYMENT_ACK); if (ackInfoFuture == null) { conn.sendToClient(ack.build()); } else { Futures.addCallback( ackInfoFuture, new FutureCallback<ByteString>() { @Override public void onSuccess(@Nullable ByteString result) { if (result != null) ack.setPaymentAck(ack.getPaymentAckBuilder().setInfo(result)); conn.sendToClient(ack.build()); } @Override public void onFailure(Throwable t) { log.info("Failed retrieving paymentIncrease info future"); error( "Failed processing payment update", Protos.Error.ErrorCode.OTHER, CloseReason.UPDATE_PAYMENT_FAILED); } }); } } if (!stillUsable) { log.info("Channel is now fully exhausted, closing/initiating settlement"); settlePayment(CloseReason.CHANNEL_EXHAUSTED); } }
/** * 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(); } }
/** * Called to indicate the connection has been opened and messages can now be generated for the * client. */ public void connectionOpen() { lock.lock(); try { log.info("New server channel active."); connectionOpen = true; } finally { lock.unlock(); } }
@GuardedBy("lock") private void receiveCloseMessage() throws InsufficientMoneyException { log.info("Got CLOSE message, closing channel"); if (state != null) { settlePayment(CloseReason.CLIENT_REQUESTED_CLOSE); } else { conn.destroyConnection(CloseReason.CLIENT_REQUESTED_CLOSE); } }
private void multisigContractPropogated( Protos.ProvideContract providedContract, Sha256Hash contractHash) { lock.lock(); try { if (!connectionOpen || channelSettling) return; state.storeChannelInWallet(PaymentChannelServer.this); try { receiveUpdatePaymentMessage(providedContract.getInitialPayment(), false /* no ack msg */); } catch (VerificationException e) { log.error("Initial payment failed to verify", e); error( e.getMessage(), Protos.Error.ErrorCode.BAD_TRANSACTION, CloseReason.REMOTE_SENT_INVALID_MESSAGE); return; } catch (ValueOutOfRangeException e) { log.error("Initial payment value was out of range", e); error( e.getMessage(), Protos.Error.ErrorCode.BAD_TRANSACTION, CloseReason.REMOTE_SENT_INVALID_MESSAGE); return; } catch (InsufficientMoneyException e) { // This shouldn't happen because the server shouldn't allow itself to get into this // situation in the // first place, by specifying a min up front payment. log.error( "Tried to settle channel and could not afford the fees whilst updating payment", e); error( e.getMessage(), Protos.Error.ErrorCode.BAD_TRANSACTION, CloseReason.REMOTE_SENT_INVALID_MESSAGE); return; } conn.sendToClient( Protos.TwoWayChannelMessage.newBuilder() .setType(Protos.TwoWayChannelMessage.MessageType.CHANNEL_OPEN) .build()); step = InitStep.CHANNEL_OPEN; conn.channelOpen(contractHash); } finally { lock.unlock(); } }
private void error(String message, Protos.Error.ErrorCode errorCode, CloseReason closeReason) { log.error(message); Protos.Error.Builder errorBuilder; errorBuilder = Protos.Error.newBuilder().setCode(errorCode).setExplanation(message); conn.sendToClient( Protos.TwoWayChannelMessage.newBuilder() .setError(errorBuilder) .setType(Protos.TwoWayChannelMessage.MessageType.ERROR) .build()); conn.destroyConnection(closeReason); }
@GuardedBy("lock") private void receiveContractMessage(Protos.TwoWayChannelMessage msg) throws VerificationException { checkState(step == InitStep.WAITING_ON_CONTRACT && msg.hasProvideContract()); log.info("Got contract, broadcasting and responding with CHANNEL_OPEN"); final Protos.ProvideContract providedContract = msg.getProvideContract(); // TODO notify connection handler that timeout should be significantly extended as we wait for // network propagation? final Transaction multisigContract = new Transaction(wallet.getParams(), providedContract.getTx().toByteArray()); step = InitStep.WAITING_ON_MULTISIG_ACCEPTANCE; state .provideMultiSigContract(multisigContract) .addListener( new Runnable() { @Override public void run() { multisigContractPropogated(providedContract, multisigContract.getHash()); } }, Threading.SAME_THREAD); }
@GuardedBy("lock") private void receiveRefundMessage(Protos.TwoWayChannelMessage msg) throws VerificationException { checkState(step == InitStep.WAITING_ON_UNSIGNED_REFUND && msg.hasProvideRefund()); log.info("Got refund transaction, returning signature"); Protos.ProvideRefund providedRefund = msg.getProvideRefund(); state = new PaymentChannelServerState(broadcaster, wallet, myKey, expireTime); byte[] signature = state.provideRefundTransaction( new Transaction(wallet.getParams(), providedRefund.getTx().toByteArray()), providedRefund.getMultisigKey().toByteArray()); step = InitStep.WAITING_ON_CONTRACT; Protos.ReturnRefund.Builder returnRefundBuilder = Protos.ReturnRefund.newBuilder().setSignature(ByteString.copyFrom(signature)); conn.sendToClient( Protos.TwoWayChannelMessage.newBuilder() .setReturnRefund(returnRefundBuilder) .setType(Protos.TwoWayChannelMessage.MessageType.RETURN_REFUND) .build()); }
/** * Called when a message is received from the client. Processes the given message and generates * events based on its content. */ public void receiveMessage(Protos.TwoWayChannelMessage msg) { lock.lock(); try { checkState(connectionOpen); if (channelSettling) return; // If we generate an error, we set errorBuilder and closeReason and break, otherwise we return Protos.Error.Builder errorBuilder; CloseReason closeReason; try { switch (msg.getType()) { case CLIENT_VERSION: receiveVersionMessage(msg); return; case PROVIDE_REFUND: receiveRefundMessage(msg); return; case PROVIDE_CONTRACT: receiveContractMessage(msg); return; case UPDATE_PAYMENT: checkState(step == InitStep.CHANNEL_OPEN && msg.hasUpdatePayment()); receiveUpdatePaymentMessage(msg.getUpdatePayment(), true); return; case CLOSE: receiveCloseMessage(); return; case ERROR: checkState(msg.hasError()); log.error( "Client sent ERROR {} with explanation {}", msg.getError().getCode().name(), msg.getError().hasExplanation() ? msg.getError().getExplanation() : ""); conn.destroyConnection(CloseReason.REMOTE_SENT_ERROR); return; default: final String errorText = "Got unknown message type or type that doesn't apply to servers."; error( errorText, Protos.Error.ErrorCode.SYNTAX_ERROR, CloseReason.REMOTE_SENT_INVALID_MESSAGE); } } catch (VerificationException e) { log.error("Caught verification exception handling message from client", e); error( e.getMessage(), Protos.Error.ErrorCode.BAD_TRANSACTION, CloseReason.REMOTE_SENT_INVALID_MESSAGE); } catch (ValueOutOfRangeException e) { log.error("Caught value out of range exception handling message from client", e); error( e.getMessage(), Protos.Error.ErrorCode.BAD_TRANSACTION, CloseReason.REMOTE_SENT_INVALID_MESSAGE); } catch (InsufficientMoneyException e) { log.error("Caught insufficient money exception handling message from client", e); error( e.getMessage(), Protos.Error.ErrorCode.BAD_TRANSACTION, CloseReason.REMOTE_SENT_INVALID_MESSAGE); } catch (IllegalStateException e) { log.error("Caught illegal state exception handling message from client", e); error( e.getMessage(), Protos.Error.ErrorCode.SYNTAX_ERROR, CloseReason.REMOTE_SENT_INVALID_MESSAGE); } } finally { lock.unlock(); } }
@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()); }