/** * Returns the amount of money sent on this channel so far. */ public synchronized Coin getValueSpent() { return getTotalValue().subtract(getValueRefunded()); }
/** * Creates the initial multisig contract and incomplete refund transaction which can be requested at the appropriate * time using {@link PaymentChannelV1ClientState#getIncompleteRefundTransaction} and * {@link PaymentChannelV1ClientState#getContract()}. The way the contract is crafted can be adjusted by * By default unconfirmed coins are allowed to be used, as for micropayments the risk should be relatively low. * * @throws ValueOutOfRangeException if the value being used is too small to be accepted by the network * @throws InsufficientMoneyException if the wallet doesn't contain enough balance to initiate */ public void initiate() throws ValueOutOfRangeException, InsufficientMoneyException { initiate(null, PaymentChannelClient.defaultChannelProperties); }
/** * Checks if the channel is expired, setting state to {@link State#EXPIRED}, removing this channel from wallet * storage and throwing an {@link IllegalStateException} if it is. */ public synchronized void checkNotExpired() { if (Utils.currentTimeSeconds() > getExpiryTime()) { stateMachine.transition(State.EXPIRED); disconnectFromChannel(); throw new IllegalStateException("Channel expired"); } }
@Override public void onCoinsReceived(Wallet wallet, Transaction tx, Coin prevBalance, Coin newBalance) { synchronized (PaymentChannelClientState.this) { if (getContractInternal() == null) return; if (isSettlementTransaction(tx)) { log.info("Close: transaction {} closed contract {}", tx.getHash(), getContractInternal().getHash()); // Record the fact that it was closed along with the transaction that closed it. stateMachine.transition(State.CLOSED); if (storedChannel == null) return; storedChannel.close = tx; updateChannelInWallet(); watchCloseConfirmations(); } } } });
@GuardedBy("lock") private void receiveRefund(Protos.TwoWayChannelMessage refundMsg, @Nullable KeyParameter userKey) throws VerificationException { checkState(majorVersion == 1); checkState(step == InitStep.WAITING_FOR_REFUND_RETURN && refundMsg.hasReturnRefund()); log.info("Got RETURN_REFUND message, providing signed contract"); Protos.ReturnRefund returnedRefund = refundMsg.getReturnRefund(); // Cast is safe since we've checked the version number ((PaymentChannelV1ClientState)state).provideRefundSignature(returnedRefund.getSignature().toByteArray(), userKey); step = InitStep.WAITING_FOR_CHANNEL_OPEN; // Before we can send the server the contract (ie send it to the network), we must ensure that our refund // transaction is safely in the wallet - thus we store it (this also keeps it up-to-date when we pay) state.storeChannelInWallet(serverId); Protos.ProvideContract.Builder contractMsg = Protos.ProvideContract.newBuilder() .setTx(ByteString.copyFrom(state.getContract().unsafeBitcoinSerialize())); try { // Make an initial payment of the dust limit, and put it into the message as well. The size of the // server-requested dust limit was already sanity checked by this point. PaymentChannelClientState.IncrementedPayment payment = state().incrementPaymentBy(Coin.valueOf(minPayment), userKey); Protos.UpdatePayment.Builder initialMsg = contractMsg.getInitialPaymentBuilder(); initialMsg.setSignature(ByteString.copyFrom(payment.signature.encodeToBitcoin())); initialMsg.setClientChangeValue(state.getValueRefunded().value); } catch (ValueOutOfRangeException e) { throw new IllegalStateException(e); // This cannot happen. } final Protos.TwoWayChannelMessage.Builder msg = Protos.TwoWayChannelMessage.newBuilder(); msg.setProvideContract(contractMsg); msg.setType(Protos.TwoWayChannelMessage.MessageType.PROVIDE_CONTRACT); conn.sendToServer(msg.build()); }
assertEquals(PaymentChannelClientState.State.NEW, clientState.getState()); try { clientState.initiate(); fail(); } catch (ValueOutOfRangeException e) {} Transaction.MIN_NONDUST_OUTPUT.subtract(Coin.SATOSHI).add(Transaction.REFERENCE_DEFAULT_MIN_TX_FEE), EXPIRE_TIME); assertEquals(PaymentChannelClientState.State.NEW, clientState.getState()); try { clientState.initiate(); fail(); } catch (ValueOutOfRangeException e) {} assertEquals(PaymentChannelClientState.State.NEW, clientState.getState()); clientState.initiate(); assertEquals(Transaction.REFERENCE_DEFAULT_MIN_TX_FEE.multiply(2), clientState.getRefundTxFees()); assertEquals(getInitialClientState(), clientState.getState()); assertEquals(PaymentChannelClientState.State.NEW, clientState.getState()); clientState.initiate(); assertEquals(Transaction.REFERENCE_DEFAULT_MIN_TX_FEE.multiply(2), clientState.getRefundTxFees()); assertEquals(getInitialClientState(), clientState.getState()); assertEquals(PaymentChannelClientState.State.SAVE_STATE_IN_WALLET, clientState.getState()); clientState.fakeSave(); assertEquals(PaymentChannelClientState.State.PROVIDE_MULTISIG_CONTRACT_TO_SERVER, clientState.getState());
throws ValueOutOfRangeException { stateMachine.checkState(State.READY); checkNotExpired(); Coin newValueToMe = getValueToMe().subtract(size); if (newValueToMe.compareTo(Transaction.MIN_NONDUST_OUTPUT) < 0 && newValueToMe.signum() > 0) { log.info("New value being sent back as change was smaller than minimum nondust output, sending all"); size = getValueToMe(); newValueToMe = Coin.ZERO; Transaction tx = makeUnsignedChannelContract(newValueToMe); log.info("Signing new payment tx {}", tx); Transaction.SigHash mode; else mode = Transaction.SigHash.SINGLE; TransactionSignature sig = tx.calculateSignature(0, myKey.maybeDecrypt(userKey), getSignedScript(), mode, true); valueToMe = newValueToMe; updateChannelInWallet(); IncrementedPayment payment = new IncrementedPayment(); payment.signature = sig;
assertEquals(PaymentChannelClientState.State.NEW, clientState.getState()); assertEquals(CENT.divide(2), clientState.getTotalValue()); clientState.initiate(); assertEquals(getInitialClientState(), clientState.getState()); assertEquals(PaymentChannelClientState.State.SAVE_STATE_IN_WALLET, clientState.getState()); clientState.fakeSave(); assertEquals(PaymentChannelClientState.State.PROVIDE_MULTISIG_CONTRACT_TO_SERVER, clientState.getState()); Transaction multisigContract = new Transaction(PARAMS, clientState.getContract().bitcoinSerialize()); assertEquals(PaymentChannelClientState.State.READY, clientState.getState()); assertEquals(2, multisigContract.getOutputs().size()); // One multi-sig, one change. Script script = multisigContract.getOutput(0).getScriptPubKey(); clientState.incrementPaymentBy(CENT.divide(10), null).signature.encodeToBitcoin()); clientState.doStoreChannelInWallet(Sha256Hash.of(new byte[]{})); TxFuturePair clientBroadcastedMultiSig = broadcasts.take(); TxFuturePair broadcastRefund = broadcasts.take(); assertEquals(clientBroadcastedRefund.getHash(), clientState.getRefundTransaction().getHash()); for (TransactionInput input : clientBroadcastedRefund.getInputs()) { clientState.incrementPaymentBy(CENT, null); fail(); } catch (IllegalStateException e) { }
throw new ECKey.KeyIsEncryptedException(); PaymentChannelV1ClientState.IncrementedPayment payment = state().incrementPaymentBy(size, userKey); Protos.UpdatePayment.Builder updatePaymentBuilder = Protos.UpdatePayment.newBuilder() .setSignature(ByteString.copyFrom(payment.signature.encodeToBitcoin())) .setClientChangeValue(state.getValueRefunded().value); if (info != null) updatePaymentBuilder.setInfo(info);
/** * <p>Stores this channel's state in the wallet as a part of a {@link StoredPaymentChannelClientStates} wallet * extension and keeps it up-to-date each time payment is incremented. This allows the * {@link StoredPaymentChannelClientStates} object to keep track of timeouts and broadcast the refund transaction * when the channel expires.</p> * * <p>A channel may only be stored after it has fully opened (ie state == State.READY). The wallet provided in the * constructor must already have a {@link StoredPaymentChannelClientStates} object in its extensions set.</p> * * @param id A hash providing this channel with an id which uniquely identifies this server. It does not have to be * unique. */ public synchronized void storeChannelInWallet(Sha256Hash id) { stateMachine.checkState(State.SAVE_STATE_IN_WALLET); checkState(id != null); if (storedChannel != null) { checkState(storedChannel.id.equals(id)); return; } doStoreChannelInWallet(id); try { wallet.commitTx(getContractInternal()); } catch (VerificationException e) { throw new RuntimeException(e); // We created it } stateMachine.transition(State.PROVIDE_MULTISIG_CONTRACT_TO_SERVER); }
@Test public void stateErrors() throws Exception { PaymentChannelClientState channelState = makeClientState(wallet, myKey, serverKey, COIN.multiply(10), 20); assertEquals(PaymentChannelClientState.State.NEW, channelState.getState()); try { channelState.getContract(); fail(); } catch (IllegalStateException e) { // Expected. } try { channelState.initiate(); fail(); } catch (InsufficientMoneyException e) { } }
/** * Skips saving state in the wallet for testing */ @VisibleForTesting synchronized void fakeSave() { try { wallet.commitTx(getContractInternal()); } catch (VerificationException e) { throw new RuntimeException(e); // We created it } stateMachine.transition(State.PROVIDE_MULTISIG_CONTRACT_TO_SERVER); }
times, client.state().getValueSpent()); final Coin MICROPAYMENT_SIZE = CENT.divide(10); for (int i = 0; i < times; i++) { log.error("Failed to increment payment by a CENT, remaining value is {}", client.state().getValueRefunded()); throw new RuntimeException(e); } catch (ExecutionException e) { throw new RuntimeException(e); log.info("Successfully sent payment of one CENT, total remaining on channel is now {}", client.state().getValueRefunded()); if (client.state().getValueRefunded().compareTo(MICROPAYMENT_SIZE) < 0) {
/** * <p>Called when the connection terminates. Notifies the {@link StoredClientChannel} object that we can attempt to * resume this channel in the future and stops generating messages for the server.</p> * * <p>For stateless protocols, this translates to a client not using the channel for the immediate future, but * intending to reopen the channel later. There is likely little reason to use this in a stateless protocol.</p> * * <p>Note that this <b>MUST</b> still be called even after either * {@link ClientConnection#destroyConnection(org.bitcoinj.protocols.channels.PaymentChannelCloseException.CloseReason)} or * {@link PaymentChannelClient#settle()} is called, to actually handle the connection close logic.</p> */ @Override public void connectionClosed() { lock.lock(); try { connectionOpen = false; if (state != null) state.disconnectFromChannel(); } finally { lock.unlock(); } }
assertNull(pair.clientRecorder.q.poll()); client.incrementPayment(client.state().getValueRefunded()); broadcastTxPause.release(); server.receiveMessage(pair.clientRecorder.checkNextMsg(MessageType.UPDATE_PAYMENT)); broadcasts.take(); assertEquals(Coin.ZERO, client.state().getValueRefunded());
@GuardedBy("lock") private void receiveRefund(Protos.TwoWayChannelMessage refundMsg, @Nullable KeyParameter userKey) throws VerificationException { checkState(majorVersion == 1); checkState(step == InitStep.WAITING_FOR_REFUND_RETURN && refundMsg.hasReturnRefund()); log.info("Got RETURN_REFUND message, providing signed contract"); Protos.ReturnRefund returnedRefund = refundMsg.getReturnRefund(); // Cast is safe since we've checked the version number ((PaymentChannelV1ClientState)state).provideRefundSignature(returnedRefund.getSignature().toByteArray(), userKey); step = InitStep.WAITING_FOR_CHANNEL_OPEN; // Before we can send the server the contract (ie send it to the network), we must ensure that our refund // transaction is safely in the wallet - thus we store it (this also keeps it up-to-date when we pay) state.storeChannelInWallet(serverId); Protos.ProvideContract.Builder contractMsg = Protos.ProvideContract.newBuilder() .setTx(ByteString.copyFrom(state.getContract().unsafeBitcoinSerialize())); try { // Make an initial payment of the dust limit, and put it into the message as well. The size of the // server-requested dust limit was already sanity checked by this point. PaymentChannelClientState.IncrementedPayment payment = state().incrementPaymentBy(Coin.valueOf(minPayment), userKey); Protos.UpdatePayment.Builder initialMsg = contractMsg.getInitialPaymentBuilder(); initialMsg.setSignature(ByteString.copyFrom(payment.signature.encodeToBitcoin())); initialMsg.setClientChangeValue(state.getValueRefunded().value); } catch (ValueOutOfRangeException e) { throw new IllegalStateException(e); // This cannot happen. } final Protos.TwoWayChannelMessage.Builder msg = Protos.TwoWayChannelMessage.newBuilder(); msg.setProvideContract(contractMsg); msg.setType(Protos.TwoWayChannelMessage.MessageType.PROVIDE_CONTRACT); conn.sendToServer(msg.build()); }
break; assertEquals(PaymentChannelClientState.State.NEW, clientState.getState()); clientState.initiate(null, new PaymentChannelClient.DefaultClientChannelProperties() { @Override public SendRequest modifyContractSendRequest(SendRequest sendRequest) { assertEquals(getInitialClientState(), clientState.getState()); assertEquals(PaymentChannelClientState.State.SAVE_STATE_IN_WALLET, clientState.getState()); clientState.fakeSave(); assertEquals(PaymentChannelClientState.State.PROVIDE_MULTISIG_CONTRACT_TO_SERVER, clientState.getState()); Transaction multisigContract = new Transaction(PARAMS, clientState.getContract().bitcoinSerialize()); assertEquals(PaymentChannelV1ClientState.State.READY, clientState.getState()); assertEquals(2, multisigContract.getOutputs().size()); // One multi-sig, one change. Script script = multisigContract.getOutput(0).getScriptPubKey(); byte[] signature = clientState.incrementPaymentBy(Transaction.REFERENCE_DEFAULT_MIN_TX_FEE.subtract(Coin.SATOSHI), null) .signature.encodeToBitcoin(); Coin totalRefund = CENT.subtract(Transaction.REFERENCE_DEFAULT_MIN_TX_FEE.subtract(SATOSHI)); signature = clientState.incrementPaymentBy(SATOSHI, null).signature.encodeToBitcoin(); totalRefund = totalRefund.subtract(SATOSHI); serverState.incrementPayment(totalRefund, signature);
throws ValueOutOfRangeException { stateMachine.checkState(State.READY); checkNotExpired(); Coin newValueToMe = getValueToMe().subtract(size); if (newValueToMe.compareTo(Transaction.MIN_NONDUST_OUTPUT) < 0 && newValueToMe.signum() > 0) { log.info("New value being sent back as change was smaller than minimum nondust output, sending all"); size = getValueToMe(); newValueToMe = Coin.ZERO; Transaction tx = makeUnsignedChannelContract(newValueToMe); log.info("Signing new payment tx {}", tx); Transaction.SigHash mode; else mode = Transaction.SigHash.SINGLE; TransactionSignature sig = tx.calculateSignature(0, myKey.maybeDecrypt(userKey), getSignedScript(), mode, true); valueToMe = newValueToMe; updateChannelInWallet(); IncrementedPayment payment = new IncrementedPayment(); payment.signature = sig;
@Override public void onCoinsReceived(Wallet wallet, Transaction tx, Coin prevBalance, Coin newBalance) { synchronized (PaymentChannelClientState.this) { if (getContractInternal() == null) return; if (isSettlementTransaction(tx)) { log.info("Close: transaction {} closed contract {}", tx.getHash(), getContractInternal().getHash()); // Record the fact that it was closed along with the transaction that closed it. stateMachine.transition(State.CLOSED); if (storedChannel == null) return; storedChannel.close = tx; updateChannelInWallet(); watchCloseConfirmations(); } } } });
throw new ECKey.KeyIsEncryptedException(); PaymentChannelV1ClientState.IncrementedPayment payment = state().incrementPaymentBy(size, userKey); Protos.UpdatePayment.Builder updatePaymentBuilder = Protos.UpdatePayment.newBuilder() .setSignature(ByteString.copyFrom(payment.signature.encodeToBitcoin())) .setClientChangeValue(state.getValueRefunded().value); if (info != null) updatePaymentBuilder.setInfo(info);