diff --git a/core/src/main/java/com/google/bitcoin/protocols/channels/PaymentChannelClient.java b/core/src/main/java/com/google/bitcoin/protocols/channels/PaymentChannelClient.java new file mode 100644 index 00000000..d5d9c26f --- /dev/null +++ b/core/src/main/java/com/google/bitcoin/protocols/channels/PaymentChannelClient.java @@ -0,0 +1,414 @@ +package com.google.bitcoin.protocols.channels; + +import java.math.BigInteger; +import java.util.concurrent.locks.ReentrantLock; + +import com.google.bitcoin.core.*; +import com.google.bitcoin.utils.Locks; +import com.google.common.annotations.VisibleForTesting; +import com.google.protobuf.ByteString; +import net.jcip.annotations.GuardedBy; +import org.bitcoin.paymentchannel.Protos; +import org.slf4j.LoggerFactory; + +import com.google.bitcoin.protocols.channels.PaymentChannelCloseException.CloseReason; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; + +/** + *

A class which handles most of the complexity of creating a payment channel connection by providing a + * simple in/out interface which is provided with protobufs from the server and which generates protobufs which should + * be sent to the server.

+ * + *

Does all required verification of server messages and properly stores state objects in the wallet-attached + * {@link StoredPaymentChannelClientStates} so that they are automatically closed when necessary and refund + * transactions are not lost if the application crashes before it unlocks.

+ */ +public class PaymentChannelClient { + //TODO: Update JavaDocs with notes for communication over stateless protocols + private static final org.slf4j.Logger log = LoggerFactory.getLogger(PaymentChannelClient.class); + + protected final ReentrantLock lock = Locks.lock("channelclient"); + + /** + * Implements the connection between this client and the server, providing an interface which allows messages to be + * sent to the server, requests for the connection to the server to be closed, and a callback which occurs when the + * channel is fully open. + */ + public interface ClientConnection { + /** + *

Requests that the given message be sent to the server. There are no blocking requirements for this method, + * however the order of messages must be preserved.

+ * + *

If the send fails, no exception should be thrown, however + * {@link PaymentChannelClient#connectionClosed()} should be called immediately. In the case of messages which + * are a part of initialization, initialization will simply fail and the refund transaction will be broadcasted + * when it unlocks (if necessary). In the case of a payment message, the payment will be lost however if the + * channel is resumed it will begin again from the channel value after the failed payment.

+ * + *

Called while holding a lock on the {@link PaymentChannelClient} object - be careful about reentrancy

+ */ + public void sendToServer(Protos.TwoWayChannelMessage msg); + + /** + *

Requests that the connection to the server be closed

+ * + *

Called while holding a lock on the {@link PaymentChannelClient} object - be careful about reentrancy

+ * + * @param reason The reason for the closure, see the individual values for more details. + * It is usually safe to ignore this and treat any value below + * {@link CloseReason#CLIENT_REQUESTED_CLOSE} as "unrecoverable error" and all others as + * "try again once and see if it works then" + */ + public void destroyConnection(CloseReason reason); + + /** + *

Indicates the channel has been successfully opened and + * {@link PaymentChannelClient#incrementPayment(java.math.BigInteger)} may be called at will.

+ * + *

Called while holding a lock on the {@link PaymentChannelClient} object - be careful about reentrancy

+ */ + public void channelOpen(); + } + @GuardedBy("lock") private final ClientConnection conn; + + // Used to keep track of whether or not the "socket" ie connection is open and we can generate messages + @VisibleForTesting @GuardedBy("lock") boolean connectionOpen = false; + + // The state object used to step through initialization and pay the server + @GuardedBy("lock") private PaymentChannelClientState state; + + // The step we are at in initialization, this is partially duplicated in the state object + private enum InitStep { + WAITING_FOR_CONNECTION_OPEN, + WAITING_FOR_VERSION_NEGOTIATION, + WAITING_FOR_INITIATE, + WAITING_FOR_REFUND_RETURN, + WAITING_FOR_CHANNEL_OPEN, + CHANNEL_OPEN + } + @GuardedBy("lock") private InitStep step = InitStep.WAITING_FOR_CONNECTION_OPEN; + + // Will either hold the StoredClientChannel of this channel or null after connectionOpen + private StoredClientChannel storedChannel; + // An arbitrary hash which identifies this channel (specified by the API user) + private final Sha256Hash serverId; + + // The wallet associated with this channel + private final Wallet wallet; + + // Information used during channel initialization to send to the server or check what the server sends to us + private final ECKey myKey; + private final BigInteger maxValue; + + /** + *

The maximum amount of time for which we will accept the server locking up our funds for the multisig + * contract.

+ * + *

Note that though this is not final, it is in all caps because it should generally not be modified unless you + * have some guarantee that the server will not request at least this (channels will fail if this is too small).

+ * + *

24 hours is the default as it is expected that clients limit risk exposure by limiting channel size instead of + * limiting lock time when dealing with potentially malicious servers.

+ */ + public long MAX_TIME_WINDOW = 24*60*60; + + /** + * Constructs a new channel manager which waits for {@link PaymentChannelClient#connectionOpen()} before acting. + * + * @param wallet The wallet which will be paid from, and where completed transactions will be committed. + * Must already have a {@link StoredPaymentChannelClientStates} object in its extensions set. + * @param myKey A freshly generated keypair used for the multisig contract and refund output. + * @param maxValue The maximum value the server is allowed to request that we lock into this channel until the + * refund transaction unlocks. Note that if there is a previously open channel, the refund + * transaction used in this channel may be larger than maxValue. Thus, maxValue is not a method for + * limiting the amount payable through this channel. + * @param serverId An arbitrary hash representing this channel. This must uniquely identify the server. If an + * existing stored channel exists in the wallet's {@link StoredPaymentChannelClientStates}, then an + * attempt will be made to resume that channel. + * @param conn A callback listener which represents the connection to the server (forwards messages we generate to + * the server) + */ + public PaymentChannelClient(Wallet wallet, ECKey myKey, BigInteger maxValue, Sha256Hash serverId, ClientConnection conn) { + this.wallet = checkNotNull(wallet); + this.myKey = checkNotNull(myKey); + this.maxValue = checkNotNull(maxValue); + this.serverId = checkNotNull(serverId); + this.conn = checkNotNull(conn); + } + + @GuardedBy("lock") + private void receiveInitiate(Protos.Initiate initiate, BigInteger minChannelSize) throws VerificationException, ValueOutOfRangeException { + log.info("Got INITIATE message, providing refund transaction"); + + state = new PaymentChannelClientState(wallet, myKey, + new ECKey(null, initiate.getMultisigKey().toByteArray()), + minChannelSize, + initiate.getExpireTimeSecs()); + state.initiate(); + step = InitStep.WAITING_FOR_REFUND_RETURN; + + Protos.ProvideRefund.Builder provideRefundBuilder = Protos.ProvideRefund.newBuilder() + .setMultisigKey(ByteString.copyFrom(myKey.getPubKey())) + .setTx(ByteString.copyFrom(state.getIncompleteRefundTransaction().bitcoinSerialize())); + + conn.sendToServer(Protos.TwoWayChannelMessage.newBuilder() + .setProvideRefund(provideRefundBuilder) + .setType(Protos.TwoWayChannelMessage.MessageType.PROVIDE_REFUND) + .build()); + } + + @GuardedBy("lock") + private void receiveRefund(Protos.TwoWayChannelMessage msg) throws VerificationException { + checkState(step == InitStep.WAITING_FOR_REFUND_RETURN && msg.hasReturnRefund()); + log.info("Got RETURN_REFUND message, providing signed contract"); + Protos.ReturnRefund returnedRefund = msg.getReturnRefund(); + state.provideRefundSignature(returnedRefund.getSignature().toByteArray()); + step = InitStep.WAITING_FOR_CHANNEL_OPEN; + + Protos.ProvideContract.Builder provideContractBuilder = Protos.ProvideContract.newBuilder() + .setTx(ByteString.copyFrom(state.getMultisigContract().bitcoinSerialize())); + + conn.sendToServer(Protos.TwoWayChannelMessage.newBuilder() + .setProvideContract(provideContractBuilder) + .setType(Protos.TwoWayChannelMessage.MessageType.PROVIDE_CONTRACT) + .build()); + } + + @GuardedBy("lock") + private void receiveChannelOpen() throws VerificationException { + checkState(step == InitStep.WAITING_FOR_CHANNEL_OPEN || (step == InitStep.WAITING_FOR_INITIATE && storedChannel != null)); + log.info("Got CHANNEL_OPEN message, ready to pay"); + + if (step == InitStep.WAITING_FOR_INITIATE) + state = new PaymentChannelClientState(storedChannel, wallet); + // Let state know its wallet id so it gets stored and stays up-to-date + state.storeChannelInWallet(serverId); + step = InitStep.CHANNEL_OPEN; + // channelOpen should disable timeouts, but + // TODO accomodate high latency between PROVIDE_CONTRACT and here + conn.channelOpen(); + } + + /** + * Called when a message is received from the server. Processes the given message and generates events based on its + * content. + */ + public void receiveMessage(Protos.TwoWayChannelMessage msg) { + lock.lock(); + try { + checkState(connectionOpen); + // 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 SERVER_VERSION: + checkState(step == InitStep.WAITING_FOR_VERSION_NEGOTIATION && msg.hasServerVersion()); + // Server might send back a major version lower than our own if they want to fallback to a lower version + // We can't handle that, so we just close the channel + if (msg.getServerVersion().getMajor() != 0) { + errorBuilder = Protos.Error.newBuilder() + .setCode(Protos.Error.ErrorCode.NO_ACCEPTABLE_VERSION); + closeReason = CloseReason.NO_ACCEPTABLE_VERSION; + break; + } + log.info("Got version handshake, awaiting INITIATE or resume CHANNEL_OPEN"); + step = InitStep.WAITING_FOR_INITIATE; + return; + case INITIATE: + checkState(step == InitStep.WAITING_FOR_INITIATE && msg.hasInitiate()); + + Protos.Initiate initiate = msg.getInitiate(); + checkState(initiate.getExpireTimeSecs() > 0 && initiate.getMinAcceptedChannelSize() >= 0); + + if (initiate.getExpireTimeSecs() > Utils.now().getTime()/1000 + MAX_TIME_WINDOW) { + errorBuilder = Protos.Error.newBuilder() + .setCode(Protos.Error.ErrorCode.TIME_WINDOW_TOO_LARGE); + closeReason = CloseReason.TIME_WINDOW_TOO_LARGE; + break; + } + + BigInteger minChannelSize = BigInteger.valueOf(initiate.getMinAcceptedChannelSize()); + if (minChannelSize.compareTo(maxValue) > 0) { + errorBuilder = Protos.Error.newBuilder() + .setCode(Protos.Error.ErrorCode.CHANNEL_VALUE_TOO_LARGE); + closeReason = CloseReason.SERVER_REQUESTED_TOO_MUCH_VALUE; + break; + } + + receiveInitiate(initiate, minChannelSize); + return; + case RETURN_REFUND: + receiveRefund(msg); + return; + case CHANNEL_OPEN: + receiveChannelOpen(); + return; + case CLOSE: + conn.destroyConnection(CloseReason.SERVER_REQUESTED_CLOSE); + return; + case ERROR: + checkState(msg.hasError()); + log.error("Server sent ERROR {} with explanation {}", msg.getError().getCode().name(), + msg.getError().hasExplanation() ? msg.getError().getExplanation() : ""); + conn.destroyConnection(CloseReason.REMOTE_SENT_ERROR); + return; + default: + log.error("Got unknown message type or type that doesn't apply to clients."); + errorBuilder = Protos.Error.newBuilder() + .setCode(Protos.Error.ErrorCode.SYNTAX_ERROR); + closeReason = CloseReason.REMOTE_SENT_INVALID_MESSAGE; + break; + } + } catch (VerificationException e) { + log.error("Caught verification exception handling message from server", e); + errorBuilder = Protos.Error.newBuilder() + .setCode(Protos.Error.ErrorCode.BAD_TRANSACTION) + .setExplanation(e.getMessage()); + closeReason = CloseReason.REMOTE_SENT_INVALID_MESSAGE; + } catch (ValueOutOfRangeException e) { + log.error("Caught value out of range exception handling message from server", e); + errorBuilder = Protos.Error.newBuilder() + .setCode(Protos.Error.ErrorCode.BAD_TRANSACTION) + .setExplanation(e.getMessage()); + closeReason = CloseReason.REMOTE_SENT_INVALID_MESSAGE; + } catch (IllegalStateException e) { + log.error("Caught illegal state exception handling message from server", e); + errorBuilder = Protos.Error.newBuilder() + .setCode(Protos.Error.ErrorCode.SYNTAX_ERROR); + closeReason = CloseReason.REMOTE_SENT_INVALID_MESSAGE; + } + conn.sendToServer(Protos.TwoWayChannelMessage.newBuilder() + .setError(errorBuilder) + .setType(Protos.TwoWayChannelMessage.MessageType.ERROR) + .build()); + conn.destroyConnection(closeReason); + } finally { + lock.unlock(); + } + } + + /** + *

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.

+ * + *

Note that this MUST still be called even after either + * {@link ClientConnection#destroyConnection(CloseReason)} or + * {@link PaymentChannelClient#close()} is called to actually handle the connection close logic.

+ */ + public void connectionClosed() { + lock.lock(); + try { + connectionOpen = false; + if (state != null) + state.disconnectFromChannel(); + } finally { + lock.unlock(); + } + } + + /** + *

Closes the connection, notifying the server it should close the channel by broadcasting the most recent payment + * transaction.

+ * + *

Note that this only generates a CLOSE message for the server and calls + * {@link ClientConnection#destroyConnection(CloseReason)} to close the connection, it does not + * actually handle connection close logic, and {@link PaymentChannelClient#connectionClosed()} must still be called + * after the connection fully closes.

+ * + * @throws IllegalStateException If the connection is not currently open (ie the CLOSE message cannot be sent) + */ + public void close() throws IllegalStateException { + lock.lock(); + try { + checkState(connectionOpen); + conn.sendToServer(Protos.TwoWayChannelMessage.newBuilder() + .setType(Protos.TwoWayChannelMessage.MessageType.CLOSE) + .build()); + conn.destroyConnection(CloseReason.CLIENT_REQUESTED_CLOSE); + } finally { + lock.unlock(); + } + } + + /** + *

Called to indicate the connection has been opened and messages can now be generated for the server.

+ * + *

Attempts to find a channel to resume and generates a CLIENT_VERSION message for the server based on the + * result.

+ */ + public void connectionOpen() { + lock.lock(); + try { + connectionOpen = true; + + StoredPaymentChannelClientStates channels = (StoredPaymentChannelClientStates) wallet.getExtensions().get(StoredPaymentChannelClientStates.EXTENSION_ID); + if (channels != null) + storedChannel = channels.getInactiveChannelById(serverId); + + step = InitStep.WAITING_FOR_VERSION_NEGOTIATION; + + Protos.ClientVersion.Builder versionNegotiationBuilder = Protos.ClientVersion.newBuilder() + .setMajor(0).setMinor(1); + + if (storedChannel != null) { + versionNegotiationBuilder.setPreviousChannelContractHash(ByteString.copyFrom(storedChannel.contract.getHash().getBytes())); + log.info("Begun version handshake, attempting to reopen channel with contract hash {}", storedChannel.contract.getHash()); + } else + log.info("Begun version handshake creating new channel"); + + conn.sendToServer(Protos.TwoWayChannelMessage.newBuilder() + .setType(Protos.TwoWayChannelMessage.MessageType.CLIENT_VERSION) + .setClientVersion(versionNegotiationBuilder) + .build()); + } finally { + lock.unlock(); + } + } + + /** + *

Gets the {@link PaymentChannelClientState} object which stores the current state of the connection with the + * server.

+ * + *

Note that if you call any methods which update state directly the server will not be notified and channel + * initialization logic in the connection may fail unexpectedly.

+ */ + public PaymentChannelClientState state() { + lock.lock(); + try { + return state; + } finally { + lock.unlock(); + } + } + + /** + * Increments the total value which we pay the server. + * + * @param size How many satoshis to increment the payment by (note: not the new total). + * @throws ValueOutOfRangeException If the size is negative or would pay more than this channel's total value + * ({@link PaymentChannelClientConnection#state()}.getTotalValue()) + * @throws IllegalStateException If the channel has been closed or is not yet open + * (see {@link PaymentChannelClientConnection#getChannelOpenFuture()} for the second) + */ + public void incrementPayment(BigInteger size) throws ValueOutOfRangeException, IllegalStateException { + lock.lock(); + try { + if (state() == null || !connectionOpen || step != InitStep.CHANNEL_OPEN) + throw new IllegalStateException("Channel is not fully initialized/has already been closed"); + + byte[] signature = state().incrementPaymentBy(size); + Protos.UpdatePayment.Builder updatePaymentBuilder = Protos.UpdatePayment.newBuilder() + .setSignature(ByteString.copyFrom(signature)) + .setClientChangeValue(state.getValueRefunded().longValue()); + + conn.sendToServer(Protos.TwoWayChannelMessage.newBuilder() + .setUpdatePayment(updatePaymentBuilder) + .setType(Protos.TwoWayChannelMessage.MessageType.UPDATE_PAYMENT) + .build()); + } finally { + lock.unlock(); + } + } +} diff --git a/core/src/main/java/com/google/bitcoin/protocols/channels/PaymentChannelClientConnection.java b/core/src/main/java/com/google/bitcoin/protocols/channels/PaymentChannelClientConnection.java new file mode 100644 index 00000000..3a8e8835 --- /dev/null +++ b/core/src/main/java/com/google/bitcoin/protocols/channels/PaymentChannelClientConnection.java @@ -0,0 +1,168 @@ +/* + * Copyright 2013 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.bitcoin.protocols.channels; + +import java.io.IOException; +import java.math.BigInteger; +import java.net.InetSocketAddress; + +import com.google.bitcoin.core.ECKey; +import com.google.bitcoin.core.Sha256Hash; +import com.google.bitcoin.core.Wallet; +import com.google.bitcoin.protocols.niowrapper.ProtobufClient; +import com.google.bitcoin.protocols.niowrapper.ProtobufParser; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.SettableFuture; +import org.bitcoin.paymentchannel.Protos; + +/** + * Manages a {@link PaymentChannelClientState} by connecting to a server over TLS and exchanging the necessary data over + * protobufs. + */ +public class PaymentChannelClientConnection { + // Various futures which will be completed later + private final SettableFuture channelOpenFuture = SettableFuture.create(); + + private final PaymentChannelClient channelClient; + private final ProtobufParser wireParser; + + /** + * Attempts to open a new connection to and open a payment channel with the given host and port, blocking until the + * connection is open + * + * @param server The host/port pair where the server is listening. + * @param timeoutSeconds The connection timeout and read timeout during initialization. This should be large enough + * to accommodate ECDSA signature operations and network latency. + * @param wallet The wallet which will be paid from, and where completed transactions will be committed. + * Must already have a {@link StoredPaymentChannelClientStates} object in its extensions set. + * @param myKey A freshly generated keypair used for the multisig contract and refund output. + * @param maxValue The maximum value this channel is allowed to request + * @param serverId A unique ID which is used to attempt reopening of an existing channel. + * This must be unique to the server, and, if your application is exposing payment channels to some + * API, this should also probably encompass some caller UID to avoid applications opening channels + * which were created by others. + * + * @throws IOException if there's an issue using the network. + * @throws ValueOutOfRangeException if the balance of wallet is lower than maxValue. + */ + public PaymentChannelClientConnection(InetSocketAddress server, int timeoutSeconds, Wallet wallet, ECKey myKey, + BigInteger maxValue, String serverId) throws IOException, ValueOutOfRangeException { + if (wallet.getBalance().compareTo(maxValue) < 0) + throw new ValueOutOfRangeException("Insufficient balance in this wallet to open the requested payment channel."); + // Glue the object which vends/ingests protobuf messages in order to manage state to the network object which + // reads/writes them to the wire in length prefixed form. + channelClient = new PaymentChannelClient(wallet, myKey, maxValue, Sha256Hash.create(serverId.getBytes()), + new PaymentChannelClient.ClientConnection() { + @Override + public void sendToServer(Protos.TwoWayChannelMessage msg) { + wireParser.write(msg); + } + + @Override + public void destroyConnection(PaymentChannelCloseException.CloseReason reason) { + channelOpenFuture.setException(new PaymentChannelCloseException("Payment channel client requested that the connection be closed", reason)); + wireParser.closeConnection(); + } + + @Override + public void channelOpen() { + wireParser.setSocketTimeout(0); + // Inform the API user that we're done and ready to roll. + channelOpenFuture.set(PaymentChannelClientConnection.this); + } + }); + + // And glue back in the opposite direction - network to the channelClient. + wireParser = new ProtobufParser(new ProtobufParser.Listener() { + @Override + public void messageReceived(ProtobufParser handler, Protos.TwoWayChannelMessage msg) { + channelClient.receiveMessage(msg); + } + + @Override + public void connectionOpen(ProtobufParser handler) { + channelClient.connectionOpen(); + } + + @Override + public void connectionClosed(ProtobufParser handler) { + channelClient.connectionClosed(); + channelOpenFuture.setException(new PaymentChannelCloseException("The TCP socket died", + PaymentChannelCloseException.CloseReason.CONNECTION_CLOSED)); + } + }, Protos.TwoWayChannelMessage.getDefaultInstance(), Short.MAX_VALUE, timeoutSeconds*1000); + + // Initiate the outbound network connection. We don't need to keep this around. The wireParser object will handle + // things from here on our. + new ProtobufClient(server, wireParser, timeoutSeconds * 1000); + } + + /** + *

Gets a future which returns this when the channel is successfully opened, or throws an exception if there is + * an error before the channel has reached the open state.

+ * + *

After this future completes successfully, you may call + * {@link PaymentChannelClientConnection#incrementPayment(java.math.BigInteger)} to begin paying the server.

+ */ + public ListenableFuture getChannelOpenFuture() { + return channelOpenFuture; + } + + /** + * Increments the total value which we pay the server. + * + * @param size How many satoshis to increment the payment by (note: not the new total). + * @throws ValueOutOfRangeException If the size is negative or would pay more than this channel's total value + * ({@link PaymentChannelClientConnection#state()}.getTotalValue()) + * @throws IllegalStateException If the channel has been closed or is not yet open + * (see {@link PaymentChannelClientConnection#getChannelOpenFuture()} for the second) + */ + public synchronized void incrementPayment(BigInteger size) throws ValueOutOfRangeException, IllegalStateException { + channelClient.incrementPayment(size); + } + + /** + *

Gets the {@link PaymentChannelClientState} object which stores the current state of the connection with the + * server.

+ * + *

Note that if you call any methods which update state directly the server will not be notified and channel + * initialization logic in the connection may fail unexpectedly.

+ */ + public synchronized PaymentChannelClientState state() { + return channelClient.state(); + } + + /** + * Closes the connection, notifying the server it should close the channel by broadcasting the most recent payment + * transaction. + */ + public synchronized void close() { + // Shutdown is a little complicated. + // + // This call will cause the CLOSE message to be written to the wire, and then the destroyConnection() method that + // we defined above will be called, which in turn will call wireParser.closeConnection(), which in turn will invoke + // ProtobufClient.closeConnection(), which will then close the socket triggering interruption of the network + // thread it had created. That causes the background thread to die, which on its way out calls + // ProtobufParser.connectionClosed which invokes the connectionClosed method we defined above which in turn + // then configures the open-future correctly and closes the state object. Phew! + try { + channelClient.close(); + } catch (IllegalStateException e) { + // Already closed...oh well + } + } +} diff --git a/core/src/main/java/com/google/bitcoin/protocols/channels/PaymentChannelClientState.java b/core/src/main/java/com/google/bitcoin/protocols/channels/PaymentChannelClientState.java new file mode 100644 index 00000000..eb11f5be --- /dev/null +++ b/core/src/main/java/com/google/bitcoin/protocols/channels/PaymentChannelClientState.java @@ -0,0 +1,408 @@ +/* + * Copyright 2013 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.bitcoin.protocols.channels; + +import java.math.BigInteger; +import java.util.List; + +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.common.collect.ImmutableList; +import com.google.common.collect.Lists; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import static com.google.common.base.Preconditions.*; + +/** + *

A payment channel is a method of sending money to someone such that the amount of money you send can be adjusted + * after the fact, in an efficient manner that does not require broadcasting to the network. This can be used to + * implement micropayments or other payment schemes in which immediate settlement is not required, but zero trust + * negotiation is. Note that this class only allows the amount of money sent to be incremented, not decremented.

+ * + *

This class implements the core state machine for the client side of the protocol. The server side is implemented + * by {@link PaymentChannelServerState} and {@link PaymentChannelClientConnection} implements a network protocol + * suitable for TCP/IP connections which moves this class through each state. We say that the party who is sending funds + * is the client or initiating party. The party that is receiving the funds is the server or + * receiving party. Although the underlying Bitcoin protocol is capable of more complex relationships than that, + * this class implements only the simplest case.

+ * + *

A channel has an expiry parameter. If the server halts after the multi-signature contract which locks + * up the given value is broadcast you could get stuck in a state where you've lost all the money put into the + * contract. To avoid this, a refund transaction is agreed ahead of time but it may only be used/broadcast after + * the expiry time. This is specified in terms of block timestamps and once the timestamp of the chain chain approaches + * the given time (within a few hours), the channel must be closed or else the client will broadcast the refund + * transaction and take back all the money once the expiry time is reached.

+ * + *

To begin, the client calls {@link PaymentChannelClientState#initiate()}, which moves the channel into state + * INITIATED and creates the initial multi-sig contract and refund transaction. If the wallet has insufficient funds an + * exception will be thrown at this point. Once this is done, call + * {@link PaymentChannelClientState#getIncompleteRefundTransaction()} and pass the resultant transaction through to the + * server. Once you have retrieved the signature, use {@link PaymentChannelClientState#provideRefundSignature(byte[])}. + * If no exception is thrown at this point, we are secure against a malicious server attempting to destroy all our coins + * and can provide the server with the multi-sig contract (via {@link PaymentChannelClientState#getMultisigContract()}) + * safely. + *

+ */ +public class PaymentChannelClientState { + private static final Logger log = LoggerFactory.getLogger(PaymentChannelClientState.class); + + private final Wallet wallet; + // Both sides need a key (private in our case, public for the server) in order to manage the multisig contract + // and transactions that spend it. + private final ECKey myKey, serverMultisigKey; + // How much value (in satoshis) is locked up into the channel. + private final BigInteger totalValue; + // When the channel will automatically close in favor of the client, if the server halts before protocol termination + // specified in terms of block timestamps (so it can off real time by a few hours). + private final long expiryTime; + + // The refund is a time locked transaction that spends all the money of the channel back to the client. + private Transaction refundTx; + private BigInteger refundFees; + // The multi-sig contract locks the value of the channel up such that the agreement of both parties is required + // to spend it. + private Transaction multisigContract; + private Script multisigScript; + // How much value is currently allocated to us. Starts as being same as totalValue. + private BigInteger valueToMe; + + /** + * The different logical states the channel can be in. The channel starts out as NEW, and then steps through the + * states until it becomes finalized. The server should have already been contacted and asked for a public key + * by the time the NEW state is reached. + */ + public enum State { + NEW, + INITIATED, + WAITING_FOR_SIGNED_REFUND, + PROVIDE_MULTISIG_CONTRACT_TO_SERVER, + READY, + EXPIRED + } + private State state; + + // The id of this channel in the StoredPaymentChannelClientStates, or null if it is not stored + private StoredClientChannel storedChannel; + + PaymentChannelClientState(StoredClientChannel storedClientChannel, Wallet wallet) throws VerificationException { + // The PaymentChannelClientConnection handles storedClientChannel.active and ensures we aren't resuming channels + this.wallet = checkNotNull(wallet); + this.multisigContract = checkNotNull(storedClientChannel.contract); + this.multisigScript = multisigContract.getOutput(0).getScriptPubKey(); + this.refundTx = checkNotNull(storedClientChannel.refund); + this.refundFees = checkNotNull(storedClientChannel.refundFees); + this.expiryTime = refundTx.getLockTime(); + this.myKey = checkNotNull(storedClientChannel.myKey); + this.serverMultisigKey = null; + this.totalValue = multisigContract.getOutput(0).getValue(); + this.valueToMe = checkNotNull(storedClientChannel.valueToMe); + this.storedChannel = storedClientChannel; + this.state = State.READY; + } + + /** + * Creates a state object for a payment channel client. It is expected that you be ready to + * {@link PaymentChannelClientState#initiate()} after construction (to avoid creating objects for channels which are + * not going to finish opening) and thus some parameters provided here are only used in + * {@link PaymentChannelClientState#initiate()} to create the Multisig contract and refund transaction. + * + * @param wallet a wallet that contains at least the specified amount of value. + * @param myKey a freshly generated private key for this channel. + * @param serverMultisigKey a public key retrieved from the server used for the initial multisig contract + * @param value how many satoshis to put into this contract. If the channel reaches this limit, it must be closed. + * It is suggested you use at least {@link Utils#CENT} to avoid paying fees if you need to spend the refund transaction + * @param expiryTimeInSeconds At what point (UNIX timestamp +/- a few hours) the channel will expire + * + * @throws VerificationException If either myKey's pubkey or serverMultisigKey's pubkey are non-canonical (ie invalid) + */ + public PaymentChannelClientState(Wallet wallet, ECKey myKey, ECKey serverMultisigKey, + BigInteger value, long expiryTimeInSeconds) throws VerificationException { + checkArgument(value.compareTo(BigInteger.ZERO) > 0); + this.wallet = checkNotNull(wallet); + this.serverMultisigKey = checkNotNull(serverMultisigKey); + if (!myKey.isPubKeyCanonical() || !serverMultisigKey.isPubKeyCanonical()) + throw new VerificationException("Pubkey was not canonical (ie non-standard)"); + this.myKey = checkNotNull(myKey); + this.valueToMe = this.totalValue = checkNotNull(value); + this.expiryTime = expiryTimeInSeconds; + this.state = State.NEW; + } + + /** + * This object implements a state machine, and this accessor returns which state it's currently in. + */ + public synchronized State getState() { + return state; + } + + /** + * Creates the initial multisig contract and incomplete refund transaction which can be requested at the appropriate + * time using {@link PaymentChannelClientState#getIncompleteRefundTransaction} and + * {@link PaymentChannelClientState#getMultisigContract()} + * + * @throws ValueOutOfRangeException If the value being used cannot be afforded or is too small to be accepted by the network + */ + public synchronized void initiate() throws ValueOutOfRangeException { + final NetworkParameters params = wallet.getParams(); + Transaction template = new Transaction(params); + // We always place the client key before the server key because, if either side wants some privacy, they can + // use a fresh key for the the multisig contract and nowhere else + List keys = Lists.newArrayList(myKey, serverMultisigKey); + // There is also probably a change output, but we don't bother shuffling them as it's obvious from the + // format which one is the change. If we start obfuscating the change output better in future this may + // be worth revisiting. + TransactionOutput multisigOutput = template.addOutput(totalValue, ScriptBuilder.createMultiSigOutputScript(2, keys)); + if (multisigOutput.getMinNonDustValue().compareTo(totalValue) > 0) + throw new ValueOutOfRangeException("totalValue too small to use"); + Wallet.SendRequest req = Wallet.SendRequest.forTx(template); + if (!wallet.completeTx(req)) + throw new ValueOutOfRangeException("Cannot afford this channel"); + BigInteger multisigFee = req.fee; + multisigContract = req.tx; + // Build a refund transaction that protects us in the case of a bad server that's just trying to cause havoc + // by locking up peoples money (perhaps as a precursor to a ransom attempt). We time lock it so the server + // has an assurance that we cannot take back our money by claiming a refund before the channel closes - this + // relies on the fact that since Bitcoin 0.8 time locked transactions are non-final. This will need to change + // in future as it breaks the intended design of timelocking/tx replacement, but for now it simplifies this + // specific protocol somewhat. + refundTx = new Transaction(params); + refundTx.addInput(multisigOutput).setSequenceNumber(0); // Allow replacement when it's eventually reactivated. + refundTx.setLockTime(expiryTime); + if (totalValue.compareTo(Utils.CENT) < 0) { + // Must pay min fee. + final BigInteger valueAfterFee = totalValue.subtract(Transaction.REFERENCE_DEFAULT_MIN_TX_FEE); + if (Transaction.MIN_NONDUST_OUTPUT.compareTo(valueAfterFee) > 0) + throw new ValueOutOfRangeException("totalValue too small to use"); + refundTx.addOutput(valueAfterFee, myKey.toAddress(params)); + refundFees = multisigFee.add(Transaction.REFERENCE_DEFAULT_MIN_TX_FEE); + } else { + refundTx.addOutput(totalValue, myKey.toAddress(params)); + refundFees = multisigFee; + } + state = State.INITIATED; + // Client should now call getIncompleteRefundTransaction() and send it to the server. + } + + /** + * Returns the transaction that locks the money to the agreement of both parties. Do not mutate the result. + * Once this step is done, you can use {@link PaymentChannelClientState#incrementPaymentBy(java.math.BigInteger)} to + * start paying the server. + */ + public synchronized Transaction getMultisigContract() { + checkState(multisigContract != null); + if (state == State.PROVIDE_MULTISIG_CONTRACT_TO_SERVER) + state = State.READY; + return multisigContract; + } + + /** + * Returns a partially signed (invalid) refund transaction that should be passed to the server. Once the server + * has checked it out and provided its own signature, call + * {@link PaymentChannelClientState#provideRefundSignature(byte[])} with the result. + */ + public synchronized Transaction getIncompleteRefundTransaction() { + checkState(refundTx != null); + if (state == State.INITIATED) + state = State.WAITING_FOR_SIGNED_REFUND; + return refundTx; + } + + /** + *

When the servers signature for the refund transaction is received, call this to verify it and sign the + * complete refund ourselves.

+ * + *

If this does not throw an exception, we are secure against the loss of funds and can safely provide the server + * with the multi-sig contract to lock in the agreement. In this case, both the multisig contract and the refund + * transaction are automatically committed to wallet so that it can handle broadcasting the refund transaction at + * the appropriate time if necessary.

+ */ + public synchronized void provideRefundSignature(byte[] theirSignature) throws VerificationException { + checkNotNull(theirSignature); + checkState(state == State.WAITING_FOR_SIGNED_REFUND); + TransactionSignature theirSig = TransactionSignature.decodeFromBitcoin(theirSignature, true); + if (theirSig.sigHashMode() != Transaction.SigHash.NONE || !theirSig.anyoneCanPay()) + throw new VerificationException("Refund signature was not SIGHASH_NONE|SIGHASH_ANYONECANPAY"); + // Sign the refund transaction ourselves. + final TransactionOutput multisigContractOutput = multisigContract.getOutput(0); + try { + multisigScript = multisigContractOutput.getScriptPubKey(); + } catch (ScriptException e) { + throw new RuntimeException(e); // Cannot happen: we built this ourselves. + } + TransactionSignature ourSignature = + refundTx.calculateSignature(0, myKey, multisigScript, Transaction.SigHash.ALL, false); + // Insert the signatures. + Script scriptSig = ScriptBuilder.createMultiSigInputScript(ImmutableList.of(ourSignature, theirSig)); + log.info("Refund scriptSig: {}", scriptSig); + log.info("Multi-sig contract scriptPubKey: {}", multisigScript); + TransactionInput refundInput = refundTx.getInput(0); + refundInput.setScriptSig(scriptSig); + refundInput.verify(multisigContractOutput); + wallet.commitTx(multisigContract); + state = State.PROVIDE_MULTISIG_CONTRACT_TO_SERVER; + } + + private synchronized Transaction makeUnsignedChannelContract(BigInteger valueToMe) throws ValueOutOfRangeException { + Transaction tx = new Transaction(wallet.getParams()); + tx.addInput(multisigContract.getOutput(0)); + // Our output always comes first. + // TODO: We should drop myKey in favor of output key + multisig key separation + // (as its always obvious who the client is based on T2 output order) + tx.addOutput(valueToMe, myKey.toAddress(wallet.getParams())); + return tx; + } + + /** + * 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.now().getTime()/1000 > expiryTime) { + state = State.EXPIRED; + disconnectFromChannel(); + throw new IllegalStateException("Channel expired"); + } + } + + /** + *

Updates the outputs on the payment contract transaction and re-signs it. The state must be READY in order to + * call this method. The signature that is returned should be sent to the server so it has the ability to broadcast + * the best seen payment when the channel closes or times out.

+ * + *

The returned signature is over the payment transaction, which we never have a valid copy of and thus there + * is no accessor for it on this object.

+ * + *

To spend the whole channel increment by {@link PaymentChannelClientState#getTotalValue()} - + * {@link PaymentChannelClientState#getValueRefunded()}

+ * + * @param size How many satoshis to increment the payment by (note: not the new total). + * @throws ValueOutOfRangeException If size is negative or the new value being returned as change is smaller than + * min nondust output size (including if the new total payment is larger than this + * channel's totalValue) + */ + public synchronized byte[] incrementPaymentBy(BigInteger size) throws ValueOutOfRangeException { + checkState(state == State.READY); + checkNotExpired(); + checkNotNull(size); // Validity of size will be checked by makeUnsignedChannelContract. + if (size.compareTo(BigInteger.ZERO) < 0) + throw new ValueOutOfRangeException("Tried to decrement payment"); + BigInteger newValueToMe = valueToMe.subtract(size); + if (Transaction.MIN_NONDUST_OUTPUT.compareTo(newValueToMe) > 0 && !newValueToMe.equals(BigInteger.ZERO)) + throw new ValueOutOfRangeException("New value being sent back as change was smaller than minimum nondust output"); + Transaction tx = makeUnsignedChannelContract(newValueToMe); + log.info("Signing new contract: {}", tx); + Transaction.SigHash mode; + // If we spent all the money we put into this channel, we (by definition) don't care what the outputs are, so + // we sign with SIGHASH_NONE to let the server do what it wants. + if (newValueToMe.equals(BigInteger.ZERO)) + mode = Transaction.SigHash.NONE; + else + mode = Transaction.SigHash.SINGLE; + TransactionSignature sig = tx.calculateSignature(0, myKey, multisigScript, mode, true); + valueToMe = newValueToMe; + updateChannelInWallet(); + return sig.encodeToBitcoin(); + } + + private synchronized void updateChannelInWallet() { + if (storedChannel == null) + return; + storedChannel.updateValueToMe(valueToMe); + StoredPaymentChannelClientStates channels = (StoredPaymentChannelClientStates) + wallet.getExtensions().get(StoredPaymentChannelClientStates.EXTENSION_ID); + wallet.addOrUpdateExtension(channels); + } + + /** + * Sets this channel's state in {@link StoredPaymentChannelClientStates} to unopened so this channel can be reopened + * later. + * + * @see PaymentChannelClientState#storeChannelInWallet(Sha256Hash) + */ + public synchronized void disconnectFromChannel() { + if (storedChannel == null) + return; + synchronized (storedChannel) { + storedChannel.active = false; + } + storedChannel = null; + } + + /** + *

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.

+ * + *

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.

+ * + * @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) { + checkState(state == State.READY && id != null); + if (storedChannel != null) { + checkState(storedChannel.id.equals(id)); + return; + } + + StoredPaymentChannelClientStates channels = (StoredPaymentChannelClientStates) + wallet.getExtensions().get(StoredPaymentChannelClientStates.EXTENSION_ID); + checkState(channels.getChannel(id, multisigContract.getHash()) == null); + storedChannel = new StoredClientChannel(id, multisigContract, refundTx, myKey, valueToMe, refundFees); + channels.putChannel(storedChannel); + wallet.addOrUpdateExtension(channels); + } + + /** + * Returns the fees that will be paid if the refund transaction has to be claimed because the server failed to close + * the channel properly. May only be called after {@link PaymentChannelClientState#initiate()} + */ + public synchronized BigInteger getRefundTxFees() { + checkState(state.compareTo(State.NEW) > 0); + return refundFees; + } + + /** + * Once the servers signature over the refund transaction has been received and provided using + * {@link PaymentChannelClientState#provideRefundSignature(byte[])} then this + * method can be called to receive the now valid and broadcastable refund transaction. + */ + public synchronized Transaction getCompletedRefundTransaction() { + checkState(state.compareTo(State.WAITING_FOR_SIGNED_REFUND) > 0); + return refundTx; + } + + /** + * Gets the total value of this channel (ie the maximum payment possible) + */ + public BigInteger getTotalValue() { + return totalValue; + } + + /** + * Gets the current amount refunded to us from the multisig contract (ie totalValue-valueSentToServer) + */ + public synchronized BigInteger getValueRefunded() { + checkState(state == State.READY); + return valueToMe; + } +} diff --git a/core/src/main/java/com/google/bitcoin/protocols/channels/PaymentChannelCloseException.java b/core/src/main/java/com/google/bitcoin/protocols/channels/PaymentChannelCloseException.java new file mode 100644 index 00000000..34daaf5d --- /dev/null +++ b/core/src/main/java/com/google/bitcoin/protocols/channels/PaymentChannelCloseException.java @@ -0,0 +1,57 @@ +package com.google.bitcoin.protocols.channels; + +/** + * Used to indicate that a channel was closed before it was expected to be closed. + * This could mean the connection timed out, the other send sent an error or a CLOSE message, etc + */ +public class PaymentChannelCloseException extends Exception { + public enum CloseReason { + /** We could not find a version which was mutually acceptable with the client/server */ + NO_ACCEPTABLE_VERSION, + /** Generated by a client when the server attempted to lock in our funds for an unacceptably long time */ + TIME_WINDOW_TOO_LARGE, + /** Generated by a client when the server requested we lock up an unacceptably high value */ + SERVER_REQUESTED_TOO_MUCH_VALUE, + + // Values after here indicate its probably possible to try reopening channel again + + /** + *

The {@link com.google.bitcoin.protocols.channels.PaymentChannelClient#close()} method was called or the + * client sent a CLOSE message.

+ *

As long as the server received the CLOSE message, this means that the channel was closed and the payment + * transaction (if any) was broadcast. If the client attempts to open a new connection, a new channel will have + * to be opened.

+ */ + CLIENT_REQUESTED_CLOSE, + + /** + *

The {@link com.google.bitcoin.protocols.channels.PaymentChannelServer#close()} method was called or server + * sent a CLOSE message.

+ * + *

This may occur if the server opts to close the connection for some reason, or automatically if the channel + * times out (called by {@link StoredPaymentChannelServerStates}).

+ * + *

For a client, this usually indicates that we should try again if we need to continue paying (either + * opening a new channel or continuing with the same one depending on the server's preference)

+ */ + SERVER_REQUESTED_CLOSE, + + /** Remote side sent an ERROR message */ + REMOTE_SENT_ERROR, + /** Remote side sent a message we did not understand */ + REMOTE_SENT_INVALID_MESSAGE, + + /** The connection was closed without an ERROR/CLOSE message */ + CONNECTION_CLOSED, + } + + CloseReason error; + public CloseReason getCloseReason() { + return error; + } + + public PaymentChannelCloseException(String message, CloseReason error) { + super(message); + this.error = error; + } +} diff --git a/core/src/main/java/com/google/bitcoin/protocols/channels/PaymentChannelServer.java b/core/src/main/java/com/google/bitcoin/protocols/channels/PaymentChannelServer.java new file mode 100644 index 00000000..cdb53ea1 --- /dev/null +++ b/core/src/main/java/com/google/bitcoin/protocols/channels/PaymentChannelServer.java @@ -0,0 +1,411 @@ +package com.google.bitcoin.protocols.channels; + +import java.math.BigInteger; +import java.util.concurrent.locks.ReentrantLock; + +import com.google.bitcoin.core.*; +import com.google.bitcoin.utils.Locks; +import com.google.common.util.concurrent.MoreExecutors; +import com.google.protobuf.ByteString; +import net.jcip.annotations.GuardedBy; +import org.bitcoin.paymentchannel.Protos; +import org.slf4j.LoggerFactory; + +import com.google.bitcoin.protocols.channels.PaymentChannelCloseException.CloseReason; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; + +/** + *

A handler class which handles most of the complexity of creating a payment channel connection by providing a + * simple in/out interface which is provided with protobufs from the client and which generates protobufs which should + * be sent to the client.

+ * + *

Does all required verification of messages and properly stores state objects in the wallet-attached + * {@link StoredPaymentChannelServerStates} so that they are automatically closed when necessary and payment + * transactions are not lost if the application crashes before it unlocks.

+ */ +public class PaymentChannelServer { + //TODO: Update JavaDocs with notes for communication over stateless protocols + private static final org.slf4j.Logger log = LoggerFactory.getLogger(PaymentChannelServer.class); + + protected final ReentrantLock lock = Locks.lock("channelserver"); + + // The step in the initialization process we are in, some of this is duplicated in the PaymentChannelServerState + private enum InitStep { + WAITING_ON_CLIENT_VERSION, + WAITING_ON_UNSIGNED_REFUND, + WAITING_ON_CONTRACT, + WAITING_ON_MULTISIG_ACCEPTANCE, + CHANNEL_OPEN + } + @GuardedBy("lock") private InitStep step = InitStep.WAITING_ON_CLIENT_VERSION; + + /** + * Implements the connection between this server and the client, providing an interface which allows messages to be + * sent to the client, requests for the connection to the client to be closed, and callbacks which occur when the + * channel is fully open or the client completes a payment. + */ + public interface ServerConnection { + /** + *

Requests that the given message be sent to the client. There are no blocking requirements for this method, + * however the order of messages must be preserved.

+ * + *

If the send fails, no exception should be thrown, however + * {@link PaymentChannelServer#connectionClosed()} should be called immediately.

+ * + *

Called while holding a lock on the {@link PaymentChannelServer} object - be careful about reentrancy

+ */ + public void sendToClient(Protos.TwoWayChannelMessage msg); + + /** + *

Requests that the connection to the client be closed

+ * + *

Called while holding a lock on the {@link PaymentChannelServer} object - be careful about reentrancy

+ * + * @param reason The reason for the closure, see the individual values for more details. + * It is usually safe to ignore this value. + */ + public void destroyConnection(CloseReason reason); + + /** + *

Triggered when the channel is opened and payments can begin

+ * + *

Called while holding a lock on the {@link PaymentChannelServer} object - be careful about reentrancy

+ * + * @param contractHash A unique identifier which represents this channel (actually the hash of the multisig contract) + */ + public void channelOpen(Sha256Hash contractHash); + + /** + *

Called when the payment in this channel was successfully incremented by the client

+ * + *

Called while holding a lock on the {@link PaymentChannelServer} object - be careful about reentrancy

+ * + * @param by The increase in total payment + * @param to The new total payment to us (not including fees which may be required to claim the payment) + */ + public void paymentIncrease(BigInteger by, BigInteger to); + } + @GuardedBy("lock") private final ServerConnection conn; + + // Used to keep track of whether or not the "socket" ie connection is open and we can generate messages + @GuardedBy("lock") private boolean connectionOpen = false; + // Indicates that no further messages should be sent and we intend to close the connection + @GuardedBy("lock") private boolean connectionClosing = false; + + // The wallet and peergroup which are used to complete/broadcast transactions + private final Wallet wallet; + private final PeerGroup peerGroup; + + // The key used for multisig in this channel + @GuardedBy("lock") private ECKey myKey; + + // The minimum accepted channel value + private final BigInteger minAcceptedChannelSize; + + // The state manager for this channel + @GuardedBy("lock") private PaymentChannelServerState state; + + // The time this channel expires (ie the refund transaction's locktime) + @GuardedBy("lock") private long expireTime; + + /** + *

The amount of time we request the client lock in their funds.

+ * + *

The value defaults to 24 hours - 60 seconds and should always be greater than 2 hours plus the amount of time + * the channel is expected to be used and smaller than 24 hours minus the client <-> server latency minus some + * factor to account for client clock inaccuracy.

+ */ + public long timeWindow = 24*60*60 - 60; + + /** + * Creates a new server-side state manager which handles a single client connection. + * + * @param peerGroup The PeerGroup on which transactions will be broadcast - should have multiple connections. + * @param wallet The wallet which will be used to complete transactions. + * Unlike {@link PaymentChannelClient}, this does not have to already contain a StoredState manager + * @param minAcceptedChannelSize The minimum value the client must lock into this channel. A value too large will be + * rejected by clients, and a value too low will require excessive channel reopening + * and may cause fees to be require to close the channel. A reasonable value depends + * entirely on the expected maximum for the channel, and should likely be somewhere + * between a few bitcents and a bitcoin. + * @param conn A callback listener which represents the connection to the client (forwards messages we generate to + * the client and will close the connection on request) + */ + public PaymentChannelServer(PeerGroup peerGroup, Wallet wallet, BigInteger minAcceptedChannelSize, ServerConnection conn) { + this.peerGroup = checkNotNull(peerGroup); + this.wallet = checkNotNull(wallet); + this.minAcceptedChannelSize = checkNotNull(minAcceptedChannelSize); + this.conn = checkNotNull(conn); + } + + @GuardedBy("lock") + private void receiveVersionMessage(Protos.TwoWayChannelMessage msg) throws VerificationException { + Protos.ServerVersion.Builder versionNegotiationBuilder = Protos.ServerVersion.newBuilder() + .setMajor(0).setMinor(1); + conn.sendToClient(Protos.TwoWayChannelMessage.newBuilder() + .setType(Protos.TwoWayChannelMessage.MessageType.SERVER_VERSION) + .setServerVersion(versionNegotiationBuilder) + .build()); + + ByteString reopenChannelContractHash = msg.getClientVersion().getPreviousChannelContractHash(); + if (reopenChannelContractHash != null && reopenChannelContractHash.size() == 32) { + StoredPaymentChannelServerStates channels = (StoredPaymentChannelServerStates) + wallet.getExtensions().get(StoredPaymentChannelServerStates.EXTENSION_ID); + if (channels != null) { + Sha256Hash contractHash = new Sha256Hash(reopenChannelContractHash.toByteArray()); + StoredServerChannel storedServerChannel = channels.getChannel(contractHash); + if (storedServerChannel != null) { + if (storedServerChannel.setConnectedHandler(this)) { + log.info("Got resume version message, responding with VERSIONS and CHANNEL_OPEN"); + + state = storedServerChannel.getState(wallet, peerGroup); + step = InitStep.CHANNEL_OPEN; + conn.sendToClient(Protos.TwoWayChannelMessage.newBuilder() + .setType(Protos.TwoWayChannelMessage.MessageType.CHANNEL_OPEN) + .build()); + conn.channelOpen(contractHash); + return; + } + } + } + } + log.info("Got initial version message, responding with VERSIONS and INITIATE"); + + myKey = new ECKey(); + wallet.addKey(myKey); + + expireTime = Utils.now().getTime() / 1000 + timeWindow; + step = InitStep.WAITING_ON_UNSIGNED_REFUND; + + Protos.Initiate.Builder initiateBuilder = Protos.Initiate.newBuilder() + .setMultisigKey(ByteString.copyFrom(myKey.getPubKey())) + .setExpireTimeSecs(expireTime) + .setMinAcceptedChannelSize(minAcceptedChannelSize.longValue()); + + conn.sendToClient(Protos.TwoWayChannelMessage.newBuilder() + .setInitiate(initiateBuilder) + .setType(Protos.TwoWayChannelMessage.MessageType.INITIATE) + .build()); + } + + @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(peerGroup, 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()); + } + + private void multisigContractPropogated(Sha256Hash contractHash) { + lock.lock(); + try { + if (!connectionOpen || connectionClosing) + return; + state.storeChannelInWallet(PaymentChannelServer.this); + conn.sendToClient(Protos.TwoWayChannelMessage.newBuilder() + .setType(Protos.TwoWayChannelMessage.MessageType.CHANNEL_OPEN) + .build()); + step = InitStep.CHANNEL_OPEN; + conn.channelOpen(contractHash); + } finally { + lock.unlock(); + } + } + + @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"); + 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(multisigContract.getHash()); + } + }, MoreExecutors.sameThreadExecutor()); + } + + @GuardedBy("lock") + private void receiveUpdatePaymentMessage(Protos.TwoWayChannelMessage msg) throws VerificationException, ValueOutOfRangeException { + checkState(step == InitStep.CHANNEL_OPEN && msg.hasUpdatePayment()); + log.info("Got a payment update"); + + Protos.UpdatePayment updatePayment = msg.getUpdatePayment(); + BigInteger lastBestPayment = state.getBestValueToMe(); + state.incrementPayment(BigInteger.valueOf(updatePayment.getClientChangeValue()), updatePayment.getSignature().toByteArray()); + BigInteger bestPaymentChange = state.getBestValueToMe().subtract(lastBestPayment); + + if (bestPaymentChange.compareTo(BigInteger.ZERO) > 0) + conn.paymentIncrease(bestPaymentChange, state.getBestValueToMe()); + } + + /** + * 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 (connectionClosing) + 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: + checkState(step == InitStep.WAITING_ON_CLIENT_VERSION && msg.hasClientVersion()); + if (msg.getClientVersion().getMajor() != 0) { + errorBuilder = Protos.Error.newBuilder() + .setCode(Protos.Error.ErrorCode.NO_ACCEPTABLE_VERSION); + closeReason = CloseReason.NO_ACCEPTABLE_VERSION; + break; + } + + receiveVersionMessage(msg); + return; + case PROVIDE_REFUND: + receiveRefundMessage(msg); + return; + case PROVIDE_CONTRACT: + receiveContractMessage(msg); + return; + case UPDATE_PAYMENT: + receiveUpdatePaymentMessage(msg); + return; + case CLOSE: + log.info("Got CLOSE message, closing channel"); + connectionClosing = true; + if (state != null) + state.close(); + conn.destroyConnection(CloseReason.CLIENT_REQUESTED_CLOSE); + 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: + log.error("Got unknown message type or type that doesn't apply to servers."); + errorBuilder = Protos.Error.newBuilder() + .setCode(Protos.Error.ErrorCode.SYNTAX_ERROR); + closeReason = CloseReason.REMOTE_SENT_INVALID_MESSAGE; + break; + } + } catch (VerificationException e) { + log.error("Caught verification exception handling message from client {}", e); + errorBuilder = Protos.Error.newBuilder() + .setCode(Protos.Error.ErrorCode.BAD_TRANSACTION) + .setExplanation(e.getMessage()); + closeReason = CloseReason.REMOTE_SENT_INVALID_MESSAGE; + } catch (ValueOutOfRangeException e) { + log.error("Caught value out of range exception handling message from client {}", e); + errorBuilder = Protos.Error.newBuilder() + .setCode(Protos.Error.ErrorCode.BAD_TRANSACTION) + .setExplanation(e.getMessage()); + closeReason = CloseReason.REMOTE_SENT_INVALID_MESSAGE; + } catch (IllegalStateException e) { + log.error("Caught illegal state exception handling message from client {}", e); + errorBuilder = Protos.Error.newBuilder() + .setCode(Protos.Error.ErrorCode.SYNTAX_ERROR); + closeReason = CloseReason.REMOTE_SENT_INVALID_MESSAGE; + } + conn.sendToClient(Protos.TwoWayChannelMessage.newBuilder() + .setError(errorBuilder) + .setType(Protos.TwoWayChannelMessage.MessageType.ERROR) + .build()); + conn.destroyConnection(closeReason); + } finally { + lock.unlock(); + } + } + + /** + *

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.

+ * + *

Note that this MUST 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.setConnectedHandler(null); + } + } + } + } 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(); + } + } + + /** + *

Closes the connection by generating a close message for the client and calls + * {@link ServerConnection#destroyConnection(CloseReason)}. Note that this does not broadcast + * the payment transaction and the client may still resume the same channel if they reconnect

+ * + *

Note that {@link PaymentChannelServer#connectionClosed()} must still be called after the connection fully + * closes.

+ */ + public void close() { + lock.lock(); + try { + if (connectionOpen && !connectionClosing) { + conn.sendToClient(Protos.TwoWayChannelMessage.newBuilder() + .setType(Protos.TwoWayChannelMessage.MessageType.CLOSE) + .build()); + conn.destroyConnection(CloseReason.SERVER_REQUESTED_CLOSE); + } + } finally { + lock.unlock(); + } + } +} diff --git a/core/src/main/java/com/google/bitcoin/protocols/channels/PaymentChannelServerListener.java b/core/src/main/java/com/google/bitcoin/protocols/channels/PaymentChannelServerListener.java new file mode 100644 index 00000000..87bab817 --- /dev/null +++ b/core/src/main/java/com/google/bitcoin/protocols/channels/PaymentChannelServerListener.java @@ -0,0 +1,183 @@ +/* + * Copyright 2013 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.bitcoin.protocols.channels; + +import java.io.IOException; +import java.math.BigInteger; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +import com.google.bitcoin.core.PeerGroup; +import com.google.bitcoin.core.Sha256Hash; +import com.google.bitcoin.core.Wallet; +import com.google.bitcoin.protocols.niowrapper.ProtobufParser; +import com.google.bitcoin.protocols.niowrapper.ProtobufParserFactory; +import com.google.bitcoin.protocols.niowrapper.ProtobufServer; +import org.bitcoin.paymentchannel.Protos; + +import static com.google.common.base.Preconditions.checkNotNull; + +/** + * Manages a {@link PaymentChannelClient} by connecting to a server using a simple TCP socket and exchanging the + * necessary protobufs. + */ +public class PaymentChannelServerListener { + // The wallet and peergroup which are used to complete/broadcast transactions + private final Wallet wallet; + private final PeerGroup peerGroup; + + // The event handler factory which creates new ServerConnectionEventHandler per connection + private final HandlerFactory eventHandlerFactory; + private final BigInteger minAcceptedChannelSize; + + private final ProtobufServer server; + + /** + * A factory which generates connection-specific event handlers. + */ + public static interface HandlerFactory { + /** + * Called when a new connection completes version handshake to get a new connection-specific listener. + * If null is returned, the connection is immediately closed. + */ + @Nullable public ServerConnectionEventHandler onNewConnection(SocketAddress clientAddress); + } + + private class ServerHandler { + public ServerHandler(final SocketAddress address, final int timeoutSeconds) { + paymentChannelManager = new PaymentChannelServer(peerGroup, wallet, minAcceptedChannelSize, new PaymentChannelServer.ServerConnection() { + @Override public void sendToClient(Protos.TwoWayChannelMessage msg) { + socketProtobufHandler.write(msg); + } + + @Override public void destroyConnection(PaymentChannelCloseException.CloseReason reason) { + if (closeReason != null) + closeReason = reason; + socketProtobufHandler.closeConnection(); + } + + @Override public void channelOpen(Sha256Hash contractHash) { + socketProtobufHandler.setSocketTimeout(0); + eventHandler.channelOpen(contractHash); + } + + @Override public void paymentIncrease(BigInteger by, BigInteger to) { + eventHandler.paymentIncrease(by, to); + } + }); + + protobufHandlerListener = new ProtobufParser.Listener() { + @Override + public synchronized void messageReceived(ProtobufParser handler, Protos.TwoWayChannelMessage msg) { + paymentChannelManager.receiveMessage(msg); + } + + @Override + public synchronized void connectionClosed(ProtobufParser handler) { + paymentChannelManager.connectionClosed(); + if (closeReason != null) + eventHandler.channelClosed(closeReason); + else + eventHandler.channelClosed(PaymentChannelCloseException.CloseReason.CONNECTION_CLOSED); + eventHandler.setConnectionChannel(null); + } + + @Override + public synchronized void connectionOpen(ProtobufParser handler) { + ServerConnectionEventHandler eventHandler = eventHandlerFactory.onNewConnection(address); + if (eventHandler == null) + handler.closeConnection(); + else { + ServerHandler.this.eventHandler = eventHandler; + paymentChannelManager.connectionOpen(); + } + } + }; + + socketProtobufHandler = new ProtobufParser + (protobufHandlerListener, Protos.TwoWayChannelMessage.getDefaultInstance(), Short.MAX_VALUE, timeoutSeconds*1000); + } + + private PaymentChannelCloseException.CloseReason closeReason; + + // The user-provided event handler + @Nonnull private ServerConnectionEventHandler eventHandler; + + // The payment channel server which does the actual payment channel handling + private final PaymentChannelServer paymentChannelManager; + + // The connection handler which puts/gets protobufs from the TCP socket + private final ProtobufParser socketProtobufHandler; + + // The listener which connects to socketProtobufHandler + private final ProtobufParser.Listener protobufHandlerListener; + } + + /** + * Binds to the given port and starts accepting new client connections. + * @throws Exception If binding to the given port fails (eg SocketException: Permission denied for privileged ports) + */ + public void bindAndStart(int port) throws Exception { + server.start(new InetSocketAddress(port)); + } + + /** + * Sets up a new payment channel server which listens on the given port. + * + * @param peerGroup The PeerGroup on which transactions will be broadcast - should have multiple connections. + * @param wallet The wallet which will be used to complete transactions + * @param timeoutSeconds The read timeout between messages. This should accommodate latency and client ECDSA + * signature operations. + * @param minAcceptedChannelSize The minimum amount of coins clients must lock in to create a channel. Clients which + * are unwilling or unable to lock in at least this value will immediately disconnect. + * For this reason, a fairly conservative value (in terms of average value spent on a + * channel) should generally be chosen. + * @param eventHandlerFactory A factory which generates event handlers which are created for each new connection + */ + public PaymentChannelServerListener(PeerGroup peerGroup, Wallet wallet, final int timeoutSeconds, BigInteger minAcceptedChannelSize, + HandlerFactory eventHandlerFactory) throws IOException { + this.wallet = checkNotNull(wallet); + this.peerGroup = checkNotNull(peerGroup); + this.eventHandlerFactory = checkNotNull(eventHandlerFactory); + this.minAcceptedChannelSize = checkNotNull(minAcceptedChannelSize); + + server = new ProtobufServer(new ProtobufParserFactory() { + @Override + public ProtobufParser getNewParser(InetAddress inetAddress, int port) { + return new ServerHandler(new InetSocketAddress(inetAddress, port), timeoutSeconds).socketProtobufHandler; + } + }); + } + + /** + *

Closes all client connections currently connected gracefully.

+ * + *

Note that this does not close the actual payment channels (and broadcast payment transactions), which + * must be done using the {@link StoredPaymentChannelServerStates} which manages the states for the associated + * wallet.

+ */ + public void close() { + try { + server.stop(); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } +} diff --git a/core/src/main/java/com/google/bitcoin/protocols/channels/PaymentChannelServerState.java b/core/src/main/java/com/google/bitcoin/protocols/channels/PaymentChannelServerState.java new file mode 100644 index 00000000..e5395dd1 --- /dev/null +++ b/core/src/main/java/com/google/bitcoin/protocols/channels/PaymentChannelServerState.java @@ -0,0 +1,472 @@ +/* + * Copyright 2013 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.bitcoin.protocols.channels; + +import java.math.BigInteger; +import java.util.Arrays; +import javax.annotation.Nullable; + +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.common.collect.ImmutableList; +import com.google.common.collect.Lists; +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.SettableFuture; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import static com.google.common.base.Preconditions.*; + +/** + *

A payment channel is a method of sending money to someone such that the amount of money you send can be adjusted + * after the fact, in an efficient manner that does not require broadcasting to the network. This can be used to + * implement micropayments or other payment schemes in which immediate settlement is not required, but zero trust + * negotiation is. Note that this class only allows the amount of money received to be incremented, not decremented.

+ * + *

This class implements the core state machine for the server side of the protocol. The client side is implemented + * by {@link PaymentChannelClientState} and {@link PaymentChannelServerListener} implements the server-side network + * protocol listening for TCP/IP connections and moving this class through each state. We say that the party who is + * sending funds is the client or initiating party. The party that is receiving the funds is the + * server or receiving party. Although the underlying Bitcoin protocol is capable of more complex + * relationships than that, this class implements only the simplest case.

+ * + *

To protect clients from malicious servers, a channel has an expiry parameter. When this expiration is reached, the + * client will broadcast the created refund transaction and take back all the money in this channel. Because this is + * specified in terms of block timestamps, it is fairly fuzzy and it is possible to spend the refund transaction up to a + * few hours before the actual timestamp. Thus, it is very important that the channel be closed with plenty of time left + * to get the highest value payment transaction confirmed before the expire time (minimum 3-4 hours is suggested if the + * payment transaction has enough fee to be confirmed in the next block or two).

+ * + *

To begin, we must provide the client with a pubkey which we wish to use for the multi-sig contract which locks in + * the channel. The client will then provide us with an incomplete refund transaction and the pubkey which they used in + * the multi-sig contract. We use this pubkey to recreate the multi-sig output and then sign that to the refund + * transaction. We provide that signature to the client and they then have the ability to spend the refund transaction + * at the specified expire time. The client then provides us with the full, signed multi-sig contract which we verify + * and broadcast, locking in their funds until we spend a payment transaction or the expire time is reached. The client + * can then begin paying by providing us with signatures for the multi-sig contract which pay some amount back to the + * client, and the rest is ours to do with as we wish.

+ */ +public class PaymentChannelServerState { + private static final Logger log = LoggerFactory.getLogger(PaymentChannelServerState.class); + + /** + * The different logical states the channel can be in. Because the first action we need to track is the client + * providing the refund transaction, we begin in WAITING_FOR_REFUND_TRANSACTION. We then step through the states + * until READY, at which time the client can increase payment incrementally. + */ + public enum State { + WAITING_FOR_REFUND_TRANSACTION, + WAITING_FOR_MULTISIG_CONTRACT, + WAITING_FOR_MULTISIG_ACCEPTANCE, + READY, + CLOSING, + CLOSED, + ERROR, + } + private State state; + + // The client and server keys for the multi-sig contract + // We currently also use the serverKey for payouts, but this is not required + private ECKey clientKey, serverKey; + + // Package-local for checkArguments in StoredServerChannel + final Wallet wallet; + + // The peer group we will broadcast transactions to + private final PeerGroup peerGroup; + + // The multi-sig contract and the output script from it + private Transaction multisigContract = null; + private Script multisigScript; + + // The last signature the client provided for a payment transaction. + private byte[] bestValueSignature; + + // The total value locked into the multi-sig output and the value to us in the last signature the client provided + private BigInteger totalValue; + private BigInteger bestValueToMe = BigInteger.ZERO; + private BigInteger feePaidForPayment; + + // The refund/change transaction output that goes back to the client + private TransactionOutput clientOutput; + private long refundTransactionUnlockTimeSecs; + + private long minExpireTime; + + private StoredServerChannel storedServerChannel = null; + + PaymentChannelServerState(StoredServerChannel storedServerChannel, Wallet wallet, PeerGroup peerGroup) throws VerificationException { + synchronized (storedServerChannel) { + this.wallet = checkNotNull(wallet); + this.peerGroup = checkNotNull(peerGroup); + this.multisigContract = checkNotNull(storedServerChannel.contract); + this.multisigScript = multisigContract.getOutput(0).getScriptPubKey(); + this.clientKey = new ECKey(null, multisigScript.getChunks().get(1).data); + this.clientOutput = checkNotNull(storedServerChannel.clientOutput); + this.refundTransactionUnlockTimeSecs = storedServerChannel.refundTransactionUnlockTimeSecs; + this.serverKey = checkNotNull(storedServerChannel.myKey); + this.totalValue = multisigContract.getOutput(0).getValue(); + this.bestValueToMe = checkNotNull(storedServerChannel.bestValueToMe); + this.bestValueSignature = storedServerChannel.bestValueSignature; + checkArgument(bestValueToMe.equals(BigInteger.ZERO) || bestValueSignature != null); + this.storedServerChannel = storedServerChannel; + storedServerChannel.state = this; + this.state = State.READY; + } + } + + /** + * Creates a new state object to track the server side of a payment channel. + * + * @param peerGroup The peer group which we will broadcast transactions to, this should have multiple peers + * @param wallet The wallet which will be used to complete transactions + * @param serverKey The private key which we use for our part of the multi-sig contract + * (this MUST be fresh and CANNOT be used elsewhere) + * @param minExpireTime The earliest time at which the client can claim the refund transaction (UNIX timestamp of block) + */ + public PaymentChannelServerState(PeerGroup peerGroup, Wallet wallet, ECKey serverKey, long minExpireTime) { + this.state = State.WAITING_FOR_REFUND_TRANSACTION; + this.serverKey = checkNotNull(serverKey); + this.wallet = checkNotNull(wallet); + this.peerGroup = checkNotNull(peerGroup); + this.minExpireTime = minExpireTime; + } + + /** + * This object implements a state machine, and this accessor returns which state it's currently in. + */ + public synchronized State getState() { + return state; + } + + /** + * Called when the client provides the refund transaction. + * The refund transaction must have one input from the multisig contract (that we don't have yet) and one output + * that the client creates to themselves. This object will later be modified when we start getting paid. + * + * @param refundTx The refund transaction, this object will be mutated when payment is incremented. + * @param clientMultiSigPubKey The client's pubkey which is required for the multisig output + * @return Our signature that makes the refund transaction valid + * @throws VerificationException If the transaction isnt valid or did not meet the requirements of a refund transaction. + */ + public synchronized byte[] provideRefundTransaction(Transaction refundTx, byte[] clientMultiSigPubKey) throws VerificationException { + checkNotNull(refundTx); + checkNotNull(clientMultiSigPubKey); + checkState(state == State.WAITING_FOR_REFUND_TRANSACTION); + log.info("Provided with refund transaction: {}", refundTx); + // Do a few very basic syntax sanity checks. + refundTx.verify(); + // Verify that the refund transaction has a single input (that we can fill to sign the multisig output). + if (refundTx.getInputs().size() != 1) + throw new VerificationException("Refund transaction does not have exactly one input"); + // Verify that the refund transaction has a time lock on it and a sequence number of zero. + if (refundTx.getInput(0).getSequenceNumber() != 0) + throw new VerificationException("Refund transaction's input's sequence number is non-0"); + if (refundTx.getLockTime() < minExpireTime) + throw new VerificationException("Refund transaction has a lock time too soon"); + // Verify the transaction has one output (we don't care about its contents, its up to the client) + // Note that because we sign with SIGHASH_NONE|SIGHASH_ANYOENCANPAY the client can later add more outputs and + // inputs, but we will need only one output later to create the paying transactions + if (refundTx.getOutputs().size() != 1) + throw new VerificationException("Refund transaction does not have exactly one output"); + + refundTransactionUnlockTimeSecs = refundTx.getLockTime(); + + // Sign the refund tx with the scriptPubKey and return the signature. We don't have the spending transaction + // so do the steps individually. + clientKey = new ECKey(null, clientMultiSigPubKey); + Script multisigPubKey = ScriptBuilder.createMultiSigOutputScript(2, ImmutableList.of(clientKey, serverKey)); + // We are really only signing the fact that the transaction has a proper lock time and don't care about anything + // else, so we sign SIGHASH_NONE and SIGHASH_ANYONECANPAY. + TransactionSignature sig = refundTx.calculateSignature(0, serverKey, multisigPubKey, Transaction.SigHash.NONE, true); + log.info("Signed refund transaction."); + this.clientOutput = refundTx.getOutput(0); + state = State.WAITING_FOR_MULTISIG_CONTRACT; + return sig.encodeToBitcoin(); + } + + /** + * Called when the client provides the multi-sig contract. Checks that the previously-provided refund transaction + * spends this transaction (because we will use it as a base to create payment transactions) as well as output value + * and form (ie it is a 2-of-2 multisig to the correct keys). + * + * @param multisigContract The provided multisig contract. Do not mutate this object after this call. + * @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 VerificationException If the provided multisig contract is not well-formed or does not meet previously-specified parameters + */ + public synchronized ListenableFuture provideMultiSigContract(Transaction multisigContract) throws VerificationException { + checkNotNull(multisigContract); + checkState(state == State.WAITING_FOR_MULTISIG_CONTRACT); + try { + multisigContract.verify(); + this.multisigContract = multisigContract; + this.multisigScript = multisigContract.getOutput(0).getScriptPubKey(); + + // Check that multisigContract's first output is a 2-of-2 multisig to the correct pubkeys in the correct order + final Script expectedSecript = ScriptBuilder.createMultiSigOutputScript(2, Lists.newArrayList(clientKey, serverKey)); + if (!Arrays.equals(multisigScript.getProgram(), expectedSecript.getProgram())) + throw new VerificationException("Multisig contract's first output was not a standard 2-of-2 multisig to client and server in that order."); + + this.totalValue = multisigContract.getOutput(0).getValue(); + if (this.totalValue.compareTo(BigInteger.ZERO) <= 0) + throw new VerificationException("Not accepting an attempt to open a contract with zero value."); + } catch (VerificationException e) { + // We couldn't parse the multisig transaction or its output. + log.error("Provided multisig contract did not verify: {}", multisigContract.toString()); + throw e; + } + log.info("Broadcasting multisig contract: {}", multisigContract); + state = State.WAITING_FOR_MULTISIG_ACCEPTANCE; + final SettableFuture future = SettableFuture.create(); + Futures.addCallback(peerGroup.broadcastTransaction(multisigContract), new FutureCallback() { + @Override public void onSuccess(Transaction transaction) { + log.info("Successfully broadcast multisig contract {}. Channel now open.", transaction.getHashAsString()); + state = State.READY; + future.set(PaymentChannelServerState.this); + } + + @Override public void onFailure(Throwable throwable) { + // Couldn't broadcast the transaction for some reason. + log.error(throwable.toString()); + throwable.printStackTrace(); + state = State.ERROR; + future.setException(throwable); + } + }); + return future; + } + + // Create a payment transaction with valueToMe going back to us + private synchronized Wallet.SendRequest makeUnsignedChannelContract(BigInteger valueToMe) { + Transaction tx = new Transaction(wallet.getParams()); + if (!totalValue.subtract(valueToMe).equals(BigInteger.ZERO)) { + clientOutput.setValue(totalValue.subtract(valueToMe)); + tx.addOutput(clientOutput); + } + tx.addInput(multisigContract.getOutput(0)); + return Wallet.SendRequest.forTx(tx); + } + + /** + * Called when the client provides us with a new signature and wishes to increment total payment by size. + * Verifies the provided signature and only updates values if everything checks out. + * If the new refundSize is not the lowest we have seen, it is simply ignored. + * + * @param refundSize How many satoshis of the original contract are refunded to the client (the rest are ours) + * @param signatureBytes The new signature spending the multi-sig contract to a new payment transaction + * @throws VerificationException If the signature does not verify or size is out of range (incl being rejected by the network as dust). + */ + public synchronized void incrementPayment(BigInteger refundSize, byte[] signatureBytes) throws VerificationException, ValueOutOfRangeException { + checkState(state == State.READY); + checkNotNull(refundSize); + checkNotNull(signatureBytes); + TransactionSignature signature = TransactionSignature.decodeFromBitcoin(signatureBytes, true); + // We allow snapping to zero for the payment amount because it's treated specially later, but not less than + // the dust level because that would prevent the transaction from being relayed/mined. + final boolean fullyUsedUp = refundSize.equals(BigInteger.ZERO); + if (refundSize.compareTo(clientOutput.getMinNonDustValue()) < 0 && !fullyUsedUp) + throw new ValueOutOfRangeException("Attempt to refund negative value or value too small to be accepted by the network"); + BigInteger newValueToMe = totalValue.subtract(refundSize); + if (newValueToMe.compareTo(BigInteger.ZERO) < 0) + throw new ValueOutOfRangeException("Attempt to refund more than the contract allows."); + if (newValueToMe.compareTo(bestValueToMe) < 0) + return; + + Transaction.SigHash mode; + // If the client doesn't want anything back, they shouldn't sign any outputs at all. + if (fullyUsedUp) + mode = Transaction.SigHash.NONE; + else + mode = Transaction.SigHash.SINGLE; + + if (signature.sigHashMode() != mode || !signature.anyoneCanPay()) + throw new VerificationException("New payment signature was not signed with the right SIGHASH flags."); + + Wallet.SendRequest req = makeUnsignedChannelContract(newValueToMe); + // Now check the signature is correct. + // Note that the client must sign with SIGHASH_{SINGLE/NONE} | SIGHASH_ANYONECANPAY to allow us to add additional + // inputs (in case we need to add significant fee, or something...) and any outputs we want to pay to. + Sha256Hash sighash = req.tx.hashForSignature(0, multisigScript, mode, true); + + if (!clientKey.verify(sighash, signature)) + throw new VerificationException("Signature does not verify on tx\n" + req.tx); + bestValueToMe = newValueToMe; + bestValueSignature = signatureBytes; + updateChannelInWallet(); + } + + // Signs the first input of the transaction which must spend the multisig contract. + private void signMultisigInput(Transaction tx, Transaction.SigHash hashType, boolean anyoneCanPay) { + TransactionSignature signature = tx.calculateSignature(0, serverKey, multisigScript, hashType, anyoneCanPay); + byte[] mySig = signature.encodeToBitcoin(); + Script scriptSig = ScriptBuilder.createMultiSigInputScriptBytes(ImmutableList.of(bestValueSignature, mySig)); + tx.getInput(0).setScriptSig(scriptSig); + } + + final SettableFuture closedFuture = SettableFuture.create(); + /** + *

Closes this channel and broadcasts the highest value payment transaction on the network.

+ * + *

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}.

+ * + *

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 ValueOutOfRangeException If the payment transaction would have cost more in fees to spend than it was worth + */ + public synchronized ListenableFuture close() throws ValueOutOfRangeException { + if (storedServerChannel != null) { + StoredServerChannel temp = storedServerChannel; + storedServerChannel = null; + StoredPaymentChannelServerStates channels = (StoredPaymentChannelServerStates) + wallet.getExtensions().get(StoredPaymentChannelServerStates.EXTENSION_ID); + channels.closeChannel(temp); // Calls this method again for us + checkState(state.compareTo(State.CLOSING) >= 0); + return closedFuture; + } + + if (state.ordinal() < State.READY.ordinal()) { + state = State.CLOSED; + closedFuture.set(this); + return closedFuture; + } + if (state != State.READY) // We are already closing/closed/in an error state + return closedFuture; + + if (bestValueToMe.equals(BigInteger.ZERO)) { + state = State.CLOSED; + closedFuture.set(this); + return closedFuture; + } + Transaction tx = null; + try { + Wallet.SendRequest req = makeUnsignedChannelContract(bestValueToMe); + tx = req.tx; + // Provide a BS signature so that completeTx wont freak out about unsigned inputs. + signMultisigInput(tx, Transaction.SigHash.NONE, true); + if (!wallet.completeTx(req)) // Let wallet handle adding additional inputs/fee as necessary. + throw new ValueOutOfRangeException("Unable to complete transaction - unable to pay required fee"); + feePaidForPayment = req.fee; + if (feePaidForPayment.compareTo(bestValueToMe) >= 0) + throw new ValueOutOfRangeException("Had to pay more in fees than the channel was worth"); + // 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 (ValueOutOfRangeException 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 future = peerGroup.broadcastTransaction(tx); + Futures.addCallback(future, new FutureCallback() { + @Override public void onSuccess(Transaction transaction) { + log.info("TX {} propagated, channel successfully closed.", transaction.getHash()); + state = State.CLOSED; + closedFuture.set(PaymentChannelServerState.this); + } + + @Override public void onFailure(Throwable throwable) { + log.error("Failed to close channel, could not broadcast: {}", throwable.toString()); + throwable.printStackTrace(); + state = State.ERROR; + closedFuture.setException(throwable); + } + }); + return closedFuture; + } + + /** + * Gets the highest payment to ourselves (which we will receive on close(), not including fees) + */ + public synchronized BigInteger getBestValueToMe() { + return bestValueToMe; + } + + /** + * Gets the fee paid in the final payment transaction (only available if close() did not throw an exception) + */ + public synchronized BigInteger getFeePaid() { + checkState(state == State.CLOSED || state == State.CLOSING); + return feePaidForPayment; + } + + /** + * Gets the multisig contract which was used to initialize this channel + */ + public synchronized Transaction getMultisigContract() { + checkState(multisigContract != null); + return multisigContract; + } + + /** + * Gets the client's refund transaction which they can spend to get the entire channel value back if it reaches its + * lock time. + */ + public synchronized long getRefundTransactionUnlockTime() { + checkState(state.compareTo(State.WAITING_FOR_MULTISIG_CONTRACT) > 0 && state != State.ERROR); + return refundTransactionUnlockTimeSecs; + } + + private synchronized void updateChannelInWallet() { + if (storedServerChannel != null) { + storedServerChannel.updateValueToMe(bestValueToMe, bestValueSignature); + StoredPaymentChannelServerStates channels = (StoredPaymentChannelServerStates) + wallet.getExtensions().get(StoredPaymentChannelServerStates.EXTENSION_ID); + wallet.addOrUpdateExtension(channels); + } + } + + /** + * 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 The {@link PaymentChannelClientState} object which is managing this object. This will + * set the appropriate pointer in the newly created {@link StoredServerChannel} before it is + * committed to wallet. + */ + 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, peerGroup)); + storedServerChannel = new StoredServerChannel(this, multisigContract, clientOutput, refundTransactionUnlockTimeSecs, serverKey, bestValueToMe, bestValueSignature); + checkState(storedServerChannel.setConnectedHandler(connectedHandler)); + channels.putChannel(storedServerChannel); + wallet.addOrUpdateExtension(channels); + } +} diff --git a/core/src/main/java/com/google/bitcoin/protocols/channels/ServerConnectionEventHandler.java b/core/src/main/java/com/google/bitcoin/protocols/channels/ServerConnectionEventHandler.java new file mode 100644 index 00000000..bf6acf75 --- /dev/null +++ b/core/src/main/java/com/google/bitcoin/protocols/channels/ServerConnectionEventHandler.java @@ -0,0 +1,77 @@ +/* + * Copyright 2013 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.bitcoin.protocols.channels; + +import java.math.BigInteger; + +import com.google.bitcoin.core.Sha256Hash; +import com.google.bitcoin.protocols.niowrapper.ProtobufParser; +import org.bitcoin.paymentchannel.Protos; + +/** +* A connection-specific event handler that handles events generated by client connections on a {@link PaymentChannelServerListener} +*/ +public abstract class ServerConnectionEventHandler { + private ProtobufParser connectionChannel; + // Called by ServerListener before channelOpen to set connectionChannel when it is ready to received application messages + // Also called with null to clear connectionChannel after channelClosed() + synchronized void setConnectionChannel(ProtobufParser connectionChannel) { this.connectionChannel = connectionChannel; } + + /** + *

Closes the channel with the client (will generate a + * {@link ServerConnectionEventHandler#channelClosed(PaymentChannelCloseException.CloseReason)} event)

+ * + *

Note that this does NOT actually broadcast the most recent payment transaction, which will be triggered + * automatically when the channel times out by the {@link StoredPaymentChannelServerStates}, or manually by calling + * {@link StoredPaymentChannelServerStates#closeChannel(StoredServerChannel)} with the channel returned by + * {@link StoredPaymentChannelServerStates#getChannel(com.google.bitcoin.core.Sha256Hash)} with the id provided in + * {@link ServerConnectionEventHandler#channelOpen(com.google.bitcoin.core.Sha256Hash)}

+ */ + protected final synchronized void closeChannel() { + if (connectionChannel == null) + throw new IllegalStateException("Channel is not fully initialized/has already been closed"); + + connectionChannel.write(Protos.TwoWayChannelMessage.newBuilder() + .setType(Protos.TwoWayChannelMessage.MessageType.CLOSE) + .build()); + connectionChannel.closeConnection(); + } + + /** + * Triggered when the channel is opened and application messages/payments can begin + * + * @param channelId A unique identifier which represents this channel (actually the hash of the multisig contract) + */ + public abstract void channelOpen(Sha256Hash channelId); + + /** + * Called when the payment in this channel was successfully incremented by the client + * + * @param by The increase in total payment + * @param to The new total payment to us (not including fees which may be required to claim the payment) + */ + public abstract void paymentIncrease(BigInteger by, BigInteger to); + + /** + *

Called when the channel was closed for some reason. May be called without a call to + * {@link ServerConnectionEventHandler#channelOpen(Sha256Hash)}.

+ * + *

Note that the same channel can be reopened at any point before it expires if the client reconnects and + * requests it.

+ */ + public abstract void channelClosed(PaymentChannelCloseException.CloseReason reason); +} diff --git a/core/src/main/java/com/google/bitcoin/protocols/channels/Simple channels protocol.txt b/core/src/main/java/com/google/bitcoin/protocols/channels/Simple channels protocol.txt new file mode 100644 index 00000000..af89a6a6 --- /dev/null +++ b/core/src/main/java/com/google/bitcoin/protocols/channels/Simple channels protocol.txt @@ -0,0 +1,26 @@ +Using the protocol suggestion by Jeremy Spillman + +1) Client connects to server and asks for a public key. +2) Server provides a fresh key. Client creates TX1 which pays to a 2-of-2 multisig output. It creates an invalid + TX2 which spends TX1 and pays all money back to itself. The refund TX is time locked. +3) Client sends TX2 to server which verifies that it's valid and not connected to any transaction in its wallet. + Server signs TX2 and sends back the signature. +4) Client verifies that the server signed TX2 correctly and then sends TX1 to the server, which verifies that it + was the tx connected to the thing it just signed, and then broadcasts it thus locking in the money. +5) Each time the channel is adjusted, the client sends a new signed TX2 to the server which keeps it (does not need + to sign itself). + +If the client or server wants to close the channel, the last TX2 is broadcast. It's a normal, final transaction so +it ends the negotiation at that point. + +If the server goes away and does not finalize the channel properly, the refund TX can be broadcast once the time lock +expires. Note that you cannot broadcast the refund tx before the time lock expires (thus filling the mempool) due to +the recent change to change non-final transactions non-standard. Thus TX replacement is not needed in this particular +configuration. + +When TX replacement is re-activated, this configuration would become vulnerable to having the refund TX be broadcast +by the client. We can require the refund TX to have an input sequence number of zero. The adjustment transactions have +a sequence number of UINT_MAX as before, this means they would replace the refund tx if it were to be broadcast. + +This configuration is less general than a full payment channel with tx replacement activated, but for our purposes +it does the trick. diff --git a/core/src/main/java/com/google/bitcoin/protocols/channels/StoredPaymentChannelClientStates.java b/core/src/main/java/com/google/bitcoin/protocols/channels/StoredPaymentChannelClientStates.java new file mode 100644 index 00000000..569b2b44 --- /dev/null +++ b/core/src/main/java/com/google/bitcoin/protocols/channels/StoredPaymentChannelClientStates.java @@ -0,0 +1,185 @@ +/* + * Copyright 2013 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.bitcoin.protocols.channels; + +import java.io.*; +import java.math.BigInteger; +import java.util.Date; +import java.util.Set; +import java.util.Timer; +import java.util.TimerTask; + +import com.google.bitcoin.core.*; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.collect.HashMultimap; + +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; + +/** + * This class maintains a set of {@link StoredClientChannel}s, automatically (re)broadcasting the contract transaction + * and broadcasting the refund transaction over the given {@link TransactionBroadcaster}. + */ +public class StoredPaymentChannelClientStates implements WalletExtension { + static final String EXTENSION_ID = StoredPaymentChannelClientStates.class.getName(); + + @VisibleForTesting final HashMultimap mapChannels = HashMultimap.create(); + @VisibleForTesting final Timer channelTimeoutHandler = new Timer(); + + private Wallet containingWallet; + private final TransactionBroadcaster announcePeerGroup; + + /** + * Creates a new StoredPaymentChannelClientStates and associates it with the given {@link Wallet} and + * {@link TransactionBroadcaster} which are used to complete and announce contract and refund + * transactions. + */ + public StoredPaymentChannelClientStates(TransactionBroadcaster announcePeerGroup, Wallet containingWallet) { + this.announcePeerGroup = checkNotNull(announcePeerGroup); + this.containingWallet = checkNotNull(containingWallet); + } + + /** + * Finds an inactive channel with the given id and returns it, or returns null. + */ + public synchronized StoredClientChannel getInactiveChannelById(Sha256Hash id) { + Set setChannels = mapChannels.get(id); + for (StoredClientChannel channel : setChannels) { + synchronized (channel) { + if (!channel.active) { + channel.active = true; + return channel; + } + } + } + return null; + } + + /** + * Finds a channel with the given id and contract hash and returns it, or returns null. + */ + public synchronized StoredClientChannel getChannel(Sha256Hash id, Sha256Hash contractHash) { + Set setChannels = mapChannels.get(id); + for (StoredClientChannel channel : setChannels) { + if (channel.contract.getHash().equals(contractHash)) + return channel; + } + return null; + } + + /** + * Adds the given channel to this set of stored states, broadcasting the contract and refund transactions when the + * channel expires and notifies the wallet of an update to this wallet extension + */ + public void putChannel(final StoredClientChannel channel) { + putChannel(channel, true); + } + + // Adds this channel and optionally notifies the wallet of an update to this extension (used during deserialize) + private synchronized void putChannel(final StoredClientChannel channel, boolean updateWallet) { + mapChannels.put(channel.id, channel); + channelTimeoutHandler.schedule(new TimerTask() { + @Override + public void run() { + removeChannel(channel); + announcePeerGroup.broadcastTransaction(channel.contract); + announcePeerGroup.broadcastTransaction(channel.refund); + } + // Add the difference between real time and Utils.now() so that test-cases can use a mock clock. + }, new Date((channel.refund.getLockTime() + 60 * 5) * 1000 + (System.currentTimeMillis() - Utils.now().getTime()))); + if (updateWallet) + containingWallet.addOrUpdateExtension(this); + } + + /** + *

Removes the channel with the given id from this set of stored states and notifies the wallet of an update to + * this wallet extension.

+ * + *

Note that the channel will still have its contract and refund transactions broadcast via the connected + * {@link TransactionBroadcaster} as long as this {@link StoredPaymentChannelClientStates} continues to + * exist in memory.

+ */ + public synchronized void removeChannel(StoredClientChannel channel) { + mapChannels.remove(channel.id, channel); + containingWallet.addOrUpdateExtension(this); + } + + @Override + public String getWalletExtensionID() { + return EXTENSION_ID; + } + + @Override + public boolean isWalletExtensionMandatory() { + return false; + } + + @Override + public synchronized byte[] serializeWalletExtension() { + try { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + ObjectOutputStream oos = new ObjectOutputStream(out); + for (StoredClientChannel channel : mapChannels.values()) { + oos.writeObject(channel); + } + return out.toByteArray(); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + @Override + public synchronized void deserializeWalletExtension(Wallet containingWallet, byte[] data) throws Exception { + checkState(this.containingWallet == null || this.containingWallet == containingWallet); + this.containingWallet = containingWallet; + ByteArrayInputStream inStream = new ByteArrayInputStream(data); + ObjectInputStream ois = new ObjectInputStream(inStream); + while (inStream.available() > 0) { + StoredClientChannel channel = (StoredClientChannel)ois.readObject(); + putChannel(channel, false); + } + } +} + +/** + * Represents the state of a channel once it has been opened in such a way that it can be stored and used to resume a + * channel which was interrupted (eg on connection failure) or keep track of refund transactions which need broadcast + * when they expire. + */ +class StoredClientChannel implements Serializable { + Sha256Hash id; + Transaction contract, refund; + ECKey myKey; + BigInteger valueToMe, refundFees; + + // In-memory flag to indicate intent to resume this channel (or that the channel is already in use) + transient boolean active = false; + + StoredClientChannel(Sha256Hash id, Transaction contract, Transaction refund, ECKey myKey, BigInteger valueToMe, BigInteger refundFees) { + this.id = id; + this.contract = contract; + this.refund = refund; + this.myKey = myKey; + this.valueToMe = valueToMe; + this.refundFees = refundFees; + this.active = true; + } + + void updateValueToMe(BigInteger newValue) { + this.valueToMe = newValue; + } +} diff --git a/core/src/main/java/com/google/bitcoin/protocols/channels/StoredPaymentChannelServerStates.java b/core/src/main/java/com/google/bitcoin/protocols/channels/StoredPaymentChannelServerStates.java new file mode 100644 index 00000000..fef8d755 --- /dev/null +++ b/core/src/main/java/com/google/bitcoin/protocols/channels/StoredPaymentChannelServerStates.java @@ -0,0 +1,144 @@ +/* + * Copyright 2013 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.bitcoin.protocols.channels; + +import java.io.*; +import java.util.*; + +import com.google.bitcoin.core.*; +import com.google.common.annotations.VisibleForTesting; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +/** + * Keeps track of a set of {@link StoredServerChannel}s and expires them 2 hours before their refund transactions + * unlock. + */ +public class StoredPaymentChannelServerStates implements WalletExtension { + static final String EXTENSION_ID = StoredPaymentChannelServerStates.class.getName(); + + @VisibleForTesting final Map mapChannels = new HashMap(); + private final Wallet wallet; + private final PeerGroup announcePeerGroup; + + private final Timer channelTimeoutHandler = new Timer(); + + /** + * The offset between the refund transaction's lock time and the time channels will be automatically closed. + * This defines a window during which we must get the last payment transaction verified, ie it should allow time for + * network propagation and for the payment transaction to be included in a block. Note that the channel expire time + * is measured in terms of our local clock, and the refund transaction's lock time is measured in terms of Bitcoin + * block header timestamps, which are allowed to drift up to two hours in the future, as measured by relaying nodes. + */ + public static final long CHANNEL_EXPIRE_OFFSET = -2*60*60; + + /** + * Creates a new PaymentChannelServerStateManager and associates it with the given {@link Wallet} and + * {@link PeerGroup} which are used to complete and announce payment transactions. + */ + public StoredPaymentChannelServerStates(Wallet wallet, PeerGroup announcePeerGroup) { + this.wallet = checkNotNull(wallet); + this.announcePeerGroup = checkNotNull(announcePeerGroup); + } + + /** + *

Closes the given channel using {@link ServerConnectionEventHandler#closeChannel()} and + * {@link PaymentChannelServerState#close()} to notify any connected client of channel closure and to complete and + * broadcast the latest payment transaction.

+ * + *

Removes the given channel from this set of {@link StoredServerChannel}s and notifies the wallet of a change to + * this wallet extension.

+ */ + public synchronized void closeChannel(StoredServerChannel channel) { + synchronized (channel) { + if (channel.connectedHandler != null) + channel.connectedHandler.close(); // connectedHandler will be reset to null in connectionClosed + try {//TODO add event listener to PaymentChannelServerStateManager + channel.getState(wallet, announcePeerGroup).close(); // Closes the actual connection, not the channel + } catch (ValueOutOfRangeException e) { + e.printStackTrace(); //To change body of catch statement use File | Settings | File Templates. + } catch (VerificationException e) { + e.printStackTrace(); //To change body of catch statement use File | Settings | File Templates. + } + channel.state = null; + mapChannels.remove(channel.contract.getHash()); + } + wallet.addOrUpdateExtension(this); + } + + /** + * Gets the {@link StoredServerChannel} with the given channel id (ie contract transaction hash). + */ + public synchronized StoredServerChannel getChannel(Sha256Hash id) { + return mapChannels.get(id); + } + + /** + *

Puts the given channel in the channels map and automatically closes it 2 hours before its refund transaction + * becomes spendable.

+ * + *

Because there must be only one, canonical {@link StoredServerChannel} per channel, this method throws if the + * channel is already present in the set of channels.

+ */ + public synchronized void putChannel(final StoredServerChannel channel) { + checkArgument(mapChannels.put(channel.contract.getHash(), checkNotNull(channel)) == null); + channelTimeoutHandler.schedule(new TimerTask() { + @Override + public void run() { + closeChannel(channel); + } + // Add the difference between real time and Utils.now() so that test-cases can use a mock clock. + }, new Date((channel.refundTransactionUnlockTimeSecs + CHANNEL_EXPIRE_OFFSET)*1000L + + (System.currentTimeMillis() - Utils.now().getTime()))); + } + + @Override + public String getWalletExtensionID() { + return EXTENSION_ID; + } + + @Override + public boolean isWalletExtensionMandatory() { + return false; + } + + @Override + public synchronized byte[] serializeWalletExtension() { + try { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + ObjectOutputStream oos = new ObjectOutputStream(out); + for (StoredServerChannel channel : mapChannels.values()) { + oos.writeObject(channel); + } + return out.toByteArray(); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + @Override + public synchronized void deserializeWalletExtension(Wallet containingWallet, byte[] data) throws Exception { + checkArgument(containingWallet == wallet); + ByteArrayInputStream inStream = new ByteArrayInputStream(data); + ObjectInputStream ois = new ObjectInputStream(inStream); + while (inStream.available() > 0) { + StoredServerChannel channel = (StoredServerChannel)ois.readObject(); + putChannel(channel); + } + } +} diff --git a/core/src/main/java/com/google/bitcoin/protocols/channels/StoredServerChannel.java b/core/src/main/java/com/google/bitcoin/protocols/channels/StoredServerChannel.java new file mode 100644 index 00000000..239c9707 --- /dev/null +++ b/core/src/main/java/com/google/bitcoin/protocols/channels/StoredServerChannel.java @@ -0,0 +1,89 @@ +/* + * Copyright 2013 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.bitcoin.protocols.channels; + +import java.io.Serializable; +import java.math.BigInteger; + +import com.google.bitcoin.core.*; + +import static com.google.common.base.Preconditions.checkArgument; + +/** + * Represents the state of a channel once it has been opened in such a way that it can be stored and used to resume a + * channel which was interrupted (eg on connection failure) or close the channel automatically as the channel expire + * time approaches. + */ +public class StoredServerChannel implements Serializable { + BigInteger bestValueToMe; + byte[] bestValueSignature; + long refundTransactionUnlockTimeSecs; + Transaction contract; + TransactionOutput clientOutput; + ECKey myKey; + + // In-memory pointer to the event handler which handles this channel if the client is connected. + // Used as a flag to prevent duplicate connections and to disconnect the channel if its expire time approaches. + transient PaymentChannelServer connectedHandler = null; + transient PaymentChannelServerState state = null; + + StoredServerChannel(PaymentChannelServerState state, Transaction contract, TransactionOutput clientOutput, + long refundTransactionUnlockTimeSecs, ECKey myKey, BigInteger bestValueToMe, byte[] bestValueSignature) { + this.contract = contract; + this.clientOutput = clientOutput; + this.refundTransactionUnlockTimeSecs = refundTransactionUnlockTimeSecs; + this.myKey = myKey; + this.bestValueToMe = bestValueToMe; + this.bestValueSignature = bestValueSignature; + this.state = state; + } + + /** + *

Updates the best value to the server to the given newValue and newSignature without any checking.

+ *

Does NOT notify the wallet of an update to the {@link com.google.bitcoin.protocols.channels.StoredPaymentChannelServerStates}.

+ */ + public synchronized void updateValueToMe(BigInteger newValue, byte[] newSignature) { + this.bestValueToMe = newValue; + this.bestValueSignature = newSignature; + } + + /** + * Attempts to connect the given handler to this, returning true if it is the new handler, false if there was + * already one attached. A null connectedHandler clears this's connected handler no matter its current state. + */ + synchronized boolean setConnectedHandler(PaymentChannelServer connectedHandler) { + if (this.connectedHandler != null && connectedHandler != null) + return false; + this.connectedHandler = connectedHandler; + return true; + } + + /** + * Gets the canonical {@link com.google.bitcoin.protocols.channels.PaymentChannelServerState} object for this channel, either by returning an existing one + * or by creating a new one. + * + * @param wallet The wallet which holds the {@link com.google.bitcoin.protocols.channels.PaymentChannelServerState} in which this is saved and which will + * be used to complete transactions + * @param peerGroup The {@link com.google.bitcoin.core.PeerGroup} which will be used to broadcast contract/payment transactions. + */ + public synchronized PaymentChannelServerState getState(Wallet wallet, PeerGroup peerGroup) throws VerificationException { + if (state == null) + state = new PaymentChannelServerState(this, wallet, peerGroup); + checkArgument(wallet == state.wallet); + return state; + } +} diff --git a/core/src/main/java/com/google/bitcoin/protocols/channels/ValueOutOfRangeException.java b/core/src/main/java/com/google/bitcoin/protocols/channels/ValueOutOfRangeException.java new file mode 100644 index 00000000..8095dffd --- /dev/null +++ b/core/src/main/java/com/google/bitcoin/protocols/channels/ValueOutOfRangeException.java @@ -0,0 +1,10 @@ +package com.google.bitcoin.protocols.channels; + +/** + * Used when a given value is either too large too afford or too small for the network to accept. + */ +public class ValueOutOfRangeException extends Exception { + public ValueOutOfRangeException(String message) { + super(message); + } +} diff --git a/core/src/main/java/org/bitcoin/paymentchannel/Protos.java b/core/src/main/java/org/bitcoin/paymentchannel/Protos.java new file mode 100644 index 00000000..439ba1da --- /dev/null +++ b/core/src/main/java/org/bitcoin/paymentchannel/Protos.java @@ -0,0 +1,5265 @@ +// Generated by the protocol buffer compiler. DO NOT EDIT! +// source: paymentchannel.proto + +package org.bitcoin.paymentchannel; + +public final class Protos { + private Protos() {} + public static void registerAllExtensions( + com.google.protobuf.ExtensionRegistry registry) { + } + public interface TwoWayChannelMessageOrBuilder + extends com.google.protobuf.MessageOrBuilder { + + // required .paymentchannels.TwoWayChannelMessage.MessageType type = 1; + boolean hasType(); + org.bitcoin.paymentchannel.Protos.TwoWayChannelMessage.MessageType getType(); + + // optional .paymentchannels.ClientVersion client_version = 2; + boolean hasClientVersion(); + org.bitcoin.paymentchannel.Protos.ClientVersion getClientVersion(); + org.bitcoin.paymentchannel.Protos.ClientVersionOrBuilder getClientVersionOrBuilder(); + + // optional .paymentchannels.ServerVersion server_version = 3; + boolean hasServerVersion(); + org.bitcoin.paymentchannel.Protos.ServerVersion getServerVersion(); + org.bitcoin.paymentchannel.Protos.ServerVersionOrBuilder getServerVersionOrBuilder(); + + // optional .paymentchannels.Initiate initiate = 4; + boolean hasInitiate(); + org.bitcoin.paymentchannel.Protos.Initiate getInitiate(); + org.bitcoin.paymentchannel.Protos.InitiateOrBuilder getInitiateOrBuilder(); + + // optional .paymentchannels.ProvideRefund provide_refund = 5; + boolean hasProvideRefund(); + org.bitcoin.paymentchannel.Protos.ProvideRefund getProvideRefund(); + org.bitcoin.paymentchannel.Protos.ProvideRefundOrBuilder getProvideRefundOrBuilder(); + + // optional .paymentchannels.ReturnRefund return_refund = 6; + boolean hasReturnRefund(); + org.bitcoin.paymentchannel.Protos.ReturnRefund getReturnRefund(); + org.bitcoin.paymentchannel.Protos.ReturnRefundOrBuilder getReturnRefundOrBuilder(); + + // optional .paymentchannels.ProvideContract provide_contract = 7; + boolean hasProvideContract(); + org.bitcoin.paymentchannel.Protos.ProvideContract getProvideContract(); + org.bitcoin.paymentchannel.Protos.ProvideContractOrBuilder getProvideContractOrBuilder(); + + // optional .paymentchannels.UpdatePayment update_payment = 8; + boolean hasUpdatePayment(); + org.bitcoin.paymentchannel.Protos.UpdatePayment getUpdatePayment(); + org.bitcoin.paymentchannel.Protos.UpdatePaymentOrBuilder getUpdatePaymentOrBuilder(); + + // optional .paymentchannels.Error error = 10; + boolean hasError(); + org.bitcoin.paymentchannel.Protos.Error getError(); + org.bitcoin.paymentchannel.Protos.ErrorOrBuilder getErrorOrBuilder(); + } + public static final class TwoWayChannelMessage extends + com.google.protobuf.GeneratedMessage + implements TwoWayChannelMessageOrBuilder { + // Use TwoWayChannelMessage.newBuilder() to construct. + private TwoWayChannelMessage(Builder builder) { + super(builder); + } + private TwoWayChannelMessage(boolean noInit) {} + + private static final TwoWayChannelMessage defaultInstance; + public static TwoWayChannelMessage getDefaultInstance() { + return defaultInstance; + } + + public TwoWayChannelMessage getDefaultInstanceForType() { + return defaultInstance; + } + + public static final com.google.protobuf.Descriptors.Descriptor + getDescriptor() { + return org.bitcoin.paymentchannel.Protos.internal_static_paymentchannels_TwoWayChannelMessage_descriptor; + } + + protected com.google.protobuf.GeneratedMessage.FieldAccessorTable + internalGetFieldAccessorTable() { + return org.bitcoin.paymentchannel.Protos.internal_static_paymentchannels_TwoWayChannelMessage_fieldAccessorTable; + } + + public enum MessageType + implements com.google.protobuf.ProtocolMessageEnum { + CLIENT_VERSION(0, 1), + SERVER_VERSION(1, 2), + INITIATE(2, 3), + PROVIDE_REFUND(3, 4), + RETURN_REFUND(4, 5), + PROVIDE_CONTRACT(5, 6), + CHANNEL_OPEN(6, 7), + UPDATE_PAYMENT(7, 8), + CLOSE(8, 9), + ERROR(9, 10), + ; + + public static final int CLIENT_VERSION_VALUE = 1; + public static final int SERVER_VERSION_VALUE = 2; + public static final int INITIATE_VALUE = 3; + public static final int PROVIDE_REFUND_VALUE = 4; + public static final int RETURN_REFUND_VALUE = 5; + public static final int PROVIDE_CONTRACT_VALUE = 6; + public static final int CHANNEL_OPEN_VALUE = 7; + public static final int UPDATE_PAYMENT_VALUE = 8; + public static final int CLOSE_VALUE = 9; + public static final int ERROR_VALUE = 10; + + + public final int getNumber() { return value; } + + public static MessageType valueOf(int value) { + switch (value) { + case 1: return CLIENT_VERSION; + case 2: return SERVER_VERSION; + case 3: return INITIATE; + case 4: return PROVIDE_REFUND; + case 5: return RETURN_REFUND; + case 6: return PROVIDE_CONTRACT; + case 7: return CHANNEL_OPEN; + case 8: return UPDATE_PAYMENT; + case 9: return CLOSE; + case 10: return ERROR; + default: return null; + } + } + + public static com.google.protobuf.Internal.EnumLiteMap + internalGetValueMap() { + return internalValueMap; + } + private static com.google.protobuf.Internal.EnumLiteMap + internalValueMap = + new com.google.protobuf.Internal.EnumLiteMap() { + public MessageType findValueByNumber(int number) { + return MessageType.valueOf(number); + } + }; + + public final com.google.protobuf.Descriptors.EnumValueDescriptor + getValueDescriptor() { + return getDescriptor().getValues().get(index); + } + public final com.google.protobuf.Descriptors.EnumDescriptor + getDescriptorForType() { + return getDescriptor(); + } + public static final com.google.protobuf.Descriptors.EnumDescriptor + getDescriptor() { + return org.bitcoin.paymentchannel.Protos.TwoWayChannelMessage.getDescriptor().getEnumTypes().get(0); + } + + private static final MessageType[] VALUES = { + CLIENT_VERSION, SERVER_VERSION, INITIATE, PROVIDE_REFUND, RETURN_REFUND, PROVIDE_CONTRACT, CHANNEL_OPEN, UPDATE_PAYMENT, CLOSE, ERROR, + }; + + public static MessageType valueOf( + com.google.protobuf.Descriptors.EnumValueDescriptor desc) { + if (desc.getType() != getDescriptor()) { + throw new java.lang.IllegalArgumentException( + "EnumValueDescriptor is not for this type."); + } + return VALUES[desc.getIndex()]; + } + + private final int index; + private final int value; + + private MessageType(int index, int value) { + this.index = index; + this.value = value; + } + + // @@protoc_insertion_point(enum_scope:paymentchannels.TwoWayChannelMessage.MessageType) + } + + private int bitField0_; + // required .paymentchannels.TwoWayChannelMessage.MessageType type = 1; + public static final int TYPE_FIELD_NUMBER = 1; + private org.bitcoin.paymentchannel.Protos.TwoWayChannelMessage.MessageType type_; + public boolean hasType() { + return ((bitField0_ & 0x00000001) == 0x00000001); + } + public org.bitcoin.paymentchannel.Protos.TwoWayChannelMessage.MessageType getType() { + return type_; + } + + // optional .paymentchannels.ClientVersion client_version = 2; + public static final int CLIENT_VERSION_FIELD_NUMBER = 2; + private org.bitcoin.paymentchannel.Protos.ClientVersion clientVersion_; + public boolean hasClientVersion() { + return ((bitField0_ & 0x00000002) == 0x00000002); + } + public org.bitcoin.paymentchannel.Protos.ClientVersion getClientVersion() { + return clientVersion_; + } + public org.bitcoin.paymentchannel.Protos.ClientVersionOrBuilder getClientVersionOrBuilder() { + return clientVersion_; + } + + // optional .paymentchannels.ServerVersion server_version = 3; + public static final int SERVER_VERSION_FIELD_NUMBER = 3; + private org.bitcoin.paymentchannel.Protos.ServerVersion serverVersion_; + public boolean hasServerVersion() { + return ((bitField0_ & 0x00000004) == 0x00000004); + } + public org.bitcoin.paymentchannel.Protos.ServerVersion getServerVersion() { + return serverVersion_; + } + public org.bitcoin.paymentchannel.Protos.ServerVersionOrBuilder getServerVersionOrBuilder() { + return serverVersion_; + } + + // optional .paymentchannels.Initiate initiate = 4; + public static final int INITIATE_FIELD_NUMBER = 4; + private org.bitcoin.paymentchannel.Protos.Initiate initiate_; + public boolean hasInitiate() { + return ((bitField0_ & 0x00000008) == 0x00000008); + } + public org.bitcoin.paymentchannel.Protos.Initiate getInitiate() { + return initiate_; + } + public org.bitcoin.paymentchannel.Protos.InitiateOrBuilder getInitiateOrBuilder() { + return initiate_; + } + + // optional .paymentchannels.ProvideRefund provide_refund = 5; + public static final int PROVIDE_REFUND_FIELD_NUMBER = 5; + private org.bitcoin.paymentchannel.Protos.ProvideRefund provideRefund_; + public boolean hasProvideRefund() { + return ((bitField0_ & 0x00000010) == 0x00000010); + } + public org.bitcoin.paymentchannel.Protos.ProvideRefund getProvideRefund() { + return provideRefund_; + } + public org.bitcoin.paymentchannel.Protos.ProvideRefundOrBuilder getProvideRefundOrBuilder() { + return provideRefund_; + } + + // optional .paymentchannels.ReturnRefund return_refund = 6; + public static final int RETURN_REFUND_FIELD_NUMBER = 6; + private org.bitcoin.paymentchannel.Protos.ReturnRefund returnRefund_; + public boolean hasReturnRefund() { + return ((bitField0_ & 0x00000020) == 0x00000020); + } + public org.bitcoin.paymentchannel.Protos.ReturnRefund getReturnRefund() { + return returnRefund_; + } + public org.bitcoin.paymentchannel.Protos.ReturnRefundOrBuilder getReturnRefundOrBuilder() { + return returnRefund_; + } + + // optional .paymentchannels.ProvideContract provide_contract = 7; + public static final int PROVIDE_CONTRACT_FIELD_NUMBER = 7; + private org.bitcoin.paymentchannel.Protos.ProvideContract provideContract_; + public boolean hasProvideContract() { + return ((bitField0_ & 0x00000040) == 0x00000040); + } + public org.bitcoin.paymentchannel.Protos.ProvideContract getProvideContract() { + return provideContract_; + } + public org.bitcoin.paymentchannel.Protos.ProvideContractOrBuilder getProvideContractOrBuilder() { + return provideContract_; + } + + // optional .paymentchannels.UpdatePayment update_payment = 8; + public static final int UPDATE_PAYMENT_FIELD_NUMBER = 8; + private org.bitcoin.paymentchannel.Protos.UpdatePayment updatePayment_; + public boolean hasUpdatePayment() { + return ((bitField0_ & 0x00000080) == 0x00000080); + } + public org.bitcoin.paymentchannel.Protos.UpdatePayment getUpdatePayment() { + return updatePayment_; + } + public org.bitcoin.paymentchannel.Protos.UpdatePaymentOrBuilder getUpdatePaymentOrBuilder() { + return updatePayment_; + } + + // optional .paymentchannels.Error error = 10; + public static final int ERROR_FIELD_NUMBER = 10; + private org.bitcoin.paymentchannel.Protos.Error error_; + public boolean hasError() { + return ((bitField0_ & 0x00000100) == 0x00000100); + } + public org.bitcoin.paymentchannel.Protos.Error getError() { + return error_; + } + public org.bitcoin.paymentchannel.Protos.ErrorOrBuilder getErrorOrBuilder() { + return error_; + } + + private void initFields() { + type_ = org.bitcoin.paymentchannel.Protos.TwoWayChannelMessage.MessageType.CLIENT_VERSION; + clientVersion_ = org.bitcoin.paymentchannel.Protos.ClientVersion.getDefaultInstance(); + serverVersion_ = org.bitcoin.paymentchannel.Protos.ServerVersion.getDefaultInstance(); + initiate_ = org.bitcoin.paymentchannel.Protos.Initiate.getDefaultInstance(); + provideRefund_ = org.bitcoin.paymentchannel.Protos.ProvideRefund.getDefaultInstance(); + returnRefund_ = org.bitcoin.paymentchannel.Protos.ReturnRefund.getDefaultInstance(); + provideContract_ = org.bitcoin.paymentchannel.Protos.ProvideContract.getDefaultInstance(); + updatePayment_ = org.bitcoin.paymentchannel.Protos.UpdatePayment.getDefaultInstance(); + error_ = org.bitcoin.paymentchannel.Protos.Error.getDefaultInstance(); + } + private byte memoizedIsInitialized = -1; + public final boolean isInitialized() { + byte isInitialized = memoizedIsInitialized; + if (isInitialized != -1) return isInitialized == 1; + + if (!hasType()) { + memoizedIsInitialized = 0; + return false; + } + if (hasClientVersion()) { + if (!getClientVersion().isInitialized()) { + memoizedIsInitialized = 0; + return false; + } + } + if (hasServerVersion()) { + if (!getServerVersion().isInitialized()) { + memoizedIsInitialized = 0; + return false; + } + } + if (hasInitiate()) { + if (!getInitiate().isInitialized()) { + memoizedIsInitialized = 0; + return false; + } + } + if (hasProvideRefund()) { + if (!getProvideRefund().isInitialized()) { + memoizedIsInitialized = 0; + return false; + } + } + if (hasReturnRefund()) { + if (!getReturnRefund().isInitialized()) { + memoizedIsInitialized = 0; + return false; + } + } + if (hasProvideContract()) { + if (!getProvideContract().isInitialized()) { + memoizedIsInitialized = 0; + return false; + } + } + if (hasUpdatePayment()) { + if (!getUpdatePayment().isInitialized()) { + memoizedIsInitialized = 0; + return false; + } + } + memoizedIsInitialized = 1; + return true; + } + + public void writeTo(com.google.protobuf.CodedOutputStream output) + throws java.io.IOException { + getSerializedSize(); + if (((bitField0_ & 0x00000001) == 0x00000001)) { + output.writeEnum(1, type_.getNumber()); + } + if (((bitField0_ & 0x00000002) == 0x00000002)) { + output.writeMessage(2, clientVersion_); + } + if (((bitField0_ & 0x00000004) == 0x00000004)) { + output.writeMessage(3, serverVersion_); + } + if (((bitField0_ & 0x00000008) == 0x00000008)) { + output.writeMessage(4, initiate_); + } + if (((bitField0_ & 0x00000010) == 0x00000010)) { + output.writeMessage(5, provideRefund_); + } + if (((bitField0_ & 0x00000020) == 0x00000020)) { + output.writeMessage(6, returnRefund_); + } + if (((bitField0_ & 0x00000040) == 0x00000040)) { + output.writeMessage(7, provideContract_); + } + if (((bitField0_ & 0x00000080) == 0x00000080)) { + output.writeMessage(8, updatePayment_); + } + if (((bitField0_ & 0x00000100) == 0x00000100)) { + output.writeMessage(10, error_); + } + getUnknownFields().writeTo(output); + } + + private int memoizedSerializedSize = -1; + public int getSerializedSize() { + int size = memoizedSerializedSize; + if (size != -1) return size; + + size = 0; + if (((bitField0_ & 0x00000001) == 0x00000001)) { + size += com.google.protobuf.CodedOutputStream + .computeEnumSize(1, type_.getNumber()); + } + if (((bitField0_ & 0x00000002) == 0x00000002)) { + size += com.google.protobuf.CodedOutputStream + .computeMessageSize(2, clientVersion_); + } + if (((bitField0_ & 0x00000004) == 0x00000004)) { + size += com.google.protobuf.CodedOutputStream + .computeMessageSize(3, serverVersion_); + } + if (((bitField0_ & 0x00000008) == 0x00000008)) { + size += com.google.protobuf.CodedOutputStream + .computeMessageSize(4, initiate_); + } + if (((bitField0_ & 0x00000010) == 0x00000010)) { + size += com.google.protobuf.CodedOutputStream + .computeMessageSize(5, provideRefund_); + } + if (((bitField0_ & 0x00000020) == 0x00000020)) { + size += com.google.protobuf.CodedOutputStream + .computeMessageSize(6, returnRefund_); + } + if (((bitField0_ & 0x00000040) == 0x00000040)) { + size += com.google.protobuf.CodedOutputStream + .computeMessageSize(7, provideContract_); + } + if (((bitField0_ & 0x00000080) == 0x00000080)) { + size += com.google.protobuf.CodedOutputStream + .computeMessageSize(8, updatePayment_); + } + if (((bitField0_ & 0x00000100) == 0x00000100)) { + size += com.google.protobuf.CodedOutputStream + .computeMessageSize(10, error_); + } + size += getUnknownFields().getSerializedSize(); + memoizedSerializedSize = size; + return size; + } + + private static final long serialVersionUID = 0L; + @java.lang.Override + protected java.lang.Object writeReplace() + throws java.io.ObjectStreamException { + return super.writeReplace(); + } + + public static org.bitcoin.paymentchannel.Protos.TwoWayChannelMessage parseFrom( + com.google.protobuf.ByteString data) + throws com.google.protobuf.InvalidProtocolBufferException { + return newBuilder().mergeFrom(data).buildParsed(); + } + public static org.bitcoin.paymentchannel.Protos.TwoWayChannelMessage parseFrom( + com.google.protobuf.ByteString data, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws com.google.protobuf.InvalidProtocolBufferException { + return newBuilder().mergeFrom(data, extensionRegistry) + .buildParsed(); + } + public static org.bitcoin.paymentchannel.Protos.TwoWayChannelMessage parseFrom(byte[] data) + throws com.google.protobuf.InvalidProtocolBufferException { + return newBuilder().mergeFrom(data).buildParsed(); + } + public static org.bitcoin.paymentchannel.Protos.TwoWayChannelMessage parseFrom( + byte[] data, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws com.google.protobuf.InvalidProtocolBufferException { + return newBuilder().mergeFrom(data, extensionRegistry) + .buildParsed(); + } + public static org.bitcoin.paymentchannel.Protos.TwoWayChannelMessage parseFrom(java.io.InputStream input) + throws java.io.IOException { + return newBuilder().mergeFrom(input).buildParsed(); + } + public static org.bitcoin.paymentchannel.Protos.TwoWayChannelMessage parseFrom( + java.io.InputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + return newBuilder().mergeFrom(input, extensionRegistry) + .buildParsed(); + } + public static org.bitcoin.paymentchannel.Protos.TwoWayChannelMessage parseDelimitedFrom(java.io.InputStream input) + throws java.io.IOException { + Builder builder = newBuilder(); + if (builder.mergeDelimitedFrom(input)) { + return builder.buildParsed(); + } else { + return null; + } + } + public static org.bitcoin.paymentchannel.Protos.TwoWayChannelMessage parseDelimitedFrom( + java.io.InputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + Builder builder = newBuilder(); + if (builder.mergeDelimitedFrom(input, extensionRegistry)) { + return builder.buildParsed(); + } else { + return null; + } + } + public static org.bitcoin.paymentchannel.Protos.TwoWayChannelMessage parseFrom( + com.google.protobuf.CodedInputStream input) + throws java.io.IOException { + return newBuilder().mergeFrom(input).buildParsed(); + } + public static org.bitcoin.paymentchannel.Protos.TwoWayChannelMessage parseFrom( + com.google.protobuf.CodedInputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + return newBuilder().mergeFrom(input, extensionRegistry) + .buildParsed(); + } + + public static Builder newBuilder() { return Builder.create(); } + public Builder newBuilderForType() { return newBuilder(); } + public static Builder newBuilder(org.bitcoin.paymentchannel.Protos.TwoWayChannelMessage prototype) { + return newBuilder().mergeFrom(prototype); + } + public Builder toBuilder() { return newBuilder(this); } + + @java.lang.Override + protected Builder newBuilderForType( + com.google.protobuf.GeneratedMessage.BuilderParent parent) { + Builder builder = new Builder(parent); + return builder; + } + public static final class Builder extends + com.google.protobuf.GeneratedMessage.Builder + implements org.bitcoin.paymentchannel.Protos.TwoWayChannelMessageOrBuilder { + public static final com.google.protobuf.Descriptors.Descriptor + getDescriptor() { + return org.bitcoin.paymentchannel.Protos.internal_static_paymentchannels_TwoWayChannelMessage_descriptor; + } + + protected com.google.protobuf.GeneratedMessage.FieldAccessorTable + internalGetFieldAccessorTable() { + return org.bitcoin.paymentchannel.Protos.internal_static_paymentchannels_TwoWayChannelMessage_fieldAccessorTable; + } + + // Construct using org.bitcoin.paymentchannel.Protos.TwoWayChannelMessage.newBuilder() + private Builder() { + maybeForceBuilderInitialization(); + } + + private Builder(BuilderParent parent) { + super(parent); + maybeForceBuilderInitialization(); + } + private void maybeForceBuilderInitialization() { + if (com.google.protobuf.GeneratedMessage.alwaysUseFieldBuilders) { + getClientVersionFieldBuilder(); + getServerVersionFieldBuilder(); + getInitiateFieldBuilder(); + getProvideRefundFieldBuilder(); + getReturnRefundFieldBuilder(); + getProvideContractFieldBuilder(); + getUpdatePaymentFieldBuilder(); + getErrorFieldBuilder(); + } + } + private static Builder create() { + return new Builder(); + } + + public Builder clear() { + super.clear(); + type_ = org.bitcoin.paymentchannel.Protos.TwoWayChannelMessage.MessageType.CLIENT_VERSION; + bitField0_ = (bitField0_ & ~0x00000001); + if (clientVersionBuilder_ == null) { + clientVersion_ = org.bitcoin.paymentchannel.Protos.ClientVersion.getDefaultInstance(); + } else { + clientVersionBuilder_.clear(); + } + bitField0_ = (bitField0_ & ~0x00000002); + if (serverVersionBuilder_ == null) { + serverVersion_ = org.bitcoin.paymentchannel.Protos.ServerVersion.getDefaultInstance(); + } else { + serverVersionBuilder_.clear(); + } + bitField0_ = (bitField0_ & ~0x00000004); + if (initiateBuilder_ == null) { + initiate_ = org.bitcoin.paymentchannel.Protos.Initiate.getDefaultInstance(); + } else { + initiateBuilder_.clear(); + } + bitField0_ = (bitField0_ & ~0x00000008); + if (provideRefundBuilder_ == null) { + provideRefund_ = org.bitcoin.paymentchannel.Protos.ProvideRefund.getDefaultInstance(); + } else { + provideRefundBuilder_.clear(); + } + bitField0_ = (bitField0_ & ~0x00000010); + if (returnRefundBuilder_ == null) { + returnRefund_ = org.bitcoin.paymentchannel.Protos.ReturnRefund.getDefaultInstance(); + } else { + returnRefundBuilder_.clear(); + } + bitField0_ = (bitField0_ & ~0x00000020); + if (provideContractBuilder_ == null) { + provideContract_ = org.bitcoin.paymentchannel.Protos.ProvideContract.getDefaultInstance(); + } else { + provideContractBuilder_.clear(); + } + bitField0_ = (bitField0_ & ~0x00000040); + if (updatePaymentBuilder_ == null) { + updatePayment_ = org.bitcoin.paymentchannel.Protos.UpdatePayment.getDefaultInstance(); + } else { + updatePaymentBuilder_.clear(); + } + bitField0_ = (bitField0_ & ~0x00000080); + if (errorBuilder_ == null) { + error_ = org.bitcoin.paymentchannel.Protos.Error.getDefaultInstance(); + } else { + errorBuilder_.clear(); + } + bitField0_ = (bitField0_ & ~0x00000100); + return this; + } + + public Builder clone() { + return create().mergeFrom(buildPartial()); + } + + public com.google.protobuf.Descriptors.Descriptor + getDescriptorForType() { + return org.bitcoin.paymentchannel.Protos.TwoWayChannelMessage.getDescriptor(); + } + + public org.bitcoin.paymentchannel.Protos.TwoWayChannelMessage getDefaultInstanceForType() { + return org.bitcoin.paymentchannel.Protos.TwoWayChannelMessage.getDefaultInstance(); + } + + public org.bitcoin.paymentchannel.Protos.TwoWayChannelMessage build() { + org.bitcoin.paymentchannel.Protos.TwoWayChannelMessage result = buildPartial(); + if (!result.isInitialized()) { + throw newUninitializedMessageException(result); + } + return result; + } + + private org.bitcoin.paymentchannel.Protos.TwoWayChannelMessage buildParsed() + throws com.google.protobuf.InvalidProtocolBufferException { + org.bitcoin.paymentchannel.Protos.TwoWayChannelMessage result = buildPartial(); + if (!result.isInitialized()) { + throw newUninitializedMessageException( + result).asInvalidProtocolBufferException(); + } + return result; + } + + public org.bitcoin.paymentchannel.Protos.TwoWayChannelMessage buildPartial() { + org.bitcoin.paymentchannel.Protos.TwoWayChannelMessage result = new org.bitcoin.paymentchannel.Protos.TwoWayChannelMessage(this); + int from_bitField0_ = bitField0_; + int to_bitField0_ = 0; + if (((from_bitField0_ & 0x00000001) == 0x00000001)) { + to_bitField0_ |= 0x00000001; + } + result.type_ = type_; + if (((from_bitField0_ & 0x00000002) == 0x00000002)) { + to_bitField0_ |= 0x00000002; + } + if (clientVersionBuilder_ == null) { + result.clientVersion_ = clientVersion_; + } else { + result.clientVersion_ = clientVersionBuilder_.build(); + } + if (((from_bitField0_ & 0x00000004) == 0x00000004)) { + to_bitField0_ |= 0x00000004; + } + if (serverVersionBuilder_ == null) { + result.serverVersion_ = serverVersion_; + } else { + result.serverVersion_ = serverVersionBuilder_.build(); + } + if (((from_bitField0_ & 0x00000008) == 0x00000008)) { + to_bitField0_ |= 0x00000008; + } + if (initiateBuilder_ == null) { + result.initiate_ = initiate_; + } else { + result.initiate_ = initiateBuilder_.build(); + } + if (((from_bitField0_ & 0x00000010) == 0x00000010)) { + to_bitField0_ |= 0x00000010; + } + if (provideRefundBuilder_ == null) { + result.provideRefund_ = provideRefund_; + } else { + result.provideRefund_ = provideRefundBuilder_.build(); + } + if (((from_bitField0_ & 0x00000020) == 0x00000020)) { + to_bitField0_ |= 0x00000020; + } + if (returnRefundBuilder_ == null) { + result.returnRefund_ = returnRefund_; + } else { + result.returnRefund_ = returnRefundBuilder_.build(); + } + if (((from_bitField0_ & 0x00000040) == 0x00000040)) { + to_bitField0_ |= 0x00000040; + } + if (provideContractBuilder_ == null) { + result.provideContract_ = provideContract_; + } else { + result.provideContract_ = provideContractBuilder_.build(); + } + if (((from_bitField0_ & 0x00000080) == 0x00000080)) { + to_bitField0_ |= 0x00000080; + } + if (updatePaymentBuilder_ == null) { + result.updatePayment_ = updatePayment_; + } else { + result.updatePayment_ = updatePaymentBuilder_.build(); + } + if (((from_bitField0_ & 0x00000100) == 0x00000100)) { + to_bitField0_ |= 0x00000100; + } + if (errorBuilder_ == null) { + result.error_ = error_; + } else { + result.error_ = errorBuilder_.build(); + } + result.bitField0_ = to_bitField0_; + onBuilt(); + return result; + } + + public Builder mergeFrom(com.google.protobuf.Message other) { + if (other instanceof org.bitcoin.paymentchannel.Protos.TwoWayChannelMessage) { + return mergeFrom((org.bitcoin.paymentchannel.Protos.TwoWayChannelMessage)other); + } else { + super.mergeFrom(other); + return this; + } + } + + public Builder mergeFrom(org.bitcoin.paymentchannel.Protos.TwoWayChannelMessage other) { + if (other == org.bitcoin.paymentchannel.Protos.TwoWayChannelMessage.getDefaultInstance()) return this; + if (other.hasType()) { + setType(other.getType()); + } + if (other.hasClientVersion()) { + mergeClientVersion(other.getClientVersion()); + } + if (other.hasServerVersion()) { + mergeServerVersion(other.getServerVersion()); + } + if (other.hasInitiate()) { + mergeInitiate(other.getInitiate()); + } + if (other.hasProvideRefund()) { + mergeProvideRefund(other.getProvideRefund()); + } + if (other.hasReturnRefund()) { + mergeReturnRefund(other.getReturnRefund()); + } + if (other.hasProvideContract()) { + mergeProvideContract(other.getProvideContract()); + } + if (other.hasUpdatePayment()) { + mergeUpdatePayment(other.getUpdatePayment()); + } + if (other.hasError()) { + mergeError(other.getError()); + } + this.mergeUnknownFields(other.getUnknownFields()); + return this; + } + + public final boolean isInitialized() { + if (!hasType()) { + + return false; + } + if (hasClientVersion()) { + if (!getClientVersion().isInitialized()) { + + return false; + } + } + if (hasServerVersion()) { + if (!getServerVersion().isInitialized()) { + + return false; + } + } + if (hasInitiate()) { + if (!getInitiate().isInitialized()) { + + return false; + } + } + if (hasProvideRefund()) { + if (!getProvideRefund().isInitialized()) { + + return false; + } + } + if (hasReturnRefund()) { + if (!getReturnRefund().isInitialized()) { + + return false; + } + } + if (hasProvideContract()) { + if (!getProvideContract().isInitialized()) { + + return false; + } + } + if (hasUpdatePayment()) { + if (!getUpdatePayment().isInitialized()) { + + return false; + } + } + return true; + } + + public Builder mergeFrom( + com.google.protobuf.CodedInputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + com.google.protobuf.UnknownFieldSet.Builder unknownFields = + com.google.protobuf.UnknownFieldSet.newBuilder( + this.getUnknownFields()); + while (true) { + int tag = input.readTag(); + switch (tag) { + case 0: + this.setUnknownFields(unknownFields.build()); + onChanged(); + return this; + default: { + if (!parseUnknownField(input, unknownFields, + extensionRegistry, tag)) { + this.setUnknownFields(unknownFields.build()); + onChanged(); + return this; + } + break; + } + case 8: { + int rawValue = input.readEnum(); + org.bitcoin.paymentchannel.Protos.TwoWayChannelMessage.MessageType value = org.bitcoin.paymentchannel.Protos.TwoWayChannelMessage.MessageType.valueOf(rawValue); + if (value == null) { + unknownFields.mergeVarintField(1, rawValue); + } else { + bitField0_ |= 0x00000001; + type_ = value; + } + break; + } + case 18: { + org.bitcoin.paymentchannel.Protos.ClientVersion.Builder subBuilder = org.bitcoin.paymentchannel.Protos.ClientVersion.newBuilder(); + if (hasClientVersion()) { + subBuilder.mergeFrom(getClientVersion()); + } + input.readMessage(subBuilder, extensionRegistry); + setClientVersion(subBuilder.buildPartial()); + break; + } + case 26: { + org.bitcoin.paymentchannel.Protos.ServerVersion.Builder subBuilder = org.bitcoin.paymentchannel.Protos.ServerVersion.newBuilder(); + if (hasServerVersion()) { + subBuilder.mergeFrom(getServerVersion()); + } + input.readMessage(subBuilder, extensionRegistry); + setServerVersion(subBuilder.buildPartial()); + break; + } + case 34: { + org.bitcoin.paymentchannel.Protos.Initiate.Builder subBuilder = org.bitcoin.paymentchannel.Protos.Initiate.newBuilder(); + if (hasInitiate()) { + subBuilder.mergeFrom(getInitiate()); + } + input.readMessage(subBuilder, extensionRegistry); + setInitiate(subBuilder.buildPartial()); + break; + } + case 42: { + org.bitcoin.paymentchannel.Protos.ProvideRefund.Builder subBuilder = org.bitcoin.paymentchannel.Protos.ProvideRefund.newBuilder(); + if (hasProvideRefund()) { + subBuilder.mergeFrom(getProvideRefund()); + } + input.readMessage(subBuilder, extensionRegistry); + setProvideRefund(subBuilder.buildPartial()); + break; + } + case 50: { + org.bitcoin.paymentchannel.Protos.ReturnRefund.Builder subBuilder = org.bitcoin.paymentchannel.Protos.ReturnRefund.newBuilder(); + if (hasReturnRefund()) { + subBuilder.mergeFrom(getReturnRefund()); + } + input.readMessage(subBuilder, extensionRegistry); + setReturnRefund(subBuilder.buildPartial()); + break; + } + case 58: { + org.bitcoin.paymentchannel.Protos.ProvideContract.Builder subBuilder = org.bitcoin.paymentchannel.Protos.ProvideContract.newBuilder(); + if (hasProvideContract()) { + subBuilder.mergeFrom(getProvideContract()); + } + input.readMessage(subBuilder, extensionRegistry); + setProvideContract(subBuilder.buildPartial()); + break; + } + case 66: { + org.bitcoin.paymentchannel.Protos.UpdatePayment.Builder subBuilder = org.bitcoin.paymentchannel.Protos.UpdatePayment.newBuilder(); + if (hasUpdatePayment()) { + subBuilder.mergeFrom(getUpdatePayment()); + } + input.readMessage(subBuilder, extensionRegistry); + setUpdatePayment(subBuilder.buildPartial()); + break; + } + case 82: { + org.bitcoin.paymentchannel.Protos.Error.Builder subBuilder = org.bitcoin.paymentchannel.Protos.Error.newBuilder(); + if (hasError()) { + subBuilder.mergeFrom(getError()); + } + input.readMessage(subBuilder, extensionRegistry); + setError(subBuilder.buildPartial()); + break; + } + } + } + } + + private int bitField0_; + + // required .paymentchannels.TwoWayChannelMessage.MessageType type = 1; + private org.bitcoin.paymentchannel.Protos.TwoWayChannelMessage.MessageType type_ = org.bitcoin.paymentchannel.Protos.TwoWayChannelMessage.MessageType.CLIENT_VERSION; + public boolean hasType() { + return ((bitField0_ & 0x00000001) == 0x00000001); + } + public org.bitcoin.paymentchannel.Protos.TwoWayChannelMessage.MessageType getType() { + return type_; + } + public Builder setType(org.bitcoin.paymentchannel.Protos.TwoWayChannelMessage.MessageType value) { + if (value == null) { + throw new NullPointerException(); + } + bitField0_ |= 0x00000001; + type_ = value; + onChanged(); + return this; + } + public Builder clearType() { + bitField0_ = (bitField0_ & ~0x00000001); + type_ = org.bitcoin.paymentchannel.Protos.TwoWayChannelMessage.MessageType.CLIENT_VERSION; + onChanged(); + return this; + } + + // optional .paymentchannels.ClientVersion client_version = 2; + private org.bitcoin.paymentchannel.Protos.ClientVersion clientVersion_ = org.bitcoin.paymentchannel.Protos.ClientVersion.getDefaultInstance(); + private com.google.protobuf.SingleFieldBuilder< + org.bitcoin.paymentchannel.Protos.ClientVersion, org.bitcoin.paymentchannel.Protos.ClientVersion.Builder, org.bitcoin.paymentchannel.Protos.ClientVersionOrBuilder> clientVersionBuilder_; + public boolean hasClientVersion() { + return ((bitField0_ & 0x00000002) == 0x00000002); + } + public org.bitcoin.paymentchannel.Protos.ClientVersion getClientVersion() { + if (clientVersionBuilder_ == null) { + return clientVersion_; + } else { + return clientVersionBuilder_.getMessage(); + } + } + public Builder setClientVersion(org.bitcoin.paymentchannel.Protos.ClientVersion value) { + if (clientVersionBuilder_ == null) { + if (value == null) { + throw new NullPointerException(); + } + clientVersion_ = value; + onChanged(); + } else { + clientVersionBuilder_.setMessage(value); + } + bitField0_ |= 0x00000002; + return this; + } + public Builder setClientVersion( + org.bitcoin.paymentchannel.Protos.ClientVersion.Builder builderForValue) { + if (clientVersionBuilder_ == null) { + clientVersion_ = builderForValue.build(); + onChanged(); + } else { + clientVersionBuilder_.setMessage(builderForValue.build()); + } + bitField0_ |= 0x00000002; + return this; + } + public Builder mergeClientVersion(org.bitcoin.paymentchannel.Protos.ClientVersion value) { + if (clientVersionBuilder_ == null) { + if (((bitField0_ & 0x00000002) == 0x00000002) && + clientVersion_ != org.bitcoin.paymentchannel.Protos.ClientVersion.getDefaultInstance()) { + clientVersion_ = + org.bitcoin.paymentchannel.Protos.ClientVersion.newBuilder(clientVersion_).mergeFrom(value).buildPartial(); + } else { + clientVersion_ = value; + } + onChanged(); + } else { + clientVersionBuilder_.mergeFrom(value); + } + bitField0_ |= 0x00000002; + return this; + } + public Builder clearClientVersion() { + if (clientVersionBuilder_ == null) { + clientVersion_ = org.bitcoin.paymentchannel.Protos.ClientVersion.getDefaultInstance(); + onChanged(); + } else { + clientVersionBuilder_.clear(); + } + bitField0_ = (bitField0_ & ~0x00000002); + return this; + } + public org.bitcoin.paymentchannel.Protos.ClientVersion.Builder getClientVersionBuilder() { + bitField0_ |= 0x00000002; + onChanged(); + return getClientVersionFieldBuilder().getBuilder(); + } + public org.bitcoin.paymentchannel.Protos.ClientVersionOrBuilder getClientVersionOrBuilder() { + if (clientVersionBuilder_ != null) { + return clientVersionBuilder_.getMessageOrBuilder(); + } else { + return clientVersion_; + } + } + private com.google.protobuf.SingleFieldBuilder< + org.bitcoin.paymentchannel.Protos.ClientVersion, org.bitcoin.paymentchannel.Protos.ClientVersion.Builder, org.bitcoin.paymentchannel.Protos.ClientVersionOrBuilder> + getClientVersionFieldBuilder() { + if (clientVersionBuilder_ == null) { + clientVersionBuilder_ = new com.google.protobuf.SingleFieldBuilder< + org.bitcoin.paymentchannel.Protos.ClientVersion, org.bitcoin.paymentchannel.Protos.ClientVersion.Builder, org.bitcoin.paymentchannel.Protos.ClientVersionOrBuilder>( + clientVersion_, + getParentForChildren(), + isClean()); + clientVersion_ = null; + } + return clientVersionBuilder_; + } + + // optional .paymentchannels.ServerVersion server_version = 3; + private org.bitcoin.paymentchannel.Protos.ServerVersion serverVersion_ = org.bitcoin.paymentchannel.Protos.ServerVersion.getDefaultInstance(); + private com.google.protobuf.SingleFieldBuilder< + org.bitcoin.paymentchannel.Protos.ServerVersion, org.bitcoin.paymentchannel.Protos.ServerVersion.Builder, org.bitcoin.paymentchannel.Protos.ServerVersionOrBuilder> serverVersionBuilder_; + public boolean hasServerVersion() { + return ((bitField0_ & 0x00000004) == 0x00000004); + } + public org.bitcoin.paymentchannel.Protos.ServerVersion getServerVersion() { + if (serverVersionBuilder_ == null) { + return serverVersion_; + } else { + return serverVersionBuilder_.getMessage(); + } + } + public Builder setServerVersion(org.bitcoin.paymentchannel.Protos.ServerVersion value) { + if (serverVersionBuilder_ == null) { + if (value == null) { + throw new NullPointerException(); + } + serverVersion_ = value; + onChanged(); + } else { + serverVersionBuilder_.setMessage(value); + } + bitField0_ |= 0x00000004; + return this; + } + public Builder setServerVersion( + org.bitcoin.paymentchannel.Protos.ServerVersion.Builder builderForValue) { + if (serverVersionBuilder_ == null) { + serverVersion_ = builderForValue.build(); + onChanged(); + } else { + serverVersionBuilder_.setMessage(builderForValue.build()); + } + bitField0_ |= 0x00000004; + return this; + } + public Builder mergeServerVersion(org.bitcoin.paymentchannel.Protos.ServerVersion value) { + if (serverVersionBuilder_ == null) { + if (((bitField0_ & 0x00000004) == 0x00000004) && + serverVersion_ != org.bitcoin.paymentchannel.Protos.ServerVersion.getDefaultInstance()) { + serverVersion_ = + org.bitcoin.paymentchannel.Protos.ServerVersion.newBuilder(serverVersion_).mergeFrom(value).buildPartial(); + } else { + serverVersion_ = value; + } + onChanged(); + } else { + serverVersionBuilder_.mergeFrom(value); + } + bitField0_ |= 0x00000004; + return this; + } + public Builder clearServerVersion() { + if (serverVersionBuilder_ == null) { + serverVersion_ = org.bitcoin.paymentchannel.Protos.ServerVersion.getDefaultInstance(); + onChanged(); + } else { + serverVersionBuilder_.clear(); + } + bitField0_ = (bitField0_ & ~0x00000004); + return this; + } + public org.bitcoin.paymentchannel.Protos.ServerVersion.Builder getServerVersionBuilder() { + bitField0_ |= 0x00000004; + onChanged(); + return getServerVersionFieldBuilder().getBuilder(); + } + public org.bitcoin.paymentchannel.Protos.ServerVersionOrBuilder getServerVersionOrBuilder() { + if (serverVersionBuilder_ != null) { + return serverVersionBuilder_.getMessageOrBuilder(); + } else { + return serverVersion_; + } + } + private com.google.protobuf.SingleFieldBuilder< + org.bitcoin.paymentchannel.Protos.ServerVersion, org.bitcoin.paymentchannel.Protos.ServerVersion.Builder, org.bitcoin.paymentchannel.Protos.ServerVersionOrBuilder> + getServerVersionFieldBuilder() { + if (serverVersionBuilder_ == null) { + serverVersionBuilder_ = new com.google.protobuf.SingleFieldBuilder< + org.bitcoin.paymentchannel.Protos.ServerVersion, org.bitcoin.paymentchannel.Protos.ServerVersion.Builder, org.bitcoin.paymentchannel.Protos.ServerVersionOrBuilder>( + serverVersion_, + getParentForChildren(), + isClean()); + serverVersion_ = null; + } + return serverVersionBuilder_; + } + + // optional .paymentchannels.Initiate initiate = 4; + private org.bitcoin.paymentchannel.Protos.Initiate initiate_ = org.bitcoin.paymentchannel.Protos.Initiate.getDefaultInstance(); + private com.google.protobuf.SingleFieldBuilder< + org.bitcoin.paymentchannel.Protos.Initiate, org.bitcoin.paymentchannel.Protos.Initiate.Builder, org.bitcoin.paymentchannel.Protos.InitiateOrBuilder> initiateBuilder_; + public boolean hasInitiate() { + return ((bitField0_ & 0x00000008) == 0x00000008); + } + public org.bitcoin.paymentchannel.Protos.Initiate getInitiate() { + if (initiateBuilder_ == null) { + return initiate_; + } else { + return initiateBuilder_.getMessage(); + } + } + public Builder setInitiate(org.bitcoin.paymentchannel.Protos.Initiate value) { + if (initiateBuilder_ == null) { + if (value == null) { + throw new NullPointerException(); + } + initiate_ = value; + onChanged(); + } else { + initiateBuilder_.setMessage(value); + } + bitField0_ |= 0x00000008; + return this; + } + public Builder setInitiate( + org.bitcoin.paymentchannel.Protos.Initiate.Builder builderForValue) { + if (initiateBuilder_ == null) { + initiate_ = builderForValue.build(); + onChanged(); + } else { + initiateBuilder_.setMessage(builderForValue.build()); + } + bitField0_ |= 0x00000008; + return this; + } + public Builder mergeInitiate(org.bitcoin.paymentchannel.Protos.Initiate value) { + if (initiateBuilder_ == null) { + if (((bitField0_ & 0x00000008) == 0x00000008) && + initiate_ != org.bitcoin.paymentchannel.Protos.Initiate.getDefaultInstance()) { + initiate_ = + org.bitcoin.paymentchannel.Protos.Initiate.newBuilder(initiate_).mergeFrom(value).buildPartial(); + } else { + initiate_ = value; + } + onChanged(); + } else { + initiateBuilder_.mergeFrom(value); + } + bitField0_ |= 0x00000008; + return this; + } + public Builder clearInitiate() { + if (initiateBuilder_ == null) { + initiate_ = org.bitcoin.paymentchannel.Protos.Initiate.getDefaultInstance(); + onChanged(); + } else { + initiateBuilder_.clear(); + } + bitField0_ = (bitField0_ & ~0x00000008); + return this; + } + public org.bitcoin.paymentchannel.Protos.Initiate.Builder getInitiateBuilder() { + bitField0_ |= 0x00000008; + onChanged(); + return getInitiateFieldBuilder().getBuilder(); + } + public org.bitcoin.paymentchannel.Protos.InitiateOrBuilder getInitiateOrBuilder() { + if (initiateBuilder_ != null) { + return initiateBuilder_.getMessageOrBuilder(); + } else { + return initiate_; + } + } + private com.google.protobuf.SingleFieldBuilder< + org.bitcoin.paymentchannel.Protos.Initiate, org.bitcoin.paymentchannel.Protos.Initiate.Builder, org.bitcoin.paymentchannel.Protos.InitiateOrBuilder> + getInitiateFieldBuilder() { + if (initiateBuilder_ == null) { + initiateBuilder_ = new com.google.protobuf.SingleFieldBuilder< + org.bitcoin.paymentchannel.Protos.Initiate, org.bitcoin.paymentchannel.Protos.Initiate.Builder, org.bitcoin.paymentchannel.Protos.InitiateOrBuilder>( + initiate_, + getParentForChildren(), + isClean()); + initiate_ = null; + } + return initiateBuilder_; + } + + // optional .paymentchannels.ProvideRefund provide_refund = 5; + private org.bitcoin.paymentchannel.Protos.ProvideRefund provideRefund_ = org.bitcoin.paymentchannel.Protos.ProvideRefund.getDefaultInstance(); + private com.google.protobuf.SingleFieldBuilder< + org.bitcoin.paymentchannel.Protos.ProvideRefund, org.bitcoin.paymentchannel.Protos.ProvideRefund.Builder, org.bitcoin.paymentchannel.Protos.ProvideRefundOrBuilder> provideRefundBuilder_; + public boolean hasProvideRefund() { + return ((bitField0_ & 0x00000010) == 0x00000010); + } + public org.bitcoin.paymentchannel.Protos.ProvideRefund getProvideRefund() { + if (provideRefundBuilder_ == null) { + return provideRefund_; + } else { + return provideRefundBuilder_.getMessage(); + } + } + public Builder setProvideRefund(org.bitcoin.paymentchannel.Protos.ProvideRefund value) { + if (provideRefundBuilder_ == null) { + if (value == null) { + throw new NullPointerException(); + } + provideRefund_ = value; + onChanged(); + } else { + provideRefundBuilder_.setMessage(value); + } + bitField0_ |= 0x00000010; + return this; + } + public Builder setProvideRefund( + org.bitcoin.paymentchannel.Protos.ProvideRefund.Builder builderForValue) { + if (provideRefundBuilder_ == null) { + provideRefund_ = builderForValue.build(); + onChanged(); + } else { + provideRefundBuilder_.setMessage(builderForValue.build()); + } + bitField0_ |= 0x00000010; + return this; + } + public Builder mergeProvideRefund(org.bitcoin.paymentchannel.Protos.ProvideRefund value) { + if (provideRefundBuilder_ == null) { + if (((bitField0_ & 0x00000010) == 0x00000010) && + provideRefund_ != org.bitcoin.paymentchannel.Protos.ProvideRefund.getDefaultInstance()) { + provideRefund_ = + org.bitcoin.paymentchannel.Protos.ProvideRefund.newBuilder(provideRefund_).mergeFrom(value).buildPartial(); + } else { + provideRefund_ = value; + } + onChanged(); + } else { + provideRefundBuilder_.mergeFrom(value); + } + bitField0_ |= 0x00000010; + return this; + } + public Builder clearProvideRefund() { + if (provideRefundBuilder_ == null) { + provideRefund_ = org.bitcoin.paymentchannel.Protos.ProvideRefund.getDefaultInstance(); + onChanged(); + } else { + provideRefundBuilder_.clear(); + } + bitField0_ = (bitField0_ & ~0x00000010); + return this; + } + public org.bitcoin.paymentchannel.Protos.ProvideRefund.Builder getProvideRefundBuilder() { + bitField0_ |= 0x00000010; + onChanged(); + return getProvideRefundFieldBuilder().getBuilder(); + } + public org.bitcoin.paymentchannel.Protos.ProvideRefundOrBuilder getProvideRefundOrBuilder() { + if (provideRefundBuilder_ != null) { + return provideRefundBuilder_.getMessageOrBuilder(); + } else { + return provideRefund_; + } + } + private com.google.protobuf.SingleFieldBuilder< + org.bitcoin.paymentchannel.Protos.ProvideRefund, org.bitcoin.paymentchannel.Protos.ProvideRefund.Builder, org.bitcoin.paymentchannel.Protos.ProvideRefundOrBuilder> + getProvideRefundFieldBuilder() { + if (provideRefundBuilder_ == null) { + provideRefundBuilder_ = new com.google.protobuf.SingleFieldBuilder< + org.bitcoin.paymentchannel.Protos.ProvideRefund, org.bitcoin.paymentchannel.Protos.ProvideRefund.Builder, org.bitcoin.paymentchannel.Protos.ProvideRefundOrBuilder>( + provideRefund_, + getParentForChildren(), + isClean()); + provideRefund_ = null; + } + return provideRefundBuilder_; + } + + // optional .paymentchannels.ReturnRefund return_refund = 6; + private org.bitcoin.paymentchannel.Protos.ReturnRefund returnRefund_ = org.bitcoin.paymentchannel.Protos.ReturnRefund.getDefaultInstance(); + private com.google.protobuf.SingleFieldBuilder< + org.bitcoin.paymentchannel.Protos.ReturnRefund, org.bitcoin.paymentchannel.Protos.ReturnRefund.Builder, org.bitcoin.paymentchannel.Protos.ReturnRefundOrBuilder> returnRefundBuilder_; + public boolean hasReturnRefund() { + return ((bitField0_ & 0x00000020) == 0x00000020); + } + public org.bitcoin.paymentchannel.Protos.ReturnRefund getReturnRefund() { + if (returnRefundBuilder_ == null) { + return returnRefund_; + } else { + return returnRefundBuilder_.getMessage(); + } + } + public Builder setReturnRefund(org.bitcoin.paymentchannel.Protos.ReturnRefund value) { + if (returnRefundBuilder_ == null) { + if (value == null) { + throw new NullPointerException(); + } + returnRefund_ = value; + onChanged(); + } else { + returnRefundBuilder_.setMessage(value); + } + bitField0_ |= 0x00000020; + return this; + } + public Builder setReturnRefund( + org.bitcoin.paymentchannel.Protos.ReturnRefund.Builder builderForValue) { + if (returnRefundBuilder_ == null) { + returnRefund_ = builderForValue.build(); + onChanged(); + } else { + returnRefundBuilder_.setMessage(builderForValue.build()); + } + bitField0_ |= 0x00000020; + return this; + } + public Builder mergeReturnRefund(org.bitcoin.paymentchannel.Protos.ReturnRefund value) { + if (returnRefundBuilder_ == null) { + if (((bitField0_ & 0x00000020) == 0x00000020) && + returnRefund_ != org.bitcoin.paymentchannel.Protos.ReturnRefund.getDefaultInstance()) { + returnRefund_ = + org.bitcoin.paymentchannel.Protos.ReturnRefund.newBuilder(returnRefund_).mergeFrom(value).buildPartial(); + } else { + returnRefund_ = value; + } + onChanged(); + } else { + returnRefundBuilder_.mergeFrom(value); + } + bitField0_ |= 0x00000020; + return this; + } + public Builder clearReturnRefund() { + if (returnRefundBuilder_ == null) { + returnRefund_ = org.bitcoin.paymentchannel.Protos.ReturnRefund.getDefaultInstance(); + onChanged(); + } else { + returnRefundBuilder_.clear(); + } + bitField0_ = (bitField0_ & ~0x00000020); + return this; + } + public org.bitcoin.paymentchannel.Protos.ReturnRefund.Builder getReturnRefundBuilder() { + bitField0_ |= 0x00000020; + onChanged(); + return getReturnRefundFieldBuilder().getBuilder(); + } + public org.bitcoin.paymentchannel.Protos.ReturnRefundOrBuilder getReturnRefundOrBuilder() { + if (returnRefundBuilder_ != null) { + return returnRefundBuilder_.getMessageOrBuilder(); + } else { + return returnRefund_; + } + } + private com.google.protobuf.SingleFieldBuilder< + org.bitcoin.paymentchannel.Protos.ReturnRefund, org.bitcoin.paymentchannel.Protos.ReturnRefund.Builder, org.bitcoin.paymentchannel.Protos.ReturnRefundOrBuilder> + getReturnRefundFieldBuilder() { + if (returnRefundBuilder_ == null) { + returnRefundBuilder_ = new com.google.protobuf.SingleFieldBuilder< + org.bitcoin.paymentchannel.Protos.ReturnRefund, org.bitcoin.paymentchannel.Protos.ReturnRefund.Builder, org.bitcoin.paymentchannel.Protos.ReturnRefundOrBuilder>( + returnRefund_, + getParentForChildren(), + isClean()); + returnRefund_ = null; + } + return returnRefundBuilder_; + } + + // optional .paymentchannels.ProvideContract provide_contract = 7; + private org.bitcoin.paymentchannel.Protos.ProvideContract provideContract_ = org.bitcoin.paymentchannel.Protos.ProvideContract.getDefaultInstance(); + private com.google.protobuf.SingleFieldBuilder< + org.bitcoin.paymentchannel.Protos.ProvideContract, org.bitcoin.paymentchannel.Protos.ProvideContract.Builder, org.bitcoin.paymentchannel.Protos.ProvideContractOrBuilder> provideContractBuilder_; + public boolean hasProvideContract() { + return ((bitField0_ & 0x00000040) == 0x00000040); + } + public org.bitcoin.paymentchannel.Protos.ProvideContract getProvideContract() { + if (provideContractBuilder_ == null) { + return provideContract_; + } else { + return provideContractBuilder_.getMessage(); + } + } + public Builder setProvideContract(org.bitcoin.paymentchannel.Protos.ProvideContract value) { + if (provideContractBuilder_ == null) { + if (value == null) { + throw new NullPointerException(); + } + provideContract_ = value; + onChanged(); + } else { + provideContractBuilder_.setMessage(value); + } + bitField0_ |= 0x00000040; + return this; + } + public Builder setProvideContract( + org.bitcoin.paymentchannel.Protos.ProvideContract.Builder builderForValue) { + if (provideContractBuilder_ == null) { + provideContract_ = builderForValue.build(); + onChanged(); + } else { + provideContractBuilder_.setMessage(builderForValue.build()); + } + bitField0_ |= 0x00000040; + return this; + } + public Builder mergeProvideContract(org.bitcoin.paymentchannel.Protos.ProvideContract value) { + if (provideContractBuilder_ == null) { + if (((bitField0_ & 0x00000040) == 0x00000040) && + provideContract_ != org.bitcoin.paymentchannel.Protos.ProvideContract.getDefaultInstance()) { + provideContract_ = + org.bitcoin.paymentchannel.Protos.ProvideContract.newBuilder(provideContract_).mergeFrom(value).buildPartial(); + } else { + provideContract_ = value; + } + onChanged(); + } else { + provideContractBuilder_.mergeFrom(value); + } + bitField0_ |= 0x00000040; + return this; + } + public Builder clearProvideContract() { + if (provideContractBuilder_ == null) { + provideContract_ = org.bitcoin.paymentchannel.Protos.ProvideContract.getDefaultInstance(); + onChanged(); + } else { + provideContractBuilder_.clear(); + } + bitField0_ = (bitField0_ & ~0x00000040); + return this; + } + public org.bitcoin.paymentchannel.Protos.ProvideContract.Builder getProvideContractBuilder() { + bitField0_ |= 0x00000040; + onChanged(); + return getProvideContractFieldBuilder().getBuilder(); + } + public org.bitcoin.paymentchannel.Protos.ProvideContractOrBuilder getProvideContractOrBuilder() { + if (provideContractBuilder_ != null) { + return provideContractBuilder_.getMessageOrBuilder(); + } else { + return provideContract_; + } + } + private com.google.protobuf.SingleFieldBuilder< + org.bitcoin.paymentchannel.Protos.ProvideContract, org.bitcoin.paymentchannel.Protos.ProvideContract.Builder, org.bitcoin.paymentchannel.Protos.ProvideContractOrBuilder> + getProvideContractFieldBuilder() { + if (provideContractBuilder_ == null) { + provideContractBuilder_ = new com.google.protobuf.SingleFieldBuilder< + org.bitcoin.paymentchannel.Protos.ProvideContract, org.bitcoin.paymentchannel.Protos.ProvideContract.Builder, org.bitcoin.paymentchannel.Protos.ProvideContractOrBuilder>( + provideContract_, + getParentForChildren(), + isClean()); + provideContract_ = null; + } + return provideContractBuilder_; + } + + // optional .paymentchannels.UpdatePayment update_payment = 8; + private org.bitcoin.paymentchannel.Protos.UpdatePayment updatePayment_ = org.bitcoin.paymentchannel.Protos.UpdatePayment.getDefaultInstance(); + private com.google.protobuf.SingleFieldBuilder< + org.bitcoin.paymentchannel.Protos.UpdatePayment, org.bitcoin.paymentchannel.Protos.UpdatePayment.Builder, org.bitcoin.paymentchannel.Protos.UpdatePaymentOrBuilder> updatePaymentBuilder_; + public boolean hasUpdatePayment() { + return ((bitField0_ & 0x00000080) == 0x00000080); + } + public org.bitcoin.paymentchannel.Protos.UpdatePayment getUpdatePayment() { + if (updatePaymentBuilder_ == null) { + return updatePayment_; + } else { + return updatePaymentBuilder_.getMessage(); + } + } + public Builder setUpdatePayment(org.bitcoin.paymentchannel.Protos.UpdatePayment value) { + if (updatePaymentBuilder_ == null) { + if (value == null) { + throw new NullPointerException(); + } + updatePayment_ = value; + onChanged(); + } else { + updatePaymentBuilder_.setMessage(value); + } + bitField0_ |= 0x00000080; + return this; + } + public Builder setUpdatePayment( + org.bitcoin.paymentchannel.Protos.UpdatePayment.Builder builderForValue) { + if (updatePaymentBuilder_ == null) { + updatePayment_ = builderForValue.build(); + onChanged(); + } else { + updatePaymentBuilder_.setMessage(builderForValue.build()); + } + bitField0_ |= 0x00000080; + return this; + } + public Builder mergeUpdatePayment(org.bitcoin.paymentchannel.Protos.UpdatePayment value) { + if (updatePaymentBuilder_ == null) { + if (((bitField0_ & 0x00000080) == 0x00000080) && + updatePayment_ != org.bitcoin.paymentchannel.Protos.UpdatePayment.getDefaultInstance()) { + updatePayment_ = + org.bitcoin.paymentchannel.Protos.UpdatePayment.newBuilder(updatePayment_).mergeFrom(value).buildPartial(); + } else { + updatePayment_ = value; + } + onChanged(); + } else { + updatePaymentBuilder_.mergeFrom(value); + } + bitField0_ |= 0x00000080; + return this; + } + public Builder clearUpdatePayment() { + if (updatePaymentBuilder_ == null) { + updatePayment_ = org.bitcoin.paymentchannel.Protos.UpdatePayment.getDefaultInstance(); + onChanged(); + } else { + updatePaymentBuilder_.clear(); + } + bitField0_ = (bitField0_ & ~0x00000080); + return this; + } + public org.bitcoin.paymentchannel.Protos.UpdatePayment.Builder getUpdatePaymentBuilder() { + bitField0_ |= 0x00000080; + onChanged(); + return getUpdatePaymentFieldBuilder().getBuilder(); + } + public org.bitcoin.paymentchannel.Protos.UpdatePaymentOrBuilder getUpdatePaymentOrBuilder() { + if (updatePaymentBuilder_ != null) { + return updatePaymentBuilder_.getMessageOrBuilder(); + } else { + return updatePayment_; + } + } + private com.google.protobuf.SingleFieldBuilder< + org.bitcoin.paymentchannel.Protos.UpdatePayment, org.bitcoin.paymentchannel.Protos.UpdatePayment.Builder, org.bitcoin.paymentchannel.Protos.UpdatePaymentOrBuilder> + getUpdatePaymentFieldBuilder() { + if (updatePaymentBuilder_ == null) { + updatePaymentBuilder_ = new com.google.protobuf.SingleFieldBuilder< + org.bitcoin.paymentchannel.Protos.UpdatePayment, org.bitcoin.paymentchannel.Protos.UpdatePayment.Builder, org.bitcoin.paymentchannel.Protos.UpdatePaymentOrBuilder>( + updatePayment_, + getParentForChildren(), + isClean()); + updatePayment_ = null; + } + return updatePaymentBuilder_; + } + + // optional .paymentchannels.Error error = 10; + private org.bitcoin.paymentchannel.Protos.Error error_ = org.bitcoin.paymentchannel.Protos.Error.getDefaultInstance(); + private com.google.protobuf.SingleFieldBuilder< + org.bitcoin.paymentchannel.Protos.Error, org.bitcoin.paymentchannel.Protos.Error.Builder, org.bitcoin.paymentchannel.Protos.ErrorOrBuilder> errorBuilder_; + public boolean hasError() { + return ((bitField0_ & 0x00000100) == 0x00000100); + } + public org.bitcoin.paymentchannel.Protos.Error getError() { + if (errorBuilder_ == null) { + return error_; + } else { + return errorBuilder_.getMessage(); + } + } + public Builder setError(org.bitcoin.paymentchannel.Protos.Error value) { + if (errorBuilder_ == null) { + if (value == null) { + throw new NullPointerException(); + } + error_ = value; + onChanged(); + } else { + errorBuilder_.setMessage(value); + } + bitField0_ |= 0x00000100; + return this; + } + public Builder setError( + org.bitcoin.paymentchannel.Protos.Error.Builder builderForValue) { + if (errorBuilder_ == null) { + error_ = builderForValue.build(); + onChanged(); + } else { + errorBuilder_.setMessage(builderForValue.build()); + } + bitField0_ |= 0x00000100; + return this; + } + public Builder mergeError(org.bitcoin.paymentchannel.Protos.Error value) { + if (errorBuilder_ == null) { + if (((bitField0_ & 0x00000100) == 0x00000100) && + error_ != org.bitcoin.paymentchannel.Protos.Error.getDefaultInstance()) { + error_ = + org.bitcoin.paymentchannel.Protos.Error.newBuilder(error_).mergeFrom(value).buildPartial(); + } else { + error_ = value; + } + onChanged(); + } else { + errorBuilder_.mergeFrom(value); + } + bitField0_ |= 0x00000100; + return this; + } + public Builder clearError() { + if (errorBuilder_ == null) { + error_ = org.bitcoin.paymentchannel.Protos.Error.getDefaultInstance(); + onChanged(); + } else { + errorBuilder_.clear(); + } + bitField0_ = (bitField0_ & ~0x00000100); + return this; + } + public org.bitcoin.paymentchannel.Protos.Error.Builder getErrorBuilder() { + bitField0_ |= 0x00000100; + onChanged(); + return getErrorFieldBuilder().getBuilder(); + } + public org.bitcoin.paymentchannel.Protos.ErrorOrBuilder getErrorOrBuilder() { + if (errorBuilder_ != null) { + return errorBuilder_.getMessageOrBuilder(); + } else { + return error_; + } + } + private com.google.protobuf.SingleFieldBuilder< + org.bitcoin.paymentchannel.Protos.Error, org.bitcoin.paymentchannel.Protos.Error.Builder, org.bitcoin.paymentchannel.Protos.ErrorOrBuilder> + getErrorFieldBuilder() { + if (errorBuilder_ == null) { + errorBuilder_ = new com.google.protobuf.SingleFieldBuilder< + org.bitcoin.paymentchannel.Protos.Error, org.bitcoin.paymentchannel.Protos.Error.Builder, org.bitcoin.paymentchannel.Protos.ErrorOrBuilder>( + error_, + getParentForChildren(), + isClean()); + error_ = null; + } + return errorBuilder_; + } + + // @@protoc_insertion_point(builder_scope:paymentchannels.TwoWayChannelMessage) + } + + static { + defaultInstance = new TwoWayChannelMessage(true); + defaultInstance.initFields(); + } + + // @@protoc_insertion_point(class_scope:paymentchannels.TwoWayChannelMessage) + } + + public interface ClientVersionOrBuilder + extends com.google.protobuf.MessageOrBuilder { + + // required int32 major = 1; + boolean hasMajor(); + int getMajor(); + + // optional int32 minor = 2 [default = 0]; + boolean hasMinor(); + int getMinor(); + + // optional bytes previous_channel_contract_hash = 3; + boolean hasPreviousChannelContractHash(); + com.google.protobuf.ByteString getPreviousChannelContractHash(); + } + public static final class ClientVersion extends + com.google.protobuf.GeneratedMessage + implements ClientVersionOrBuilder { + // Use ClientVersion.newBuilder() to construct. + private ClientVersion(Builder builder) { + super(builder); + } + private ClientVersion(boolean noInit) {} + + private static final ClientVersion defaultInstance; + public static ClientVersion getDefaultInstance() { + return defaultInstance; + } + + public ClientVersion getDefaultInstanceForType() { + return defaultInstance; + } + + public static final com.google.protobuf.Descriptors.Descriptor + getDescriptor() { + return org.bitcoin.paymentchannel.Protos.internal_static_paymentchannels_ClientVersion_descriptor; + } + + protected com.google.protobuf.GeneratedMessage.FieldAccessorTable + internalGetFieldAccessorTable() { + return org.bitcoin.paymentchannel.Protos.internal_static_paymentchannels_ClientVersion_fieldAccessorTable; + } + + private int bitField0_; + // required int32 major = 1; + public static final int MAJOR_FIELD_NUMBER = 1; + private int major_; + public boolean hasMajor() { + return ((bitField0_ & 0x00000001) == 0x00000001); + } + public int getMajor() { + return major_; + } + + // optional int32 minor = 2 [default = 0]; + public static final int MINOR_FIELD_NUMBER = 2; + private int minor_; + public boolean hasMinor() { + return ((bitField0_ & 0x00000002) == 0x00000002); + } + public int getMinor() { + return minor_; + } + + // optional bytes previous_channel_contract_hash = 3; + public static final int PREVIOUS_CHANNEL_CONTRACT_HASH_FIELD_NUMBER = 3; + private com.google.protobuf.ByteString previousChannelContractHash_; + public boolean hasPreviousChannelContractHash() { + return ((bitField0_ & 0x00000004) == 0x00000004); + } + public com.google.protobuf.ByteString getPreviousChannelContractHash() { + return previousChannelContractHash_; + } + + private void initFields() { + major_ = 0; + minor_ = 0; + previousChannelContractHash_ = com.google.protobuf.ByteString.EMPTY; + } + private byte memoizedIsInitialized = -1; + public final boolean isInitialized() { + byte isInitialized = memoizedIsInitialized; + if (isInitialized != -1) return isInitialized == 1; + + if (!hasMajor()) { + memoizedIsInitialized = 0; + return false; + } + memoizedIsInitialized = 1; + return true; + } + + public void writeTo(com.google.protobuf.CodedOutputStream output) + throws java.io.IOException { + getSerializedSize(); + if (((bitField0_ & 0x00000001) == 0x00000001)) { + output.writeInt32(1, major_); + } + if (((bitField0_ & 0x00000002) == 0x00000002)) { + output.writeInt32(2, minor_); + } + if (((bitField0_ & 0x00000004) == 0x00000004)) { + output.writeBytes(3, previousChannelContractHash_); + } + getUnknownFields().writeTo(output); + } + + private int memoizedSerializedSize = -1; + public int getSerializedSize() { + int size = memoizedSerializedSize; + if (size != -1) return size; + + size = 0; + if (((bitField0_ & 0x00000001) == 0x00000001)) { + size += com.google.protobuf.CodedOutputStream + .computeInt32Size(1, major_); + } + if (((bitField0_ & 0x00000002) == 0x00000002)) { + size += com.google.protobuf.CodedOutputStream + .computeInt32Size(2, minor_); + } + if (((bitField0_ & 0x00000004) == 0x00000004)) { + size += com.google.protobuf.CodedOutputStream + .computeBytesSize(3, previousChannelContractHash_); + } + size += getUnknownFields().getSerializedSize(); + memoizedSerializedSize = size; + return size; + } + + private static final long serialVersionUID = 0L; + @java.lang.Override + protected java.lang.Object writeReplace() + throws java.io.ObjectStreamException { + return super.writeReplace(); + } + + public static org.bitcoin.paymentchannel.Protos.ClientVersion parseFrom( + com.google.protobuf.ByteString data) + throws com.google.protobuf.InvalidProtocolBufferException { + return newBuilder().mergeFrom(data).buildParsed(); + } + public static org.bitcoin.paymentchannel.Protos.ClientVersion parseFrom( + com.google.protobuf.ByteString data, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws com.google.protobuf.InvalidProtocolBufferException { + return newBuilder().mergeFrom(data, extensionRegistry) + .buildParsed(); + } + public static org.bitcoin.paymentchannel.Protos.ClientVersion parseFrom(byte[] data) + throws com.google.protobuf.InvalidProtocolBufferException { + return newBuilder().mergeFrom(data).buildParsed(); + } + public static org.bitcoin.paymentchannel.Protos.ClientVersion parseFrom( + byte[] data, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws com.google.protobuf.InvalidProtocolBufferException { + return newBuilder().mergeFrom(data, extensionRegistry) + .buildParsed(); + } + public static org.bitcoin.paymentchannel.Protos.ClientVersion parseFrom(java.io.InputStream input) + throws java.io.IOException { + return newBuilder().mergeFrom(input).buildParsed(); + } + public static org.bitcoin.paymentchannel.Protos.ClientVersion parseFrom( + java.io.InputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + return newBuilder().mergeFrom(input, extensionRegistry) + .buildParsed(); + } + public static org.bitcoin.paymentchannel.Protos.ClientVersion parseDelimitedFrom(java.io.InputStream input) + throws java.io.IOException { + Builder builder = newBuilder(); + if (builder.mergeDelimitedFrom(input)) { + return builder.buildParsed(); + } else { + return null; + } + } + public static org.bitcoin.paymentchannel.Protos.ClientVersion parseDelimitedFrom( + java.io.InputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + Builder builder = newBuilder(); + if (builder.mergeDelimitedFrom(input, extensionRegistry)) { + return builder.buildParsed(); + } else { + return null; + } + } + public static org.bitcoin.paymentchannel.Protos.ClientVersion parseFrom( + com.google.protobuf.CodedInputStream input) + throws java.io.IOException { + return newBuilder().mergeFrom(input).buildParsed(); + } + public static org.bitcoin.paymentchannel.Protos.ClientVersion parseFrom( + com.google.protobuf.CodedInputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + return newBuilder().mergeFrom(input, extensionRegistry) + .buildParsed(); + } + + public static Builder newBuilder() { return Builder.create(); } + public Builder newBuilderForType() { return newBuilder(); } + public static Builder newBuilder(org.bitcoin.paymentchannel.Protos.ClientVersion prototype) { + return newBuilder().mergeFrom(prototype); + } + public Builder toBuilder() { return newBuilder(this); } + + @java.lang.Override + protected Builder newBuilderForType( + com.google.protobuf.GeneratedMessage.BuilderParent parent) { + Builder builder = new Builder(parent); + return builder; + } + public static final class Builder extends + com.google.protobuf.GeneratedMessage.Builder + implements org.bitcoin.paymentchannel.Protos.ClientVersionOrBuilder { + public static final com.google.protobuf.Descriptors.Descriptor + getDescriptor() { + return org.bitcoin.paymentchannel.Protos.internal_static_paymentchannels_ClientVersion_descriptor; + } + + protected com.google.protobuf.GeneratedMessage.FieldAccessorTable + internalGetFieldAccessorTable() { + return org.bitcoin.paymentchannel.Protos.internal_static_paymentchannels_ClientVersion_fieldAccessorTable; + } + + // Construct using org.bitcoin.paymentchannel.Protos.ClientVersion.newBuilder() + private Builder() { + maybeForceBuilderInitialization(); + } + + private Builder(BuilderParent parent) { + super(parent); + maybeForceBuilderInitialization(); + } + private void maybeForceBuilderInitialization() { + if (com.google.protobuf.GeneratedMessage.alwaysUseFieldBuilders) { + } + } + private static Builder create() { + return new Builder(); + } + + public Builder clear() { + super.clear(); + major_ = 0; + bitField0_ = (bitField0_ & ~0x00000001); + minor_ = 0; + bitField0_ = (bitField0_ & ~0x00000002); + previousChannelContractHash_ = com.google.protobuf.ByteString.EMPTY; + bitField0_ = (bitField0_ & ~0x00000004); + return this; + } + + public Builder clone() { + return create().mergeFrom(buildPartial()); + } + + public com.google.protobuf.Descriptors.Descriptor + getDescriptorForType() { + return org.bitcoin.paymentchannel.Protos.ClientVersion.getDescriptor(); + } + + public org.bitcoin.paymentchannel.Protos.ClientVersion getDefaultInstanceForType() { + return org.bitcoin.paymentchannel.Protos.ClientVersion.getDefaultInstance(); + } + + public org.bitcoin.paymentchannel.Protos.ClientVersion build() { + org.bitcoin.paymentchannel.Protos.ClientVersion result = buildPartial(); + if (!result.isInitialized()) { + throw newUninitializedMessageException(result); + } + return result; + } + + private org.bitcoin.paymentchannel.Protos.ClientVersion buildParsed() + throws com.google.protobuf.InvalidProtocolBufferException { + org.bitcoin.paymentchannel.Protos.ClientVersion result = buildPartial(); + if (!result.isInitialized()) { + throw newUninitializedMessageException( + result).asInvalidProtocolBufferException(); + } + return result; + } + + public org.bitcoin.paymentchannel.Protos.ClientVersion buildPartial() { + org.bitcoin.paymentchannel.Protos.ClientVersion result = new org.bitcoin.paymentchannel.Protos.ClientVersion(this); + int from_bitField0_ = bitField0_; + int to_bitField0_ = 0; + if (((from_bitField0_ & 0x00000001) == 0x00000001)) { + to_bitField0_ |= 0x00000001; + } + result.major_ = major_; + if (((from_bitField0_ & 0x00000002) == 0x00000002)) { + to_bitField0_ |= 0x00000002; + } + result.minor_ = minor_; + if (((from_bitField0_ & 0x00000004) == 0x00000004)) { + to_bitField0_ |= 0x00000004; + } + result.previousChannelContractHash_ = previousChannelContractHash_; + result.bitField0_ = to_bitField0_; + onBuilt(); + return result; + } + + public Builder mergeFrom(com.google.protobuf.Message other) { + if (other instanceof org.bitcoin.paymentchannel.Protos.ClientVersion) { + return mergeFrom((org.bitcoin.paymentchannel.Protos.ClientVersion)other); + } else { + super.mergeFrom(other); + return this; + } + } + + public Builder mergeFrom(org.bitcoin.paymentchannel.Protos.ClientVersion other) { + if (other == org.bitcoin.paymentchannel.Protos.ClientVersion.getDefaultInstance()) return this; + if (other.hasMajor()) { + setMajor(other.getMajor()); + } + if (other.hasMinor()) { + setMinor(other.getMinor()); + } + if (other.hasPreviousChannelContractHash()) { + setPreviousChannelContractHash(other.getPreviousChannelContractHash()); + } + this.mergeUnknownFields(other.getUnknownFields()); + return this; + } + + public final boolean isInitialized() { + if (!hasMajor()) { + + return false; + } + return true; + } + + public Builder mergeFrom( + com.google.protobuf.CodedInputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + com.google.protobuf.UnknownFieldSet.Builder unknownFields = + com.google.protobuf.UnknownFieldSet.newBuilder( + this.getUnknownFields()); + while (true) { + int tag = input.readTag(); + switch (tag) { + case 0: + this.setUnknownFields(unknownFields.build()); + onChanged(); + return this; + default: { + if (!parseUnknownField(input, unknownFields, + extensionRegistry, tag)) { + this.setUnknownFields(unknownFields.build()); + onChanged(); + return this; + } + break; + } + case 8: { + bitField0_ |= 0x00000001; + major_ = input.readInt32(); + break; + } + case 16: { + bitField0_ |= 0x00000002; + minor_ = input.readInt32(); + break; + } + case 26: { + bitField0_ |= 0x00000004; + previousChannelContractHash_ = input.readBytes(); + break; + } + } + } + } + + private int bitField0_; + + // required int32 major = 1; + private int major_ ; + public boolean hasMajor() { + return ((bitField0_ & 0x00000001) == 0x00000001); + } + public int getMajor() { + return major_; + } + public Builder setMajor(int value) { + bitField0_ |= 0x00000001; + major_ = value; + onChanged(); + return this; + } + public Builder clearMajor() { + bitField0_ = (bitField0_ & ~0x00000001); + major_ = 0; + onChanged(); + return this; + } + + // optional int32 minor = 2 [default = 0]; + private int minor_ ; + public boolean hasMinor() { + return ((bitField0_ & 0x00000002) == 0x00000002); + } + public int getMinor() { + return minor_; + } + public Builder setMinor(int value) { + bitField0_ |= 0x00000002; + minor_ = value; + onChanged(); + return this; + } + public Builder clearMinor() { + bitField0_ = (bitField0_ & ~0x00000002); + minor_ = 0; + onChanged(); + return this; + } + + // optional bytes previous_channel_contract_hash = 3; + private com.google.protobuf.ByteString previousChannelContractHash_ = com.google.protobuf.ByteString.EMPTY; + public boolean hasPreviousChannelContractHash() { + return ((bitField0_ & 0x00000004) == 0x00000004); + } + public com.google.protobuf.ByteString getPreviousChannelContractHash() { + return previousChannelContractHash_; + } + public Builder setPreviousChannelContractHash(com.google.protobuf.ByteString value) { + if (value == null) { + throw new NullPointerException(); + } + bitField0_ |= 0x00000004; + previousChannelContractHash_ = value; + onChanged(); + return this; + } + public Builder clearPreviousChannelContractHash() { + bitField0_ = (bitField0_ & ~0x00000004); + previousChannelContractHash_ = getDefaultInstance().getPreviousChannelContractHash(); + onChanged(); + return this; + } + + // @@protoc_insertion_point(builder_scope:paymentchannels.ClientVersion) + } + + static { + defaultInstance = new ClientVersion(true); + defaultInstance.initFields(); + } + + // @@protoc_insertion_point(class_scope:paymentchannels.ClientVersion) + } + + public interface ServerVersionOrBuilder + extends com.google.protobuf.MessageOrBuilder { + + // required int32 major = 1; + boolean hasMajor(); + int getMajor(); + + // optional int32 minor = 2 [default = 0]; + boolean hasMinor(); + int getMinor(); + } + public static final class ServerVersion extends + com.google.protobuf.GeneratedMessage + implements ServerVersionOrBuilder { + // Use ServerVersion.newBuilder() to construct. + private ServerVersion(Builder builder) { + super(builder); + } + private ServerVersion(boolean noInit) {} + + private static final ServerVersion defaultInstance; + public static ServerVersion getDefaultInstance() { + return defaultInstance; + } + + public ServerVersion getDefaultInstanceForType() { + return defaultInstance; + } + + public static final com.google.protobuf.Descriptors.Descriptor + getDescriptor() { + return org.bitcoin.paymentchannel.Protos.internal_static_paymentchannels_ServerVersion_descriptor; + } + + protected com.google.protobuf.GeneratedMessage.FieldAccessorTable + internalGetFieldAccessorTable() { + return org.bitcoin.paymentchannel.Protos.internal_static_paymentchannels_ServerVersion_fieldAccessorTable; + } + + private int bitField0_; + // required int32 major = 1; + public static final int MAJOR_FIELD_NUMBER = 1; + private int major_; + public boolean hasMajor() { + return ((bitField0_ & 0x00000001) == 0x00000001); + } + public int getMajor() { + return major_; + } + + // optional int32 minor = 2 [default = 0]; + public static final int MINOR_FIELD_NUMBER = 2; + private int minor_; + public boolean hasMinor() { + return ((bitField0_ & 0x00000002) == 0x00000002); + } + public int getMinor() { + return minor_; + } + + private void initFields() { + major_ = 0; + minor_ = 0; + } + private byte memoizedIsInitialized = -1; + public final boolean isInitialized() { + byte isInitialized = memoizedIsInitialized; + if (isInitialized != -1) return isInitialized == 1; + + if (!hasMajor()) { + memoizedIsInitialized = 0; + return false; + } + memoizedIsInitialized = 1; + return true; + } + + public void writeTo(com.google.protobuf.CodedOutputStream output) + throws java.io.IOException { + getSerializedSize(); + if (((bitField0_ & 0x00000001) == 0x00000001)) { + output.writeInt32(1, major_); + } + if (((bitField0_ & 0x00000002) == 0x00000002)) { + output.writeInt32(2, minor_); + } + getUnknownFields().writeTo(output); + } + + private int memoizedSerializedSize = -1; + public int getSerializedSize() { + int size = memoizedSerializedSize; + if (size != -1) return size; + + size = 0; + if (((bitField0_ & 0x00000001) == 0x00000001)) { + size += com.google.protobuf.CodedOutputStream + .computeInt32Size(1, major_); + } + if (((bitField0_ & 0x00000002) == 0x00000002)) { + size += com.google.protobuf.CodedOutputStream + .computeInt32Size(2, minor_); + } + size += getUnknownFields().getSerializedSize(); + memoizedSerializedSize = size; + return size; + } + + private static final long serialVersionUID = 0L; + @java.lang.Override + protected java.lang.Object writeReplace() + throws java.io.ObjectStreamException { + return super.writeReplace(); + } + + public static org.bitcoin.paymentchannel.Protos.ServerVersion parseFrom( + com.google.protobuf.ByteString data) + throws com.google.protobuf.InvalidProtocolBufferException { + return newBuilder().mergeFrom(data).buildParsed(); + } + public static org.bitcoin.paymentchannel.Protos.ServerVersion parseFrom( + com.google.protobuf.ByteString data, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws com.google.protobuf.InvalidProtocolBufferException { + return newBuilder().mergeFrom(data, extensionRegistry) + .buildParsed(); + } + public static org.bitcoin.paymentchannel.Protos.ServerVersion parseFrom(byte[] data) + throws com.google.protobuf.InvalidProtocolBufferException { + return newBuilder().mergeFrom(data).buildParsed(); + } + public static org.bitcoin.paymentchannel.Protos.ServerVersion parseFrom( + byte[] data, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws com.google.protobuf.InvalidProtocolBufferException { + return newBuilder().mergeFrom(data, extensionRegistry) + .buildParsed(); + } + public static org.bitcoin.paymentchannel.Protos.ServerVersion parseFrom(java.io.InputStream input) + throws java.io.IOException { + return newBuilder().mergeFrom(input).buildParsed(); + } + public static org.bitcoin.paymentchannel.Protos.ServerVersion parseFrom( + java.io.InputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + return newBuilder().mergeFrom(input, extensionRegistry) + .buildParsed(); + } + public static org.bitcoin.paymentchannel.Protos.ServerVersion parseDelimitedFrom(java.io.InputStream input) + throws java.io.IOException { + Builder builder = newBuilder(); + if (builder.mergeDelimitedFrom(input)) { + return builder.buildParsed(); + } else { + return null; + } + } + public static org.bitcoin.paymentchannel.Protos.ServerVersion parseDelimitedFrom( + java.io.InputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + Builder builder = newBuilder(); + if (builder.mergeDelimitedFrom(input, extensionRegistry)) { + return builder.buildParsed(); + } else { + return null; + } + } + public static org.bitcoin.paymentchannel.Protos.ServerVersion parseFrom( + com.google.protobuf.CodedInputStream input) + throws java.io.IOException { + return newBuilder().mergeFrom(input).buildParsed(); + } + public static org.bitcoin.paymentchannel.Protos.ServerVersion parseFrom( + com.google.protobuf.CodedInputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + return newBuilder().mergeFrom(input, extensionRegistry) + .buildParsed(); + } + + public static Builder newBuilder() { return Builder.create(); } + public Builder newBuilderForType() { return newBuilder(); } + public static Builder newBuilder(org.bitcoin.paymentchannel.Protos.ServerVersion prototype) { + return newBuilder().mergeFrom(prototype); + } + public Builder toBuilder() { return newBuilder(this); } + + @java.lang.Override + protected Builder newBuilderForType( + com.google.protobuf.GeneratedMessage.BuilderParent parent) { + Builder builder = new Builder(parent); + return builder; + } + public static final class Builder extends + com.google.protobuf.GeneratedMessage.Builder + implements org.bitcoin.paymentchannel.Protos.ServerVersionOrBuilder { + public static final com.google.protobuf.Descriptors.Descriptor + getDescriptor() { + return org.bitcoin.paymentchannel.Protos.internal_static_paymentchannels_ServerVersion_descriptor; + } + + protected com.google.protobuf.GeneratedMessage.FieldAccessorTable + internalGetFieldAccessorTable() { + return org.bitcoin.paymentchannel.Protos.internal_static_paymentchannels_ServerVersion_fieldAccessorTable; + } + + // Construct using org.bitcoin.paymentchannel.Protos.ServerVersion.newBuilder() + private Builder() { + maybeForceBuilderInitialization(); + } + + private Builder(BuilderParent parent) { + super(parent); + maybeForceBuilderInitialization(); + } + private void maybeForceBuilderInitialization() { + if (com.google.protobuf.GeneratedMessage.alwaysUseFieldBuilders) { + } + } + private static Builder create() { + return new Builder(); + } + + public Builder clear() { + super.clear(); + major_ = 0; + bitField0_ = (bitField0_ & ~0x00000001); + minor_ = 0; + bitField0_ = (bitField0_ & ~0x00000002); + return this; + } + + public Builder clone() { + return create().mergeFrom(buildPartial()); + } + + public com.google.protobuf.Descriptors.Descriptor + getDescriptorForType() { + return org.bitcoin.paymentchannel.Protos.ServerVersion.getDescriptor(); + } + + public org.bitcoin.paymentchannel.Protos.ServerVersion getDefaultInstanceForType() { + return org.bitcoin.paymentchannel.Protos.ServerVersion.getDefaultInstance(); + } + + public org.bitcoin.paymentchannel.Protos.ServerVersion build() { + org.bitcoin.paymentchannel.Protos.ServerVersion result = buildPartial(); + if (!result.isInitialized()) { + throw newUninitializedMessageException(result); + } + return result; + } + + private org.bitcoin.paymentchannel.Protos.ServerVersion buildParsed() + throws com.google.protobuf.InvalidProtocolBufferException { + org.bitcoin.paymentchannel.Protos.ServerVersion result = buildPartial(); + if (!result.isInitialized()) { + throw newUninitializedMessageException( + result).asInvalidProtocolBufferException(); + } + return result; + } + + public org.bitcoin.paymentchannel.Protos.ServerVersion buildPartial() { + org.bitcoin.paymentchannel.Protos.ServerVersion result = new org.bitcoin.paymentchannel.Protos.ServerVersion(this); + int from_bitField0_ = bitField0_; + int to_bitField0_ = 0; + if (((from_bitField0_ & 0x00000001) == 0x00000001)) { + to_bitField0_ |= 0x00000001; + } + result.major_ = major_; + if (((from_bitField0_ & 0x00000002) == 0x00000002)) { + to_bitField0_ |= 0x00000002; + } + result.minor_ = minor_; + result.bitField0_ = to_bitField0_; + onBuilt(); + return result; + } + + public Builder mergeFrom(com.google.protobuf.Message other) { + if (other instanceof org.bitcoin.paymentchannel.Protos.ServerVersion) { + return mergeFrom((org.bitcoin.paymentchannel.Protos.ServerVersion)other); + } else { + super.mergeFrom(other); + return this; + } + } + + public Builder mergeFrom(org.bitcoin.paymentchannel.Protos.ServerVersion other) { + if (other == org.bitcoin.paymentchannel.Protos.ServerVersion.getDefaultInstance()) return this; + if (other.hasMajor()) { + setMajor(other.getMajor()); + } + if (other.hasMinor()) { + setMinor(other.getMinor()); + } + this.mergeUnknownFields(other.getUnknownFields()); + return this; + } + + public final boolean isInitialized() { + if (!hasMajor()) { + + return false; + } + return true; + } + + public Builder mergeFrom( + com.google.protobuf.CodedInputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + com.google.protobuf.UnknownFieldSet.Builder unknownFields = + com.google.protobuf.UnknownFieldSet.newBuilder( + this.getUnknownFields()); + while (true) { + int tag = input.readTag(); + switch (tag) { + case 0: + this.setUnknownFields(unknownFields.build()); + onChanged(); + return this; + default: { + if (!parseUnknownField(input, unknownFields, + extensionRegistry, tag)) { + this.setUnknownFields(unknownFields.build()); + onChanged(); + return this; + } + break; + } + case 8: { + bitField0_ |= 0x00000001; + major_ = input.readInt32(); + break; + } + case 16: { + bitField0_ |= 0x00000002; + minor_ = input.readInt32(); + break; + } + } + } + } + + private int bitField0_; + + // required int32 major = 1; + private int major_ ; + public boolean hasMajor() { + return ((bitField0_ & 0x00000001) == 0x00000001); + } + public int getMajor() { + return major_; + } + public Builder setMajor(int value) { + bitField0_ |= 0x00000001; + major_ = value; + onChanged(); + return this; + } + public Builder clearMajor() { + bitField0_ = (bitField0_ & ~0x00000001); + major_ = 0; + onChanged(); + return this; + } + + // optional int32 minor = 2 [default = 0]; + private int minor_ ; + public boolean hasMinor() { + return ((bitField0_ & 0x00000002) == 0x00000002); + } + public int getMinor() { + return minor_; + } + public Builder setMinor(int value) { + bitField0_ |= 0x00000002; + minor_ = value; + onChanged(); + return this; + } + public Builder clearMinor() { + bitField0_ = (bitField0_ & ~0x00000002); + minor_ = 0; + onChanged(); + return this; + } + + // @@protoc_insertion_point(builder_scope:paymentchannels.ServerVersion) + } + + static { + defaultInstance = new ServerVersion(true); + defaultInstance.initFields(); + } + + // @@protoc_insertion_point(class_scope:paymentchannels.ServerVersion) + } + + public interface InitiateOrBuilder + extends com.google.protobuf.MessageOrBuilder { + + // required bytes multisig_key = 1; + boolean hasMultisigKey(); + com.google.protobuf.ByteString getMultisigKey(); + + // required uint64 min_accepted_channel_size = 2; + boolean hasMinAcceptedChannelSize(); + long getMinAcceptedChannelSize(); + + // required uint64 expire_time_secs = 3; + boolean hasExpireTimeSecs(); + long getExpireTimeSecs(); + } + public static final class Initiate extends + com.google.protobuf.GeneratedMessage + implements InitiateOrBuilder { + // Use Initiate.newBuilder() to construct. + private Initiate(Builder builder) { + super(builder); + } + private Initiate(boolean noInit) {} + + private static final Initiate defaultInstance; + public static Initiate getDefaultInstance() { + return defaultInstance; + } + + public Initiate getDefaultInstanceForType() { + return defaultInstance; + } + + public static final com.google.protobuf.Descriptors.Descriptor + getDescriptor() { + return org.bitcoin.paymentchannel.Protos.internal_static_paymentchannels_Initiate_descriptor; + } + + protected com.google.protobuf.GeneratedMessage.FieldAccessorTable + internalGetFieldAccessorTable() { + return org.bitcoin.paymentchannel.Protos.internal_static_paymentchannels_Initiate_fieldAccessorTable; + } + + private int bitField0_; + // required bytes multisig_key = 1; + public static final int MULTISIG_KEY_FIELD_NUMBER = 1; + private com.google.protobuf.ByteString multisigKey_; + public boolean hasMultisigKey() { + return ((bitField0_ & 0x00000001) == 0x00000001); + } + public com.google.protobuf.ByteString getMultisigKey() { + return multisigKey_; + } + + // required uint64 min_accepted_channel_size = 2; + public static final int MIN_ACCEPTED_CHANNEL_SIZE_FIELD_NUMBER = 2; + private long minAcceptedChannelSize_; + public boolean hasMinAcceptedChannelSize() { + return ((bitField0_ & 0x00000002) == 0x00000002); + } + public long getMinAcceptedChannelSize() { + return minAcceptedChannelSize_; + } + + // required uint64 expire_time_secs = 3; + public static final int EXPIRE_TIME_SECS_FIELD_NUMBER = 3; + private long expireTimeSecs_; + public boolean hasExpireTimeSecs() { + return ((bitField0_ & 0x00000004) == 0x00000004); + } + public long getExpireTimeSecs() { + return expireTimeSecs_; + } + + private void initFields() { + multisigKey_ = com.google.protobuf.ByteString.EMPTY; + minAcceptedChannelSize_ = 0L; + expireTimeSecs_ = 0L; + } + private byte memoizedIsInitialized = -1; + public final boolean isInitialized() { + byte isInitialized = memoizedIsInitialized; + if (isInitialized != -1) return isInitialized == 1; + + if (!hasMultisigKey()) { + memoizedIsInitialized = 0; + return false; + } + if (!hasMinAcceptedChannelSize()) { + memoizedIsInitialized = 0; + return false; + } + if (!hasExpireTimeSecs()) { + memoizedIsInitialized = 0; + return false; + } + memoizedIsInitialized = 1; + return true; + } + + public void writeTo(com.google.protobuf.CodedOutputStream output) + throws java.io.IOException { + getSerializedSize(); + if (((bitField0_ & 0x00000001) == 0x00000001)) { + output.writeBytes(1, multisigKey_); + } + if (((bitField0_ & 0x00000002) == 0x00000002)) { + output.writeUInt64(2, minAcceptedChannelSize_); + } + if (((bitField0_ & 0x00000004) == 0x00000004)) { + output.writeUInt64(3, expireTimeSecs_); + } + getUnknownFields().writeTo(output); + } + + private int memoizedSerializedSize = -1; + public int getSerializedSize() { + int size = memoizedSerializedSize; + if (size != -1) return size; + + size = 0; + if (((bitField0_ & 0x00000001) == 0x00000001)) { + size += com.google.protobuf.CodedOutputStream + .computeBytesSize(1, multisigKey_); + } + if (((bitField0_ & 0x00000002) == 0x00000002)) { + size += com.google.protobuf.CodedOutputStream + .computeUInt64Size(2, minAcceptedChannelSize_); + } + if (((bitField0_ & 0x00000004) == 0x00000004)) { + size += com.google.protobuf.CodedOutputStream + .computeUInt64Size(3, expireTimeSecs_); + } + size += getUnknownFields().getSerializedSize(); + memoizedSerializedSize = size; + return size; + } + + private static final long serialVersionUID = 0L; + @java.lang.Override + protected java.lang.Object writeReplace() + throws java.io.ObjectStreamException { + return super.writeReplace(); + } + + public static org.bitcoin.paymentchannel.Protos.Initiate parseFrom( + com.google.protobuf.ByteString data) + throws com.google.protobuf.InvalidProtocolBufferException { + return newBuilder().mergeFrom(data).buildParsed(); + } + public static org.bitcoin.paymentchannel.Protos.Initiate parseFrom( + com.google.protobuf.ByteString data, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws com.google.protobuf.InvalidProtocolBufferException { + return newBuilder().mergeFrom(data, extensionRegistry) + .buildParsed(); + } + public static org.bitcoin.paymentchannel.Protos.Initiate parseFrom(byte[] data) + throws com.google.protobuf.InvalidProtocolBufferException { + return newBuilder().mergeFrom(data).buildParsed(); + } + public static org.bitcoin.paymentchannel.Protos.Initiate parseFrom( + byte[] data, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws com.google.protobuf.InvalidProtocolBufferException { + return newBuilder().mergeFrom(data, extensionRegistry) + .buildParsed(); + } + public static org.bitcoin.paymentchannel.Protos.Initiate parseFrom(java.io.InputStream input) + throws java.io.IOException { + return newBuilder().mergeFrom(input).buildParsed(); + } + public static org.bitcoin.paymentchannel.Protos.Initiate parseFrom( + java.io.InputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + return newBuilder().mergeFrom(input, extensionRegistry) + .buildParsed(); + } + public static org.bitcoin.paymentchannel.Protos.Initiate parseDelimitedFrom(java.io.InputStream input) + throws java.io.IOException { + Builder builder = newBuilder(); + if (builder.mergeDelimitedFrom(input)) { + return builder.buildParsed(); + } else { + return null; + } + } + public static org.bitcoin.paymentchannel.Protos.Initiate parseDelimitedFrom( + java.io.InputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + Builder builder = newBuilder(); + if (builder.mergeDelimitedFrom(input, extensionRegistry)) { + return builder.buildParsed(); + } else { + return null; + } + } + public static org.bitcoin.paymentchannel.Protos.Initiate parseFrom( + com.google.protobuf.CodedInputStream input) + throws java.io.IOException { + return newBuilder().mergeFrom(input).buildParsed(); + } + public static org.bitcoin.paymentchannel.Protos.Initiate parseFrom( + com.google.protobuf.CodedInputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + return newBuilder().mergeFrom(input, extensionRegistry) + .buildParsed(); + } + + public static Builder newBuilder() { return Builder.create(); } + public Builder newBuilderForType() { return newBuilder(); } + public static Builder newBuilder(org.bitcoin.paymentchannel.Protos.Initiate prototype) { + return newBuilder().mergeFrom(prototype); + } + public Builder toBuilder() { return newBuilder(this); } + + @java.lang.Override + protected Builder newBuilderForType( + com.google.protobuf.GeneratedMessage.BuilderParent parent) { + Builder builder = new Builder(parent); + return builder; + } + public static final class Builder extends + com.google.protobuf.GeneratedMessage.Builder + implements org.bitcoin.paymentchannel.Protos.InitiateOrBuilder { + public static final com.google.protobuf.Descriptors.Descriptor + getDescriptor() { + return org.bitcoin.paymentchannel.Protos.internal_static_paymentchannels_Initiate_descriptor; + } + + protected com.google.protobuf.GeneratedMessage.FieldAccessorTable + internalGetFieldAccessorTable() { + return org.bitcoin.paymentchannel.Protos.internal_static_paymentchannels_Initiate_fieldAccessorTable; + } + + // Construct using org.bitcoin.paymentchannel.Protos.Initiate.newBuilder() + private Builder() { + maybeForceBuilderInitialization(); + } + + private Builder(BuilderParent parent) { + super(parent); + maybeForceBuilderInitialization(); + } + private void maybeForceBuilderInitialization() { + if (com.google.protobuf.GeneratedMessage.alwaysUseFieldBuilders) { + } + } + private static Builder create() { + return new Builder(); + } + + public Builder clear() { + super.clear(); + multisigKey_ = com.google.protobuf.ByteString.EMPTY; + bitField0_ = (bitField0_ & ~0x00000001); + minAcceptedChannelSize_ = 0L; + bitField0_ = (bitField0_ & ~0x00000002); + expireTimeSecs_ = 0L; + bitField0_ = (bitField0_ & ~0x00000004); + return this; + } + + public Builder clone() { + return create().mergeFrom(buildPartial()); + } + + public com.google.protobuf.Descriptors.Descriptor + getDescriptorForType() { + return org.bitcoin.paymentchannel.Protos.Initiate.getDescriptor(); + } + + public org.bitcoin.paymentchannel.Protos.Initiate getDefaultInstanceForType() { + return org.bitcoin.paymentchannel.Protos.Initiate.getDefaultInstance(); + } + + public org.bitcoin.paymentchannel.Protos.Initiate build() { + org.bitcoin.paymentchannel.Protos.Initiate result = buildPartial(); + if (!result.isInitialized()) { + throw newUninitializedMessageException(result); + } + return result; + } + + private org.bitcoin.paymentchannel.Protos.Initiate buildParsed() + throws com.google.protobuf.InvalidProtocolBufferException { + org.bitcoin.paymentchannel.Protos.Initiate result = buildPartial(); + if (!result.isInitialized()) { + throw newUninitializedMessageException( + result).asInvalidProtocolBufferException(); + } + return result; + } + + public org.bitcoin.paymentchannel.Protos.Initiate buildPartial() { + org.bitcoin.paymentchannel.Protos.Initiate result = new org.bitcoin.paymentchannel.Protos.Initiate(this); + int from_bitField0_ = bitField0_; + int to_bitField0_ = 0; + if (((from_bitField0_ & 0x00000001) == 0x00000001)) { + to_bitField0_ |= 0x00000001; + } + result.multisigKey_ = multisigKey_; + if (((from_bitField0_ & 0x00000002) == 0x00000002)) { + to_bitField0_ |= 0x00000002; + } + result.minAcceptedChannelSize_ = minAcceptedChannelSize_; + if (((from_bitField0_ & 0x00000004) == 0x00000004)) { + to_bitField0_ |= 0x00000004; + } + result.expireTimeSecs_ = expireTimeSecs_; + result.bitField0_ = to_bitField0_; + onBuilt(); + return result; + } + + public Builder mergeFrom(com.google.protobuf.Message other) { + if (other instanceof org.bitcoin.paymentchannel.Protos.Initiate) { + return mergeFrom((org.bitcoin.paymentchannel.Protos.Initiate)other); + } else { + super.mergeFrom(other); + return this; + } + } + + public Builder mergeFrom(org.bitcoin.paymentchannel.Protos.Initiate other) { + if (other == org.bitcoin.paymentchannel.Protos.Initiate.getDefaultInstance()) return this; + if (other.hasMultisigKey()) { + setMultisigKey(other.getMultisigKey()); + } + if (other.hasMinAcceptedChannelSize()) { + setMinAcceptedChannelSize(other.getMinAcceptedChannelSize()); + } + if (other.hasExpireTimeSecs()) { + setExpireTimeSecs(other.getExpireTimeSecs()); + } + this.mergeUnknownFields(other.getUnknownFields()); + return this; + } + + public final boolean isInitialized() { + if (!hasMultisigKey()) { + + return false; + } + if (!hasMinAcceptedChannelSize()) { + + return false; + } + if (!hasExpireTimeSecs()) { + + return false; + } + return true; + } + + public Builder mergeFrom( + com.google.protobuf.CodedInputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + com.google.protobuf.UnknownFieldSet.Builder unknownFields = + com.google.protobuf.UnknownFieldSet.newBuilder( + this.getUnknownFields()); + while (true) { + int tag = input.readTag(); + switch (tag) { + case 0: + this.setUnknownFields(unknownFields.build()); + onChanged(); + return this; + default: { + if (!parseUnknownField(input, unknownFields, + extensionRegistry, tag)) { + this.setUnknownFields(unknownFields.build()); + onChanged(); + return this; + } + break; + } + case 10: { + bitField0_ |= 0x00000001; + multisigKey_ = input.readBytes(); + break; + } + case 16: { + bitField0_ |= 0x00000002; + minAcceptedChannelSize_ = input.readUInt64(); + break; + } + case 24: { + bitField0_ |= 0x00000004; + expireTimeSecs_ = input.readUInt64(); + break; + } + } + } + } + + private int bitField0_; + + // required bytes multisig_key = 1; + private com.google.protobuf.ByteString multisigKey_ = com.google.protobuf.ByteString.EMPTY; + public boolean hasMultisigKey() { + return ((bitField0_ & 0x00000001) == 0x00000001); + } + public com.google.protobuf.ByteString getMultisigKey() { + return multisigKey_; + } + public Builder setMultisigKey(com.google.protobuf.ByteString value) { + if (value == null) { + throw new NullPointerException(); + } + bitField0_ |= 0x00000001; + multisigKey_ = value; + onChanged(); + return this; + } + public Builder clearMultisigKey() { + bitField0_ = (bitField0_ & ~0x00000001); + multisigKey_ = getDefaultInstance().getMultisigKey(); + onChanged(); + return this; + } + + // required uint64 min_accepted_channel_size = 2; + private long minAcceptedChannelSize_ ; + public boolean hasMinAcceptedChannelSize() { + return ((bitField0_ & 0x00000002) == 0x00000002); + } + public long getMinAcceptedChannelSize() { + return minAcceptedChannelSize_; + } + public Builder setMinAcceptedChannelSize(long value) { + bitField0_ |= 0x00000002; + minAcceptedChannelSize_ = value; + onChanged(); + return this; + } + public Builder clearMinAcceptedChannelSize() { + bitField0_ = (bitField0_ & ~0x00000002); + minAcceptedChannelSize_ = 0L; + onChanged(); + return this; + } + + // required uint64 expire_time_secs = 3; + private long expireTimeSecs_ ; + public boolean hasExpireTimeSecs() { + return ((bitField0_ & 0x00000004) == 0x00000004); + } + public long getExpireTimeSecs() { + return expireTimeSecs_; + } + public Builder setExpireTimeSecs(long value) { + bitField0_ |= 0x00000004; + expireTimeSecs_ = value; + onChanged(); + return this; + } + public Builder clearExpireTimeSecs() { + bitField0_ = (bitField0_ & ~0x00000004); + expireTimeSecs_ = 0L; + onChanged(); + return this; + } + + // @@protoc_insertion_point(builder_scope:paymentchannels.Initiate) + } + + static { + defaultInstance = new Initiate(true); + defaultInstance.initFields(); + } + + // @@protoc_insertion_point(class_scope:paymentchannels.Initiate) + } + + public interface ProvideRefundOrBuilder + extends com.google.protobuf.MessageOrBuilder { + + // required bytes multisig_key = 1; + boolean hasMultisigKey(); + com.google.protobuf.ByteString getMultisigKey(); + + // required bytes tx = 2; + boolean hasTx(); + com.google.protobuf.ByteString getTx(); + } + public static final class ProvideRefund extends + com.google.protobuf.GeneratedMessage + implements ProvideRefundOrBuilder { + // Use ProvideRefund.newBuilder() to construct. + private ProvideRefund(Builder builder) { + super(builder); + } + private ProvideRefund(boolean noInit) {} + + private static final ProvideRefund defaultInstance; + public static ProvideRefund getDefaultInstance() { + return defaultInstance; + } + + public ProvideRefund getDefaultInstanceForType() { + return defaultInstance; + } + + public static final com.google.protobuf.Descriptors.Descriptor + getDescriptor() { + return org.bitcoin.paymentchannel.Protos.internal_static_paymentchannels_ProvideRefund_descriptor; + } + + protected com.google.protobuf.GeneratedMessage.FieldAccessorTable + internalGetFieldAccessorTable() { + return org.bitcoin.paymentchannel.Protos.internal_static_paymentchannels_ProvideRefund_fieldAccessorTable; + } + + private int bitField0_; + // required bytes multisig_key = 1; + public static final int MULTISIG_KEY_FIELD_NUMBER = 1; + private com.google.protobuf.ByteString multisigKey_; + public boolean hasMultisigKey() { + return ((bitField0_ & 0x00000001) == 0x00000001); + } + public com.google.protobuf.ByteString getMultisigKey() { + return multisigKey_; + } + + // required bytes tx = 2; + public static final int TX_FIELD_NUMBER = 2; + private com.google.protobuf.ByteString tx_; + public boolean hasTx() { + return ((bitField0_ & 0x00000002) == 0x00000002); + } + public com.google.protobuf.ByteString getTx() { + return tx_; + } + + private void initFields() { + multisigKey_ = com.google.protobuf.ByteString.EMPTY; + tx_ = com.google.protobuf.ByteString.EMPTY; + } + private byte memoizedIsInitialized = -1; + public final boolean isInitialized() { + byte isInitialized = memoizedIsInitialized; + if (isInitialized != -1) return isInitialized == 1; + + if (!hasMultisigKey()) { + memoizedIsInitialized = 0; + return false; + } + if (!hasTx()) { + memoizedIsInitialized = 0; + return false; + } + memoizedIsInitialized = 1; + return true; + } + + public void writeTo(com.google.protobuf.CodedOutputStream output) + throws java.io.IOException { + getSerializedSize(); + if (((bitField0_ & 0x00000001) == 0x00000001)) { + output.writeBytes(1, multisigKey_); + } + if (((bitField0_ & 0x00000002) == 0x00000002)) { + output.writeBytes(2, tx_); + } + getUnknownFields().writeTo(output); + } + + private int memoizedSerializedSize = -1; + public int getSerializedSize() { + int size = memoizedSerializedSize; + if (size != -1) return size; + + size = 0; + if (((bitField0_ & 0x00000001) == 0x00000001)) { + size += com.google.protobuf.CodedOutputStream + .computeBytesSize(1, multisigKey_); + } + if (((bitField0_ & 0x00000002) == 0x00000002)) { + size += com.google.protobuf.CodedOutputStream + .computeBytesSize(2, tx_); + } + size += getUnknownFields().getSerializedSize(); + memoizedSerializedSize = size; + return size; + } + + private static final long serialVersionUID = 0L; + @java.lang.Override + protected java.lang.Object writeReplace() + throws java.io.ObjectStreamException { + return super.writeReplace(); + } + + public static org.bitcoin.paymentchannel.Protos.ProvideRefund parseFrom( + com.google.protobuf.ByteString data) + throws com.google.protobuf.InvalidProtocolBufferException { + return newBuilder().mergeFrom(data).buildParsed(); + } + public static org.bitcoin.paymentchannel.Protos.ProvideRefund parseFrom( + com.google.protobuf.ByteString data, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws com.google.protobuf.InvalidProtocolBufferException { + return newBuilder().mergeFrom(data, extensionRegistry) + .buildParsed(); + } + public static org.bitcoin.paymentchannel.Protos.ProvideRefund parseFrom(byte[] data) + throws com.google.protobuf.InvalidProtocolBufferException { + return newBuilder().mergeFrom(data).buildParsed(); + } + public static org.bitcoin.paymentchannel.Protos.ProvideRefund parseFrom( + byte[] data, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws com.google.protobuf.InvalidProtocolBufferException { + return newBuilder().mergeFrom(data, extensionRegistry) + .buildParsed(); + } + public static org.bitcoin.paymentchannel.Protos.ProvideRefund parseFrom(java.io.InputStream input) + throws java.io.IOException { + return newBuilder().mergeFrom(input).buildParsed(); + } + public static org.bitcoin.paymentchannel.Protos.ProvideRefund parseFrom( + java.io.InputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + return newBuilder().mergeFrom(input, extensionRegistry) + .buildParsed(); + } + public static org.bitcoin.paymentchannel.Protos.ProvideRefund parseDelimitedFrom(java.io.InputStream input) + throws java.io.IOException { + Builder builder = newBuilder(); + if (builder.mergeDelimitedFrom(input)) { + return builder.buildParsed(); + } else { + return null; + } + } + public static org.bitcoin.paymentchannel.Protos.ProvideRefund parseDelimitedFrom( + java.io.InputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + Builder builder = newBuilder(); + if (builder.mergeDelimitedFrom(input, extensionRegistry)) { + return builder.buildParsed(); + } else { + return null; + } + } + public static org.bitcoin.paymentchannel.Protos.ProvideRefund parseFrom( + com.google.protobuf.CodedInputStream input) + throws java.io.IOException { + return newBuilder().mergeFrom(input).buildParsed(); + } + public static org.bitcoin.paymentchannel.Protos.ProvideRefund parseFrom( + com.google.protobuf.CodedInputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + return newBuilder().mergeFrom(input, extensionRegistry) + .buildParsed(); + } + + public static Builder newBuilder() { return Builder.create(); } + public Builder newBuilderForType() { return newBuilder(); } + public static Builder newBuilder(org.bitcoin.paymentchannel.Protos.ProvideRefund prototype) { + return newBuilder().mergeFrom(prototype); + } + public Builder toBuilder() { return newBuilder(this); } + + @java.lang.Override + protected Builder newBuilderForType( + com.google.protobuf.GeneratedMessage.BuilderParent parent) { + Builder builder = new Builder(parent); + return builder; + } + public static final class Builder extends + com.google.protobuf.GeneratedMessage.Builder + implements org.bitcoin.paymentchannel.Protos.ProvideRefundOrBuilder { + public static final com.google.protobuf.Descriptors.Descriptor + getDescriptor() { + return org.bitcoin.paymentchannel.Protos.internal_static_paymentchannels_ProvideRefund_descriptor; + } + + protected com.google.protobuf.GeneratedMessage.FieldAccessorTable + internalGetFieldAccessorTable() { + return org.bitcoin.paymentchannel.Protos.internal_static_paymentchannels_ProvideRefund_fieldAccessorTable; + } + + // Construct using org.bitcoin.paymentchannel.Protos.ProvideRefund.newBuilder() + private Builder() { + maybeForceBuilderInitialization(); + } + + private Builder(BuilderParent parent) { + super(parent); + maybeForceBuilderInitialization(); + } + private void maybeForceBuilderInitialization() { + if (com.google.protobuf.GeneratedMessage.alwaysUseFieldBuilders) { + } + } + private static Builder create() { + return new Builder(); + } + + public Builder clear() { + super.clear(); + multisigKey_ = com.google.protobuf.ByteString.EMPTY; + bitField0_ = (bitField0_ & ~0x00000001); + tx_ = com.google.protobuf.ByteString.EMPTY; + bitField0_ = (bitField0_ & ~0x00000002); + return this; + } + + public Builder clone() { + return create().mergeFrom(buildPartial()); + } + + public com.google.protobuf.Descriptors.Descriptor + getDescriptorForType() { + return org.bitcoin.paymentchannel.Protos.ProvideRefund.getDescriptor(); + } + + public org.bitcoin.paymentchannel.Protos.ProvideRefund getDefaultInstanceForType() { + return org.bitcoin.paymentchannel.Protos.ProvideRefund.getDefaultInstance(); + } + + public org.bitcoin.paymentchannel.Protos.ProvideRefund build() { + org.bitcoin.paymentchannel.Protos.ProvideRefund result = buildPartial(); + if (!result.isInitialized()) { + throw newUninitializedMessageException(result); + } + return result; + } + + private org.bitcoin.paymentchannel.Protos.ProvideRefund buildParsed() + throws com.google.protobuf.InvalidProtocolBufferException { + org.bitcoin.paymentchannel.Protos.ProvideRefund result = buildPartial(); + if (!result.isInitialized()) { + throw newUninitializedMessageException( + result).asInvalidProtocolBufferException(); + } + return result; + } + + public org.bitcoin.paymentchannel.Protos.ProvideRefund buildPartial() { + org.bitcoin.paymentchannel.Protos.ProvideRefund result = new org.bitcoin.paymentchannel.Protos.ProvideRefund(this); + int from_bitField0_ = bitField0_; + int to_bitField0_ = 0; + if (((from_bitField0_ & 0x00000001) == 0x00000001)) { + to_bitField0_ |= 0x00000001; + } + result.multisigKey_ = multisigKey_; + if (((from_bitField0_ & 0x00000002) == 0x00000002)) { + to_bitField0_ |= 0x00000002; + } + result.tx_ = tx_; + result.bitField0_ = to_bitField0_; + onBuilt(); + return result; + } + + public Builder mergeFrom(com.google.protobuf.Message other) { + if (other instanceof org.bitcoin.paymentchannel.Protos.ProvideRefund) { + return mergeFrom((org.bitcoin.paymentchannel.Protos.ProvideRefund)other); + } else { + super.mergeFrom(other); + return this; + } + } + + public Builder mergeFrom(org.bitcoin.paymentchannel.Protos.ProvideRefund other) { + if (other == org.bitcoin.paymentchannel.Protos.ProvideRefund.getDefaultInstance()) return this; + if (other.hasMultisigKey()) { + setMultisigKey(other.getMultisigKey()); + } + if (other.hasTx()) { + setTx(other.getTx()); + } + this.mergeUnknownFields(other.getUnknownFields()); + return this; + } + + public final boolean isInitialized() { + if (!hasMultisigKey()) { + + return false; + } + if (!hasTx()) { + + return false; + } + return true; + } + + public Builder mergeFrom( + com.google.protobuf.CodedInputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + com.google.protobuf.UnknownFieldSet.Builder unknownFields = + com.google.protobuf.UnknownFieldSet.newBuilder( + this.getUnknownFields()); + while (true) { + int tag = input.readTag(); + switch (tag) { + case 0: + this.setUnknownFields(unknownFields.build()); + onChanged(); + return this; + default: { + if (!parseUnknownField(input, unknownFields, + extensionRegistry, tag)) { + this.setUnknownFields(unknownFields.build()); + onChanged(); + return this; + } + break; + } + case 10: { + bitField0_ |= 0x00000001; + multisigKey_ = input.readBytes(); + break; + } + case 18: { + bitField0_ |= 0x00000002; + tx_ = input.readBytes(); + break; + } + } + } + } + + private int bitField0_; + + // required bytes multisig_key = 1; + private com.google.protobuf.ByteString multisigKey_ = com.google.protobuf.ByteString.EMPTY; + public boolean hasMultisigKey() { + return ((bitField0_ & 0x00000001) == 0x00000001); + } + public com.google.protobuf.ByteString getMultisigKey() { + return multisigKey_; + } + public Builder setMultisigKey(com.google.protobuf.ByteString value) { + if (value == null) { + throw new NullPointerException(); + } + bitField0_ |= 0x00000001; + multisigKey_ = value; + onChanged(); + return this; + } + public Builder clearMultisigKey() { + bitField0_ = (bitField0_ & ~0x00000001); + multisigKey_ = getDefaultInstance().getMultisigKey(); + onChanged(); + return this; + } + + // required bytes tx = 2; + private com.google.protobuf.ByteString tx_ = com.google.protobuf.ByteString.EMPTY; + public boolean hasTx() { + return ((bitField0_ & 0x00000002) == 0x00000002); + } + public com.google.protobuf.ByteString getTx() { + return tx_; + } + public Builder setTx(com.google.protobuf.ByteString value) { + if (value == null) { + throw new NullPointerException(); + } + bitField0_ |= 0x00000002; + tx_ = value; + onChanged(); + return this; + } + public Builder clearTx() { + bitField0_ = (bitField0_ & ~0x00000002); + tx_ = getDefaultInstance().getTx(); + onChanged(); + return this; + } + + // @@protoc_insertion_point(builder_scope:paymentchannels.ProvideRefund) + } + + static { + defaultInstance = new ProvideRefund(true); + defaultInstance.initFields(); + } + + // @@protoc_insertion_point(class_scope:paymentchannels.ProvideRefund) + } + + public interface ReturnRefundOrBuilder + extends com.google.protobuf.MessageOrBuilder { + + // required bytes signature = 1; + boolean hasSignature(); + com.google.protobuf.ByteString getSignature(); + } + public static final class ReturnRefund extends + com.google.protobuf.GeneratedMessage + implements ReturnRefundOrBuilder { + // Use ReturnRefund.newBuilder() to construct. + private ReturnRefund(Builder builder) { + super(builder); + } + private ReturnRefund(boolean noInit) {} + + private static final ReturnRefund defaultInstance; + public static ReturnRefund getDefaultInstance() { + return defaultInstance; + } + + public ReturnRefund getDefaultInstanceForType() { + return defaultInstance; + } + + public static final com.google.protobuf.Descriptors.Descriptor + getDescriptor() { + return org.bitcoin.paymentchannel.Protos.internal_static_paymentchannels_ReturnRefund_descriptor; + } + + protected com.google.protobuf.GeneratedMessage.FieldAccessorTable + internalGetFieldAccessorTable() { + return org.bitcoin.paymentchannel.Protos.internal_static_paymentchannels_ReturnRefund_fieldAccessorTable; + } + + private int bitField0_; + // required bytes signature = 1; + public static final int SIGNATURE_FIELD_NUMBER = 1; + private com.google.protobuf.ByteString signature_; + public boolean hasSignature() { + return ((bitField0_ & 0x00000001) == 0x00000001); + } + public com.google.protobuf.ByteString getSignature() { + return signature_; + } + + private void initFields() { + signature_ = com.google.protobuf.ByteString.EMPTY; + } + private byte memoizedIsInitialized = -1; + public final boolean isInitialized() { + byte isInitialized = memoizedIsInitialized; + if (isInitialized != -1) return isInitialized == 1; + + if (!hasSignature()) { + memoizedIsInitialized = 0; + return false; + } + memoizedIsInitialized = 1; + return true; + } + + public void writeTo(com.google.protobuf.CodedOutputStream output) + throws java.io.IOException { + getSerializedSize(); + if (((bitField0_ & 0x00000001) == 0x00000001)) { + output.writeBytes(1, signature_); + } + getUnknownFields().writeTo(output); + } + + private int memoizedSerializedSize = -1; + public int getSerializedSize() { + int size = memoizedSerializedSize; + if (size != -1) return size; + + size = 0; + if (((bitField0_ & 0x00000001) == 0x00000001)) { + size += com.google.protobuf.CodedOutputStream + .computeBytesSize(1, signature_); + } + size += getUnknownFields().getSerializedSize(); + memoizedSerializedSize = size; + return size; + } + + private static final long serialVersionUID = 0L; + @java.lang.Override + protected java.lang.Object writeReplace() + throws java.io.ObjectStreamException { + return super.writeReplace(); + } + + public static org.bitcoin.paymentchannel.Protos.ReturnRefund parseFrom( + com.google.protobuf.ByteString data) + throws com.google.protobuf.InvalidProtocolBufferException { + return newBuilder().mergeFrom(data).buildParsed(); + } + public static org.bitcoin.paymentchannel.Protos.ReturnRefund parseFrom( + com.google.protobuf.ByteString data, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws com.google.protobuf.InvalidProtocolBufferException { + return newBuilder().mergeFrom(data, extensionRegistry) + .buildParsed(); + } + public static org.bitcoin.paymentchannel.Protos.ReturnRefund parseFrom(byte[] data) + throws com.google.protobuf.InvalidProtocolBufferException { + return newBuilder().mergeFrom(data).buildParsed(); + } + public static org.bitcoin.paymentchannel.Protos.ReturnRefund parseFrom( + byte[] data, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws com.google.protobuf.InvalidProtocolBufferException { + return newBuilder().mergeFrom(data, extensionRegistry) + .buildParsed(); + } + public static org.bitcoin.paymentchannel.Protos.ReturnRefund parseFrom(java.io.InputStream input) + throws java.io.IOException { + return newBuilder().mergeFrom(input).buildParsed(); + } + public static org.bitcoin.paymentchannel.Protos.ReturnRefund parseFrom( + java.io.InputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + return newBuilder().mergeFrom(input, extensionRegistry) + .buildParsed(); + } + public static org.bitcoin.paymentchannel.Protos.ReturnRefund parseDelimitedFrom(java.io.InputStream input) + throws java.io.IOException { + Builder builder = newBuilder(); + if (builder.mergeDelimitedFrom(input)) { + return builder.buildParsed(); + } else { + return null; + } + } + public static org.bitcoin.paymentchannel.Protos.ReturnRefund parseDelimitedFrom( + java.io.InputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + Builder builder = newBuilder(); + if (builder.mergeDelimitedFrom(input, extensionRegistry)) { + return builder.buildParsed(); + } else { + return null; + } + } + public static org.bitcoin.paymentchannel.Protos.ReturnRefund parseFrom( + com.google.protobuf.CodedInputStream input) + throws java.io.IOException { + return newBuilder().mergeFrom(input).buildParsed(); + } + public static org.bitcoin.paymentchannel.Protos.ReturnRefund parseFrom( + com.google.protobuf.CodedInputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + return newBuilder().mergeFrom(input, extensionRegistry) + .buildParsed(); + } + + public static Builder newBuilder() { return Builder.create(); } + public Builder newBuilderForType() { return newBuilder(); } + public static Builder newBuilder(org.bitcoin.paymentchannel.Protos.ReturnRefund prototype) { + return newBuilder().mergeFrom(prototype); + } + public Builder toBuilder() { return newBuilder(this); } + + @java.lang.Override + protected Builder newBuilderForType( + com.google.protobuf.GeneratedMessage.BuilderParent parent) { + Builder builder = new Builder(parent); + return builder; + } + public static final class Builder extends + com.google.protobuf.GeneratedMessage.Builder + implements org.bitcoin.paymentchannel.Protos.ReturnRefundOrBuilder { + public static final com.google.protobuf.Descriptors.Descriptor + getDescriptor() { + return org.bitcoin.paymentchannel.Protos.internal_static_paymentchannels_ReturnRefund_descriptor; + } + + protected com.google.protobuf.GeneratedMessage.FieldAccessorTable + internalGetFieldAccessorTable() { + return org.bitcoin.paymentchannel.Protos.internal_static_paymentchannels_ReturnRefund_fieldAccessorTable; + } + + // Construct using org.bitcoin.paymentchannel.Protos.ReturnRefund.newBuilder() + private Builder() { + maybeForceBuilderInitialization(); + } + + private Builder(BuilderParent parent) { + super(parent); + maybeForceBuilderInitialization(); + } + private void maybeForceBuilderInitialization() { + if (com.google.protobuf.GeneratedMessage.alwaysUseFieldBuilders) { + } + } + private static Builder create() { + return new Builder(); + } + + public Builder clear() { + super.clear(); + signature_ = com.google.protobuf.ByteString.EMPTY; + bitField0_ = (bitField0_ & ~0x00000001); + return this; + } + + public Builder clone() { + return create().mergeFrom(buildPartial()); + } + + public com.google.protobuf.Descriptors.Descriptor + getDescriptorForType() { + return org.bitcoin.paymentchannel.Protos.ReturnRefund.getDescriptor(); + } + + public org.bitcoin.paymentchannel.Protos.ReturnRefund getDefaultInstanceForType() { + return org.bitcoin.paymentchannel.Protos.ReturnRefund.getDefaultInstance(); + } + + public org.bitcoin.paymentchannel.Protos.ReturnRefund build() { + org.bitcoin.paymentchannel.Protos.ReturnRefund result = buildPartial(); + if (!result.isInitialized()) { + throw newUninitializedMessageException(result); + } + return result; + } + + private org.bitcoin.paymentchannel.Protos.ReturnRefund buildParsed() + throws com.google.protobuf.InvalidProtocolBufferException { + org.bitcoin.paymentchannel.Protos.ReturnRefund result = buildPartial(); + if (!result.isInitialized()) { + throw newUninitializedMessageException( + result).asInvalidProtocolBufferException(); + } + return result; + } + + public org.bitcoin.paymentchannel.Protos.ReturnRefund buildPartial() { + org.bitcoin.paymentchannel.Protos.ReturnRefund result = new org.bitcoin.paymentchannel.Protos.ReturnRefund(this); + int from_bitField0_ = bitField0_; + int to_bitField0_ = 0; + if (((from_bitField0_ & 0x00000001) == 0x00000001)) { + to_bitField0_ |= 0x00000001; + } + result.signature_ = signature_; + result.bitField0_ = to_bitField0_; + onBuilt(); + return result; + } + + public Builder mergeFrom(com.google.protobuf.Message other) { + if (other instanceof org.bitcoin.paymentchannel.Protos.ReturnRefund) { + return mergeFrom((org.bitcoin.paymentchannel.Protos.ReturnRefund)other); + } else { + super.mergeFrom(other); + return this; + } + } + + public Builder mergeFrom(org.bitcoin.paymentchannel.Protos.ReturnRefund other) { + if (other == org.bitcoin.paymentchannel.Protos.ReturnRefund.getDefaultInstance()) return this; + if (other.hasSignature()) { + setSignature(other.getSignature()); + } + this.mergeUnknownFields(other.getUnknownFields()); + return this; + } + + public final boolean isInitialized() { + if (!hasSignature()) { + + return false; + } + return true; + } + + public Builder mergeFrom( + com.google.protobuf.CodedInputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + com.google.protobuf.UnknownFieldSet.Builder unknownFields = + com.google.protobuf.UnknownFieldSet.newBuilder( + this.getUnknownFields()); + while (true) { + int tag = input.readTag(); + switch (tag) { + case 0: + this.setUnknownFields(unknownFields.build()); + onChanged(); + return this; + default: { + if (!parseUnknownField(input, unknownFields, + extensionRegistry, tag)) { + this.setUnknownFields(unknownFields.build()); + onChanged(); + return this; + } + break; + } + case 10: { + bitField0_ |= 0x00000001; + signature_ = input.readBytes(); + break; + } + } + } + } + + private int bitField0_; + + // required bytes signature = 1; + private com.google.protobuf.ByteString signature_ = com.google.protobuf.ByteString.EMPTY; + public boolean hasSignature() { + return ((bitField0_ & 0x00000001) == 0x00000001); + } + public com.google.protobuf.ByteString getSignature() { + return signature_; + } + public Builder setSignature(com.google.protobuf.ByteString value) { + if (value == null) { + throw new NullPointerException(); + } + bitField0_ |= 0x00000001; + signature_ = value; + onChanged(); + return this; + } + public Builder clearSignature() { + bitField0_ = (bitField0_ & ~0x00000001); + signature_ = getDefaultInstance().getSignature(); + onChanged(); + return this; + } + + // @@protoc_insertion_point(builder_scope:paymentchannels.ReturnRefund) + } + + static { + defaultInstance = new ReturnRefund(true); + defaultInstance.initFields(); + } + + // @@protoc_insertion_point(class_scope:paymentchannels.ReturnRefund) + } + + public interface ProvideContractOrBuilder + extends com.google.protobuf.MessageOrBuilder { + + // required bytes tx = 1; + boolean hasTx(); + com.google.protobuf.ByteString getTx(); + } + public static final class ProvideContract extends + com.google.protobuf.GeneratedMessage + implements ProvideContractOrBuilder { + // Use ProvideContract.newBuilder() to construct. + private ProvideContract(Builder builder) { + super(builder); + } + private ProvideContract(boolean noInit) {} + + private static final ProvideContract defaultInstance; + public static ProvideContract getDefaultInstance() { + return defaultInstance; + } + + public ProvideContract getDefaultInstanceForType() { + return defaultInstance; + } + + public static final com.google.protobuf.Descriptors.Descriptor + getDescriptor() { + return org.bitcoin.paymentchannel.Protos.internal_static_paymentchannels_ProvideContract_descriptor; + } + + protected com.google.protobuf.GeneratedMessage.FieldAccessorTable + internalGetFieldAccessorTable() { + return org.bitcoin.paymentchannel.Protos.internal_static_paymentchannels_ProvideContract_fieldAccessorTable; + } + + private int bitField0_; + // required bytes tx = 1; + public static final int TX_FIELD_NUMBER = 1; + private com.google.protobuf.ByteString tx_; + public boolean hasTx() { + return ((bitField0_ & 0x00000001) == 0x00000001); + } + public com.google.protobuf.ByteString getTx() { + return tx_; + } + + private void initFields() { + tx_ = com.google.protobuf.ByteString.EMPTY; + } + private byte memoizedIsInitialized = -1; + public final boolean isInitialized() { + byte isInitialized = memoizedIsInitialized; + if (isInitialized != -1) return isInitialized == 1; + + if (!hasTx()) { + memoizedIsInitialized = 0; + return false; + } + memoizedIsInitialized = 1; + return true; + } + + public void writeTo(com.google.protobuf.CodedOutputStream output) + throws java.io.IOException { + getSerializedSize(); + if (((bitField0_ & 0x00000001) == 0x00000001)) { + output.writeBytes(1, tx_); + } + getUnknownFields().writeTo(output); + } + + private int memoizedSerializedSize = -1; + public int getSerializedSize() { + int size = memoizedSerializedSize; + if (size != -1) return size; + + size = 0; + if (((bitField0_ & 0x00000001) == 0x00000001)) { + size += com.google.protobuf.CodedOutputStream + .computeBytesSize(1, tx_); + } + size += getUnknownFields().getSerializedSize(); + memoizedSerializedSize = size; + return size; + } + + private static final long serialVersionUID = 0L; + @java.lang.Override + protected java.lang.Object writeReplace() + throws java.io.ObjectStreamException { + return super.writeReplace(); + } + + public static org.bitcoin.paymentchannel.Protos.ProvideContract parseFrom( + com.google.protobuf.ByteString data) + throws com.google.protobuf.InvalidProtocolBufferException { + return newBuilder().mergeFrom(data).buildParsed(); + } + public static org.bitcoin.paymentchannel.Protos.ProvideContract parseFrom( + com.google.protobuf.ByteString data, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws com.google.protobuf.InvalidProtocolBufferException { + return newBuilder().mergeFrom(data, extensionRegistry) + .buildParsed(); + } + public static org.bitcoin.paymentchannel.Protos.ProvideContract parseFrom(byte[] data) + throws com.google.protobuf.InvalidProtocolBufferException { + return newBuilder().mergeFrom(data).buildParsed(); + } + public static org.bitcoin.paymentchannel.Protos.ProvideContract parseFrom( + byte[] data, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws com.google.protobuf.InvalidProtocolBufferException { + return newBuilder().mergeFrom(data, extensionRegistry) + .buildParsed(); + } + public static org.bitcoin.paymentchannel.Protos.ProvideContract parseFrom(java.io.InputStream input) + throws java.io.IOException { + return newBuilder().mergeFrom(input).buildParsed(); + } + public static org.bitcoin.paymentchannel.Protos.ProvideContract parseFrom( + java.io.InputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + return newBuilder().mergeFrom(input, extensionRegistry) + .buildParsed(); + } + public static org.bitcoin.paymentchannel.Protos.ProvideContract parseDelimitedFrom(java.io.InputStream input) + throws java.io.IOException { + Builder builder = newBuilder(); + if (builder.mergeDelimitedFrom(input)) { + return builder.buildParsed(); + } else { + return null; + } + } + public static org.bitcoin.paymentchannel.Protos.ProvideContract parseDelimitedFrom( + java.io.InputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + Builder builder = newBuilder(); + if (builder.mergeDelimitedFrom(input, extensionRegistry)) { + return builder.buildParsed(); + } else { + return null; + } + } + public static org.bitcoin.paymentchannel.Protos.ProvideContract parseFrom( + com.google.protobuf.CodedInputStream input) + throws java.io.IOException { + return newBuilder().mergeFrom(input).buildParsed(); + } + public static org.bitcoin.paymentchannel.Protos.ProvideContract parseFrom( + com.google.protobuf.CodedInputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + return newBuilder().mergeFrom(input, extensionRegistry) + .buildParsed(); + } + + public static Builder newBuilder() { return Builder.create(); } + public Builder newBuilderForType() { return newBuilder(); } + public static Builder newBuilder(org.bitcoin.paymentchannel.Protos.ProvideContract prototype) { + return newBuilder().mergeFrom(prototype); + } + public Builder toBuilder() { return newBuilder(this); } + + @java.lang.Override + protected Builder newBuilderForType( + com.google.protobuf.GeneratedMessage.BuilderParent parent) { + Builder builder = new Builder(parent); + return builder; + } + public static final class Builder extends + com.google.protobuf.GeneratedMessage.Builder + implements org.bitcoin.paymentchannel.Protos.ProvideContractOrBuilder { + public static final com.google.protobuf.Descriptors.Descriptor + getDescriptor() { + return org.bitcoin.paymentchannel.Protos.internal_static_paymentchannels_ProvideContract_descriptor; + } + + protected com.google.protobuf.GeneratedMessage.FieldAccessorTable + internalGetFieldAccessorTable() { + return org.bitcoin.paymentchannel.Protos.internal_static_paymentchannels_ProvideContract_fieldAccessorTable; + } + + // Construct using org.bitcoin.paymentchannel.Protos.ProvideContract.newBuilder() + private Builder() { + maybeForceBuilderInitialization(); + } + + private Builder(BuilderParent parent) { + super(parent); + maybeForceBuilderInitialization(); + } + private void maybeForceBuilderInitialization() { + if (com.google.protobuf.GeneratedMessage.alwaysUseFieldBuilders) { + } + } + private static Builder create() { + return new Builder(); + } + + public Builder clear() { + super.clear(); + tx_ = com.google.protobuf.ByteString.EMPTY; + bitField0_ = (bitField0_ & ~0x00000001); + return this; + } + + public Builder clone() { + return create().mergeFrom(buildPartial()); + } + + public com.google.protobuf.Descriptors.Descriptor + getDescriptorForType() { + return org.bitcoin.paymentchannel.Protos.ProvideContract.getDescriptor(); + } + + public org.bitcoin.paymentchannel.Protos.ProvideContract getDefaultInstanceForType() { + return org.bitcoin.paymentchannel.Protos.ProvideContract.getDefaultInstance(); + } + + public org.bitcoin.paymentchannel.Protos.ProvideContract build() { + org.bitcoin.paymentchannel.Protos.ProvideContract result = buildPartial(); + if (!result.isInitialized()) { + throw newUninitializedMessageException(result); + } + return result; + } + + private org.bitcoin.paymentchannel.Protos.ProvideContract buildParsed() + throws com.google.protobuf.InvalidProtocolBufferException { + org.bitcoin.paymentchannel.Protos.ProvideContract result = buildPartial(); + if (!result.isInitialized()) { + throw newUninitializedMessageException( + result).asInvalidProtocolBufferException(); + } + return result; + } + + public org.bitcoin.paymentchannel.Protos.ProvideContract buildPartial() { + org.bitcoin.paymentchannel.Protos.ProvideContract result = new org.bitcoin.paymentchannel.Protos.ProvideContract(this); + int from_bitField0_ = bitField0_; + int to_bitField0_ = 0; + if (((from_bitField0_ & 0x00000001) == 0x00000001)) { + to_bitField0_ |= 0x00000001; + } + result.tx_ = tx_; + result.bitField0_ = to_bitField0_; + onBuilt(); + return result; + } + + public Builder mergeFrom(com.google.protobuf.Message other) { + if (other instanceof org.bitcoin.paymentchannel.Protos.ProvideContract) { + return mergeFrom((org.bitcoin.paymentchannel.Protos.ProvideContract)other); + } else { + super.mergeFrom(other); + return this; + } + } + + public Builder mergeFrom(org.bitcoin.paymentchannel.Protos.ProvideContract other) { + if (other == org.bitcoin.paymentchannel.Protos.ProvideContract.getDefaultInstance()) return this; + if (other.hasTx()) { + setTx(other.getTx()); + } + this.mergeUnknownFields(other.getUnknownFields()); + return this; + } + + public final boolean isInitialized() { + if (!hasTx()) { + + return false; + } + return true; + } + + public Builder mergeFrom( + com.google.protobuf.CodedInputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + com.google.protobuf.UnknownFieldSet.Builder unknownFields = + com.google.protobuf.UnknownFieldSet.newBuilder( + this.getUnknownFields()); + while (true) { + int tag = input.readTag(); + switch (tag) { + case 0: + this.setUnknownFields(unknownFields.build()); + onChanged(); + return this; + default: { + if (!parseUnknownField(input, unknownFields, + extensionRegistry, tag)) { + this.setUnknownFields(unknownFields.build()); + onChanged(); + return this; + } + break; + } + case 10: { + bitField0_ |= 0x00000001; + tx_ = input.readBytes(); + break; + } + } + } + } + + private int bitField0_; + + // required bytes tx = 1; + private com.google.protobuf.ByteString tx_ = com.google.protobuf.ByteString.EMPTY; + public boolean hasTx() { + return ((bitField0_ & 0x00000001) == 0x00000001); + } + public com.google.protobuf.ByteString getTx() { + return tx_; + } + public Builder setTx(com.google.protobuf.ByteString value) { + if (value == null) { + throw new NullPointerException(); + } + bitField0_ |= 0x00000001; + tx_ = value; + onChanged(); + return this; + } + public Builder clearTx() { + bitField0_ = (bitField0_ & ~0x00000001); + tx_ = getDefaultInstance().getTx(); + onChanged(); + return this; + } + + // @@protoc_insertion_point(builder_scope:paymentchannels.ProvideContract) + } + + static { + defaultInstance = new ProvideContract(true); + defaultInstance.initFields(); + } + + // @@protoc_insertion_point(class_scope:paymentchannels.ProvideContract) + } + + public interface UpdatePaymentOrBuilder + extends com.google.protobuf.MessageOrBuilder { + + // required uint64 client_change_value = 1; + boolean hasClientChangeValue(); + long getClientChangeValue(); + + // required bytes signature = 2; + boolean hasSignature(); + com.google.protobuf.ByteString getSignature(); + } + public static final class UpdatePayment extends + com.google.protobuf.GeneratedMessage + implements UpdatePaymentOrBuilder { + // Use UpdatePayment.newBuilder() to construct. + private UpdatePayment(Builder builder) { + super(builder); + } + private UpdatePayment(boolean noInit) {} + + private static final UpdatePayment defaultInstance; + public static UpdatePayment getDefaultInstance() { + return defaultInstance; + } + + public UpdatePayment getDefaultInstanceForType() { + return defaultInstance; + } + + public static final com.google.protobuf.Descriptors.Descriptor + getDescriptor() { + return org.bitcoin.paymentchannel.Protos.internal_static_paymentchannels_UpdatePayment_descriptor; + } + + protected com.google.protobuf.GeneratedMessage.FieldAccessorTable + internalGetFieldAccessorTable() { + return org.bitcoin.paymentchannel.Protos.internal_static_paymentchannels_UpdatePayment_fieldAccessorTable; + } + + private int bitField0_; + // required uint64 client_change_value = 1; + public static final int CLIENT_CHANGE_VALUE_FIELD_NUMBER = 1; + private long clientChangeValue_; + public boolean hasClientChangeValue() { + return ((bitField0_ & 0x00000001) == 0x00000001); + } + public long getClientChangeValue() { + return clientChangeValue_; + } + + // required bytes signature = 2; + public static final int SIGNATURE_FIELD_NUMBER = 2; + private com.google.protobuf.ByteString signature_; + public boolean hasSignature() { + return ((bitField0_ & 0x00000002) == 0x00000002); + } + public com.google.protobuf.ByteString getSignature() { + return signature_; + } + + private void initFields() { + clientChangeValue_ = 0L; + signature_ = com.google.protobuf.ByteString.EMPTY; + } + private byte memoizedIsInitialized = -1; + public final boolean isInitialized() { + byte isInitialized = memoizedIsInitialized; + if (isInitialized != -1) return isInitialized == 1; + + if (!hasClientChangeValue()) { + memoizedIsInitialized = 0; + return false; + } + if (!hasSignature()) { + memoizedIsInitialized = 0; + return false; + } + memoizedIsInitialized = 1; + return true; + } + + public void writeTo(com.google.protobuf.CodedOutputStream output) + throws java.io.IOException { + getSerializedSize(); + if (((bitField0_ & 0x00000001) == 0x00000001)) { + output.writeUInt64(1, clientChangeValue_); + } + if (((bitField0_ & 0x00000002) == 0x00000002)) { + output.writeBytes(2, signature_); + } + getUnknownFields().writeTo(output); + } + + private int memoizedSerializedSize = -1; + public int getSerializedSize() { + int size = memoizedSerializedSize; + if (size != -1) return size; + + size = 0; + if (((bitField0_ & 0x00000001) == 0x00000001)) { + size += com.google.protobuf.CodedOutputStream + .computeUInt64Size(1, clientChangeValue_); + } + if (((bitField0_ & 0x00000002) == 0x00000002)) { + size += com.google.protobuf.CodedOutputStream + .computeBytesSize(2, signature_); + } + size += getUnknownFields().getSerializedSize(); + memoizedSerializedSize = size; + return size; + } + + private static final long serialVersionUID = 0L; + @java.lang.Override + protected java.lang.Object writeReplace() + throws java.io.ObjectStreamException { + return super.writeReplace(); + } + + public static org.bitcoin.paymentchannel.Protos.UpdatePayment parseFrom( + com.google.protobuf.ByteString data) + throws com.google.protobuf.InvalidProtocolBufferException { + return newBuilder().mergeFrom(data).buildParsed(); + } + public static org.bitcoin.paymentchannel.Protos.UpdatePayment parseFrom( + com.google.protobuf.ByteString data, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws com.google.protobuf.InvalidProtocolBufferException { + return newBuilder().mergeFrom(data, extensionRegistry) + .buildParsed(); + } + public static org.bitcoin.paymentchannel.Protos.UpdatePayment parseFrom(byte[] data) + throws com.google.protobuf.InvalidProtocolBufferException { + return newBuilder().mergeFrom(data).buildParsed(); + } + public static org.bitcoin.paymentchannel.Protos.UpdatePayment parseFrom( + byte[] data, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws com.google.protobuf.InvalidProtocolBufferException { + return newBuilder().mergeFrom(data, extensionRegistry) + .buildParsed(); + } + public static org.bitcoin.paymentchannel.Protos.UpdatePayment parseFrom(java.io.InputStream input) + throws java.io.IOException { + return newBuilder().mergeFrom(input).buildParsed(); + } + public static org.bitcoin.paymentchannel.Protos.UpdatePayment parseFrom( + java.io.InputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + return newBuilder().mergeFrom(input, extensionRegistry) + .buildParsed(); + } + public static org.bitcoin.paymentchannel.Protos.UpdatePayment parseDelimitedFrom(java.io.InputStream input) + throws java.io.IOException { + Builder builder = newBuilder(); + if (builder.mergeDelimitedFrom(input)) { + return builder.buildParsed(); + } else { + return null; + } + } + public static org.bitcoin.paymentchannel.Protos.UpdatePayment parseDelimitedFrom( + java.io.InputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + Builder builder = newBuilder(); + if (builder.mergeDelimitedFrom(input, extensionRegistry)) { + return builder.buildParsed(); + } else { + return null; + } + } + public static org.bitcoin.paymentchannel.Protos.UpdatePayment parseFrom( + com.google.protobuf.CodedInputStream input) + throws java.io.IOException { + return newBuilder().mergeFrom(input).buildParsed(); + } + public static org.bitcoin.paymentchannel.Protos.UpdatePayment parseFrom( + com.google.protobuf.CodedInputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + return newBuilder().mergeFrom(input, extensionRegistry) + .buildParsed(); + } + + public static Builder newBuilder() { return Builder.create(); } + public Builder newBuilderForType() { return newBuilder(); } + public static Builder newBuilder(org.bitcoin.paymentchannel.Protos.UpdatePayment prototype) { + return newBuilder().mergeFrom(prototype); + } + public Builder toBuilder() { return newBuilder(this); } + + @java.lang.Override + protected Builder newBuilderForType( + com.google.protobuf.GeneratedMessage.BuilderParent parent) { + Builder builder = new Builder(parent); + return builder; + } + public static final class Builder extends + com.google.protobuf.GeneratedMessage.Builder + implements org.bitcoin.paymentchannel.Protos.UpdatePaymentOrBuilder { + public static final com.google.protobuf.Descriptors.Descriptor + getDescriptor() { + return org.bitcoin.paymentchannel.Protos.internal_static_paymentchannels_UpdatePayment_descriptor; + } + + protected com.google.protobuf.GeneratedMessage.FieldAccessorTable + internalGetFieldAccessorTable() { + return org.bitcoin.paymentchannel.Protos.internal_static_paymentchannels_UpdatePayment_fieldAccessorTable; + } + + // Construct using org.bitcoin.paymentchannel.Protos.UpdatePayment.newBuilder() + private Builder() { + maybeForceBuilderInitialization(); + } + + private Builder(BuilderParent parent) { + super(parent); + maybeForceBuilderInitialization(); + } + private void maybeForceBuilderInitialization() { + if (com.google.protobuf.GeneratedMessage.alwaysUseFieldBuilders) { + } + } + private static Builder create() { + return new Builder(); + } + + public Builder clear() { + super.clear(); + clientChangeValue_ = 0L; + bitField0_ = (bitField0_ & ~0x00000001); + signature_ = com.google.protobuf.ByteString.EMPTY; + bitField0_ = (bitField0_ & ~0x00000002); + return this; + } + + public Builder clone() { + return create().mergeFrom(buildPartial()); + } + + public com.google.protobuf.Descriptors.Descriptor + getDescriptorForType() { + return org.bitcoin.paymentchannel.Protos.UpdatePayment.getDescriptor(); + } + + public org.bitcoin.paymentchannel.Protos.UpdatePayment getDefaultInstanceForType() { + return org.bitcoin.paymentchannel.Protos.UpdatePayment.getDefaultInstance(); + } + + public org.bitcoin.paymentchannel.Protos.UpdatePayment build() { + org.bitcoin.paymentchannel.Protos.UpdatePayment result = buildPartial(); + if (!result.isInitialized()) { + throw newUninitializedMessageException(result); + } + return result; + } + + private org.bitcoin.paymentchannel.Protos.UpdatePayment buildParsed() + throws com.google.protobuf.InvalidProtocolBufferException { + org.bitcoin.paymentchannel.Protos.UpdatePayment result = buildPartial(); + if (!result.isInitialized()) { + throw newUninitializedMessageException( + result).asInvalidProtocolBufferException(); + } + return result; + } + + public org.bitcoin.paymentchannel.Protos.UpdatePayment buildPartial() { + org.bitcoin.paymentchannel.Protos.UpdatePayment result = new org.bitcoin.paymentchannel.Protos.UpdatePayment(this); + int from_bitField0_ = bitField0_; + int to_bitField0_ = 0; + if (((from_bitField0_ & 0x00000001) == 0x00000001)) { + to_bitField0_ |= 0x00000001; + } + result.clientChangeValue_ = clientChangeValue_; + if (((from_bitField0_ & 0x00000002) == 0x00000002)) { + to_bitField0_ |= 0x00000002; + } + result.signature_ = signature_; + result.bitField0_ = to_bitField0_; + onBuilt(); + return result; + } + + public Builder mergeFrom(com.google.protobuf.Message other) { + if (other instanceof org.bitcoin.paymentchannel.Protos.UpdatePayment) { + return mergeFrom((org.bitcoin.paymentchannel.Protos.UpdatePayment)other); + } else { + super.mergeFrom(other); + return this; + } + } + + public Builder mergeFrom(org.bitcoin.paymentchannel.Protos.UpdatePayment other) { + if (other == org.bitcoin.paymentchannel.Protos.UpdatePayment.getDefaultInstance()) return this; + if (other.hasClientChangeValue()) { + setClientChangeValue(other.getClientChangeValue()); + } + if (other.hasSignature()) { + setSignature(other.getSignature()); + } + this.mergeUnknownFields(other.getUnknownFields()); + return this; + } + + public final boolean isInitialized() { + if (!hasClientChangeValue()) { + + return false; + } + if (!hasSignature()) { + + return false; + } + return true; + } + + public Builder mergeFrom( + com.google.protobuf.CodedInputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + com.google.protobuf.UnknownFieldSet.Builder unknownFields = + com.google.protobuf.UnknownFieldSet.newBuilder( + this.getUnknownFields()); + while (true) { + int tag = input.readTag(); + switch (tag) { + case 0: + this.setUnknownFields(unknownFields.build()); + onChanged(); + return this; + default: { + if (!parseUnknownField(input, unknownFields, + extensionRegistry, tag)) { + this.setUnknownFields(unknownFields.build()); + onChanged(); + return this; + } + break; + } + case 8: { + bitField0_ |= 0x00000001; + clientChangeValue_ = input.readUInt64(); + break; + } + case 18: { + bitField0_ |= 0x00000002; + signature_ = input.readBytes(); + break; + } + } + } + } + + private int bitField0_; + + // required uint64 client_change_value = 1; + private long clientChangeValue_ ; + public boolean hasClientChangeValue() { + return ((bitField0_ & 0x00000001) == 0x00000001); + } + public long getClientChangeValue() { + return clientChangeValue_; + } + public Builder setClientChangeValue(long value) { + bitField0_ |= 0x00000001; + clientChangeValue_ = value; + onChanged(); + return this; + } + public Builder clearClientChangeValue() { + bitField0_ = (bitField0_ & ~0x00000001); + clientChangeValue_ = 0L; + onChanged(); + return this; + } + + // required bytes signature = 2; + private com.google.protobuf.ByteString signature_ = com.google.protobuf.ByteString.EMPTY; + public boolean hasSignature() { + return ((bitField0_ & 0x00000002) == 0x00000002); + } + public com.google.protobuf.ByteString getSignature() { + return signature_; + } + public Builder setSignature(com.google.protobuf.ByteString value) { + if (value == null) { + throw new NullPointerException(); + } + bitField0_ |= 0x00000002; + signature_ = value; + onChanged(); + return this; + } + public Builder clearSignature() { + bitField0_ = (bitField0_ & ~0x00000002); + signature_ = getDefaultInstance().getSignature(); + onChanged(); + return this; + } + + // @@protoc_insertion_point(builder_scope:paymentchannels.UpdatePayment) + } + + static { + defaultInstance = new UpdatePayment(true); + defaultInstance.initFields(); + } + + // @@protoc_insertion_point(class_scope:paymentchannels.UpdatePayment) + } + + public interface ErrorOrBuilder + extends com.google.protobuf.MessageOrBuilder { + + // optional .paymentchannels.Error.ErrorCode code = 1 [default = OTHER]; + boolean hasCode(); + org.bitcoin.paymentchannel.Protos.Error.ErrorCode getCode(); + + // optional string explanation = 2; + boolean hasExplanation(); + String getExplanation(); + } + public static final class Error extends + com.google.protobuf.GeneratedMessage + implements ErrorOrBuilder { + // Use Error.newBuilder() to construct. + private Error(Builder builder) { + super(builder); + } + private Error(boolean noInit) {} + + private static final Error defaultInstance; + public static Error getDefaultInstance() { + return defaultInstance; + } + + public Error getDefaultInstanceForType() { + return defaultInstance; + } + + public static final com.google.protobuf.Descriptors.Descriptor + getDescriptor() { + return org.bitcoin.paymentchannel.Protos.internal_static_paymentchannels_Error_descriptor; + } + + protected com.google.protobuf.GeneratedMessage.FieldAccessorTable + internalGetFieldAccessorTable() { + return org.bitcoin.paymentchannel.Protos.internal_static_paymentchannels_Error_fieldAccessorTable; + } + + public enum ErrorCode + implements com.google.protobuf.ProtocolMessageEnum { + TIMEOUT(0, 1), + SYNTAX_ERROR(1, 2), + NO_ACCEPTABLE_VERSION(2, 3), + BAD_TRANSACTION(3, 4), + TIME_WINDOW_TOO_LARGE(4, 5), + CHANNEL_VALUE_TOO_LARGE(5, 6), + OTHER(6, 7), + ; + + public static final int TIMEOUT_VALUE = 1; + public static final int SYNTAX_ERROR_VALUE = 2; + public static final int NO_ACCEPTABLE_VERSION_VALUE = 3; + public static final int BAD_TRANSACTION_VALUE = 4; + public static final int TIME_WINDOW_TOO_LARGE_VALUE = 5; + public static final int CHANNEL_VALUE_TOO_LARGE_VALUE = 6; + public static final int OTHER_VALUE = 7; + + + public final int getNumber() { return value; } + + public static ErrorCode valueOf(int value) { + switch (value) { + case 1: return TIMEOUT; + case 2: return SYNTAX_ERROR; + case 3: return NO_ACCEPTABLE_VERSION; + case 4: return BAD_TRANSACTION; + case 5: return TIME_WINDOW_TOO_LARGE; + case 6: return CHANNEL_VALUE_TOO_LARGE; + case 7: return OTHER; + default: return null; + } + } + + public static com.google.protobuf.Internal.EnumLiteMap + internalGetValueMap() { + return internalValueMap; + } + private static com.google.protobuf.Internal.EnumLiteMap + internalValueMap = + new com.google.protobuf.Internal.EnumLiteMap() { + public ErrorCode findValueByNumber(int number) { + return ErrorCode.valueOf(number); + } + }; + + public final com.google.protobuf.Descriptors.EnumValueDescriptor + getValueDescriptor() { + return getDescriptor().getValues().get(index); + } + public final com.google.protobuf.Descriptors.EnumDescriptor + getDescriptorForType() { + return getDescriptor(); + } + public static final com.google.protobuf.Descriptors.EnumDescriptor + getDescriptor() { + return org.bitcoin.paymentchannel.Protos.Error.getDescriptor().getEnumTypes().get(0); + } + + private static final ErrorCode[] VALUES = { + TIMEOUT, SYNTAX_ERROR, NO_ACCEPTABLE_VERSION, BAD_TRANSACTION, TIME_WINDOW_TOO_LARGE, CHANNEL_VALUE_TOO_LARGE, OTHER, + }; + + public static ErrorCode valueOf( + com.google.protobuf.Descriptors.EnumValueDescriptor desc) { + if (desc.getType() != getDescriptor()) { + throw new java.lang.IllegalArgumentException( + "EnumValueDescriptor is not for this type."); + } + return VALUES[desc.getIndex()]; + } + + private final int index; + private final int value; + + private ErrorCode(int index, int value) { + this.index = index; + this.value = value; + } + + // @@protoc_insertion_point(enum_scope:paymentchannels.Error.ErrorCode) + } + + private int bitField0_; + // optional .paymentchannels.Error.ErrorCode code = 1 [default = OTHER]; + public static final int CODE_FIELD_NUMBER = 1; + private org.bitcoin.paymentchannel.Protos.Error.ErrorCode code_; + public boolean hasCode() { + return ((bitField0_ & 0x00000001) == 0x00000001); + } + public org.bitcoin.paymentchannel.Protos.Error.ErrorCode getCode() { + return code_; + } + + // optional string explanation = 2; + public static final int EXPLANATION_FIELD_NUMBER = 2; + private java.lang.Object explanation_; + public boolean hasExplanation() { + return ((bitField0_ & 0x00000002) == 0x00000002); + } + public String getExplanation() { + java.lang.Object ref = explanation_; + if (ref instanceof String) { + return (String) ref; + } else { + com.google.protobuf.ByteString bs = + (com.google.protobuf.ByteString) ref; + String s = bs.toStringUtf8(); + if (com.google.protobuf.Internal.isValidUtf8(bs)) { + explanation_ = s; + } + return s; + } + } + private com.google.protobuf.ByteString getExplanationBytes() { + java.lang.Object ref = explanation_; + if (ref instanceof String) { + com.google.protobuf.ByteString b = + com.google.protobuf.ByteString.copyFromUtf8((String) ref); + explanation_ = b; + return b; + } else { + return (com.google.protobuf.ByteString) ref; + } + } + + private void initFields() { + code_ = org.bitcoin.paymentchannel.Protos.Error.ErrorCode.OTHER; + explanation_ = ""; + } + private byte memoizedIsInitialized = -1; + public final boolean isInitialized() { + byte isInitialized = memoizedIsInitialized; + if (isInitialized != -1) return isInitialized == 1; + + memoizedIsInitialized = 1; + return true; + } + + public void writeTo(com.google.protobuf.CodedOutputStream output) + throws java.io.IOException { + getSerializedSize(); + if (((bitField0_ & 0x00000001) == 0x00000001)) { + output.writeEnum(1, code_.getNumber()); + } + if (((bitField0_ & 0x00000002) == 0x00000002)) { + output.writeBytes(2, getExplanationBytes()); + } + getUnknownFields().writeTo(output); + } + + private int memoizedSerializedSize = -1; + public int getSerializedSize() { + int size = memoizedSerializedSize; + if (size != -1) return size; + + size = 0; + if (((bitField0_ & 0x00000001) == 0x00000001)) { + size += com.google.protobuf.CodedOutputStream + .computeEnumSize(1, code_.getNumber()); + } + if (((bitField0_ & 0x00000002) == 0x00000002)) { + size += com.google.protobuf.CodedOutputStream + .computeBytesSize(2, getExplanationBytes()); + } + size += getUnknownFields().getSerializedSize(); + memoizedSerializedSize = size; + return size; + } + + private static final long serialVersionUID = 0L; + @java.lang.Override + protected java.lang.Object writeReplace() + throws java.io.ObjectStreamException { + return super.writeReplace(); + } + + public static org.bitcoin.paymentchannel.Protos.Error parseFrom( + com.google.protobuf.ByteString data) + throws com.google.protobuf.InvalidProtocolBufferException { + return newBuilder().mergeFrom(data).buildParsed(); + } + public static org.bitcoin.paymentchannel.Protos.Error parseFrom( + com.google.protobuf.ByteString data, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws com.google.protobuf.InvalidProtocolBufferException { + return newBuilder().mergeFrom(data, extensionRegistry) + .buildParsed(); + } + public static org.bitcoin.paymentchannel.Protos.Error parseFrom(byte[] data) + throws com.google.protobuf.InvalidProtocolBufferException { + return newBuilder().mergeFrom(data).buildParsed(); + } + public static org.bitcoin.paymentchannel.Protos.Error parseFrom( + byte[] data, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws com.google.protobuf.InvalidProtocolBufferException { + return newBuilder().mergeFrom(data, extensionRegistry) + .buildParsed(); + } + public static org.bitcoin.paymentchannel.Protos.Error parseFrom(java.io.InputStream input) + throws java.io.IOException { + return newBuilder().mergeFrom(input).buildParsed(); + } + public static org.bitcoin.paymentchannel.Protos.Error parseFrom( + java.io.InputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + return newBuilder().mergeFrom(input, extensionRegistry) + .buildParsed(); + } + public static org.bitcoin.paymentchannel.Protos.Error parseDelimitedFrom(java.io.InputStream input) + throws java.io.IOException { + Builder builder = newBuilder(); + if (builder.mergeDelimitedFrom(input)) { + return builder.buildParsed(); + } else { + return null; + } + } + public static org.bitcoin.paymentchannel.Protos.Error parseDelimitedFrom( + java.io.InputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + Builder builder = newBuilder(); + if (builder.mergeDelimitedFrom(input, extensionRegistry)) { + return builder.buildParsed(); + } else { + return null; + } + } + public static org.bitcoin.paymentchannel.Protos.Error parseFrom( + com.google.protobuf.CodedInputStream input) + throws java.io.IOException { + return newBuilder().mergeFrom(input).buildParsed(); + } + public static org.bitcoin.paymentchannel.Protos.Error parseFrom( + com.google.protobuf.CodedInputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + return newBuilder().mergeFrom(input, extensionRegistry) + .buildParsed(); + } + + public static Builder newBuilder() { return Builder.create(); } + public Builder newBuilderForType() { return newBuilder(); } + public static Builder newBuilder(org.bitcoin.paymentchannel.Protos.Error prototype) { + return newBuilder().mergeFrom(prototype); + } + public Builder toBuilder() { return newBuilder(this); } + + @java.lang.Override + protected Builder newBuilderForType( + com.google.protobuf.GeneratedMessage.BuilderParent parent) { + Builder builder = new Builder(parent); + return builder; + } + public static final class Builder extends + com.google.protobuf.GeneratedMessage.Builder + implements org.bitcoin.paymentchannel.Protos.ErrorOrBuilder { + public static final com.google.protobuf.Descriptors.Descriptor + getDescriptor() { + return org.bitcoin.paymentchannel.Protos.internal_static_paymentchannels_Error_descriptor; + } + + protected com.google.protobuf.GeneratedMessage.FieldAccessorTable + internalGetFieldAccessorTable() { + return org.bitcoin.paymentchannel.Protos.internal_static_paymentchannels_Error_fieldAccessorTable; + } + + // Construct using org.bitcoin.paymentchannel.Protos.Error.newBuilder() + private Builder() { + maybeForceBuilderInitialization(); + } + + private Builder(BuilderParent parent) { + super(parent); + maybeForceBuilderInitialization(); + } + private void maybeForceBuilderInitialization() { + if (com.google.protobuf.GeneratedMessage.alwaysUseFieldBuilders) { + } + } + private static Builder create() { + return new Builder(); + } + + public Builder clear() { + super.clear(); + code_ = org.bitcoin.paymentchannel.Protos.Error.ErrorCode.OTHER; + bitField0_ = (bitField0_ & ~0x00000001); + explanation_ = ""; + bitField0_ = (bitField0_ & ~0x00000002); + return this; + } + + public Builder clone() { + return create().mergeFrom(buildPartial()); + } + + public com.google.protobuf.Descriptors.Descriptor + getDescriptorForType() { + return org.bitcoin.paymentchannel.Protos.Error.getDescriptor(); + } + + public org.bitcoin.paymentchannel.Protos.Error getDefaultInstanceForType() { + return org.bitcoin.paymentchannel.Protos.Error.getDefaultInstance(); + } + + public org.bitcoin.paymentchannel.Protos.Error build() { + org.bitcoin.paymentchannel.Protos.Error result = buildPartial(); + if (!result.isInitialized()) { + throw newUninitializedMessageException(result); + } + return result; + } + + private org.bitcoin.paymentchannel.Protos.Error buildParsed() + throws com.google.protobuf.InvalidProtocolBufferException { + org.bitcoin.paymentchannel.Protos.Error result = buildPartial(); + if (!result.isInitialized()) { + throw newUninitializedMessageException( + result).asInvalidProtocolBufferException(); + } + return result; + } + + public org.bitcoin.paymentchannel.Protos.Error buildPartial() { + org.bitcoin.paymentchannel.Protos.Error result = new org.bitcoin.paymentchannel.Protos.Error(this); + int from_bitField0_ = bitField0_; + int to_bitField0_ = 0; + if (((from_bitField0_ & 0x00000001) == 0x00000001)) { + to_bitField0_ |= 0x00000001; + } + result.code_ = code_; + if (((from_bitField0_ & 0x00000002) == 0x00000002)) { + to_bitField0_ |= 0x00000002; + } + result.explanation_ = explanation_; + result.bitField0_ = to_bitField0_; + onBuilt(); + return result; + } + + public Builder mergeFrom(com.google.protobuf.Message other) { + if (other instanceof org.bitcoin.paymentchannel.Protos.Error) { + return mergeFrom((org.bitcoin.paymentchannel.Protos.Error)other); + } else { + super.mergeFrom(other); + return this; + } + } + + public Builder mergeFrom(org.bitcoin.paymentchannel.Protos.Error other) { + if (other == org.bitcoin.paymentchannel.Protos.Error.getDefaultInstance()) return this; + if (other.hasCode()) { + setCode(other.getCode()); + } + if (other.hasExplanation()) { + setExplanation(other.getExplanation()); + } + this.mergeUnknownFields(other.getUnknownFields()); + return this; + } + + public final boolean isInitialized() { + return true; + } + + public Builder mergeFrom( + com.google.protobuf.CodedInputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + com.google.protobuf.UnknownFieldSet.Builder unknownFields = + com.google.protobuf.UnknownFieldSet.newBuilder( + this.getUnknownFields()); + while (true) { + int tag = input.readTag(); + switch (tag) { + case 0: + this.setUnknownFields(unknownFields.build()); + onChanged(); + return this; + default: { + if (!parseUnknownField(input, unknownFields, + extensionRegistry, tag)) { + this.setUnknownFields(unknownFields.build()); + onChanged(); + return this; + } + break; + } + case 8: { + int rawValue = input.readEnum(); + org.bitcoin.paymentchannel.Protos.Error.ErrorCode value = org.bitcoin.paymentchannel.Protos.Error.ErrorCode.valueOf(rawValue); + if (value == null) { + unknownFields.mergeVarintField(1, rawValue); + } else { + bitField0_ |= 0x00000001; + code_ = value; + } + break; + } + case 18: { + bitField0_ |= 0x00000002; + explanation_ = input.readBytes(); + break; + } + } + } + } + + private int bitField0_; + + // optional .paymentchannels.Error.ErrorCode code = 1 [default = OTHER]; + private org.bitcoin.paymentchannel.Protos.Error.ErrorCode code_ = org.bitcoin.paymentchannel.Protos.Error.ErrorCode.OTHER; + public boolean hasCode() { + return ((bitField0_ & 0x00000001) == 0x00000001); + } + public org.bitcoin.paymentchannel.Protos.Error.ErrorCode getCode() { + return code_; + } + public Builder setCode(org.bitcoin.paymentchannel.Protos.Error.ErrorCode value) { + if (value == null) { + throw new NullPointerException(); + } + bitField0_ |= 0x00000001; + code_ = value; + onChanged(); + return this; + } + public Builder clearCode() { + bitField0_ = (bitField0_ & ~0x00000001); + code_ = org.bitcoin.paymentchannel.Protos.Error.ErrorCode.OTHER; + onChanged(); + return this; + } + + // optional string explanation = 2; + private java.lang.Object explanation_ = ""; + public boolean hasExplanation() { + return ((bitField0_ & 0x00000002) == 0x00000002); + } + public String getExplanation() { + java.lang.Object ref = explanation_; + if (!(ref instanceof String)) { + String s = ((com.google.protobuf.ByteString) ref).toStringUtf8(); + explanation_ = s; + return s; + } else { + return (String) ref; + } + } + public Builder setExplanation(String value) { + if (value == null) { + throw new NullPointerException(); + } + bitField0_ |= 0x00000002; + explanation_ = value; + onChanged(); + return this; + } + public Builder clearExplanation() { + bitField0_ = (bitField0_ & ~0x00000002); + explanation_ = getDefaultInstance().getExplanation(); + onChanged(); + return this; + } + void setExplanation(com.google.protobuf.ByteString value) { + bitField0_ |= 0x00000002; + explanation_ = value; + onChanged(); + } + + // @@protoc_insertion_point(builder_scope:paymentchannels.Error) + } + + static { + defaultInstance = new Error(true); + defaultInstance.initFields(); + } + + // @@protoc_insertion_point(class_scope:paymentchannels.Error) + } + + private static com.google.protobuf.Descriptors.Descriptor + internal_static_paymentchannels_TwoWayChannelMessage_descriptor; + private static + com.google.protobuf.GeneratedMessage.FieldAccessorTable + internal_static_paymentchannels_TwoWayChannelMessage_fieldAccessorTable; + private static com.google.protobuf.Descriptors.Descriptor + internal_static_paymentchannels_ClientVersion_descriptor; + private static + com.google.protobuf.GeneratedMessage.FieldAccessorTable + internal_static_paymentchannels_ClientVersion_fieldAccessorTable; + private static com.google.protobuf.Descriptors.Descriptor + internal_static_paymentchannels_ServerVersion_descriptor; + private static + com.google.protobuf.GeneratedMessage.FieldAccessorTable + internal_static_paymentchannels_ServerVersion_fieldAccessorTable; + private static com.google.protobuf.Descriptors.Descriptor + internal_static_paymentchannels_Initiate_descriptor; + private static + com.google.protobuf.GeneratedMessage.FieldAccessorTable + internal_static_paymentchannels_Initiate_fieldAccessorTable; + private static com.google.protobuf.Descriptors.Descriptor + internal_static_paymentchannels_ProvideRefund_descriptor; + private static + com.google.protobuf.GeneratedMessage.FieldAccessorTable + internal_static_paymentchannels_ProvideRefund_fieldAccessorTable; + private static com.google.protobuf.Descriptors.Descriptor + internal_static_paymentchannels_ReturnRefund_descriptor; + private static + com.google.protobuf.GeneratedMessage.FieldAccessorTable + internal_static_paymentchannels_ReturnRefund_fieldAccessorTable; + private static com.google.protobuf.Descriptors.Descriptor + internal_static_paymentchannels_ProvideContract_descriptor; + private static + com.google.protobuf.GeneratedMessage.FieldAccessorTable + internal_static_paymentchannels_ProvideContract_fieldAccessorTable; + private static com.google.protobuf.Descriptors.Descriptor + internal_static_paymentchannels_UpdatePayment_descriptor; + private static + com.google.protobuf.GeneratedMessage.FieldAccessorTable + internal_static_paymentchannels_UpdatePayment_fieldAccessorTable; + private static com.google.protobuf.Descriptors.Descriptor + internal_static_paymentchannels_Error_descriptor; + private static + com.google.protobuf.GeneratedMessage.FieldAccessorTable + internal_static_paymentchannels_Error_fieldAccessorTable; + + public static com.google.protobuf.Descriptors.FileDescriptor + getDescriptor() { + return descriptor; + } + private static com.google.protobuf.Descriptors.FileDescriptor + descriptor; + static { + java.lang.String[] descriptorData = { + "\n\024paymentchannel.proto\022\017paymentchannels\"" + + "\274\005\n\024TwoWayChannelMessage\022?\n\004type\030\001 \002(\01621" + + ".paymentchannels.TwoWayChannelMessage.Me" + + "ssageType\0226\n\016client_version\030\002 \001(\0132\036.paym" + + "entchannels.ClientVersion\0226\n\016server_vers" + + "ion\030\003 \001(\0132\036.paymentchannels.ServerVersio" + + "n\022+\n\010initiate\030\004 \001(\0132\031.paymentchannels.In" + + "itiate\0226\n\016provide_refund\030\005 \001(\0132\036.payment" + + "channels.ProvideRefund\0224\n\rreturn_refund\030" + + "\006 \001(\0132\035.paymentchannels.ReturnRefund\022:\n\020", + "provide_contract\030\007 \001(\0132 .paymentchannels" + + ".ProvideContract\0226\n\016update_payment\030\010 \001(\013" + + "2\036.paymentchannels.UpdatePayment\022%\n\005erro" + + "r\030\n \001(\0132\026.paymentchannels.Error\"\274\001\n\013Mess" + + "ageType\022\022\n\016CLIENT_VERSION\020\001\022\022\n\016SERVER_VE" + + "RSION\020\002\022\014\n\010INITIATE\020\003\022\022\n\016PROVIDE_REFUND\020" + + "\004\022\021\n\rRETURN_REFUND\020\005\022\024\n\020PROVIDE_CONTRACT" + + "\020\006\022\020\n\014CHANNEL_OPEN\020\007\022\022\n\016UPDATE_PAYMENT\020\010" + + "\022\t\n\005CLOSE\020\t\022\t\n\005ERROR\020\n\"X\n\rClientVersion\022" + + "\r\n\005major\030\001 \002(\005\022\020\n\005minor\030\002 \001(\005:\0010\022&\n\036prev", + "ious_channel_contract_hash\030\003 \001(\014\"0\n\rServ" + + "erVersion\022\r\n\005major\030\001 \002(\005\022\020\n\005minor\030\002 \001(\005:" + + "\0010\"]\n\010Initiate\022\024\n\014multisig_key\030\001 \002(\014\022!\n\031" + + "min_accepted_channel_size\030\002 \002(\004\022\030\n\020expir" + + "e_time_secs\030\003 \002(\004\"1\n\rProvideRefund\022\024\n\014mu" + + "ltisig_key\030\001 \002(\014\022\n\n\002tx\030\002 \002(\014\"!\n\014ReturnRe" + + "fund\022\021\n\tsignature\030\001 \002(\014\"\035\n\017ProvideContra" + + "ct\022\n\n\002tx\030\001 \002(\014\"?\n\rUpdatePayment\022\033\n\023clien" + + "t_change_value\030\001 \002(\004\022\021\n\tsignature\030\002 \002(\014\"" + + "\363\001\n\005Error\0225\n\004code\030\001 \001(\0162 .paymentchannel", + "s.Error.ErrorCode:\005OTHER\022\023\n\013explanation\030" + + "\002 \001(\t\"\235\001\n\tErrorCode\022\013\n\007TIMEOUT\020\001\022\020\n\014SYNT" + + "AX_ERROR\020\002\022\031\n\025NO_ACCEPTABLE_VERSION\020\003\022\023\n" + + "\017BAD_TRANSACTION\020\004\022\031\n\025TIME_WINDOW_TOO_LA" + + "RGE\020\005\022\033\n\027CHANNEL_VALUE_TOO_LARGE\020\006\022\t\n\005OT" + + "HER\020\007B$\n\032org.bitcoin.paymentchannelB\006Pro" + + "tos" + }; + com.google.protobuf.Descriptors.FileDescriptor.InternalDescriptorAssigner assigner = + new com.google.protobuf.Descriptors.FileDescriptor.InternalDescriptorAssigner() { + public com.google.protobuf.ExtensionRegistry assignDescriptors( + com.google.protobuf.Descriptors.FileDescriptor root) { + descriptor = root; + internal_static_paymentchannels_TwoWayChannelMessage_descriptor = + getDescriptor().getMessageTypes().get(0); + internal_static_paymentchannels_TwoWayChannelMessage_fieldAccessorTable = new + com.google.protobuf.GeneratedMessage.FieldAccessorTable( + internal_static_paymentchannels_TwoWayChannelMessage_descriptor, + new java.lang.String[] { "Type", "ClientVersion", "ServerVersion", "Initiate", "ProvideRefund", "ReturnRefund", "ProvideContract", "UpdatePayment", "Error", }, + org.bitcoin.paymentchannel.Protos.TwoWayChannelMessage.class, + org.bitcoin.paymentchannel.Protos.TwoWayChannelMessage.Builder.class); + internal_static_paymentchannels_ClientVersion_descriptor = + getDescriptor().getMessageTypes().get(1); + internal_static_paymentchannels_ClientVersion_fieldAccessorTable = new + com.google.protobuf.GeneratedMessage.FieldAccessorTable( + internal_static_paymentchannels_ClientVersion_descriptor, + new java.lang.String[] { "Major", "Minor", "PreviousChannelContractHash", }, + org.bitcoin.paymentchannel.Protos.ClientVersion.class, + org.bitcoin.paymentchannel.Protos.ClientVersion.Builder.class); + internal_static_paymentchannels_ServerVersion_descriptor = + getDescriptor().getMessageTypes().get(2); + internal_static_paymentchannels_ServerVersion_fieldAccessorTable = new + com.google.protobuf.GeneratedMessage.FieldAccessorTable( + internal_static_paymentchannels_ServerVersion_descriptor, + new java.lang.String[] { "Major", "Minor", }, + org.bitcoin.paymentchannel.Protos.ServerVersion.class, + org.bitcoin.paymentchannel.Protos.ServerVersion.Builder.class); + internal_static_paymentchannels_Initiate_descriptor = + getDescriptor().getMessageTypes().get(3); + internal_static_paymentchannels_Initiate_fieldAccessorTable = new + com.google.protobuf.GeneratedMessage.FieldAccessorTable( + internal_static_paymentchannels_Initiate_descriptor, + new java.lang.String[] { "MultisigKey", "MinAcceptedChannelSize", "ExpireTimeSecs", }, + org.bitcoin.paymentchannel.Protos.Initiate.class, + org.bitcoin.paymentchannel.Protos.Initiate.Builder.class); + internal_static_paymentchannels_ProvideRefund_descriptor = + getDescriptor().getMessageTypes().get(4); + internal_static_paymentchannels_ProvideRefund_fieldAccessorTable = new + com.google.protobuf.GeneratedMessage.FieldAccessorTable( + internal_static_paymentchannels_ProvideRefund_descriptor, + new java.lang.String[] { "MultisigKey", "Tx", }, + org.bitcoin.paymentchannel.Protos.ProvideRefund.class, + org.bitcoin.paymentchannel.Protos.ProvideRefund.Builder.class); + internal_static_paymentchannels_ReturnRefund_descriptor = + getDescriptor().getMessageTypes().get(5); + internal_static_paymentchannels_ReturnRefund_fieldAccessorTable = new + com.google.protobuf.GeneratedMessage.FieldAccessorTable( + internal_static_paymentchannels_ReturnRefund_descriptor, + new java.lang.String[] { "Signature", }, + org.bitcoin.paymentchannel.Protos.ReturnRefund.class, + org.bitcoin.paymentchannel.Protos.ReturnRefund.Builder.class); + internal_static_paymentchannels_ProvideContract_descriptor = + getDescriptor().getMessageTypes().get(6); + internal_static_paymentchannels_ProvideContract_fieldAccessorTable = new + com.google.protobuf.GeneratedMessage.FieldAccessorTable( + internal_static_paymentchannels_ProvideContract_descriptor, + new java.lang.String[] { "Tx", }, + org.bitcoin.paymentchannel.Protos.ProvideContract.class, + org.bitcoin.paymentchannel.Protos.ProvideContract.Builder.class); + internal_static_paymentchannels_UpdatePayment_descriptor = + getDescriptor().getMessageTypes().get(7); + internal_static_paymentchannels_UpdatePayment_fieldAccessorTable = new + com.google.protobuf.GeneratedMessage.FieldAccessorTable( + internal_static_paymentchannels_UpdatePayment_descriptor, + new java.lang.String[] { "ClientChangeValue", "Signature", }, + org.bitcoin.paymentchannel.Protos.UpdatePayment.class, + org.bitcoin.paymentchannel.Protos.UpdatePayment.Builder.class); + internal_static_paymentchannels_Error_descriptor = + getDescriptor().getMessageTypes().get(8); + internal_static_paymentchannels_Error_fieldAccessorTable = new + com.google.protobuf.GeneratedMessage.FieldAccessorTable( + internal_static_paymentchannels_Error_descriptor, + new java.lang.String[] { "Code", "Explanation", }, + org.bitcoin.paymentchannel.Protos.Error.class, + org.bitcoin.paymentchannel.Protos.Error.Builder.class); + return null; + } + }; + com.google.protobuf.Descriptors.FileDescriptor + .internalBuildGeneratedFileFrom(descriptorData, + new com.google.protobuf.Descriptors.FileDescriptor[] { + }, assigner); + } + + // @@protoc_insertion_point(outer_class_scope) +} diff --git a/core/src/paymentchannel.proto b/core/src/paymentchannel.proto new file mode 100644 index 00000000..4cccd087 --- /dev/null +++ b/core/src/paymentchannel.proto @@ -0,0 +1,211 @@ +/** Copyright 2013 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* + * Authors: Mike Hearn, Matt Corallo + */ + +/* Notes: + * - Endianness: All byte arrays that represent numbers (such as hashes and private keys) are Big Endian + * - To regenerate after editing, run mvn clean package -DupdateProtobuf + */ + +package paymentchannels; + +option java_package = "org.bitcoin.paymentchannel"; +option java_outer_classname = "Protos"; + + +// The connection should be a standard TLS connection and all messages sent over this socket are +// serialized TwoWayChannelMessages prefixed with 2-byte size in big-endian (smaller than or +// equal to 32767 bytes in size) +message TwoWayChannelMessage { + enum MessageType { + CLIENT_VERSION = 1; + SERVER_VERSION = 2; + INITIATE = 3; + PROVIDE_REFUND = 4; + RETURN_REFUND = 5; + PROVIDE_CONTRACT = 6; + // Note that there are no optional fields set for CHANNEL_OPEN, it is sent from the + // secondary to the primary to indicate that the provided contract was received, + // verified, and broadcast successfully and the primary can now provide UPDATE messages + // at will to begin paying secondary. If the channel is interrupted after the + // CHANNEL_OPEN message (ie closed without an explicit CLOSE or ERROR) the primary may + // reopen the channel by setting the contract transaction hash in its CLIENT_VERSION + // message. + CHANNEL_OPEN = 7; + UPDATE_PAYMENT = 8; + // Note that there are no optional fields set for CLOSE, it is sent by either party to + // indicate that the channel is now closed and no further updates can happen. After this, + // the secondary takes the most recent signature it received in an UPDATE_PAYMENT and + // uses it to create a valid transaction, which it then broadcasts on the network. + CLOSE = 9; + + // Used to indicate an error condition. + // Both parties should make an effort to send either an ERROR or a CLOSE immediately + // before closing the socket (unless they just received an ERROR or a CLOSE) + ERROR = 10; + }; + + // This is required so if a new message type is added in future, old software aborts trying + // to read the message as early as possible. If the message doesn't parse, the socket should + // be closed. + required MessageType type = 1; + + // Now one optional field for each message. Only the field specified by type should be read. + optional ClientVersion client_version = 2; + optional ServerVersion server_version = 3; + optional Initiate initiate = 4; + optional ProvideRefund provide_refund = 5; + optional ReturnRefund return_refund = 6; + optional ProvideContract provide_contract = 7; + optional UpdatePayment update_payment = 8; + + optional Error error = 10; +} + +// Sent by primary to secondary on opening the connection. If anything is received before this is +// sent, the socket is closed. +message ClientVersion { + required int32 major = 1; + optional int32 minor = 2 [default = 0]; + + // The hash of the multisig contract of a previous channel. This indicates that the primary + // wishes to reopen the given channel. If the server is willing to reopen it, it simply + // responds with a SERVER_VERSION and then immediately sends a CHANNEL_OPEN, it otherwise + // follows SERVER_VERSION with an Initiate representing a new channel + optional bytes previous_channel_contract_hash = 3; +} + +// Send by secondary to primary upon receiving the ClientVersion message. If it is willing to +// speak the given major version, it sends back the same major version and the minor version it +// speaks. If it is not, it may send back a lower major version representing the highest version +// it is willing to speak, or sends a NO_ACCEPTABLE_VERSION Error. If the secondary sends back a +// lower major version, the secondary should either expect to continue with that version, or +// should immediately close the connection with a NO_ACCEPTABLE_VERSION Error. Backwards +// incompatible changes to the protocol bump the major version. Extensions bump the minor version +message ServerVersion { + required int32 major = 1; + optional int32 minor = 2 [default = 0]; +} + +// Sent from secondary to primary once version nego is done. +message Initiate { + // This must be a raw pubkey in regular ECDSA form. Both compressed and non-compressed forms + // are accepted. It is used only in the creation of the multisig contract, as outputs are + // created entirely by the secondary + required bytes multisig_key = 1; + + // Once a channel is exhausted a new one must be set up. So secondary indicates the minimum + // size it's willing to accept here. This can be lower to trade off resources against + // security but shouldn't be so low the transactions get rejected by the network as spam. + // Zero isn't a sensible value to have here, so we make the field required. + required uint64 min_accepted_channel_size = 2; + + // Rough UNIX time for when the channel expires. This is determined by the block header + // timestamps which can be very inaccurate when miners use the obsolete RollNTime hack. + // Channels could also be specified in terms of block heights but then how do you know the + // current chain height if you don't have internet access? Trust secondary? Probably opens up + // attack vectors. We can assume primary has an independent clock, however. If primary + // considers this value too far off (eg more than a day), it may send an ERROR and close the + // channel. + required uint64 expire_time_secs = 3; +} + +// Sent from primary to secondary after Initiate to begin the refund transaction signing. +message ProvideRefund { + // This must be a raw pubkey in regular ECDSA form. Both compressed and non-compressed forms + // are accepted. It is only used in the creation of the multisig contract. + required bytes multisig_key = 1; + + // The serialized bytes of the return transaction in Satoshi format. + // * It must have exactly one input which spends the multisig output (see ProvideContract for + // details of exactly what that output must look like). This output must have a sequence + // number of 0. + // * It must have the lock time set to a time after the min_time_window_secs (from the + // Initiate message). + // * It must have exactly one output which goes back to the primary. This output's + // scriptPubKey will be reused to create payment transactions. + required bytes tx = 2; +} + +// Sent from secondary to primary after it has done initial verification of the refund +// transaction. Contains the primary's signature which is required to spend the multisig contract +// to the refund transaction. Must be signed using SIGHASH_NONE|SIGHASH_ANYONECANPAY (and include +// the postfix type byte) to allow the client to add any outputs/inputs it wants as long as the +// input's sequence and transaction's nLockTime remain set. +message ReturnRefund { + required bytes signature = 1; +} + +// Sent from the primary to the secondary to complete initialization. +message ProvideContract { + // The serialized bytes of the transaction in Satoshi format. + // * It must be signed and completely valid and ready for broadcast (ie it includes the + // necessary fees) TODO: tell the client how much fee it needs + // * Its first output must be a 2-of-2 multisig output with the first pubkey being the + // primary's and the second being the secondary's (ie the script must be exactly "OP_2 + // ProvideRefund.multisig_key Initiate.multisig_key OP_2 OP_CHECKMULTISIG") + required bytes tx = 1; +} + +// This message can only be used by the primary after it has received a CHANNEL_OPEN message. It +// creates a new payment transaction. Note that we don't resubmit the entire TX, this is to avoid +// (re)parsing bugs and overhead. The payment transaction is created by the primary by: +// * Adding an input which spends the multisig contract +// * Setting this input's scriptSig to the given signature and a new signature created by the +// primary (the primary should ensure the signature provided correctly spends the multisig +// contract) +// * Adding an output who's scriptPubKey is the same as the refund output (the only output) in +// the refund transaction +// * Setting this output's value to client_change_value (which must be lower than the most recent +// client_change_value and lower than the multisig contract's output value) +// * Adding any number of additional outputs as desired (leaving sufficient fee, if necessary) +// * Adding any number of additional inputs as desired (eg to add more fee) +message UpdatePayment { + // The value which is sent back to the primary. The rest of the multisig output is left for + // the secondary to do with as they wish. + required uint64 client_change_value = 1; + // A SIGHASH_SINGLE|SIGHASH_ANYONECANPAY signature (including the postfix type byte) which + // spends the primary's part of the multisig contract's output. This signature only covers + // the primary's refund output and thus the secondary is free to do what they wish with their + // part of the multisig output. + required bytes signature = 2; +} + + +// An Error can be sent by either party at any time +// Both parties should make an effort to send either an ERROR or a CLOSE immediately before +// closing the socket (unless they just received an ERROR or a CLOSE) +message Error { + enum ErrorCode { + TIMEOUT = 1; // Protocol timeout occurred (one party hung). + SYNTAX_ERROR = 2; // Generic error indicating some message was not properly + // formatted or was out of order. + NO_ACCEPTABLE_VERSION = 3; // We don't speak the version the other side asked for. + BAD_TRANSACTION = 4; // A provided transaction was not in the proper structure + // (wrong inputs/outputs, sequence, lock time, signature, + // etc) + TIME_WINDOW_TOO_LARGE = 5; // The expire time specified by the secondary was too large + // for the primary + CHANNEL_VALUE_TOO_LARGE = 6; // The minimum channel value specified by the secondary was + // too large for the primary + + OTHER = 7; + }; + optional ErrorCode code = 1 [default=OTHER]; + optional string explanation = 2; // NOT SAFE FOR HTML WITHOUT ESCAPING +} \ No newline at end of file diff --git a/core/src/test/java/com/google/bitcoin/protocols/channels/ChannelConnectionTest.java b/core/src/test/java/com/google/bitcoin/protocols/channels/ChannelConnectionTest.java new file mode 100644 index 00000000..81d34aa0 --- /dev/null +++ b/core/src/test/java/com/google/bitcoin/protocols/channels/ChannelConnectionTest.java @@ -0,0 +1,1260 @@ +/* + * Copyright 2013 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.bitcoin.protocols.channels; + +import java.io.File; +import java.math.BigInteger; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import javax.annotation.Nullable; + +import com.google.bitcoin.core.*; +import com.google.bitcoin.protocols.niowrapper.ProtobufParser; +import com.google.bitcoin.protocols.niowrapper.ProtobufParserFactory; +import com.google.bitcoin.protocols.niowrapper.ProtobufServer; +import com.google.bitcoin.utils.Locks; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.SettableFuture; +import com.google.protobuf.ByteString; +import org.bitcoin.paymentchannel.Protos; +import org.easymock.Capture; +import org.easymock.IMocksControl; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import static com.google.bitcoin.protocols.channels.PaymentChannelCloseException.CloseReason; +import static org.easymock.EasyMock.*; +import static org.junit.Assert.*; + +public class ChannelConnectionTest extends TestWithWallet { + private Wallet serverWallet; + private AtomicBoolean fail; + + private interface PaymentChannelClientReceiver { + void receiveMessage(Protos.TwoWayChannelMessage msg); + void connectionOpen(); + void connectionClosed(); + void close(); + } + private class PaymentChannelClientReceiverImpl implements PaymentChannelClientReceiver { + private PaymentChannelClient client; + public PaymentChannelClientReceiverImpl(PaymentChannelClient client) { this.client = client; } + public void receiveMessage(Protos.TwoWayChannelMessage msg) { client.receiveMessage(msg); } + public void connectionOpen() { client.connectionOpen(); } + public void connectionClosed() { client.connectionClosed(); } + public void close() { client.close(); } + } + private PaymentChannelClientReceiver sendClient; + + @Before + public void setUp() throws Exception { + super.setUp(); + sendMoneyToWallet(Utils.COIN, AbstractBlockChain.NewBlockType.BEST_CHAIN); + sendMoneyToWallet(Utils.COIN, AbstractBlockChain.NewBlockType.BEST_CHAIN); + wallet.addExtension(new StoredPaymentChannelClientStates(new TransactionBroadcaster() { + @Override + public ListenableFuture broadcastTransaction(Transaction tx) { + fail(); + return null; + } + }, wallet)); + chain = new BlockChain(params, wallet, blockStore); // Recreate chain as sendMoneyToWallet will confuse it + serverWallet = new Wallet(params); + serverWallet.addKey(new ECKey()); + chain.addWallet(serverWallet); + // Use an atomic boolean to indicate failure because fail()/assert*() dont work in network threads + fail = new AtomicBoolean(false); + // Because there are no separate threads in the tests here (we call back into client/server in server/client + // handlers), we have lots of lock cycles. A normal user shouldn't have this issue as they are probably not both + // client+server running in the same thread. + Locks.warnOnLockCycles(); + } + + @After + public void checkFail() { + assertFalse(fail.get()); + Locks.throwOnLockCycles(); + } + + @Test + public void testSimpleChannel() throws Exception { + // Test without any issues + + // Set up a mock peergroup. + IMocksControl control = createStrictControl(); + PeerGroup mockPeerGroup = control.createMock(PeerGroup.class); + // We'll broadcast two txns: multisig contract and close transaction. + SettableFuture multiSigFuture = SettableFuture.create(); + SettableFuture closeFuture = SettableFuture.create(); + final Capture broadcastMultiSig = new Capture(); + Capture broadcastClose = new Capture(); + expect(mockPeerGroup.broadcastTransaction(capture(broadcastMultiSig))).andReturn(multiSigFuture); + expect(mockPeerGroup.broadcastTransaction(capture(broadcastClose))).andReturn(closeFuture); + control.replay(); + + final SettableFuture> serverCloseFuture = SettableFuture.create(); + final SettableFuture channelOpenFuture = SettableFuture.create(); + final SettableFuture twoCentsReceivedFuture = SettableFuture.create(); + final PaymentChannelServerListener server = new PaymentChannelServerListener(mockPeerGroup, serverWallet, 1, Utils.COIN, + new PaymentChannelServerListener.HandlerFactory() { + @Nullable + @Override + public ServerConnectionEventHandler onNewConnection(SocketAddress clientAddress) { + return new ServerConnectionEventHandler() { + @Override + public void channelOpen(Sha256Hash channelId) { + if (!channelId.equals(broadcastMultiSig.getValue().getHash())) + fail.set(true); + channelOpenFuture.set(null); + } + + @Override + public void paymentIncrease(BigInteger by, BigInteger to) { + if (to.equals(Utils.CENT.shiftLeft(1))) + twoCentsReceivedFuture.set(null); + } + + @Override + public void channelClosed(CloseReason reason) { + serverCloseFuture.set(null); + } + }; + } + }); + server.bindAndStart(4243); + + PaymentChannelClientConnection client = new PaymentChannelClientConnection(new InetSocketAddress("localhost", 4243), 1, wallet, myKey, Utils.COIN, ""); + + while (!broadcastMultiSig.hasCaptured()) + Thread.sleep(100); + multiSigFuture.set(broadcastMultiSig.getValue()); + + client.getChannelOpenFuture().get(); + assertTrue(channelOpenFuture.isDone()); + + // Set up an autosave listener to make sure the server is saving the wallet after each payment increase + final AtomicInteger autoSaveCount = new AtomicInteger(0); + File tempFile = File.createTempFile("channel_connection_test", ".wallet"); + tempFile.deleteOnExit(); + serverWallet.autosaveToFile(tempFile, 0, TimeUnit.SECONDS, new Wallet.AutosaveEventListener() { + @Override + public boolean caughtException(Throwable t) { + fail.set(true); + return false; + } + + @Override + public void onBeforeAutoSave(File tempFile) { + autoSaveCount.incrementAndGet(); + } + + @Override public void onAfterAutoSave(File newlySavedFile) { } + }); + assertEquals(0, autoSaveCount.get()); + + Thread.sleep(1250); // No timeouts once the channel is open + client.incrementPayment(Utils.CENT); + while (autoSaveCount.get() != 1) + Thread.sleep(100); + client.incrementPayment(Utils.CENT); + while (autoSaveCount.get() != 2) + Thread.sleep(100); + twoCentsReceivedFuture.get(); + client.incrementPayment(Utils.CENT); + while (autoSaveCount.get() != 3) + Thread.sleep(100); + + StoredPaymentChannelServerStates channels = (StoredPaymentChannelServerStates)serverWallet.getExtensions().get(StoredPaymentChannelServerStates.EXTENSION_ID); + StoredServerChannel storedServerChannel = channels.getChannel(broadcastMultiSig.getValue().getHash()); + PaymentChannelServerState serverState; + synchronized (storedServerChannel) { + serverState = storedServerChannel.getState(serverWallet, mockPeerGroup); + } + + client.close(); + client.close(); + + while (serverState.getState() != PaymentChannelServerState.State.CLOSING) + Thread.sleep(100); + + client.close(); + + closeFuture.set(broadcastClose.getValue()); + + if (!serverState.getBestValueToMe().equals(Utils.CENT.multiply(BigInteger.valueOf(3))) || !serverState.getFeePaid().equals(BigInteger.ZERO)) + fail(); + + assertTrue(channels.mapChannels.isEmpty()); + + control.verify(); + server.close(); + server.close(); + } + + @Test + public void testServerErrorHandling() throws Exception { + // Gives the server crap and checks proper error responses are sent + + // Set up a mock peergroup. + IMocksControl control = createStrictControl(); + PeerGroup mockPeerGroup = control.createMock(PeerGroup.class); + control.replay(); + + final PaymentChannelServer server = new PaymentChannelServer(mockPeerGroup, serverWallet, Utils.COIN, + new PaymentChannelServer.ServerConnection() { + @Override + public void sendToClient(Protos.TwoWayChannelMessage msg) { + sendClient.receiveMessage(msg); + } + + @Override + public void destroyConnection(CloseReason reason) { + if (reason != CloseReason.NO_ACCEPTABLE_VERSION) + fail.set(true); + sendClient.connectionClosed(); + } + + @Override public void channelOpen(Sha256Hash contractHash) { fail.set(true); } + @Override public void paymentIncrease(BigInteger by, BigInteger to) { fail.set(true); } + }); + + // Make sure we get back NO_ACCEPTABLE_VERSION if we send a version message that is very high + final SettableFuture inactiveFuture = SettableFuture.create(); + sendClient = new PaymentChannelClientReceiver() { + @Override + public void receiveMessage(Protos.TwoWayChannelMessage msg) { + if (msg.getType() != Protos.TwoWayChannelMessage.MessageType.ERROR || + !msg.hasError() || msg.getError().getCode() != Protos.Error.ErrorCode.NO_ACCEPTABLE_VERSION) + fail.set(true); + inactiveFuture.set(null); + } + + @Override + public void connectionOpen() { + Protos.ClientVersion.Builder versionNegotiationBuilder = Protos.ClientVersion.newBuilder(); + versionNegotiationBuilder.setMajor(10); + versionNegotiationBuilder.setMinor(42); + server.receiveMessage(Protos.TwoWayChannelMessage.newBuilder() + .setType(Protos.TwoWayChannelMessage.MessageType.CLIENT_VERSION) + .setClientVersion(versionNegotiationBuilder) + .build()); + } + + @Override public void connectionClosed() { } + @Override public void close() { } + }; + server.connectionOpen(); + sendClient.connectionOpen(); + inactiveFuture.get(); + + // Make sure we get back SYNTAX_ERROR if we send messages in the wrong order + final SettableFuture inactiveFuture2 = SettableFuture.create(); + final PaymentChannelServer server2 = new PaymentChannelServer(mockPeerGroup, serverWallet, Utils.COIN, + new PaymentChannelServer.ServerConnection() { + @Override + public void sendToClient(Protos.TwoWayChannelMessage msg) { + sendClient.receiveMessage(msg); + } + + @Override + public void destroyConnection(CloseReason reason) { + if (reason != CloseReason.REMOTE_SENT_INVALID_MESSAGE) + fail.set(true); + sendClient.connectionClosed(); + } + + @Override public void channelOpen(Sha256Hash contractHash) { fail.set(true); } + @Override public void paymentIncrease(BigInteger by, BigInteger to) { fail.set(true); } + }); + sendClient = new PaymentChannelClientReceiver() { + int step = 0; + @Override + public void receiveMessage(Protos.TwoWayChannelMessage msg) { + if ((step != 0 || msg.getType() != Protos.TwoWayChannelMessage.MessageType.SERVER_VERSION) && + (step != 1 || msg.getType() != Protos.TwoWayChannelMessage.MessageType.INITIATE) && + (step != 2 || msg.getType() != Protos.TwoWayChannelMessage.MessageType.ERROR || msg.getError().getCode() != Protos.Error.ErrorCode.SYNTAX_ERROR)) + fail.set(true); + step++; + if (step == 2) { + Protos.UpdatePayment.Builder updatePaymentBuilder = Protos.UpdatePayment.newBuilder() + .setClientChangeValue(0).setSignature(ByteString.EMPTY); + server2.receiveMessage(Protos.TwoWayChannelMessage.newBuilder() + .setType(Protos.TwoWayChannelMessage.MessageType.UPDATE_PAYMENT) + .setUpdatePayment(updatePaymentBuilder) + .build()); + } else if (step == 3) + inactiveFuture2.set(null); + } + + @Override + public void connectionOpen() { + Protos.ClientVersion.Builder versionNegotiationBuilder = Protos.ClientVersion.newBuilder() + .setMajor(0).setMinor(42); + server2.receiveMessage(Protos.TwoWayChannelMessage.newBuilder() + .setType(Protos.TwoWayChannelMessage.MessageType.CLIENT_VERSION) + .setClientVersion(versionNegotiationBuilder) + .build()); + } + @Override public void connectionClosed() { } + @Override public void close() { } + }; + server2.connectionOpen(); + sendClient.connectionOpen(); + inactiveFuture2.get(); + + // Make sure we get back a BAD_TRANSACTION if we send crap for a refund transaction + final SettableFuture inactiveFuture3 = SettableFuture.create(); + final PaymentChannelServer server3 = new PaymentChannelServer(mockPeerGroup, serverWallet, Utils.COIN, + new PaymentChannelServer.ServerConnection() { + @Override + public void sendToClient(Protos.TwoWayChannelMessage msg) { + sendClient.receiveMessage(msg); + } + + @Override + public void destroyConnection(CloseReason reason) { + if (reason != CloseReason.REMOTE_SENT_INVALID_MESSAGE) + fail.set(true); + sendClient.connectionClosed(); + } + + @Override public void channelOpen(Sha256Hash contractHash) { fail.set(true); } + @Override public void paymentIncrease(BigInteger by, BigInteger to) { fail.set(true); } + }); + sendClient = new PaymentChannelClientReceiver() { + int step = 0; + @Override + public void receiveMessage(Protos.TwoWayChannelMessage msg) { + if ((step != 0 || msg.getType() != Protos.TwoWayChannelMessage.MessageType.SERVER_VERSION) && + (step != 1 || msg.getType() != Protos.TwoWayChannelMessage.MessageType.INITIATE) && + (step != 2 || msg.getType() != Protos.TwoWayChannelMessage.MessageType.ERROR || msg.getError().getCode() != Protos.Error.ErrorCode.BAD_TRANSACTION)) + fail.set(true); + step++; + if (step == 2) { + Protos.ProvideRefund.Builder provideRefundBuilder = Protos.ProvideRefund.newBuilder() + .setMultisigKey(ByteString.EMPTY).setTx(ByteString.EMPTY); + server3.receiveMessage(Protos.TwoWayChannelMessage.newBuilder() + .setType(Protos.TwoWayChannelMessage.MessageType.PROVIDE_REFUND) + .setProvideRefund(provideRefundBuilder) + .build()); + } else if (step == 3) + inactiveFuture3.set(null); + } + + @Override + public void connectionOpen() { + Protos.ClientVersion.Builder versionNegotiationBuilder = Protos.ClientVersion.newBuilder() + .setMajor(0).setMinor(42); + server3.receiveMessage(Protos.TwoWayChannelMessage.newBuilder() + .setType(Protos.TwoWayChannelMessage.MessageType.CLIENT_VERSION) + .setClientVersion(versionNegotiationBuilder) + .build()); + } + @Override public void connectionClosed() { } + @Override public void close() { } + }; + server3.connectionOpen(); + sendClient.connectionOpen(); + inactiveFuture3.get(); + + // Make sure the server closes the socket on CLOSE + final SettableFuture inactiveFuture4 = SettableFuture.create(); + final PaymentChannelServer server4 = new PaymentChannelServer(mockPeerGroup, serverWallet, Utils.COIN, + new PaymentChannelServer.ServerConnection() { + @Override + public void sendToClient(Protos.TwoWayChannelMessage msg) { + sendClient.receiveMessage(msg); + } + + @Override + public void destroyConnection(CloseReason reason) { + if (reason != CloseReason.CLIENT_REQUESTED_CLOSE) + fail.set(true); + sendClient.connectionClosed(); + } + + @Override public void channelOpen(Sha256Hash contractHash) { fail.set(true); } + @Override public void paymentIncrease(BigInteger by, BigInteger to) { fail.set(true); } + }); + sendClient = new PaymentChannelClientReceiver() { + int step = 0; + @Override + public void receiveMessage(Protos.TwoWayChannelMessage msg) { + // Server may send SERVER_VERSION + INITIATE in one go, so we could get both + if ((step != 0 || msg.getType() != Protos.TwoWayChannelMessage.MessageType.SERVER_VERSION) && + (step != 1 || msg.getType() != Protos.TwoWayChannelMessage.MessageType.INITIATE)) + fail.set(true); + step++; + server4.receiveMessage(Protos.TwoWayChannelMessage.newBuilder() + .setType(Protos.TwoWayChannelMessage.MessageType.CLOSE) + .build()); + } + + @Override + public void connectionOpen() { + Protos.ClientVersion.Builder versionNegotiationBuilder = Protos.ClientVersion.newBuilder() + .setMajor(0).setMinor(42); + server4.receiveMessage(Protos.TwoWayChannelMessage.newBuilder() + .setType(Protos.TwoWayChannelMessage.MessageType.CLIENT_VERSION) + .setClientVersion(versionNegotiationBuilder) + .build()); + } + + @Override + public void connectionClosed() { + inactiveFuture4.set(null); + } + @Override public void close() { } + }; + server4.connectionOpen(); + sendClient.connectionOpen(); + inactiveFuture4.get(); + + // Make sure the server closes the socket on ERROR + final SettableFuture inactiveFuture5 = SettableFuture.create(); + final PaymentChannelServer server5 = new PaymentChannelServer(mockPeerGroup, serverWallet, Utils.COIN, + new PaymentChannelServer.ServerConnection() { + @Override + public void sendToClient(Protos.TwoWayChannelMessage msg) { + sendClient.receiveMessage(msg); + } + + @Override + public void destroyConnection(CloseReason reason) { + if (reason != CloseReason.REMOTE_SENT_ERROR) + fail.set(true); + sendClient.connectionClosed(); + } + + @Override public void channelOpen(Sha256Hash contractHash) { fail.set(true); } + @Override public void paymentIncrease(BigInteger by, BigInteger to) { fail.set(true); } + }); + sendClient = new PaymentChannelClientReceiver() { + int step = 0; + @Override + public void receiveMessage(Protos.TwoWayChannelMessage msg) { + // Server may send SERVER_VERSION + INITIATE in one go, so we could get both + if ((step != 0 || msg.getType() != Protos.TwoWayChannelMessage.MessageType.SERVER_VERSION) && + (step != 1 || msg.getType() != Protos.TwoWayChannelMessage.MessageType.INITIATE)) + fail.set(true); + server5.receiveMessage(Protos.TwoWayChannelMessage.newBuilder() + .setType(Protos.TwoWayChannelMessage.MessageType.ERROR) + .setError(Protos.Error.newBuilder().setCode(Protos.Error.ErrorCode.TIMEOUT)) + .build()); + step++; + } + + @Override + public void connectionOpen() { + Protos.ClientVersion.Builder versionNegotiationBuilder = Protos.ClientVersion.newBuilder() + .setMajor(0).setMinor(42); + server5.receiveMessage(Protos.TwoWayChannelMessage.newBuilder() + .setType(Protos.TwoWayChannelMessage.MessageType.CLIENT_VERSION) + .setClientVersion(versionNegotiationBuilder) + .build()); + } + + @Override + public void connectionClosed() { + inactiveFuture5.set(null); + } + @Override public void close() { } + }; + server5.connectionOpen(); + sendClient.connectionOpen(); + inactiveFuture5.get(); + + control.verify(); + } + + @Test + public void testChannelResume() throws Exception { + // Tests various aspects of channel resuming + + // Set up a mock peergroup. + IMocksControl control = createStrictControl(); + final PeerGroup mockPeerGroup = control.createMock(PeerGroup.class); + final SettableFuture multiSigFuture = SettableFuture.create(); + final SettableFuture multiSigFuture2 = SettableFuture.create(); + SettableFuture closeFuture = SettableFuture.create(); + SettableFuture closeFuture2 = SettableFuture.create(); + final Capture broadcastMultiSig = new Capture(); + final Capture broadcastMultiSig2 = new Capture(); + Capture broadcastClose = new Capture(); + Capture broadcastClose2 = new Capture(); + expect(mockPeerGroup.broadcastTransaction(capture(broadcastMultiSig))).andReturn(multiSigFuture); + expect(mockPeerGroup.broadcastTransaction(capture(broadcastMultiSig2))).andReturn(multiSigFuture2); + expect(mockPeerGroup.broadcastTransaction(capture(broadcastClose))).andReturn(closeFuture); + expect(mockPeerGroup.broadcastTransaction(capture(broadcastClose2))).andReturn(closeFuture2); + control.replay(); + + // Use a mock clock + Utils.rollMockClock(0); + + StoredPaymentChannelClientStates clientStoredChannels = (StoredPaymentChannelClientStates)wallet.getExtensions().get(StoredPaymentChannelClientStates.EXTENSION_ID); + + + // Check that server-side will reject incorrectly formatted hashes + final SettableFuture server1VersionSent = SettableFuture.create(); + final SettableFuture server1InitiateSent = SettableFuture.create(); + PaymentChannelServer server1 = new PaymentChannelServer(mockPeerGroup, serverWallet, Utils.COIN, + new PaymentChannelServer.ServerConnection() { + @Override + public void sendToClient(Protos.TwoWayChannelMessage msg) { + if (!server1VersionSent.isDone()) { + assertEquals(Protos.TwoWayChannelMessage.MessageType.SERVER_VERSION, msg.getType()); + server1VersionSent.set(null); + return; + } + assertTrue(!server1InitiateSent.isDone() && msg.getType() == Protos.TwoWayChannelMessage.MessageType.INITIATE); + server1InitiateSent.set(null); + } + @Override public void destroyConnection(CloseReason reason) { fail(); } + @Override public void channelOpen(Sha256Hash contractHash) { fail(); } + @Override public void paymentIncrease(BigInteger by, BigInteger to) { fail(); } + }); + server1.connectionOpen(); + server1.receiveMessage(Protos.TwoWayChannelMessage.newBuilder() + .setType(Protos.TwoWayChannelMessage.MessageType.CLIENT_VERSION) + .setClientVersion(Protos.ClientVersion.newBuilder() + .setPreviousChannelContractHash(ByteString.copyFrom(new byte[]{0x00, 0x01})) + .setMajor(0).setMinor(42)) + .build()); + + assertTrue(server1InitiateSent.isDone()); + + // Now open a normal channel + final SettableFuture client2OpenFuture = SettableFuture.create(); + final SettableFuture client2CloseFuture = SettableFuture.create(); + final SettableFuture server2PaymentFuture = SettableFuture.create(); + final SettableFuture server2ContractHashFuture = SettableFuture.create(); + final PaymentChannelServer server2 = new PaymentChannelServer(mockPeerGroup, serverWallet, Utils.COIN, new PaymentChannelServer.ServerConnection() { + @Override public void sendToClient(Protos.TwoWayChannelMessage msg) { sendClient.receiveMessage(msg); } + + @Override + public void destroyConnection(CloseReason reason) { + assertEquals(CloseReason.SERVER_REQUESTED_CLOSE, reason); + } + + @Override + public void channelOpen(Sha256Hash contractHash) { + server2ContractHashFuture.set(contractHash); + } + + @Override + public void paymentIncrease(BigInteger by, BigInteger to) { + assertTrue(by.equals(Utils.CENT) && to.equals(Utils.CENT)); + server2PaymentFuture.set(null); + } + }); + + PaymentChannelClient client2 = new PaymentChannelClient(wallet, myKey, Utils.COIN, Sha256Hash.create(new byte[] {}), + new PaymentChannelClient.ClientConnection() { + @Override public void sendToServer(Protos.TwoWayChannelMessage msg) { server2.receiveMessage(msg); } + + @Override + public void destroyConnection(CloseReason reason) { + assertEquals(CloseReason.SERVER_REQUESTED_CLOSE, reason); + client2CloseFuture.set(null); + sendClient.connectionClosed(); + } + + @Override + public void channelOpen() { + client2OpenFuture.set(null); + } + }); + sendClient = new PaymentChannelClientReceiverImpl(client2); + server2.connectionOpen(); + client2.connectionOpen(); + + multiSigFuture.set(broadcastMultiSig.getValue()); + assertTrue(client2OpenFuture.isDone() && server2ContractHashFuture.isDone()); + assertEquals(broadcastMultiSig.getValue().getHash(), server2ContractHashFuture.get()); + + client2.incrementPayment(Utils.CENT); + assertTrue(server2PaymentFuture.isDone()); + + server2.close(); + server2.connectionClosed(); + assertFalse(client2.connectionOpen); + assertTrue(client2CloseFuture.isDone()); + // There is now an open channel worth COIN-CENT with id Sha256.create(new byte[] {}) + + assertEquals(1, clientStoredChannels.mapChannels.size()); + + // Check that server-side won't attempt to reopen a nonexistent channel + final SettableFuture server3VersionSent = SettableFuture.create(); + final SettableFuture server3InitiateSent = SettableFuture.create(); + PaymentChannelServer server3 = new PaymentChannelServer(mockPeerGroup, serverWallet, Utils.COIN, + new PaymentChannelServer.ServerConnection() { + @Override + public void sendToClient(Protos.TwoWayChannelMessage msg) { + if (!server3VersionSent.isDone()) { + assertTrue(msg.getType() == Protos.TwoWayChannelMessage.MessageType.SERVER_VERSION); + server3VersionSent.set(null); + return; + } + assertTrue(!server3InitiateSent.isDone() && msg.getType() == Protos.TwoWayChannelMessage.MessageType.INITIATE); + server3InitiateSent.set(null); + } + @Override public void destroyConnection(CloseReason reason) { fail(); } + @Override public void channelOpen(Sha256Hash contractHash) { fail(); } + @Override public void paymentIncrease(BigInteger by, BigInteger to) { fail(); } + }); + server3.connectionOpen(); + server3.receiveMessage(Protos.TwoWayChannelMessage.newBuilder() + .setType(Protos.TwoWayChannelMessage.MessageType.CLIENT_VERSION) + .setClientVersion(Protos.ClientVersion.newBuilder() + .setPreviousChannelContractHash(ByteString.copyFrom(Sha256Hash.create(new byte[]{0x03}).getBytes())) + .setMajor(0).setMinor(42)) + .build()); + + assertTrue(server3InitiateSent.isDone()); + + + // Now reopen channel 2 + final SettableFuture client4OpenFuture = SettableFuture.create(); + final SettableFuture client4CloseFuture = SettableFuture.create(); + final SettableFuture server4CloseFuture = SettableFuture.create(); + final SettableFuture server4PaymentFuture = SettableFuture.create(); + final PaymentChannelServer server4 = new PaymentChannelServer(mockPeerGroup, serverWallet, Utils.COIN, new PaymentChannelServer.ServerConnection() { + @Override public void sendToClient(Protos.TwoWayChannelMessage msg) { sendClient.receiveMessage(msg); } + + @Override + public void destroyConnection(CloseReason reason) { + assertEquals(CloseReason.CLIENT_REQUESTED_CLOSE, reason); + server4CloseFuture.set(null); + } + + @Override + public void channelOpen(Sha256Hash contractHash) { + try { + assertEquals(server2ContractHashFuture.get(), contractHash); + } catch (Exception e) { fail(); } + } + + @Override + public void paymentIncrease(BigInteger by, BigInteger to) { + assertTrue(by.equals(Utils.CENT) && to.equals(Utils.CENT.shiftLeft(1))); + server4PaymentFuture.set(null); + } + }); + + PaymentChannelClient client4 = new PaymentChannelClient(wallet, myKey, Utils.COIN, Sha256Hash.create(new byte[] {}), + new PaymentChannelClient.ClientConnection() { + @Override public void sendToServer(Protos.TwoWayChannelMessage msg) { server4.receiveMessage(msg); } + + @Override + public void destroyConnection(CloseReason reason) { + assertEquals(CloseReason.CLIENT_REQUESTED_CLOSE, reason); + client4CloseFuture.set(null); + } + + @Override + public void channelOpen() { + client4OpenFuture.set(null); + } + }); + sendClient = new PaymentChannelClientReceiverImpl(client4); + server4.connectionOpen(); + client4.connectionOpen(); + + assertTrue(client4OpenFuture.isDone()); + + client4.incrementPayment(Utils.CENT); + assertTrue(server4PaymentFuture.isDone()); + + // Now open up a new client with the same id and make sure it doesnt attempt to reopen the channel + final SettableFuture client5OpenFuture = SettableFuture.create(); + final SettableFuture client5CloseFuture = SettableFuture.create(); + final SettableFuture server5PaymentFuture = SettableFuture.create(); + final SettableFuture server5ContractHashFuture = SettableFuture.create(); + final PaymentChannelServer server5 = new PaymentChannelServer(mockPeerGroup, serverWallet, Utils.COIN, new PaymentChannelServer.ServerConnection() { + @Override public void sendToClient(Protos.TwoWayChannelMessage msg) { sendClient.receiveMessage(msg); } + + @Override + public void destroyConnection(CloseReason reason) { + assertEquals(CloseReason.SERVER_REQUESTED_CLOSE, reason); + sendClient.connectionClosed(); + } + + @Override + public void channelOpen(Sha256Hash contractHash) { + try { + assertFalse(server2ContractHashFuture.get().equals(contractHash)); + } catch (Exception e) { fail(); } + server5ContractHashFuture.set(contractHash); + } + + @Override + public void paymentIncrease(BigInteger by, BigInteger to) { + assertTrue(by.equals(Utils.CENT.shiftLeft(1)) && to.equals(Utils.CENT.shiftLeft(1))); + server5PaymentFuture.set(null); + } + }); + PaymentChannelClient client5 = new PaymentChannelClient(wallet, myKey, Utils.COIN, Sha256Hash.create(new byte[] {}), + new PaymentChannelClient.ClientConnection() { + @Override public void sendToServer(Protos.TwoWayChannelMessage msg) { + if(msg.getType() == Protos.TwoWayChannelMessage.MessageType.CLIENT_VERSION) + assertFalse(msg.getClientVersion().hasPreviousChannelContractHash()); + server5.receiveMessage(msg); + } + + @Override + public void destroyConnection(CloseReason reason) { + assertEquals(CloseReason.SERVER_REQUESTED_CLOSE, reason); + client5CloseFuture.set(null); + } + + @Override + public void channelOpen() { + client5OpenFuture.set(null); + } + }); + sendClient = new PaymentChannelClientReceiverImpl(client5); + server5.connectionOpen(); + client5.connectionOpen(); + + multiSigFuture2.set(broadcastMultiSig2.getValue()); + assertTrue(client5OpenFuture.isDone() && server5ContractHashFuture.isDone()); + assertEquals(broadcastMultiSig2.getValue().getHash(), server5ContractHashFuture.get()); + + client5.incrementPayment(Utils.CENT.shiftLeft(1)); + assertTrue(server5PaymentFuture.isDone()); + + assertEquals(2, clientStoredChannels.mapChannels.size()); + + // Make sure the server won't allow the reopen either + // Check that server-side will reject incorrectly formatted hashes + final SettableFuture server6VersionSent = SettableFuture.create(); + final SettableFuture server6InitiateSent = SettableFuture.create(); + PaymentChannelServer server6 = new PaymentChannelServer(mockPeerGroup, serverWallet, Utils.COIN, + new PaymentChannelServer.ServerConnection() { + @Override + public void sendToClient(Protos.TwoWayChannelMessage msg) { + if (!server6VersionSent.isDone()) { + assertTrue(msg.getType() == Protos.TwoWayChannelMessage.MessageType.SERVER_VERSION); + server6VersionSent.set(null); + return; + } + assertTrue(!server6InitiateSent.isDone() && msg.getType() == Protos.TwoWayChannelMessage.MessageType.INITIATE); + server6InitiateSent.set(null); + } + @Override public void destroyConnection(CloseReason reason) { fail(); } + @Override public void channelOpen(Sha256Hash contractHash) { fail(); } + @Override public void paymentIncrease(BigInteger by, BigInteger to) { fail(); } + }); + server6.connectionOpen(); + server6.receiveMessage(Protos.TwoWayChannelMessage.newBuilder() + .setType(Protos.TwoWayChannelMessage.MessageType.CLIENT_VERSION) + .setClientVersion(Protos.ClientVersion.newBuilder() + .setPreviousChannelContractHash(ByteString.copyFrom(broadcastMultiSig2.getValue().getHash().getBytes())) + .setMajor(0).setMinor(42)) + .build()); + + assertTrue(server6InitiateSent.isDone()); + + // Now close connection 5 + server5.close(); + server5.connectionClosed(); + assertFalse(client5.connectionOpen); + assertTrue(client5CloseFuture.isDone()); + + // Now open a 4th channel with the same id and make sure it reopens the second (because the 1st is still open) + final SettableFuture client7OpenFuture = SettableFuture.create(); + final SettableFuture client7CloseFuture = SettableFuture.create(); + final SettableFuture server7CloseFuture = SettableFuture.create(); + final SettableFuture server7PaymentFuture = SettableFuture.create(); + final PaymentChannelServer server7 = new PaymentChannelServer(mockPeerGroup, serverWallet, Utils.COIN, new PaymentChannelServer.ServerConnection() { + @Override public void sendToClient(Protos.TwoWayChannelMessage msg) { sendClient.receiveMessage(msg); } + + @Override + public void destroyConnection(CloseReason reason) { + assertEquals(CloseReason.CLIENT_REQUESTED_CLOSE, reason); + server7CloseFuture.set(null); + } + + @Override + public void channelOpen(Sha256Hash contractHash) { + try { + assertEquals(server5ContractHashFuture.get(), contractHash); + } catch (Exception e) { fail(); } + } + + @Override + public void paymentIncrease(BigInteger by, BigInteger to) { + assertTrue(by.equals(Utils.CENT.shiftLeft(1)) && to.equals(Utils.CENT.shiftLeft(2))); + server7PaymentFuture.set(null); + } + }); + + PaymentChannelClient client7 = new PaymentChannelClient(wallet, myKey, Utils.COIN, Sha256Hash.create(new byte[] {}), + new PaymentChannelClient.ClientConnection() { + @Override public void sendToServer(Protos.TwoWayChannelMessage msg) { server7.receiveMessage(msg); } + + @Override + public void destroyConnection(CloseReason reason) { + assertEquals(CloseReason.CLIENT_REQUESTED_CLOSE, reason); + client7CloseFuture.set(null); + } + + @Override + public void channelOpen() { + client7OpenFuture.set(null); + } + }); + sendClient = new PaymentChannelClientReceiverImpl(client7); + server7.connectionOpen(); + client7.connectionOpen(); + + assertTrue(client7OpenFuture.isDone()); + + client7.incrementPayment(Utils.CENT.shiftLeft(1)); + assertTrue(server7PaymentFuture.isDone()); + + assertEquals(2, clientStoredChannels.mapChannels.size()); + + client7.close(); // Client-side close to broadcast close tx + assertTrue(client7CloseFuture.isDone() && server7CloseFuture.isDone()); + client7.connectionClosed(); + server7.connectionClosed(); + assertFalse(client7.connectionOpen); + + assertFalse(clientStoredChannels.getChannel(Sha256Hash.create(new byte[]{}), broadcastMultiSig2.getValue().getHash()).active); + assertTrue(clientStoredChannels.getChannel(Sha256Hash.create(new byte[]{}), broadcastMultiSig.getValue().getHash()).active); + + // Now, finally, close 4 + sendClient = new PaymentChannelClientReceiverImpl(client4); + client4.close(); // Client-side close to broadcast close tx + assertTrue(client4CloseFuture.isDone() && server4CloseFuture.isDone()); + client4.connectionClosed(); + server4.connectionClosed(); + assertFalse(client4.connectionOpen); + + assertFalse(clientStoredChannels.getChannel(Sha256Hash.create(new byte[]{}), broadcastMultiSig2.getValue().getHash()).active); + assertFalse(clientStoredChannels.getChannel(Sha256Hash.create(new byte[]{}), broadcastMultiSig.getValue().getHash()).active); + + // Now roll the mock clock and recreate the client object so that it removes the channels + Utils.rollMockClock(60 * 60 * 24 + 60*5); // Client announces refund 5 minutes after expire time + final AtomicInteger broadcastCount = new AtomicInteger(); + StoredPaymentChannelClientStates newClientStates = new StoredPaymentChannelClientStates(new TransactionBroadcaster() { + @Override + public ListenableFuture broadcastTransaction(Transaction tx) { + broadcastCount.incrementAndGet(); + return null; + } + }, wallet); + newClientStates.deserializeWalletExtension(wallet, clientStoredChannels.serializeWalletExtension()); + + while (broadcastCount.get() < 4) + Thread.sleep(100); + + assertTrue(newClientStates.mapChannels.isEmpty()); + + StoredPaymentChannelServerStates serverStoredChannels = (StoredPaymentChannelServerStates)serverWallet.getExtensions().get(StoredPaymentChannelServerStates.EXTENSION_ID); + assertTrue(serverStoredChannels.mapChannels.isEmpty()); + + control.verify(); + } + + @Test + public void testChannelExpire() throws Exception { + // Test that channels get properly closed when they expire + + // Set up a mock peergroup. + IMocksControl control = createStrictControl(); + final PeerGroup mockPeerGroup = control.createMock(PeerGroup.class); + // We'll broadcast two txns: multisig contract and close transaction. + SettableFuture multiSigFuture = SettableFuture.create(); + SettableFuture paymentFuture = SettableFuture.create(); + SettableFuture clientMultisigFuture = SettableFuture.create(); + SettableFuture refundFuture = SettableFuture.create(); + + Capture broadcastMultiSig = new Capture(); + Capture broadcastPayment = new Capture(); + Capture broadcastClientMultisig = new Capture(); + Capture broadcastRefund = new Capture(); + + expect(mockPeerGroup.broadcastTransaction(capture(broadcastMultiSig))).andReturn(multiSigFuture); + expect(mockPeerGroup.broadcastTransaction(capture(broadcastPayment))).andReturn(paymentFuture); + expect(mockPeerGroup.broadcastTransaction(capture(broadcastClientMultisig))).andReturn(clientMultisigFuture); + expect(mockPeerGroup.broadcastTransaction(capture(broadcastRefund))).andReturn(refundFuture); + control.replay(); + + // Use a mock clock + Utils.rollMockClock(0); + + final SettableFuture serverSecondPaymentProcessedFuture = SettableFuture.create(); + final SettableFuture serverCloseFuture = SettableFuture.create(); + final SettableFuture contractHashFuture = SettableFuture.create(); + final PaymentChannelServer server = new PaymentChannelServer(mockPeerGroup, serverWallet, Utils.COIN, + new PaymentChannelServer.ServerConnection() { + @Override + public void sendToClient(Protos.TwoWayChannelMessage msg) { + sendClient.receiveMessage(msg); + } + + @Override + public void destroyConnection(CloseReason reason) { + serverCloseFuture.set(null); + sendClient.connectionClosed(); + } + + @Override + public void channelOpen(Sha256Hash contractHash) { + contractHashFuture.set(contractHash); + } + + @Override + public void paymentIncrease(BigInteger by, BigInteger to) { + if (to.equals(Utils.CENT.shiftLeft(1))) + serverSecondPaymentProcessedFuture.set(null); + } + }); + + final SettableFuture clientChannelOpenFuture = SettableFuture.create(); + PaymentChannelClient clientConnection = new PaymentChannelClient(wallet, myKey, Utils.COIN, Sha256Hash.create(new byte[] {}), + new PaymentChannelClient.ClientConnection() { + @Override + public void sendToServer(Protos.TwoWayChannelMessage msg) { + server.receiveMessage(msg); + } + + @Override + public void destroyConnection(CloseReason reason) { + assertEquals(CloseReason.SERVER_REQUESTED_CLOSE, reason); + } + + @Override + public void channelOpen() { + clientChannelOpenFuture.set(null); + } + }); + sendClient = new PaymentChannelClientReceiverImpl(clientConnection); + server.connectionOpen(); + clientConnection.connectionOpen(); // Recurses until channel is open + + multiSigFuture.set(broadcastMultiSig.getValue()); + assertEquals(contractHashFuture.get(), broadcastMultiSig.getValue().getHash()); + assertTrue(clientChannelOpenFuture.isDone()); + + clientConnection.incrementPayment(Utils.CENT); + clientConnection.incrementPayment(Utils.CENT); + assertTrue(serverSecondPaymentProcessedFuture.isDone()); + + StoredPaymentChannelServerStates channels = (StoredPaymentChannelServerStates)serverWallet.getExtensions().get(StoredPaymentChannelServerStates.EXTENSION_ID); + StoredServerChannel storedServerChannel = channels.getChannel(broadcastMultiSig.getValue().getHash()); + PaymentChannelServerState serverState; + synchronized (storedServerChannel) { + serverState = storedServerChannel.getState(serverWallet, mockPeerGroup); + } + assertNotNull(serverState); + + server.close(); // Does not close channels themselves + assertTrue(serverCloseFuture.isDone()); + server.connectionClosed(); + assertNull(storedServerChannel.connectedHandler); + assertFalse(clientConnection.connectionOpen); + + // Now make the channel expire (in the server's eyes) + Utils.rollMockClock(60 * 60 * 22 + 60); // Server gives 60 seconds of extra time in the lock time calculation so + // that client can have their clock off a bit, and then announces payment + // 2 hours before the expire time + + // And make sure the server broadcasts the payment transaction + StoredPaymentChannelServerStates newManager = new StoredPaymentChannelServerStates(serverWallet, mockPeerGroup); + newManager.deserializeWalletExtension(serverWallet, channels.serializeWalletExtension()); + + while (!broadcastPayment.hasCaptured()) + Thread.sleep(100); + paymentFuture.set(broadcastPayment.getValue()); + assertEquals(Utils.COIN.subtract(Utils.CENT.shiftLeft(1)), broadcastPayment.getValue().getOutput(0).getValue()); + + // Now do the same with the client side + StoredPaymentChannelClientStates clientChannels = (StoredPaymentChannelClientStates)wallet.getExtensions().get(StoredPaymentChannelClientStates.EXTENSION_ID); + clientChannels.channelTimeoutHandler.cancel(); + StoredClientChannel storedClientChannel = clientChannels.getChannel(Sha256Hash.create(new byte[]{}), broadcastMultiSig.getValue().getHash()); + assertFalse(storedClientChannel.active); + + Utils.rollMockClock(60 * 60 * 2 + 60*4); // Client announces refund 5 minutes after expire time + StoredPaymentChannelClientStates newClientStates = new StoredPaymentChannelClientStates(new TransactionBroadcaster() { + @Override + public ListenableFuture broadcastTransaction(Transaction tx) { + return mockPeerGroup.broadcastTransaction(tx); + } + }, wallet); + newClientStates.deserializeWalletExtension(wallet, clientChannels.serializeWalletExtension()); + while (!broadcastRefund.hasCaptured()) + Thread.sleep(100); + clientMultisigFuture.set(broadcastClientMultisig.getValue()); + refundFuture.set(broadcastRefund.getValue()); + + assertEquals(broadcastMultiSig.getValue().getHash(), broadcastClientMultisig.getValue().getHash()); + assertEquals(1, broadcastRefund.getValue().getOutputs().size()); + assertTrue(broadcastRefund.getValue().isTimeLocked()); + assertEquals(0, newClientStates.mapChannels.size()); + + control.verify(); + } + + @Test + public void testClientUnknownVersion() throws Exception { + // Tests client rejects unknown version + final SettableFuture serverReceivedError = SettableFuture.create(); + + ProtobufServer server = new ProtobufServer(new ProtobufParserFactory() { + @Nullable + @Override + public ProtobufParser getNewParser(InetAddress inetAddress, int port) { + return new ProtobufParser( + new ProtobufParser.Listener() { + @Override + public void messageReceived(ProtobufParser parser, Protos.TwoWayChannelMessage msg) { + if (msg.getType() != Protos.TwoWayChannelMessage.MessageType.CLIENT_VERSION && + msg.getType() != Protos.TwoWayChannelMessage.MessageType.ERROR) + fail.set(true); + + if (msg.getType() == Protos.TwoWayChannelMessage.MessageType.ERROR && + (!msg.hasError() || msg.getError().getCode() != Protos.Error.ErrorCode.NO_ACCEPTABLE_VERSION)) + fail.set(true); + if (msg.getType() == Protos.TwoWayChannelMessage.MessageType.ERROR) + serverReceivedError.set(null); + else + parser.write(Protos.TwoWayChannelMessage.newBuilder() + .setServerVersion(Protos.ServerVersion.newBuilder().setMajor(2)) + .setType(Protos.TwoWayChannelMessage.MessageType.SERVER_VERSION).build()); + } + + @Override public void connectionOpen(ProtobufParser handler) { } + @Override public void connectionClosed(ProtobufParser handler) { } + }, Protos.TwoWayChannelMessage.getDefaultInstance(), Short.MAX_VALUE, 1000); + } + }); + server.start(new InetSocketAddress("localhost", 4243)); + + PaymentChannelClientConnection clientConnection = new PaymentChannelClientConnection(new InetSocketAddress("localhost", 4243), 1, wallet, myKey, Utils.COIN, ""); + try { + clientConnection.getChannelOpenFuture().get(); + fail(); + } catch (ExecutionException e) { + assertEquals(CloseReason.NO_ACCEPTABLE_VERSION, ((PaymentChannelCloseException)e.getCause()).getCloseReason()); + } + serverReceivedError.get(); + + // Double-check that we cant do anything that requires an open channel + try { + clientConnection.incrementPayment(BigInteger.ONE); + } catch (IllegalStateException e) { } + + server.stop(); + } + + @Test + public void testClientTimeWindowTooLarge() throws Exception { + // Tests that clients reject too large time windows + final SettableFuture serverReceivedError = SettableFuture.create(); + + ProtobufServer server = new ProtobufServer(new ProtobufParserFactory() { + @Nullable + @Override + public ProtobufParser getNewParser(InetAddress inetAddress, int port) { + return new ProtobufParser( + new ProtobufParser.Listener() { + @Override + public void messageReceived(ProtobufParser parser, Protos.TwoWayChannelMessage msg) { + if (msg.getType() != Protos.TwoWayChannelMessage.MessageType.CLIENT_VERSION && + msg.getType() != Protos.TwoWayChannelMessage.MessageType.ERROR) + fail.set(true); + + if (msg.getType() == Protos.TwoWayChannelMessage.MessageType.ERROR && + (!msg.hasError() || msg.getError().getCode() != Protos.Error.ErrorCode.TIME_WINDOW_TOO_LARGE)) + fail.set(true); + if (msg.getType() == Protos.TwoWayChannelMessage.MessageType.ERROR) + serverReceivedError.set(null); + + parser.write(Protos.TwoWayChannelMessage.newBuilder() + .setServerVersion(Protos.ServerVersion.newBuilder().setMajor(0)) + .setType(Protos.TwoWayChannelMessage.MessageType.SERVER_VERSION).build()); + parser.write(Protos.TwoWayChannelMessage.newBuilder() + .setInitiate(Protos.Initiate.newBuilder().setExpireTimeSecs(Utils.now().getTime() / 1000 + 60 * 60 * 48) + .setMinAcceptedChannelSize(100) + .setMultisigKey(ByteString.copyFrom(new ECKey().getPubKey()))) + .setType(Protos.TwoWayChannelMessage.MessageType.INITIATE).build()); + } + + @Override public void connectionOpen(ProtobufParser handler) { } + @Override public void connectionClosed(ProtobufParser handler) { } + }, Protos.TwoWayChannelMessage.getDefaultInstance(), Short.MAX_VALUE, 1000); + } + }); + server.start(new InetSocketAddress("localhost", 4243)); + + PaymentChannelClientConnection clientConnection = new PaymentChannelClientConnection(new InetSocketAddress("localhost", 4243), 1, wallet, myKey, Utils.COIN, ""); + try { + clientConnection.getChannelOpenFuture().get(); + fail(); + } catch (ExecutionException e) { + assertEquals(CloseReason.TIME_WINDOW_TOO_LARGE, ((PaymentChannelCloseException)e.getCause()).getCloseReason()); + } + serverReceivedError.get(); + + server.stop(); + } + + @Test + public void testClientValueTooLarge() throws Exception { + // Tests that clients reject too high minimum channel value + final SettableFuture serverReceivedError = SettableFuture.create(); + + ProtobufServer server = new ProtobufServer(new ProtobufParserFactory() { + @Nullable + @Override + public ProtobufParser getNewParser(InetAddress inetAddress, int port) { + return new ProtobufParser( + new ProtobufParser.Listener() { + @Override + public void messageReceived(ProtobufParser parser, Protos.TwoWayChannelMessage msg) { + if (msg.getType() != Protos.TwoWayChannelMessage.MessageType.CLIENT_VERSION && + msg.getType() != Protos.TwoWayChannelMessage.MessageType.ERROR) + fail.set(true); + + if (msg.getType() == Protos.TwoWayChannelMessage.MessageType.ERROR && + (!msg.hasError() || msg.getError().getCode() != Protos.Error.ErrorCode.CHANNEL_VALUE_TOO_LARGE)) + fail.set(true); + if (msg.getType() == Protos.TwoWayChannelMessage.MessageType.ERROR) + serverReceivedError.set(null); + + parser.write(Protos.TwoWayChannelMessage.newBuilder() + .setServerVersion(Protos.ServerVersion.newBuilder().setMajor(0)) + .setType(Protos.TwoWayChannelMessage.MessageType.SERVER_VERSION).build()); + parser.write(Protos.TwoWayChannelMessage.newBuilder() + .setInitiate(Protos.Initiate.newBuilder().setExpireTimeSecs(Utils.now().getTime() / 1000) + .setMinAcceptedChannelSize(Utils.COIN.add(BigInteger.ONE).longValue()) + .setMultisigKey(ByteString.copyFrom(new ECKey().getPubKey()))) + .setType(Protos.TwoWayChannelMessage.MessageType.INITIATE).build()); + } + @Override public void connectionOpen(ProtobufParser handler) { } + @Override public void connectionClosed(ProtobufParser handler) { } + }, Protos.TwoWayChannelMessage.getDefaultInstance(), Short.MAX_VALUE, 1000); + } + }); + server.start(new InetSocketAddress("localhost", 4243)); + + PaymentChannelClientConnection clientConnection = new PaymentChannelClientConnection(new InetSocketAddress("localhost", 4243), 1, wallet, myKey, Utils.COIN, ""); + try { + clientConnection.getChannelOpenFuture().get(); + fail(); + } catch (ExecutionException e) { + assertEquals(CloseReason.SERVER_REQUESTED_TOO_MUCH_VALUE, ((PaymentChannelCloseException) e.getCause()).getCloseReason()); + } + serverReceivedError.get(); + + server.stop(); + } + + @Test + public void testClientResumeNothing() throws Exception { + // Tests that clients rejects channels where the server attempts to resume a channel when the client didn't + // request one be resumed + final SettableFuture serverReceivedError = SettableFuture.create(); + + ProtobufServer server = new ProtobufServer(new ProtobufParserFactory() { + @Nullable + @Override + public ProtobufParser getNewParser(InetAddress inetAddress, int port) { + return new ProtobufParser( + new ProtobufParser.Listener() { + @Override + public void messageReceived(ProtobufParser parser, Protos.TwoWayChannelMessage msg) { + if (msg.getType() != Protos.TwoWayChannelMessage.MessageType.CLIENT_VERSION && + msg.getType() != Protos.TwoWayChannelMessage.MessageType.ERROR) + fail.set(true); + + if (msg.getType() == Protos.TwoWayChannelMessage.MessageType.ERROR && + (!msg.hasError() || msg.getError().getCode() != Protos.Error.ErrorCode.SYNTAX_ERROR)) + fail.set(true); + if (msg.getType() == Protos.TwoWayChannelMessage.MessageType.ERROR) + serverReceivedError.set(null); + + parser.write(Protos.TwoWayChannelMessage.newBuilder() + .setServerVersion(Protos.ServerVersion.newBuilder().setMajor(0)) + .setType(Protos.TwoWayChannelMessage.MessageType.SERVER_VERSION).build()); + parser.write(Protos.TwoWayChannelMessage.newBuilder() + .setType(Protos.TwoWayChannelMessage.MessageType.CHANNEL_OPEN).build()); + } + + @Override public void connectionOpen(ProtobufParser handler) { } + @Override public void connectionClosed(ProtobufParser handler) { } + }, Protos.TwoWayChannelMessage.getDefaultInstance(), Short.MAX_VALUE, 1000); + } + }); + server.start(new InetSocketAddress("localhost", 4243)); + + InetSocketAddress client = new InetSocketAddress("localhost", 4243); + PaymentChannelClientConnection clientConnection = new PaymentChannelClientConnection(client, 1, wallet, myKey, Utils.COIN, ""); + try { + clientConnection.getChannelOpenFuture().get(); + fail(); + } catch (ExecutionException e) { + assertEquals(CloseReason.REMOTE_SENT_INVALID_MESSAGE, ((PaymentChannelCloseException) e.getCause()).getCloseReason()); + } + serverReceivedError.get(); + + server.stop(); + } + + private Protos.TwoWayChannelMessage nextMsg; + @Test + public void testClientRandomMessage() throws Exception { + // Tests that clients rejects messages it has no idea how to handle + final SettableFuture clientReceivedError = SettableFuture.create(); + + PaymentChannelClient clientConnection = new PaymentChannelClient(wallet, myKey, Utils.COIN, Sha256Hash.create(new byte[] {}), new PaymentChannelClient.ClientConnection() { + @Override + public void sendToServer(Protos.TwoWayChannelMessage msg) { + nextMsg = msg; + } + + @Override + public void destroyConnection(CloseReason reason) { + clientReceivedError.set(null); + } + + @Override + public void channelOpen() { + fail.set(true); + } + }); + clientConnection.connectionOpen(); + assertEquals(Protos.TwoWayChannelMessage.MessageType.CLIENT_VERSION, nextMsg.getType()); + + clientConnection.receiveMessage(Protos.TwoWayChannelMessage.newBuilder() + .setType(Protos.TwoWayChannelMessage.MessageType.CLIENT_VERSION).build()); + assertEquals(Protos.TwoWayChannelMessage.MessageType.ERROR, nextMsg.getType()); + assertTrue(nextMsg.hasError()); + assertEquals(Protos.Error.ErrorCode.SYNTAX_ERROR, nextMsg.getError().getCode()); + + clientReceivedError.get(); + } +} diff --git a/core/src/test/java/com/google/bitcoin/protocols/channels/PaymentChannelStateTest.java b/core/src/test/java/com/google/bitcoin/protocols/channels/PaymentChannelStateTest.java new file mode 100644 index 00000000..705fabdc --- /dev/null +++ b/core/src/test/java/com/google/bitcoin/protocols/channels/PaymentChannelStateTest.java @@ -0,0 +1,758 @@ +/* + * Copyright 2013 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.bitcoin.protocols.channels; + +import java.math.BigInteger; +import java.util.Arrays; +import java.util.Iterator; +import java.util.concurrent.ExecutionException; + +import com.google.bitcoin.core.*; +import com.google.bitcoin.script.Script; +import com.google.bitcoin.script.ScriptBuilder; +import com.google.common.collect.Lists; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.SettableFuture; +import org.easymock.Capture; +import org.easymock.IMocksControl; +import org.junit.Before; +import org.junit.Test; + +import static com.google.bitcoin.core.TestUtils.createFakeTx; +import static com.google.bitcoin.core.TestUtils.makeSolvedTestBlock; +import static org.easymock.EasyMock.*; +import static org.junit.Assert.*; + +public class PaymentChannelStateTest extends TestWithWallet { + private ECKey serverKey; + private BigInteger halfCoin; + private Wallet serverWallet; + private PaymentChannelServerState serverState; + private PaymentChannelClientState clientState; + + @Before + public void setUp() throws Exception { + super.setUp(); + wallet.addExtension(new StoredPaymentChannelClientStates(new TransactionBroadcaster() { + @Override + public ListenableFuture broadcastTransaction(Transaction tx) { + fail(); + return null; + } + }, wallet)); + sendMoneyToWallet(Utils.COIN, AbstractBlockChain.NewBlockType.BEST_CHAIN); + chain = new BlockChain(params, wallet, blockStore); // Recreate chain as sendMoneyToWallet will confuse it + serverKey = new ECKey(); + serverWallet = new Wallet(params); + serverWallet.addKey(serverKey); + chain.addWallet(serverWallet); + halfCoin = Utils.toNanoCoins(0, 50); + } + + @Test + public void stateErrors() throws Exception { + PaymentChannelClientState channelState = new PaymentChannelClientState(wallet, myKey, serverKey, + Utils.COIN.multiply(BigInteger.TEN), 20); + assertEquals(PaymentChannelClientState.State.NEW, channelState.getState()); + try { + channelState.getMultisigContract(); + fail(); + } catch (IllegalStateException e) { + // Expected. + } + try { + channelState.initiate(); + fail(); + } catch (ValueOutOfRangeException e) { + assertTrue(e.getMessage().contains("afford")); + } + } + + @Test + public void basic() throws Exception { + // Check it all works when things are normal (no attacks, no problems). + + // Set up a mock peergroup. + IMocksControl control = createStrictControl(); + PeerGroup mockPeerGroup = control.createMock(PeerGroup.class); + // We'll broadcast two txns: multisig contract and close transaction. + SettableFuture multiSigFuture = SettableFuture.create(); + SettableFuture closeFuture = SettableFuture.create(); + Capture broadcastMultiSig = new Capture(); + Capture broadcastClose = new Capture(); + expect(mockPeerGroup.broadcastTransaction(capture(broadcastMultiSig))).andReturn(multiSigFuture); + expect(mockPeerGroup.broadcastTransaction(capture(broadcastClose))).andReturn(closeFuture); + control.replay(); + + Utils.rollMockClock(0); // Use mock clock + final long EXPIRE_TIME = Utils.now().getTime()/1000 + 60*60*24; + + serverState = new PaymentChannelServerState(mockPeerGroup, serverWallet, serverKey, EXPIRE_TIME); + assertEquals(PaymentChannelServerState.State.WAITING_FOR_REFUND_TRANSACTION, serverState.getState()); + + clientState = new PaymentChannelClientState(wallet, myKey, new ECKey(null, serverKey.getPubKey()), halfCoin, EXPIRE_TIME); + assertEquals(PaymentChannelClientState.State.NEW, clientState.getState()); + clientState.initiate(); + assertEquals(PaymentChannelClientState.State.INITIATED, clientState.getState()); + + // Send the refund tx from client to server and get back the signature. + Transaction refund = new Transaction(params, clientState.getIncompleteRefundTransaction().bitcoinSerialize()); + byte[] refundSig = serverState.provideRefundTransaction(refund, myKey.getPubKey()); + assertEquals(PaymentChannelServerState.State.WAITING_FOR_MULTISIG_CONTRACT, serverState.getState()); + // This verifies that the refund can spend the multi-sig output when run. + clientState.provideRefundSignature(refundSig); + assertEquals(PaymentChannelClientState.State.PROVIDE_MULTISIG_CONTRACT_TO_SERVER, clientState.getState()); + + // Validate the multisig contract looks right. + Transaction multisigContract = new Transaction(params, clientState.getMultisigContract().bitcoinSerialize()); + assertEquals(PaymentChannelClientState.State.READY, clientState.getState()); + assertEquals(2, multisigContract.getOutputs().size()); // One multi-sig, one change. + Script script = multisigContract.getOutput(0).getScriptPubKey(); + assertTrue(script.isSentToMultiSig()); + script = multisigContract.getOutput(1).getScriptPubKey(); + assertTrue(script.isSentToAddress()); + assertTrue(wallet.getPendingTransactions().contains(multisigContract)); + + // Provide the server with the multisig contract and simulate successful propagation/acceptance. + serverState.provideMultiSigContract(multisigContract); + assertEquals(PaymentChannelServerState.State.WAITING_FOR_MULTISIG_ACCEPTANCE, serverState.getState()); + multiSigFuture.set(broadcastMultiSig.getValue()); + assertEquals(PaymentChannelServerState.State.READY, serverState.getState()); + + // Make sure the refund transaction is not in the wallet and multisig contract's output is not connected to it + assertEquals(2, wallet.getTransactions(false).size()); + Iterator walletTransactionIterator = wallet.getTransactions(false).iterator(); + Transaction clientWalletMultisigContract = walletTransactionIterator.next(); + assertFalse(clientWalletMultisigContract.getHash().equals(clientState.getCompletedRefundTransaction().getHash())); + if (!clientWalletMultisigContract.getHash().equals(multisigContract.getHash())) { + clientWalletMultisigContract = walletTransactionIterator.next(); + assertFalse(clientWalletMultisigContract.getHash().equals(clientState.getCompletedRefundTransaction().getHash())); + } else + assertFalse(walletTransactionIterator.next().getHash().equals(clientState.getCompletedRefundTransaction().getHash())); + assertEquals(multisigContract.getHash(), clientWalletMultisigContract.getHash()); + assertFalse(clientWalletMultisigContract.getInput(0).getConnectedOutput().getSpentBy().getParentTransaction().getHash().equals(refund.getHash())); + + // Both client and server are now in the ready state. Simulate a few micropayments of 0.005 bitcoins. + BigInteger size = halfCoin.divide(BigInteger.TEN).divide(BigInteger.TEN); + BigInteger totalPayment = BigInteger.ZERO; + for (int i = 0; i < 5; i++) { + byte[] signature = clientState.incrementPaymentBy(size); + totalPayment = totalPayment.add(size); + serverState.incrementPayment(halfCoin.subtract(totalPayment), signature); + } + + // And close the channel. + serverState.close(); + assertEquals(PaymentChannelServerState.State.CLOSING, serverState.getState()); + Transaction closeTx = broadcastClose.getValue(); + closeFuture.set(closeTx); + assertEquals(PaymentChannelServerState.State.CLOSED, serverState.getState()); + control.verify(); + + // Create a block with multisig contract and payment transaction in it and give it to both wallets + chain.add(makeSolvedTestBlock(blockStore.getChainHead().getHeader(), multisigContract, + new Transaction(params, closeTx.bitcoinSerialize()))); + + assertEquals(size.multiply(BigInteger.valueOf(5)), serverWallet.getBalance(new Wallet.DefaultCoinSelector() { + @Override + protected boolean shouldSelect(Transaction tx) { + if (tx.getConfidence().getConfidenceType() == TransactionConfidence.ConfidenceType.BUILDING) + return true; + return false; + } + })); + assertEquals(0, serverWallet.getPendingTransactions().size()); + + assertEquals(Utils.COIN.subtract(size.multiply(BigInteger.valueOf(5))), wallet.getBalance(new Wallet.DefaultCoinSelector() { + @Override + protected boolean shouldSelect(Transaction tx) { + if (tx.getConfidence().getConfidenceType() == TransactionConfidence.ConfidenceType.BUILDING) + return true; + return false; + } + })); + assertEquals(0, wallet.getPendingTransactions().size()); + assertEquals(3, wallet.getTransactions(false).size()); + + walletTransactionIterator = wallet.getTransactions(false).iterator(); + Transaction clientWalletCloseTransaction = walletTransactionIterator.next(); + if (!clientWalletCloseTransaction.getHash().equals(closeTx.getHash())) + clientWalletCloseTransaction = walletTransactionIterator.next(); + if (!clientWalletCloseTransaction.getHash().equals(closeTx.getHash())) + clientWalletCloseTransaction = walletTransactionIterator.next(); + assertEquals(closeTx.getHash(), clientWalletCloseTransaction.getHash()); + assertTrue(clientWalletCloseTransaction.getInput(0).getConnectedOutput() != null); + + control.verify(); + } + + @Test + public void setupDoS() throws Exception { + // Check that if the other side stops after we have provided a signed multisig contract, that after a timeout + // we can broadcast the refund and get our balance back. + + // Spend the client wallet's one coin + Transaction spendCoinTx = wallet.sendCoinsOffline(Wallet.SendRequest.to(new ECKey().toAddress(params), Utils.COIN)); + assertEquals(wallet.getBalance(), BigInteger.ZERO); + chain.add(makeSolvedTestBlock(blockStore.getChainHead().getHeader(), spendCoinTx, createFakeTx(params, Utils.CENT, myAddress))); + assertEquals(wallet.getBalance(), Utils.CENT); + + // Set up a mock peergroup. + IMocksControl control = createStrictControl(); + final PeerGroup mockPeerGroup = control.createMock(PeerGroup.class); + // We'll broadcast three txns: multisig contract twice (both server and client) and refund transaction. + SettableFuture serverMultiSigFuture = SettableFuture.create(); + SettableFuture paymentFuture = SettableFuture.create(); + SettableFuture clientMultiSigFuture = SettableFuture.create(); + SettableFuture refundFuture = SettableFuture.create(); + Capture serverBroadcastMultiSig = new Capture(); + Capture broadcastPayment = new Capture(); + Capture clientBroadcastMultiSig = new Capture(); + Capture broadcastRefund = new Capture(); + expect(mockPeerGroup.broadcastTransaction(capture(serverBroadcastMultiSig))).andReturn(serverMultiSigFuture); + expect(mockPeerGroup.broadcastTransaction(capture(broadcastPayment))).andReturn(paymentFuture); + expect(mockPeerGroup.broadcastTransaction(capture(clientBroadcastMultiSig))).andReturn(clientMultiSigFuture); + expect(mockPeerGroup.broadcastTransaction(capture(broadcastRefund))).andReturn(refundFuture); + control.replay(); + + // Set the wallet's stored states to use our real test PeerGroup + StoredPaymentChannelClientStates stateStorage = new StoredPaymentChannelClientStates(new TransactionBroadcaster() { + @Override + public ListenableFuture broadcastTransaction(Transaction tx) { + return mockPeerGroup.broadcastTransaction(tx); + } + }, wallet); + wallet.addOrUpdateExtension(stateStorage); + + Utils.rollMockClock(0); // Use mock clock + final long EXPIRE_TIME = Utils.now().getTime()/1000 + 60*60*24; + + serverState = new PaymentChannelServerState(mockPeerGroup, serverWallet, serverKey, EXPIRE_TIME); + assertEquals(PaymentChannelServerState.State.WAITING_FOR_REFUND_TRANSACTION, serverState.getState()); + + clientState = new PaymentChannelClientState(wallet, myKey, new ECKey(null, serverKey.getPubKey()), + Utils.CENT.divide(BigInteger.valueOf(2)), EXPIRE_TIME); + assertEquals(PaymentChannelClientState.State.NEW, clientState.getState()); + assertEquals(Utils.CENT.divide(BigInteger.valueOf(2)), clientState.getTotalValue()); + clientState.initiate(); + // We will have to pay min_tx_fee twice - both the multisig contract and the refund tx + assertEquals(clientState.getRefundTxFees(), Transaction.REFERENCE_DEFAULT_MIN_TX_FEE.multiply(BigInteger.valueOf(2))); + assertEquals(PaymentChannelClientState.State.INITIATED, clientState.getState()); + + // Send the refund tx from client to server and get back the signature. + Transaction refund = new Transaction(params, clientState.getIncompleteRefundTransaction().bitcoinSerialize()); + byte[] refundSig = serverState.provideRefundTransaction(refund, myKey.getPubKey()); + assertEquals(PaymentChannelServerState.State.WAITING_FOR_MULTISIG_CONTRACT, serverState.getState()); + // This verifies that the refund can spend the multi-sig output when run. + clientState.provideRefundSignature(refundSig); + assertEquals(PaymentChannelClientState.State.PROVIDE_MULTISIG_CONTRACT_TO_SERVER, clientState.getState()); + + // Validate the multisig contract looks right. + Transaction multisigContract = new Transaction(params, clientState.getMultisigContract().bitcoinSerialize()); + assertEquals(PaymentChannelClientState.State.READY, clientState.getState()); + assertEquals(2, multisigContract.getOutputs().size()); // One multi-sig, one change. + Script script = multisigContract.getOutput(0).getScriptPubKey(); + assertTrue(script.isSentToMultiSig()); + script = multisigContract.getOutput(1).getScriptPubKey(); + assertTrue(script.isSentToAddress()); + assertTrue(wallet.getPendingTransactions().contains(multisigContract)); + + // Provide the server with the multisig contract and simulate successful propagation/acceptance. + serverState.provideMultiSigContract(multisigContract); + assertEquals(PaymentChannelServerState.State.WAITING_FOR_MULTISIG_ACCEPTANCE, serverState.getState()); + serverMultiSigFuture.set(serverBroadcastMultiSig.getValue()); + assertEquals(PaymentChannelServerState.State.READY, serverState.getState()); + + // Pay a tiny bit + serverState.incrementPayment(Utils.CENT.divide(BigInteger.valueOf(2)).subtract(Utils.CENT.divide(BigInteger.TEN)), + clientState.incrementPaymentBy(Utils.CENT.divide(BigInteger.TEN))); + + // Advance time until our we get close enough to lock time that server should rebroadcast + Utils.rollMockClock(60*60*22); + // ... and store server to get it to broadcast payment transaction + serverState.storeChannelInWallet(null); + while (!broadcastPayment.hasCaptured()) + Thread.sleep(100); + Exception paymentException = new RuntimeException("I'm sorry, but the network really just doesn't like you"); + paymentFuture.setException(paymentException); + try { + serverState.close().get(); + } catch (ExecutionException e) { + assertTrue(e.getCause() == paymentException); + } + assertEquals(PaymentChannelServerState.State.ERROR, serverState.getState()); + + // Now advance until client should rebroadcast + Utils.rollMockClock(60*60*2 + 60*5); + + // Now store the client state in a stored state object which handles the rebroadcasting + clientState.storeChannelInWallet(Sha256Hash.create(new byte[] {})); + while (!broadcastRefund.hasCaptured()) + Thread.sleep(100); + + Transaction clientBroadcastedMultiSig = clientBroadcastMultiSig.getValue(); + assertTrue(clientBroadcastedMultiSig.getHash().equals(multisigContract.getHash())); + for (TransactionInput input : clientBroadcastedMultiSig.getInputs()) + input.verify(); + clientMultiSigFuture.set(clientBroadcastedMultiSig); + + Transaction clientBroadcastedRefund = broadcastRefund.getValue(); + assertTrue(clientBroadcastedRefund.getHash().equals(clientState.getCompletedRefundTransaction().getHash())); + for (TransactionInput input : clientBroadcastedRefund.getInputs()) { + // If the multisig output is connected, the wallet will fail to deserialize + if (input.getOutpoint().getHash().equals(clientBroadcastedMultiSig.getHash())) + assertNull(input.getConnectedOutput().getSpentBy()); + input.verify(clientBroadcastedMultiSig.getOutput(0)); + } + refundFuture.set(clientBroadcastedRefund); + + // Create a block with multisig contract and refund transaction in it and give it to both wallets, + // making getBalance() include the transactions + chain.add(makeSolvedTestBlock(blockStore.getChainHead().getHeader(), multisigContract,clientBroadcastedRefund)); + + // Make sure we actually had to pay what initialize() told us we would + assertEquals(wallet.getBalance(), Utils.CENT.subtract(Transaction.REFERENCE_DEFAULT_MIN_TX_FEE.multiply(BigInteger.valueOf(2)))); + + try { + // After its expired, we cant still increment payment + clientState.incrementPaymentBy(Utils.CENT); + fail(); + } catch (IllegalStateException e) { } + + control.verify(); + } + + @Test + public void checkBadData() throws Exception { + // Check that if signatures/transactions/etc are corrupted, the protocol rejects them correctly. + + // Set up a mock peergroup. + IMocksControl control = createStrictControl(); + PeerGroup mockPeerGroup = control.createMock(PeerGroup.class); + + // We'll broadcast only one tx: multisig contract + SettableFuture multiSigFuture = SettableFuture.create(); + Capture broadcastMultiSig = new Capture(); + expect(mockPeerGroup.broadcastTransaction(capture(broadcastMultiSig))).andReturn(multiSigFuture); + control.replay(); + + Utils.rollMockClock(0); // Use mock clock + final long EXPIRE_TIME = Utils.now().getTime()/1000 + 60*60*24; + + serverState = new PaymentChannelServerState(mockPeerGroup, serverWallet, serverKey, EXPIRE_TIME); + assertEquals(PaymentChannelServerState.State.WAITING_FOR_REFUND_TRANSACTION, serverState.getState()); + + try { + clientState = new PaymentChannelClientState(wallet, myKey, new ECKey(null, + Arrays.copyOf(serverKey.getPubKey(), serverKey.getPubKey().length + 1)), halfCoin, EXPIRE_TIME); + } catch (VerificationException e) { + assertTrue(e.getMessage().contains("not canonical")); + } + + clientState = new PaymentChannelClientState(wallet, myKey, new ECKey(null, serverKey.getPubKey()), halfCoin, EXPIRE_TIME); + assertEquals(PaymentChannelClientState.State.NEW, clientState.getState()); + clientState.initiate(); + assertEquals(PaymentChannelClientState.State.INITIATED, clientState.getState()); + + // Test refund transaction with any number of issues + byte[] refundTxBytes = clientState.getIncompleteRefundTransaction().bitcoinSerialize(); + Transaction refund = new Transaction(params, refundTxBytes); + refund.addOutput(BigInteger.ZERO, new ECKey().toAddress(params)); + try { + serverState.provideRefundTransaction(refund, myKey.getPubKey()); + fail(); + } catch (VerificationException e) {} + + refund = new Transaction(params, refundTxBytes); + refund.addInput(new TransactionInput(params, refund, new byte[] {}, new TransactionOutPoint(params, 42, refund.getHash()))); + try { + serverState.provideRefundTransaction(refund, myKey.getPubKey()); + fail(); + } catch (VerificationException e) {} + + refund = new Transaction(params, refundTxBytes); + refund.setLockTime(0); + try { + serverState.provideRefundTransaction(refund, myKey.getPubKey()); + fail(); + } catch (VerificationException e) {} + + refund = new Transaction(params, refundTxBytes); + refund.getInput(0).setSequenceNumber(TransactionInput.NO_SEQUENCE); + try { + serverState.provideRefundTransaction(refund, myKey.getPubKey()); + fail(); + } catch (VerificationException e) {} + + refund = new Transaction(params, refundTxBytes); + byte[] refundSig = serverState.provideRefundTransaction(refund, myKey.getPubKey()); + try { serverState.provideRefundTransaction(refund, myKey.getPubKey()); fail(); } catch (IllegalStateException e) {} + assertEquals(PaymentChannelServerState.State.WAITING_FOR_MULTISIG_CONTRACT, serverState.getState()); + + byte[] refundSigCopy = Arrays.copyOf(refundSig, refundSig.length); + refundSigCopy[refundSigCopy.length-1] = (byte) (Transaction.SigHash.NONE.ordinal() + 1); + try { + clientState.provideRefundSignature(refundSigCopy); + fail(); + } catch (VerificationException e) { + assertTrue(e.getMessage().contains("SIGHASH_NONE")); + } + + refundSigCopy = Arrays.copyOf(refundSig, refundSig.length); + refundSigCopy[3] ^= 0x42; // Make the signature fail standard checks + try { + clientState.provideRefundSignature(refundSigCopy); + fail(); + } catch (VerificationException e) { + assertTrue(e.getMessage().contains("not canonical")); + } + + refundSigCopy = Arrays.copyOf(refundSig, refundSig.length); + refundSigCopy[10] ^= 0x42; // Flip some random bits in the signature (to make it invalid, not just nonstandard) + try { + clientState.provideRefundSignature(refundSigCopy); + fail(); + } catch (VerificationException e) { + assertFalse(e.getMessage().contains("not canonical")); + } + + refundSigCopy = Arrays.copyOf(refundSig, refundSig.length); + try { clientState.getCompletedRefundTransaction(); fail(); } catch (IllegalStateException e) {} + clientState.provideRefundSignature(refundSigCopy); + try { clientState.provideRefundSignature(refundSigCopy); fail(); } catch (IllegalStateException e) {} + assertEquals(PaymentChannelClientState.State.PROVIDE_MULTISIG_CONTRACT_TO_SERVER, clientState.getState()); + + try { clientState.incrementPaymentBy(BigInteger.ONE); fail(); } catch (IllegalStateException e) {} + + byte[] multisigContractSerialized = clientState.getMultisigContract().bitcoinSerialize(); + + Transaction multisigContract = new Transaction(params, multisigContractSerialized); + multisigContract.clearOutputs(); + multisigContract.addOutput(halfCoin, ScriptBuilder.createMultiSigOutputScript(2, Lists.newArrayList(serverKey, myKey))); + try { + serverState.provideMultiSigContract(multisigContract); + fail(); + } catch (VerificationException e) { + assertTrue(e.getMessage().contains("client and server in that order")); + } + + multisigContract = new Transaction(params, multisigContractSerialized); + multisigContract.clearOutputs(); + multisigContract.addOutput(BigInteger.ZERO, ScriptBuilder.createMultiSigOutputScript(2, Lists.newArrayList(myKey, serverKey))); + try { + serverState.provideMultiSigContract(multisigContract); + fail(); + } catch (VerificationException e) { + assertTrue(e.getMessage().contains("zero value")); + } + + multisigContract = new Transaction(params, multisigContractSerialized); + multisigContract.clearOutputs(); + multisigContract.addOutput(new TransactionOutput(params, multisigContract, halfCoin, new byte[] {0x01})); + try { + serverState.provideMultiSigContract(multisigContract); + fail(); + } catch (VerificationException e) {} + + multisigContract = new Transaction(params, multisigContractSerialized); + ListenableFuture multisigStateFuture = serverState.provideMultiSigContract(multisigContract); + try { serverState.provideMultiSigContract(multisigContract); fail(); } catch (IllegalStateException e) {} + assertEquals(PaymentChannelServerState.State.WAITING_FOR_MULTISIG_ACCEPTANCE, serverState.getState()); + assertFalse(multisigStateFuture.isDone()); + multiSigFuture.set(broadcastMultiSig.getValue()); + assertEquals(multisigStateFuture.get(), serverState); + assertEquals(PaymentChannelServerState.State.READY, serverState.getState()); + + // Both client and server are now in the ready state. Simulate a few micropayments of 0.005 bitcoins. + BigInteger size = halfCoin.divide(BigInteger.TEN).divide(BigInteger.TEN); + BigInteger totalPayment = BigInteger.ZERO; + try { + clientState.incrementPaymentBy(Utils.COIN); + fail(); + } catch (ValueOutOfRangeException e) {} + + byte[] signature = clientState.incrementPaymentBy(size); + totalPayment = totalPayment.add(size); + + byte[] signatureCopy = Arrays.copyOf(signature, signature.length); + signatureCopy[signatureCopy.length - 1] = (byte) ((Transaction.SigHash.NONE.ordinal() + 1) | 0x80); + try { + serverState.incrementPayment(halfCoin.subtract(totalPayment), signatureCopy); + fail(); + } catch (VerificationException e) {} + + signatureCopy = Arrays.copyOf(signature, signature.length); + signatureCopy[2] ^= 0x42; // Make the signature fail standard checks + try { + serverState.incrementPayment(halfCoin.subtract(totalPayment), signatureCopy); + fail(); + } catch (VerificationException e) { + assertTrue(e.getMessage().contains("not canonical")); + } + + signatureCopy = Arrays.copyOf(signature, signature.length); + signatureCopy[10] ^= 0x42; // Flip some random bits in the signature (to make it invalid, not just nonstandard) + try { + serverState.incrementPayment(halfCoin.subtract(totalPayment), signatureCopy); + fail(); + } catch (VerificationException e) { + assertFalse(e.getMessage().contains("not canonical")); + } + + serverState.incrementPayment(halfCoin.subtract(totalPayment), signature); + + // Pay the rest (signed with SIGHASH_NONE|SIGHASH_ANYONECANPAY) + byte[] signature2 = clientState.incrementPaymentBy(halfCoin.subtract(totalPayment)); + totalPayment = totalPayment.add(halfCoin.subtract(totalPayment)); + assertEquals(totalPayment, halfCoin); + + signatureCopy = Arrays.copyOf(signature, signature.length); + signatureCopy[signatureCopy.length - 1] = (byte) ((Transaction.SigHash.SINGLE.ordinal() + 1) | 0x80); + try { + serverState.incrementPayment(halfCoin.subtract(totalPayment), signatureCopy); + fail(); + } catch (VerificationException e) {} + + serverState.incrementPayment(halfCoin.subtract(totalPayment), signature2); + + serverState.incrementPayment(halfCoin.subtract(totalPayment.subtract(size)), signature); + assertEquals(serverState.getBestValueToMe(), totalPayment); + + try { + clientState.incrementPaymentBy(BigInteger.ONE.negate()); + fail(); + } catch (ValueOutOfRangeException e) {} + + try { + clientState.incrementPaymentBy(halfCoin.subtract(size).add(BigInteger.ONE)); + fail(); + } catch (ValueOutOfRangeException e) {} + + control.verify(); + } + + @Test + public void feesTest() throws Exception { + // Test that transactions are getting the necessary fees + + // Spend the client wallet's one coin + wallet.sendCoinsOffline(Wallet.SendRequest.to(new ECKey().toAddress(params), Utils.COIN)); + assertEquals(wallet.getBalance(), BigInteger.ZERO); + + chain.add(makeSolvedTestBlock(blockStore.getChainHead().getHeader(), createFakeTx(params, Utils.CENT, myAddress))); + assertEquals(wallet.getBalance(), Utils.CENT); + + // Set up a mock peergroup. + IMocksControl control = createStrictControl(); + PeerGroup mockPeerGroup = control.createMock(PeerGroup.class); + // We'll broadcast two txns: multisig contract and close transaction. + SettableFuture multiSigFuture = SettableFuture.create(); + SettableFuture closeFuture = SettableFuture.create(); + Capture broadcastMultiSig = new Capture(); + Capture broadcastClose = new Capture(); + expect(mockPeerGroup.broadcastTransaction(capture(broadcastMultiSig))).andReturn(multiSigFuture); + expect(mockPeerGroup.broadcastTransaction(capture(broadcastClose))).andReturn(closeFuture); + control.replay(); + + Utils.rollMockClock(0); // Use mock clock + final long EXPIRE_TIME = Utils.now().getTime()/1000 + 60*60*24; + + serverState = new PaymentChannelServerState(mockPeerGroup, serverWallet, serverKey, EXPIRE_TIME); + assertEquals(PaymentChannelServerState.State.WAITING_FOR_REFUND_TRANSACTION, serverState.getState()); + + // Clearly ONE is far too small to be useful + clientState = new PaymentChannelClientState(wallet, myKey, new ECKey(null, serverKey.getPubKey()), BigInteger.ONE, EXPIRE_TIME); + assertEquals(PaymentChannelClientState.State.NEW, clientState.getState()); + try { + clientState.initiate(); + fail(); + } catch (ValueOutOfRangeException e) {} + + clientState = new PaymentChannelClientState(wallet, myKey, new ECKey(null, serverKey.getPubKey()), + Transaction.MIN_NONDUST_OUTPUT.subtract(BigInteger.ONE).add(Transaction.REFERENCE_DEFAULT_MIN_TX_FEE), + EXPIRE_TIME); + assertEquals(PaymentChannelClientState.State.NEW, clientState.getState()); + try { + clientState.initiate(); + fail(); + } catch (ValueOutOfRangeException e) {} + + // Verify that MIN_NONDUST_OUTPUT + MIN_TX_FEE is accepted + clientState = new PaymentChannelClientState(wallet, myKey, new ECKey(null, serverKey.getPubKey()), + Transaction.MIN_NONDUST_OUTPUT.add(Transaction.REFERENCE_DEFAULT_MIN_TX_FEE), EXPIRE_TIME); + assertEquals(PaymentChannelClientState.State.NEW, clientState.getState()); + // We'll have to pay REFERENCE_DEFAULT_MIN_TX_FEE twice (multisig+refund), and we'll end up getting back nearly nothing... + clientState.initiate(); + assertEquals(clientState.getRefundTxFees(), Transaction.REFERENCE_DEFAULT_MIN_TX_FEE.multiply(BigInteger.valueOf(2))); + assertEquals(PaymentChannelClientState.State.INITIATED, clientState.getState()); + + // Now actually use a more useful CENT + clientState = new PaymentChannelClientState(wallet, myKey, new ECKey(null, serverKey.getPubKey()), Utils.CENT, EXPIRE_TIME); + assertEquals(PaymentChannelClientState.State.NEW, clientState.getState()); + clientState.initiate(); + assertEquals(clientState.getRefundTxFees(), BigInteger.ZERO); + assertEquals(PaymentChannelClientState.State.INITIATED, clientState.getState()); + + // Send the refund tx from client to server and get back the signature. + Transaction refund = new Transaction(params, clientState.getIncompleteRefundTransaction().bitcoinSerialize()); + byte[] refundSig = serverState.provideRefundTransaction(refund, myKey.getPubKey()); + assertEquals(PaymentChannelServerState.State.WAITING_FOR_MULTISIG_CONTRACT, serverState.getState()); + // This verifies that the refund can spend the multi-sig output when run. + clientState.provideRefundSignature(refundSig); + assertEquals(PaymentChannelClientState.State.PROVIDE_MULTISIG_CONTRACT_TO_SERVER, clientState.getState()); + + // Get the multisig contract + Transaction multisigContract = new Transaction(params, clientState.getMultisigContract().bitcoinSerialize()); + assertEquals(PaymentChannelClientState.State.READY, clientState.getState()); + + // Provide the server with the multisig contract and simulate successful propagation/acceptance. + serverState.provideMultiSigContract(multisigContract); + assertEquals(PaymentChannelServerState.State.WAITING_FOR_MULTISIG_ACCEPTANCE, serverState.getState()); + multiSigFuture.set(broadcastMultiSig.getValue()); + assertEquals(PaymentChannelServerState.State.READY, serverState.getState()); + + // Both client and server are now in the ready state. Simulate a few micropayments + BigInteger totalPayment = BigInteger.ZERO; + + // We can send as little as we want - its up to the server to get the fees right + byte[] signature = clientState.incrementPaymentBy(BigInteger.ONE); + totalPayment = totalPayment.add(BigInteger.ONE); + serverState.incrementPayment(Utils.CENT.subtract(totalPayment), signature); + + // We can't refund more than the contract is worth... + try { + serverState.incrementPayment(Utils.CENT.add(BigInteger.ONE), signature); + fail(); + } catch (ValueOutOfRangeException e) {} + + // We cannot, however, send just under the total value - our refund would make it unspendable + try { + clientState.incrementPaymentBy(Utils.CENT.subtract(Transaction.MIN_NONDUST_OUTPUT)); + fail(); + } catch (ValueOutOfRangeException e) {} + // The server also won't accept it if we do that + try { + serverState.incrementPayment(Transaction.MIN_NONDUST_OUTPUT.subtract(BigInteger.ONE), signature); + fail(); + } catch (ValueOutOfRangeException e) {} + + signature = clientState.incrementPaymentBy(Utils.CENT.subtract(BigInteger.ONE)); + totalPayment = totalPayment.add(Utils.CENT.subtract(BigInteger.ONE)); + assertEquals(totalPayment, Utils.CENT); + serverState.incrementPayment(Utils.CENT.subtract(totalPayment), signature); + + // And close the channel. + serverState.close(); + assertEquals(PaymentChannelServerState.State.CLOSING, serverState.getState()); + Transaction closeTx = broadcastClose.getValue(); + closeFuture.set(closeTx); + assertEquals(PaymentChannelServerState.State.CLOSED, serverState.getState()); + serverState.close(); + assertEquals(PaymentChannelServerState.State.CLOSED, serverState.getState()); + control.verify(); + } + + @Test + public void serverAddsFeeTest() throws Exception { + // Test that the server properly adds the necessary fee at the end (or just drops the payment if its not worth it) + + // Set up a mock peergroup. + IMocksControl control = createStrictControl(); + PeerGroup mockPeerGroup = control.createMock(PeerGroup.class); + // We'll broadcast two txns: multisig contract and close transaction. + SettableFuture multiSigFuture = SettableFuture.create(); + SettableFuture closeFuture = SettableFuture.create(); + Capture broadcastMultiSig = new Capture(); + Capture broadcastClose = new Capture(); + expect(mockPeerGroup.broadcastTransaction(capture(broadcastMultiSig))).andReturn(multiSigFuture); + expect(mockPeerGroup.broadcastTransaction(capture(broadcastClose))).andReturn(closeFuture); + control.replay(); + + Utils.rollMockClock(0); // Use mock clock + final long EXPIRE_TIME = Utils.now().getTime()/1000 + 60*60*24; + + serverState = new PaymentChannelServerState(mockPeerGroup, serverWallet, serverKey, EXPIRE_TIME); + assertEquals(PaymentChannelServerState.State.WAITING_FOR_REFUND_TRANSACTION, serverState.getState()); + + clientState = new PaymentChannelClientState(wallet, myKey, new ECKey(null, serverKey.getPubKey()), Utils.CENT, EXPIRE_TIME); + assertEquals(PaymentChannelClientState.State.NEW, clientState.getState()); + clientState.initiate(); + assertEquals(PaymentChannelClientState.State.INITIATED, clientState.getState()); + + // Send the refund tx from client to server and get back the signature. + Transaction refund = new Transaction(params, clientState.getIncompleteRefundTransaction().bitcoinSerialize()); + byte[] refundSig = serverState.provideRefundTransaction(refund, myKey.getPubKey()); + assertEquals(PaymentChannelServerState.State.WAITING_FOR_MULTISIG_CONTRACT, serverState.getState()); + // This verifies that the refund can spend the multi-sig output when run. + clientState.provideRefundSignature(refundSig); + assertEquals(PaymentChannelClientState.State.PROVIDE_MULTISIG_CONTRACT_TO_SERVER, clientState.getState()); + + // Validate the multisig contract looks right. + Transaction multisigContract = new Transaction(params, clientState.getMultisigContract().bitcoinSerialize()); + assertEquals(PaymentChannelClientState.State.READY, clientState.getState()); + assertEquals(2, multisigContract.getOutputs().size()); // One multi-sig, one change. + Script script = multisigContract.getOutput(0).getScriptPubKey(); + assertTrue(script.isSentToMultiSig()); + script = multisigContract.getOutput(1).getScriptPubKey(); + assertTrue(script.isSentToAddress()); + assertTrue(wallet.getPendingTransactions().contains(multisigContract)); + + // Provide the server with the multisig contract and simulate successful propagation/acceptance. + serverState.provideMultiSigContract(multisigContract); + assertEquals(PaymentChannelServerState.State.WAITING_FOR_MULTISIG_ACCEPTANCE, serverState.getState()); + multiSigFuture.set(broadcastMultiSig.getValue()); + assertEquals(PaymentChannelServerState.State.READY, serverState.getState()); + + // Both client and server are now in the ready state, split the channel in half + byte[] signature = clientState.incrementPaymentBy(Transaction.REFERENCE_DEFAULT_MIN_TX_FEE.subtract(BigInteger.ONE)); + BigInteger totalRefund = Utils.CENT.subtract(Transaction.REFERENCE_DEFAULT_MIN_TX_FEE.subtract(BigInteger.ONE)); + serverState.incrementPayment(totalRefund, signature); + + // We need to pay MIN_TX_FEE, but we only have MIN_NONDUST_OUTPUT + try { + serverState.close(); + fail(); + } catch (ValueOutOfRangeException e) { + assertTrue(e.getMessage().contains("unable to pay required fee")); + } + + // Now give the server enough coins to pay the fee + StoredBlock block = new StoredBlock(makeSolvedTestBlock(blockStore, new ECKey().toAddress(params)), BigInteger.ONE, 1); + Transaction tx1 = createFakeTx(params, Utils.COIN, serverKey.toAddress(params)); + serverWallet.receiveFromBlock(tx1, block, AbstractBlockChain.NewBlockType.BEST_CHAIN); + + // The contract is still not worth redeeming - its worth less than we pay in fee + try { + serverState.close(); + fail(); + } catch (ValueOutOfRangeException e) { + assertTrue(e.getMessage().contains("more in fees than the channel was worth")); + } + + signature = clientState.incrementPaymentBy(BigInteger.ONE.shiftLeft(1)); + totalRefund = totalRefund.subtract(BigInteger.ONE.shiftLeft(1)); + serverState.incrementPayment(totalRefund, signature); + + // And close the channel. + serverState.close(); + assertEquals(PaymentChannelServerState.State.CLOSING, serverState.getState()); + Transaction closeTx = broadcastClose.getValue(); + closeFuture.set(closeTx); + assertEquals(PaymentChannelServerState.State.CLOSED, serverState.getState()); + control.verify(); + } +} diff --git a/examples/src/main/java/com/google/bitcoin/examples/ExamplePaymentChannelClient.java b/examples/src/main/java/com/google/bitcoin/examples/ExamplePaymentChannelClient.java new file mode 100644 index 00000000..940e4835 --- /dev/null +++ b/examples/src/main/java/com/google/bitcoin/examples/ExamplePaymentChannelClient.java @@ -0,0 +1,137 @@ +/* + * Copyright 2013 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.bitcoin.examples; + +import java.io.File; +import java.math.BigInteger; +import java.net.InetSocketAddress; + + +import com.google.bitcoin.core.*; +import com.google.bitcoin.kits.WalletAppKit; +import com.google.bitcoin.params.TestNet3Params; +import com.google.bitcoin.protocols.channels.PaymentChannelClientConnection; +import com.google.bitcoin.protocols.channels.StoredPaymentChannelClientStates; +import com.google.bitcoin.protocols.channels.ValueOutOfRangeException; +import com.google.bitcoin.utils.BriefLogFormatter; +import com.google.common.util.concurrent.*; +import org.slf4j.LoggerFactory; + +import static java.util.concurrent.TimeUnit.MILLISECONDS; + +/** + * Simple client that connects to the given host, opens a channel, and pays one cent. + */ +public class ExamplePaymentChannelClient { + private static final org.slf4j.Logger log = LoggerFactory.getLogger(ExamplePaymentChannelClient.class); + private WalletAppKit appKit; + private final BigInteger maxAcceptableRequestedAmount; + private final ECKey myKey; + private final NetworkParameters params; + + public static void main(String[] args) throws Exception { + BriefLogFormatter.init(); + System.out.println("USAGE: host"); + new ExamplePaymentChannelClient().run(args[0]); + } + + public ExamplePaymentChannelClient() { + maxAcceptableRequestedAmount = Utils.COIN; + myKey = new ECKey(); + params = TestNet3Params.get(); + } + + public void run(final String host) throws Exception { + // Bring up all the objects we need, create/load a wallet, sync the chain, etc. We override WalletAppKit so we + // can customize it by adding the extension objects - we have to do this before the wallet file is loaded so + // the plugin that knows how to parse all the additional data is present during the load. + appKit = new WalletAppKit(params, new File("."), "payment_channel_example_client") { + @Override + protected void addWalletExtensions() { + // 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. + wallet().addExtension(new StoredPaymentChannelClientStates(peerGroup(), wallet())); + } + }; + appKit.startAndWait(); + // We now have active network connections and a fully synced wallet. + // Add a new key which will be used for the multisig contract. + appKit.wallet().addKey(myKey); + + // Create the object which manages the payment channels protocol, client side. Tell it where the server to + // connect to is, along with some reasonable network timeouts, the wallet and our temporary key. We also have + // to pick an amount of value to lock up for the duration of the channel. + // + // Note that this may or may not actually construct a new channel. If an existing unclosed channel is found in + // the wallet, then it'll re-use that one instead. + final int timeoutSecs = 15; + final InetSocketAddress server = new InetSocketAddress(host, 4242); + PaymentChannelClientConnection client = null; + + while (client == null) { + try { + final String channelID = host; + client = new PaymentChannelClientConnection( + server, timeoutSecs, appKit.wallet(), myKey, maxAcceptableRequestedAmount, channelID); + } catch (ValueOutOfRangeException e) { + // We don't have enough money in our wallet yet. Wait and try again. + waitForSufficientBalance(maxAcceptableRequestedAmount); + } + } + + // Opening the channel requires talking to the server, so it's asynchronous. + Futures.addCallback(client.getChannelOpenFuture(), new FutureCallback() { + @Override + public void onSuccess(PaymentChannelClientConnection client) { + // Success! We should be able to try making micropayments now. Try doing it 10 times. + for (int i = 0; i < 10; i++) { + try { + client.incrementPayment(Utils.CENT); + } catch (ValueOutOfRangeException e) { + log.error("Failed to increment payment by a CENT, remaining value is {}", client.state().getValueRefunded()); + System.exit(-3); + } + log.info("Successfully sent payment of one CENT, total remaining on channel is now {}", client.state().getValueRefunded()); + Uninterruptibles.sleepUninterruptibly(500, MILLISECONDS); + } + // Now tell the server we're done so they should broadcast the final transaction and refund us what's + // left. If we never do this then eventually the server will time out and do it anyway and if the + // server goes away for longer, then eventually WE will time out and the refund tx will get broadcast + // by ourselves. + log.info("Closing channel!"); + client.close(); + } + + @Override + public void onFailure(Throwable throwable) { + log.error("Failed to open connection", throwable); + } + }); + } + + private void waitForSufficientBalance(BigInteger amount) { + // Not enough money in the wallet. + BigInteger amountPlusFee = amount.add(Wallet.SendRequest.DEFAULT_FEE_PER_KB); + ListenableFuture balanceFuture = appKit.wallet().getBalanceFuture(amountPlusFee, Wallet.BalanceType.AVAILABLE); + if (!balanceFuture.isDone()) { + System.out.println("Please send " + Utils.bitcoinValueToFriendlyString(amountPlusFee) + + " BTC to " + myKey.toAddress(params)); + Futures.getUnchecked(balanceFuture); + } + } +} diff --git a/examples/src/main/java/com/google/bitcoin/examples/ExamplePaymentChannelServer.java b/examples/src/main/java/com/google/bitcoin/examples/ExamplePaymentChannelServer.java new file mode 100644 index 00000000..317ae86a --- /dev/null +++ b/examples/src/main/java/com/google/bitcoin/examples/ExamplePaymentChannelServer.java @@ -0,0 +1,107 @@ +/* + * Copyright 2013 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.bitcoin.examples; + +import java.io.File; +import java.math.BigInteger; +import java.net.SocketAddress; + +import com.google.bitcoin.core.NetworkParameters; +import com.google.bitcoin.core.Sha256Hash; +import com.google.bitcoin.core.Utils; +import com.google.bitcoin.core.VerificationException; +import com.google.bitcoin.kits.WalletAppKit; +import com.google.bitcoin.params.TestNet3Params; +import com.google.bitcoin.protocols.channels.*; +import com.google.bitcoin.utils.BriefLogFormatter; +import org.slf4j.LoggerFactory; + +/** + * Simple server that listens on port 4242 for incoming payment channels. + */ +public class ExamplePaymentChannelServer implements PaymentChannelServerListener.HandlerFactory { + private static final org.slf4j.Logger log = LoggerFactory.getLogger(ExamplePaymentChannelServer.class); + + private StoredPaymentChannelServerStates storedStates; + private WalletAppKit appKit; + + public static void main(String[] args) throws Exception { + BriefLogFormatter.init(); + new ExamplePaymentChannelServer().run(); + } + + public void run() throws Exception { + NetworkParameters params = TestNet3Params.get(); + + // Bring up all the objects we need, create/load a wallet, sync the chain, etc. We override WalletAppKit so we + // can customize it by adding the extension objects - we have to do this before the wallet file is loaded so + // the plugin that knows how to parse all the additional data is present during the load. + appKit = new WalletAppKit(params, new File("."), "payment_channel_example_server") { + @Override + protected void addWalletExtensions() { + // 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. + storedStates = new StoredPaymentChannelServerStates(wallet(), peerGroup()); + wallet().addExtension(storedStates); + } + }; + appKit.startAndWait(); + + // We provide a peer group, a wallet, a timeout in seconds, the amount we require to start a channel and + // an implementation of HandlerFactory, which we just implement ourselves. + new PaymentChannelServerListener(appKit.peerGroup(), appKit.wallet(), 15, Utils.COIN, this).bindAndStart(4242); + } + + @Override + public ServerConnectionEventHandler onNewConnection(final SocketAddress clientAddress) { + // Each connection needs a handler which is informed when that payment channel gets adjusted. Here we just log + // things. In a real app this object would be connected to some business logic. + return new ServerConnectionEventHandler() { + @Override + public void channelOpen(Sha256Hash channelId) { + log.info("Channel open for {}: {}.", clientAddress, channelId); + + // Try to get the state object from the stored state set in our wallet + PaymentChannelServerState state = null; + try { + state = storedStates.getChannel(channelId).getState(appKit.wallet(), appKit.peerGroup()); + } catch (VerificationException e) { + // This indicates corrupted data, and since the channel was just opened, cannot happen + throw new RuntimeException(e); + } + log.info(" with a maximum value of {}, expiring at UNIX timestamp {}.", + // The channel's maximum value is the value of the multisig contract which locks in some + // amount of money to the channel + state.getMultisigContract().getOutput(0).getValue(), + // The channel expires at some offset from when the client's refund transaction becomes + // spendable. + state.getRefundTransactionUnlockTime() + StoredPaymentChannelServerStates.CHANNEL_EXPIRE_OFFSET); + } + + @Override + public void paymentIncrease(BigInteger by, BigInteger to) { + log.info("Client {} paid increased payment by {} for a total of " + to.toString(), clientAddress, by); + } + + @Override + public void channelClosed(PaymentChannelCloseException.CloseReason reason) { + log.info("Client {} closed channel for reason {}", clientAddress, reason); + } + }; + } +}