mirror of
https://github.com/Qortal/altcoinj.git
synced 2025-02-07 14:54:15 +00:00
Implement server-side and client-side payment channel protocols.
This implements micropayment payment channels in several parts: * Adds PaymentChannel[Server|Client]State state machines which handle initialization of the payment channel, keep track of basic in-memory state, and check data received from the other side, based on Mike Hearn's initial implementation. * StoredPaymentChannel[Client|Server]States manage channel timeout+broadcasting of relevant transactions at that time, keeping track of state objects which allow for channel resume, and are saved/loaded as a WalletExtension. * Adds PaymentChannel[Client|Server] which manage a connection by getting new protobufs, generating protobufs for the other side, properly stepping the associated State object and ensuring the StoredStates object is properly used to save state in the wallet. * Adds PaymentChannel[ClientConnection|ServerListener] which create TCP sockets to each other and use PaymentChannel[Client|Server] objects to create/use payment channels. The algorithm implemented is the one described at https://en.bitcoin.it/wiki/Contracts#Example_7:_Rapidly-adjusted_.28micro.29payments_to_a_pre-determined_party with a slight tweak to use looser SIGHASH flags so that the Wallet.completeTx code can work its magic by adding more inputs if it saves on fees. Thanks to Mike Hearn for the initial state machine implementations and all his contracts work and Jeremy Spilman for suggesting the protocol modification that works with non-standard nLockTime Transactions.
This commit is contained in:
parent
3d74934b6f
commit
4908c241f7
@ -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;
|
||||
|
||||
/**
|
||||
* <p>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.</p>
|
||||
*
|
||||
* <p>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.</p>
|
||||
*/
|
||||
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 {
|
||||
/**
|
||||
* <p>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.</p>
|
||||
*
|
||||
* <p>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 <i>after</i> the failed payment.</p>
|
||||
*
|
||||
* <p>Called while holding a lock on the {@link PaymentChannelClient} object - be careful about reentrancy</p>
|
||||
*/
|
||||
public void sendToServer(Protos.TwoWayChannelMessage msg);
|
||||
|
||||
/**
|
||||
* <p>Requests that the connection to the server be closed</p>
|
||||
*
|
||||
* <p>Called while holding a lock on the {@link PaymentChannelClient} object - be careful about reentrancy</p>
|
||||
*
|
||||
* @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);
|
||||
|
||||
/**
|
||||
* <p>Indicates the channel has been successfully opened and
|
||||
* {@link PaymentChannelClient#incrementPayment(java.math.BigInteger)} may be called at will.</p>
|
||||
*
|
||||
* <p>Called while holding a lock on the {@link PaymentChannelClient} object - be careful about reentrancy</p>
|
||||
*/
|
||||
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;
|
||||
|
||||
/**
|
||||
* <p>The maximum amount of time for which we will accept the server locking up our funds for the multisig
|
||||
* contract.</p>
|
||||
*
|
||||
* <p>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).</p>
|
||||
*
|
||||
* <p>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.</p>
|
||||
*/
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Called when the connection terminates. Notifies the {@link StoredClientChannel} object that we can attempt to
|
||||
* resume this channel in the future and stops generating messages for the server.</p>
|
||||
*
|
||||
* <p>Note that this <b>MUST</b> still be called even after either
|
||||
* {@link ClientConnection#destroyConnection(CloseReason)} or
|
||||
* {@link PaymentChannelClient#close()} is called to actually handle the connection close logic.</p>
|
||||
*/
|
||||
public void connectionClosed() {
|
||||
lock.lock();
|
||||
try {
|
||||
connectionOpen = false;
|
||||
if (state != null)
|
||||
state.disconnectFromChannel();
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Closes the connection, notifying the server it should close the channel by broadcasting the most recent payment
|
||||
* transaction.</p>
|
||||
*
|
||||
* <p>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.</p>
|
||||
*
|
||||
* @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();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Called to indicate the connection has been opened and messages can now be generated for the server.</p>
|
||||
*
|
||||
* <p>Attempts to find a channel to resume and generates a CLIENT_VERSION message for the server based on the
|
||||
* result.</p>
|
||||
*/
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Gets the {@link PaymentChannelClientState} object which stores the current state of the connection with the
|
||||
* server.</p>
|
||||
*
|
||||
* <p>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.</p>
|
||||
*/
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
@ -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<PaymentChannelClientConnection> channelOpenFuture = SettableFuture.create();
|
||||
|
||||
private final PaymentChannelClient channelClient;
|
||||
private final ProtobufParser<Protos.TwoWayChannelMessage> 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<Protos.TwoWayChannelMessage>(new ProtobufParser.Listener<Protos.TwoWayChannelMessage>() {
|
||||
@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);
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>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.</p>
|
||||
*
|
||||
* <p>After this future completes successfully, you may call
|
||||
* {@link PaymentChannelClientConnection#incrementPayment(java.math.BigInteger)} to begin paying the server.</p>
|
||||
*/
|
||||
public ListenableFuture<PaymentChannelClientConnection> 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);
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Gets the {@link PaymentChannelClientState} object which stores the current state of the connection with the
|
||||
* server.</p>
|
||||
*
|
||||
* <p>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.</p>
|
||||
*/
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
@ -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.*;
|
||||
|
||||
/**
|
||||
* <p>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.</p>
|
||||
*
|
||||
* <p>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 <i>client</i> or <i>initiating party</i>. The party that is receiving the funds is the <i>server</i> or
|
||||
* <i>receiving party</i>. Although the underlying Bitcoin protocol is capable of more complex relationships than that,
|
||||
* this class implements only the simplest case.</p>
|
||||
*
|
||||
* <p>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.</p>
|
||||
*
|
||||
* <p>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.
|
||||
* </p>
|
||||
*/
|
||||
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<ECKey> 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>When the servers signature for the refund transaction is received, call this to verify it and sign the
|
||||
* complete refund ourselves.</p>
|
||||
*
|
||||
* <p>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.</p>
|
||||
*/
|
||||
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");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>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.</p>
|
||||
*
|
||||
* <p>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.</p>
|
||||
*
|
||||
* <p>To spend the whole channel increment by {@link PaymentChannelClientState#getTotalValue()} -
|
||||
* {@link PaymentChannelClientState#getValueRefunded()}</p>
|
||||
*
|
||||
* @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;
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Stores this channel's state in the wallet as a part of a {@link StoredPaymentChannelClientStates} wallet
|
||||
* extension and keeps it up-to-date each time payment is incremented. This allows the
|
||||
* {@link StoredPaymentChannelClientStates} object to keep track of timeouts and broadcast the refund transaction
|
||||
* when the channel expires.</p>
|
||||
*
|
||||
* <p>A channel may only be stored after it has fully opened (ie state == State.READY). The wallet provided in the
|
||||
* constructor must already have a {@link StoredPaymentChannelClientStates} object in its extensions set.</p>
|
||||
*
|
||||
* @param id A hash providing this channel with an id which uniquely identifies this server. It does not have to be
|
||||
* unique.
|
||||
*/
|
||||
public synchronized void storeChannelInWallet(Sha256Hash id) {
|
||||
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;
|
||||
}
|
||||
}
|
@ -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
|
||||
|
||||
/**
|
||||
* <p>The {@link com.google.bitcoin.protocols.channels.PaymentChannelClient#close()} method was called or the
|
||||
* client sent a CLOSE message.</p>
|
||||
* <p>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.</p>
|
||||
*/
|
||||
CLIENT_REQUESTED_CLOSE,
|
||||
|
||||
/**
|
||||
* <p>The {@link com.google.bitcoin.protocols.channels.PaymentChannelServer#close()} method was called or server
|
||||
* sent a CLOSE message.</p>
|
||||
*
|
||||
* <p>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}).</p>
|
||||
*
|
||||
* <p>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)</p>
|
||||
*/
|
||||
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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
||||
/**
|
||||
* <p>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.</p>
|
||||
*
|
||||
* <p>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.</p>
|
||||
*/
|
||||
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 {
|
||||
/**
|
||||
* <p>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.</p>
|
||||
*
|
||||
* <p>If the send fails, no exception should be thrown, however
|
||||
* {@link PaymentChannelServer#connectionClosed()} should be called immediately.</p>
|
||||
*
|
||||
* <p>Called while holding a lock on the {@link PaymentChannelServer} object - be careful about reentrancy</p>
|
||||
*/
|
||||
public void sendToClient(Protos.TwoWayChannelMessage msg);
|
||||
|
||||
/**
|
||||
* <p>Requests that the connection to the client be closed</p>
|
||||
*
|
||||
* <p>Called while holding a lock on the {@link PaymentChannelServer} object - be careful about reentrancy</p>
|
||||
*
|
||||
* @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);
|
||||
|
||||
/**
|
||||
* <p>Triggered when the channel is opened and payments can begin</p>
|
||||
*
|
||||
* <p>Called while holding a lock on the {@link PaymentChannelServer} object - be careful about reentrancy</p>
|
||||
*
|
||||
* @param contractHash A unique identifier which represents this channel (actually the hash of the multisig contract)
|
||||
*/
|
||||
public void channelOpen(Sha256Hash contractHash);
|
||||
|
||||
/**
|
||||
* <p>Called when the payment in this channel was successfully incremented by the client</p>
|
||||
*
|
||||
* <p>Called while holding a lock on the {@link PaymentChannelServer} object - be careful about reentrancy</p>
|
||||
*
|
||||
* @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;
|
||||
|
||||
/**
|
||||
* <p>The amount of time we request the client lock in their funds.</p>
|
||||
*
|
||||
* <p>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.</p>
|
||||
*/
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Called when the connection terminates. Notifies the {@link StoredServerChannel} object that we can attempt to
|
||||
* resume this channel in the future and stops generating messages for the client.</p>
|
||||
*
|
||||
* <p>Note that this <b>MUST</b> still be called even after either
|
||||
* {@link ServerConnection#destroyConnection(CloseReason)} or
|
||||
* {@link PaymentChannelServer#close()} is called to actually handle the connection close logic.</p>
|
||||
*/
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>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</p>
|
||||
*
|
||||
* <p>Note that {@link PaymentChannelServer#connectionClosed()} must still be called after the connection fully
|
||||
* closes.</p>
|
||||
*/
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
@ -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<Protos.TwoWayChannelMessage>() {
|
||||
@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<Protos.TwoWayChannelMessage>
|
||||
(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<Protos.TwoWayChannelMessage> socketProtobufHandler;
|
||||
|
||||
// The listener which connects to socketProtobufHandler
|
||||
private final ProtobufParser.Listener<Protos.TwoWayChannelMessage> 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;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Closes all client connections currently connected gracefully.</p>
|
||||
*
|
||||
* <p>Note that this does <i>not</i> 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.</p>
|
||||
*/
|
||||
public void close() {
|
||||
try {
|
||||
server.stop();
|
||||
} catch (InterruptedException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
}
|
@ -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.*;
|
||||
|
||||
/**
|
||||
* <p>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.</p>
|
||||
*
|
||||
* <p>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 <i>client</i> or <i>initiating party</i>. The party that is receiving the funds is the
|
||||
* <i>server</i> or <i>receiving party</i>. Although the underlying Bitcoin protocol is capable of more complex
|
||||
* relationships than that, this class implements only the simplest case.</p>
|
||||
*
|
||||
* <p>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).</p>
|
||||
*
|
||||
* <p>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.</p>
|
||||
*/
|
||||
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<PaymentChannelServerState> 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<PaymentChannelServerState> future = SettableFuture.create();
|
||||
Futures.addCallback(peerGroup.broadcastTransaction(multisigContract), new FutureCallback<Transaction>() {
|
||||
@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<PaymentChannelServerState> closedFuture = SettableFuture.create();
|
||||
/**
|
||||
* <p>Closes this channel and broadcasts the highest value payment transaction on the network.</p>
|
||||
*
|
||||
* <p>This will set the state to {@link State#CLOSED} if the transaction is successfully broadcast on the network.
|
||||
* If we fail to broadcast for some reason, the state is set to {@link State#ERROR}.</p>
|
||||
*
|
||||
* <p>If the current state is before {@link State#READY} (ie we have not finished initializing the channel), we
|
||||
* simply set the state to {@link State#CLOSED} and let the client handle getting its refund transaction confirmed.
|
||||
* </p>
|
||||
*
|
||||
* @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<PaymentChannelServerState> 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<Transaction> future = peerGroup.broadcastTransaction(tx);
|
||||
Futures.addCallback(future, new FutureCallback<Transaction>() {
|
||||
@Override public void onSuccess(Transaction transaction) {
|
||||
log.info("TX {} propagated, channel successfully closed.", transaction.getHash());
|
||||
state = State.CLOSED;
|
||||
closedFuture.set(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);
|
||||
}
|
||||
}
|
@ -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; }
|
||||
|
||||
/**
|
||||
* <p>Closes the channel with the client (will generate a
|
||||
* {@link ServerConnectionEventHandler#channelClosed(PaymentChannelCloseException.CloseReason)} event)</p>
|
||||
*
|
||||
* <p>Note that this does <i>NOT</i> 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)}</p>
|
||||
*/
|
||||
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);
|
||||
|
||||
/**
|
||||
* <p>Called when the channel was closed for some reason. May be called without a call to
|
||||
* {@link ServerConnectionEventHandler#channelOpen(Sha256Hash)}.</p>
|
||||
*
|
||||
* <p>Note that the same channel can be reopened at any point before it expires if the client reconnects and
|
||||
* requests it.</p>
|
||||
*/
|
||||
public abstract void channelClosed(PaymentChannelCloseException.CloseReason reason);
|
||||
}
|
@ -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.
|
@ -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<Sha256Hash, StoredClientChannel> 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<StoredClientChannel> 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<StoredClientChannel> 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);
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Removes the channel with the given id from this set of stored states and notifies the wallet of an update to
|
||||
* this wallet extension.</p>
|
||||
*
|
||||
* <p>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.</p>
|
||||
*/
|
||||
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;
|
||||
}
|
||||
}
|
@ -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<Sha256Hash, StoredServerChannel> mapChannels = new HashMap<Sha256Hash, StoredServerChannel>();
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>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.</p>
|
||||
*
|
||||
* <p>Removes the given channel from this set of {@link StoredServerChannel}s and notifies the wallet of a change to
|
||||
* this wallet extension.</p>
|
||||
*/
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Puts the given channel in the channels map and automatically closes it 2 hours before its refund transaction
|
||||
* becomes spendable.</p>
|
||||
*
|
||||
* <p>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.</p>
|
||||
*/
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Updates the best value to the server to the given newValue and newSignature without any checking.</p>
|
||||
* <p>Does <i>NOT</i> notify the wallet of an update to the {@link com.google.bitcoin.protocols.channels.StoredPaymentChannelServerStates}.</p>
|
||||
*/
|
||||
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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
5265
core/src/main/java/org/bitcoin/paymentchannel/Protos.java
Normal file
5265
core/src/main/java/org/bitcoin/paymentchannel/Protos.java
Normal file
File diff suppressed because it is too large
Load Diff
211
core/src/paymentchannel.proto
Normal file
211
core/src/paymentchannel.proto
Normal file
@ -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
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -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<Transaction> 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<Transaction> multiSigFuture = SettableFuture.create();
|
||||
SettableFuture<Transaction> closeFuture = SettableFuture.create();
|
||||
Capture<Transaction> broadcastMultiSig = new Capture<Transaction>();
|
||||
Capture<Transaction> broadcastClose = new Capture<Transaction>();
|
||||
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<Transaction> 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<Transaction> serverMultiSigFuture = SettableFuture.create();
|
||||
SettableFuture<Transaction> paymentFuture = SettableFuture.create();
|
||||
SettableFuture<Transaction> clientMultiSigFuture = SettableFuture.create();
|
||||
SettableFuture<Transaction> refundFuture = SettableFuture.create();
|
||||
Capture<Transaction> serverBroadcastMultiSig = new Capture<Transaction>();
|
||||
Capture<Transaction> broadcastPayment = new Capture<Transaction>();
|
||||
Capture<Transaction> clientBroadcastMultiSig = new Capture<Transaction>();
|
||||
Capture<Transaction> broadcastRefund = new Capture<Transaction>();
|
||||
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<Transaction> 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<Transaction> multiSigFuture = SettableFuture.create();
|
||||
Capture<Transaction> broadcastMultiSig = new Capture<Transaction>();
|
||||
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<PaymentChannelServerState> 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<Transaction> multiSigFuture = SettableFuture.create();
|
||||
SettableFuture<Transaction> closeFuture = SettableFuture.create();
|
||||
Capture<Transaction> broadcastMultiSig = new Capture<Transaction>();
|
||||
Capture<Transaction> broadcastClose = new Capture<Transaction>();
|
||||
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<Transaction> multiSigFuture = SettableFuture.create();
|
||||
SettableFuture<Transaction> closeFuture = SettableFuture.create();
|
||||
Capture<Transaction> broadcastMultiSig = new Capture<Transaction>();
|
||||
Capture<Transaction> broadcastClose = new Capture<Transaction>();
|
||||
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();
|
||||
}
|
||||
}
|
@ -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<PaymentChannelClientConnection>() {
|
||||
@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<BigInteger> 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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user