3
0
mirror of https://github.com/Qortal/altcoinj.git synced 2025-02-13 02:35:52 +00:00

Payment channels: Added ClientChannelProperties and ServerChannelProperties to allow configuration of the channels.

This commit is contained in:
cyberzac 2016-04-20 09:39:34 +02:00
parent 7e3dbd5f5d
commit 2e0e81d3a2
12 changed files with 307 additions and 211 deletions

View File

@ -23,6 +23,7 @@ import com.google.common.util.concurrent.ListenableFuture;
import com.google.protobuf.ByteString;
import org.bitcoin.paymentchannel.Protos;
import org.bitcoinj.wallet.SendRequest;
import org.spongycastle.crypto.params.KeyParameter;
import javax.annotation.Nullable;
@ -147,6 +148,41 @@ public interface IPaymentChannelClient {
void channelOpen(boolean wasInitiated);
}
/**
* Set Client payment channel properties.
*/
interface ClientChannelProperties {
/**
* Modify the sendRequest used for the contract.
* @param sendRequest the current sendRequest.
* @return the modified sendRequest.
*/
SendRequest modifyContractSendRequest(SendRequest sendRequest);
/**
* The maximum acceptable min payment. If the server suggests a higher amount
* the channel creation will be aborted.
*/
Coin acceptableMinPayment();
/**
* The time in seconds, relative to now, on how long this channel should be kept open. Note that is is
* a proposal to the server. The server may in turn propose something different.
* See {@link org.bitcoinj.protocols.channels.IPaymentChannelClient.ClientConnection#acceptExpireTime(long)}
*
*/
long timeWindow();
/**
* An enum indicating which versions to support:
* VERSION_1: use only version 1 of the protocol
* VERSION_2_ALLOW_1: suggest version 2 but allow downgrade to version 1
* VERSION_2: suggest version 2 and enforce use of version 2
*
*/
PaymentChannelClient.VersionSelector versionSelector();
}
/**
* An implementor of this interface creates payment channel clients that "talk back" with the given connection.
* The client might be a PaymentChannelClient, or an RPC interface, or something else entirely.

View File

@ -20,6 +20,7 @@ package org.bitcoinj.protocols.channels;
import org.bitcoinj.core.*;
import org.bitcoinj.protocols.channels.PaymentChannelCloseException.CloseReason;
import org.bitcoinj.utils.Threading;
import org.bitcoinj.wallet.SendRequest;
import org.bitcoinj.wallet.Wallet;
import com.google.common.annotations.VisibleForTesting;
@ -56,6 +57,7 @@ public class PaymentChannelClient implements IPaymentChannelClient {
private static final org.slf4j.Logger log = LoggerFactory.getLogger(PaymentChannelClient.class);
protected final ReentrantLock lock = Threading.lock("channelclient");
protected final ClientChannelProperties clientChannelProperties;
// Used to track the negotiated version number
@GuardedBy("lock") private int majorVersion;
@ -167,36 +169,8 @@ public class PaymentChannelClient implements IPaymentChannelClient {
* @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, Coin maxValue, Sha256Hash serverId,
ClientConnection conn) {
this(wallet,myKey,maxValue,serverId, conn, VersionSelector.VERSION_2_ALLOW_1);
}
/**
* Constructs a new channel manager which waits for {@link PaymentChannelClient#connectionOpen()} before acting.
* A default time window of {@link #DEFAULT_TIME_WINDOW} will be used.
*
* @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)
* @param versionSelector An enum indicating which versions to support:
* VERSION_1: use only version 1 of the protocol
* VERSION_2_ALLOW_1: suggest version 2 but allow downgrade to version 1
* VERSION_2: suggest version 2 and enforce use of version 2
*
*/
public PaymentChannelClient(Wallet wallet, ECKey myKey, Coin maxValue, Sha256Hash serverId,
ClientConnection conn, VersionSelector versionSelector) {
this(wallet,myKey,maxValue,serverId, DEFAULT_TIME_WINDOW, null, conn, versionSelector);
public PaymentChannelClient(Wallet wallet, ECKey myKey, Coin maxValue, Sha256Hash serverId, ClientConnection conn) {
this(wallet,myKey,maxValue,serverId, null, conn);
}
/**
@ -212,16 +186,13 @@ public class PaymentChannelClient implements IPaymentChannelClient {
* @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 timeWindow The time in seconds, relative to now, on how long this channel should be kept open. Note that is is
* a proposal to the server. The server may in turn propose something different.
* See {@link org.bitcoinj.protocols.channels.IPaymentChannelClient.ClientConnection#acceptExpireTime(long)}
* @param userKeySetup Key derived from a user password, used to decrypt myKey, if it is encrypted, during setup.
* @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, Coin maxValue, Sha256Hash serverId, long timeWindow,
public PaymentChannelClient(Wallet wallet, ECKey myKey, Coin maxValue, Sha256Hash serverId,
@Nullable KeyParameter userKeySetup, ClientConnection conn) {
this(wallet, myKey, maxValue, serverId, timeWindow, userKeySetup, conn, VersionSelector.VERSION_2_ALLOW_1);
this(wallet, myKey, maxValue, serverId, userKeySetup, defaultChannelProperties, conn);
}
/**
@ -237,28 +208,28 @@ public class PaymentChannelClient implements IPaymentChannelClient {
* @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 timeWindow The time in seconds, relative to now, on how long this channel should be kept open. Note that is is
* a proposal to the server. The server may in turn propose something different.
* See {@link org.bitcoinj.protocols.channels.IPaymentChannelClient.ClientConnection#acceptExpireTime(long)}
* @param userKeySetup Key derived from a user password, used to decrypt myKey, if it is encrypted, during setup.
* @param clientChannelProperties Modify the channel's properties. You may extend {@link DefaultClientChannelProperties}
* @param conn A callback listener which represents the connection to the server (forwards messages we generate to
* the server)
* @param versionSelector An enum indicating which versions to support:
* VERSION_1: use only version 1 of the protocol
* VERSION_2_ALLOW_1: suggest version 2 but allow downgrade to version 1
* VERSION_2: suggest version 2 and enforce use of version 2
*/
public PaymentChannelClient(Wallet wallet, ECKey myKey, Coin maxValue, Sha256Hash serverId, long timeWindow,
@Nullable KeyParameter userKeySetup, ClientConnection conn, VersionSelector versionSelector) {
public PaymentChannelClient(Wallet wallet, ECKey myKey, Coin maxValue, Sha256Hash serverId,
@Nullable KeyParameter userKeySetup, @Nullable ClientChannelProperties clientChannelProperties,
ClientConnection conn) {
this.wallet = checkNotNull(wallet);
this.myKey = checkNotNull(myKey);
this.maxValue = checkNotNull(maxValue);
this.serverId = checkNotNull(serverId);
checkState(timeWindow >= 0);
this.timeWindow = timeWindow;
this.conn = checkNotNull(conn);
this.userKeySetup = userKeySetup;
this.versionSelector = versionSelector;
if (clientChannelProperties == null) {
this.clientChannelProperties = defaultChannelProperties;
} else {
this.clientChannelProperties = clientChannelProperties;
}
this.timeWindow = clientChannelProperties.timeWindow();
checkState(timeWindow >= 0);
this.versionSelector = clientChannelProperties.versionSelector();
}
/**
@ -299,12 +270,12 @@ public class PaymentChannelClient implements IPaymentChannelClient {
// For now we require a hard-coded value. In future this will have to get more complex and dynamic as the fees
// start to float.
final long MIN_PAYMENT = Transaction.REFERENCE_DEFAULT_MIN_TX_FEE.value;
if (initiate.getMinPayment() != MIN_PAYMENT) {
log.error("Server requested a min payment of {} but we expected {}", initiate.getMinPayment(), MIN_PAYMENT);
final long maxMin = clientChannelProperties.acceptableMinPayment().value;
if (initiate.getMinPayment() > maxMin) {
log.error("Server requested a min payment of {} but we only accept up to {}", initiate.getMinPayment(), maxMin);
errorBuilder.setCode(Protos.Error.ErrorCode.MIN_PAYMENT_TOO_LARGE);
errorBuilder.setExpectedValue(MIN_PAYMENT);
missing = Coin.valueOf(initiate.getMinPayment() - MIN_PAYMENT);
errorBuilder.setExpectedValue(maxMin);
missing = Coin.valueOf(initiate.getMinPayment() - maxMin);
return CloseReason.SERVER_REQUESTED_TOO_MUCH_VALUE;
}
@ -322,7 +293,7 @@ public class PaymentChannelClient implements IPaymentChannelClient {
return CloseReason.NO_ACCEPTABLE_VERSION;
}
try {
state.initiate(userKeySetup);
state.initiate(userKeySetup, clientChannelProperties);
} catch (ValueOutOfRangeException e) {
log.error("Value out of range when trying to initiate", e);
errorBuilder.setCode(Protos.Error.ErrorCode.CHANNEL_VALUE_TOO_LARGE);
@ -754,4 +725,28 @@ public class PaymentChannelClient implements IPaymentChannelClient {
// Ensure the future runs without the client lock held.
future.set(new PaymentIncrementAck(value, paymentAck.getInfo()));
}
public static class DefaultClientChannelProperties implements ClientChannelProperties {
@Override
public SendRequest modifyContractSendRequest(SendRequest sendRequest) {
return sendRequest;
}
@Override
public Coin acceptableMinPayment() { return Transaction.REFERENCE_DEFAULT_MIN_TX_FEE; }
@Override
public long timeWindow() {
return DEFAULT_TIME_WINDOW;
}
@Override
public VersionSelector versionSelector() {
return VersionSelector.VERSION_2_ALLOW_1;
}
}
public static DefaultClientChannelProperties defaultChannelProperties = new DefaultClientChannelProperties();
}

View File

@ -23,6 +23,7 @@ import org.bitcoinj.core.Sha256Hash;
import org.bitcoinj.core.Utils;
import org.bitcoinj.net.NioClient;
import org.bitcoinj.net.ProtobufConnection;
import org.bitcoinj.protocols.channels.IPaymentChannelClient.ClientChannelProperties;
import org.bitcoinj.wallet.Wallet;
import com.google.common.util.concurrent.ListenableFuture;
@ -68,44 +69,14 @@ public class PaymentChannelClientConnection {
*/
public PaymentChannelClientConnection(InetSocketAddress server, int timeoutSeconds, Wallet wallet, ECKey myKey,
Coin maxValue, String serverId) throws IOException, ValueOutOfRangeException {
this(server, timeoutSeconds, wallet, myKey, maxValue, serverId,
PaymentChannelClient.VersionSelector.VERSION_2_ALLOW_1);
this(server, timeoutSeconds, wallet, myKey, maxValue, serverId, null);
}
/**
* Attempts to open a new connection to and open a payment channel with the given host and port, blocking until the
* connection is open. The server is requested to keep the channel open for
* {@link org.bitcoinj.protocols.channels.PaymentChannelClient#DEFAULT_TIME_WINDOW}
* seconds. If the server proposes a longer time the channel will be closed.
*
* @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 be unencrypted. 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.
* @param versionSelector An enum indicating which versions to support:
* VERSION_1: use only version 1 of the protocol
* VERSION_2_ALLOW_1: suggest version 2 but allow downgrade to version 1
* VERSION_2: suggest version 2 and enforce use of version 2
* @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,
Coin maxValue, String serverId, PaymentChannelClient.VersionSelector versionSelector) throws IOException, ValueOutOfRangeException {
this(server, timeoutSeconds, wallet, myKey, maxValue, serverId,
PaymentChannelClient.DEFAULT_TIME_WINDOW, null, versionSelector);
}
/**
* Attempts to open a new connection to and open a payment channel with the given host and port, blocking until the
* connection is open. The server is requested to keep the channel open for {@param timeWindow}
* seconds. If the server proposes a longer time the channel will be closed.
* connection is open. The server is requested to keep the channel open for
* {@link org.bitcoinj.protocols.channels.PaymentChannelClient#DEFAULT_TIME_WINDOW} seconds.
* If the server proposes a longer time the channel will be closed.
*
* @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
@ -119,16 +90,13 @@ public class PaymentChannelClientConnection {
* 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.
* @param timeWindow The time in seconds, relative to now, on how long this channel should be kept open.
* @param userKeySetup Key derived from a user password, used to decrypt myKey, if it is encrypted, during setup.
* @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,
Coin maxValue, String serverId, final long timeWindow,
@Nullable KeyParameter userKeySetup) throws IOException, ValueOutOfRangeException {
this(server, timeoutSeconds, wallet, myKey, maxValue, serverId,
timeWindow, userKeySetup, PaymentChannelClient.VersionSelector.VERSION_2_ALLOW_1);
Coin maxValue, String serverId, @Nullable KeyParameter userKeySetup) throws IOException, ValueOutOfRangeException {
this(server, timeoutSeconds, wallet, myKey, maxValue, serverId, userKeySetup, PaymentChannelClient.defaultChannelProperties);
}
@ -149,24 +117,20 @@ public class PaymentChannelClientConnection {
* 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.
* @param timeWindow The time in seconds, relative to now, on how long this channel should be kept open.
* @param userKeySetup Key derived from a user password, used to decrypt myKey, if it is encrypted, during setup.
* @param versionSelector An enum indicating which versions to support:
* VERSION_1: use only version 1 of the protocol
* VERSION_2_ALLOW_1: suggest version 2 but allow downgrade to version 1
* VERSION_2: suggest version 2 and enforce use of version 2
* @param clientChannelProperties Modifier to change the channel's configuration.
*
* @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,
Coin maxValue, String serverId, final long timeWindow,
@Nullable KeyParameter userKeySetup, PaymentChannelClient.VersionSelector versionSelector)
Coin maxValue, String serverId,
@Nullable KeyParameter userKeySetup, final ClientChannelProperties clientChannelProperties)
throws IOException, ValueOutOfRangeException {
// 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.of(serverId.getBytes()), timeWindow,
userKeySetup, new PaymentChannelClient.ClientConnection() {
channelClient = new PaymentChannelClient(wallet, myKey, maxValue, Sha256Hash.of(serverId.getBytes()),
userKeySetup, clientChannelProperties, new PaymentChannelClient.ClientConnection() {
@Override
public void sendToServer(Protos.TwoWayChannelMessage msg) {
wireParser.write(msg);
@ -180,7 +144,7 @@ public class PaymentChannelClientConnection {
@Override
public boolean acceptExpireTime(long expireTime) {
return expireTime <= (timeWindow + Utils.currentTimeSeconds() + 60); // One extra minute to compensate for time skew and latency
return expireTime <= (clientChannelProperties.timeWindow() + Utils.currentTimeSeconds() + 60); // One extra minute to compensate for time skew and latency
}
@Override
@ -189,7 +153,7 @@ public class PaymentChannelClientConnection {
// Inform the API user that we're done and ready to roll.
channelOpenFuture.set(PaymentChannelClientConnection.this);
}
}, versionSelector);
});
// And glue back in the opposite direction - network to the channelClient.
wireParser = new ProtobufConnection<Protos.TwoWayChannelMessage>(new ProtobufConnection.Listener<Protos.TwoWayChannelMessage>() {

View File

@ -24,9 +24,9 @@ import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import org.bitcoinj.core.*;
import org.bitcoinj.crypto.TransactionSignature;
import org.bitcoinj.protocols.channels.IPaymentChannelClient.ClientChannelProperties;
import org.bitcoinj.script.Script;
import org.bitcoinj.utils.Threading;
import org.bitcoinj.wallet.SendRequest;
import org.bitcoinj.wallet.Wallet;
import org.bitcoinj.wallet.listeners.WalletCoinsReceivedEventListener;
import org.slf4j.Logger;
@ -60,14 +60,14 @@ import static com.google.common.base.Preconditions.*;
* 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 PaymentChannelV1ClientState#initiate()}, which moves the channel into state
* <p>To begin, the client calls {@link PaymentChannelClientState#initiate(KeyParameter, ClientChannelProperties)}, 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 PaymentChannelV1ClientState#getIncompleteRefundTransaction()} and pass the resultant transaction through to the
* server. Once you have retrieved the signature, use {@link PaymentChannelV1ClientState#provideRefundSignature(byte[], KeyParameter)}.
* You must then call {@link PaymentChannelV1ClientState#storeChannelInWallet(Sha256Hash)} to store the refund transaction
* You must then call {@link PaymentChannelClientState#storeChannelInWallet(Sha256Hash)} to store the refund transaction
* in the wallet, protecting you against a malicious server attempting to destroy all your coins. At this point, you can
* provide the server with the multi-sig contract (via {@link PaymentChannelV1ClientState#getContract()}) safely.
* provide the server with the multi-sig contract (via {@link PaymentChannelClientState#getContract()}) safely.
* </p>
*/
public abstract class PaymentChannelClientState {
@ -126,9 +126,9 @@ public abstract class PaymentChannelClientState {
/**
* Creates a state object for a payment channel client. It is expected that you be ready to
* {@link PaymentChannelV1ClientState#initiate()} after construction (to avoid creating objects for channels which are
* {@link PaymentChannelClientState#initiate(KeyParameter, ClientChannelProperties)} 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 PaymentChannelV1ClientState#initiate()} to create the Multisig contract and refund transaction.
* {@link PaymentChannelClientState#initiate(KeyParameter, ClientChannelProperties)} 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.
@ -211,37 +211,28 @@ public abstract class PaymentChannelClientState {
* Creates the initial multisig contract and incomplete refund transaction which can be requested at the appropriate
* time using {@link PaymentChannelV1ClientState#getIncompleteRefundTransaction} and
* {@link PaymentChannelV1ClientState#getContract()}. The way the contract is crafted can be adjusted by
* overriding {@link PaymentChannelV1ClientState#editContractSendRequest(org.bitcoinj.wallet.Wallet.SendRequest)}.
* By default unconfirmed coins are allowed to be used, as for micropayments the risk should be relatively low.
*
* @throws ValueOutOfRangeException if the value being used is too small to be accepted by the network
* @throws InsufficientMoneyException if the wallet doesn't contain enough balance to initiate
*/
public void initiate() throws ValueOutOfRangeException, InsufficientMoneyException {
initiate(null);
initiate(null, PaymentChannelClient.defaultChannelProperties);
}
/**
* Creates the initial multisig contract and incomplete refund transaction which can be requested at the appropriate
* time using {@link PaymentChannelV1ClientState#getIncompleteRefundTransaction} and
* {@link PaymentChannelV1ClientState#getContract()}. The way the contract is crafted can be adjusted by
* overriding {@link PaymentChannelV1ClientState#editContractSendRequest(org.bitcoinj.wallet.Wallet.SendRequest)}.
* {@link PaymentChannelClientState#getContract()}.
* By default unconfirmed coins are allowed to be used, as for micropayments the risk should be relatively low.
* @param userKey Key derived from a user password, needed for any signing when the wallet is encrypted.
* The wallet KeyCrypter is assumed.
* The wallet KeyCrypter is assumed.
* @param clientChannelProperties Modify the channel's configuration.
*
* @throws ValueOutOfRangeException if the value being used is too small to be accepted by the network
* @throws InsufficientMoneyException if the wallet doesn't contain enough balance to initiate
*/
public abstract void initiate(@Nullable KeyParameter userKey) throws ValueOutOfRangeException, InsufficientMoneyException;
/**
* You can override this method in order to control the construction of the initial contract that creates the
* channel. For example if you want it to only use specific coins, you can adjust the coin selector here.
* The default implementation does nothing.
*/
protected void editContractSendRequest(SendRequest req) {
}
public abstract void initiate(@Nullable KeyParameter userKey, ClientChannelProperties clientChannelProperties) throws ValueOutOfRangeException, InsufficientMoneyException;
/**
* Gets the contract which was used to initialize this channel
@ -392,7 +383,7 @@ public abstract class PaymentChannelClientState {
/**
* Returns the fees that will be paid if the refund transaction has to be claimed because the server failed to settle
* the channel properly. May only be called after {@link PaymentChannelV1ClientState#initiate()}
* the channel properly. May only be called after {@link PaymentChannelClientState#initiate(KeyParameter, ClientChannelProperties)}
*/
public abstract Coin getRefundTxFees();

View File

@ -129,6 +129,25 @@ public class PaymentChannelServer {
}
private final ServerConnection conn;
public interface ServerChannelProperties {
/**
* The size of the payment that the client is requested to pay in the initiate phase.
*/
Coin getMinPayment();
/**
* The maximum allowed channel time window in seconds.
* Note that the server need to be online for the whole time the channel is open.
* Failure to do this could cause loss of all payments received on the channel.
*/
long getMaxTimeWindow();
/**
* The minimum allowed channel time window in seconds, must be larger than 7200.
*/
long getMinTimeWindow();
}
// Used to track the negotiated version number
@GuardedBy("lock") private int majorVersion;
@ -144,6 +163,10 @@ public class PaymentChannelServer {
// The key used for multisig in this channel
@GuardedBy("lock") private ECKey myKey;
// The fee server charges for managing (and settling the channel).
// This is will be requested in the setup via the min_payment field in the initiate message.
private final Coin minPayment;
// The minimum accepted channel value
private final Coin minAcceptedChannelSize;
@ -187,7 +210,7 @@ public class PaymentChannelServer {
*/
public PaymentChannelServer(TransactionBroadcaster broadcaster, Wallet wallet,
Coin minAcceptedChannelSize, ServerConnection conn) {
this(broadcaster, wallet, minAcceptedChannelSize, DEFAULT_MIN_TIME_WINDOW, DEFAULT_MAX_TIME_WINDOW, conn);
this(broadcaster, wallet, minAcceptedChannelSize, new DefaultServerChannelProperties(), conn);
}
/**
@ -201,22 +224,21 @@ public class PaymentChannelServer {
* and may cause fees to be require to settle 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 minTimeWindow The minimum allowed channel time window in seconds, must be larger than 7200.
* @param maxTimeWindow The maximum allowed channel time window in seconds. Note that the server need to be online for the whole time the channel is open.
* Failure to do this could cause loss of all payments received on the channel.
* @param serverChannelProperties Modify the channel's properties. You may extend {@link DefaultServerChannelProperties}
* @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(TransactionBroadcaster broadcaster, Wallet wallet,
Coin minAcceptedChannelSize, long minTimeWindow, long maxTimeWindow, ServerConnection conn) {
Coin minAcceptedChannelSize, ServerChannelProperties serverChannelProperties, ServerConnection conn) {
minTimeWindow = serverChannelProperties.getMinTimeWindow();
maxTimeWindow = serverChannelProperties.getMaxTimeWindow();
if (minTimeWindow > maxTimeWindow) throw new IllegalArgumentException("minTimeWindow must be less or equal to maxTimeWindow");
if (minTimeWindow < HARD_MIN_TIME_WINDOW) throw new IllegalArgumentException("minTimeWindow must be larger than" + HARD_MIN_TIME_WINDOW + " seconds");
this.broadcaster = checkNotNull(broadcaster);
this.wallet = checkNotNull(wallet);
this.minPayment = checkNotNull(serverChannelProperties.getMinPayment());
this.minAcceptedChannelSize = checkNotNull(minAcceptedChannelSize);
this.conn = checkNotNull(conn);
this.minTimeWindow = minTimeWindow;
this.maxTimeWindow = maxTimeWindow;
}
/**
@ -299,7 +321,7 @@ public class PaymentChannelServer {
.setMultisigKey(ByteString.copyFrom(myKey.getPubKey()))
.setExpireTimeSecs(expireTime)
.setMinAcceptedChannelSize(minAcceptedChannelSize.value)
.setMinPayment(Transaction.REFERENCE_DEFAULT_MIN_TX_FEE.value);
.setMinPayment(minPayment.value);
conn.sendToClient(Protos.TwoWayChannelMessage.newBuilder()
.setInitiate(initiateBuilder)
@ -634,4 +656,27 @@ public class PaymentChannelServer {
lock.unlock();
}
}
/**
* Extend this class and override the values you want to change.
*/
public static class DefaultServerChannelProperties implements ServerChannelProperties {
@Override
public Coin getMinPayment() {
return Transaction.REFERENCE_DEFAULT_MIN_TX_FEE;
}
@Override
public long getMaxTimeWindow() {
return DEFAULT_MAX_TIME_WINDOW;
}
@Override
public long getMinTimeWindow() {
return DEFAULT_MIN_TIME_WINDOW;
}
}
}

View File

@ -20,6 +20,7 @@ import com.google.common.collect.Multimap;
import com.google.common.collect.MultimapBuilder;
import org.bitcoinj.core.*;
import org.bitcoinj.crypto.TransactionSignature;
import org.bitcoinj.protocols.channels.IPaymentChannelClient.ClientChannelProperties;
import org.bitcoinj.script.Script;
import org.bitcoinj.script.ScriptBuilder;
import org.bitcoinj.wallet.AllowUnconfirmedCoinSelector;
@ -71,9 +72,9 @@ public class PaymentChannelV1ClientState extends PaymentChannelClientState {
/**
* Creates a state object for a payment channel client. It is expected that you be ready to
* {@link PaymentChannelV1ClientState#initiate()} after construction (to avoid creating objects for channels which are
* {@link PaymentChannelClientState#initiate(KeyParameter, ClientChannelProperties)} 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 PaymentChannelV1ClientState#initiate()} to create the Multisig contract and refund transaction.
* {@link PaymentChannelClientState#initiate(KeyParameter, ClientChannelProperties)} 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.
@ -115,17 +116,17 @@ public class PaymentChannelV1ClientState extends PaymentChannelClientState {
/**
* Creates the initial multisig contract and incomplete refund transaction which can be requested at the appropriate
* time using {@link PaymentChannelV1ClientState#getIncompleteRefundTransaction} and
* {@link PaymentChannelV1ClientState#getContract()}. The way the contract is crafted can be adjusted by
* overriding {@link PaymentChannelV1ClientState#editContractSendRequest(org.bitcoinj.core.Wallet.SendRequest)}.
* {@link PaymentChannelV1ClientState#getContract()}.
* By default unconfirmed coins are allowed to be used, as for micropayments the risk should be relatively low.
* @param userKey Key derived from a user password, needed for any signing when the wallet is encrypted.
* The wallet KeyCrypter is assumed.
* The wallet KeyCrypter is assumed.
* @param clientChannelProperties Modify the channel's configuration.
*
* @throws ValueOutOfRangeException if the value being used is too small to be accepted by the network
* @throws InsufficientMoneyException if the wallet doesn't contain enough balance to initiate
*/
@Override
public synchronized void initiate(@Nullable KeyParameter userKey) throws ValueOutOfRangeException, InsufficientMoneyException {
public synchronized void initiate(@Nullable KeyParameter userKey, ClientChannelProperties clientChannelProperties) throws ValueOutOfRangeException, InsufficientMoneyException {
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
@ -139,9 +140,9 @@ public class PaymentChannelV1ClientState extends PaymentChannelClientState {
throw new ValueOutOfRangeException("totalValue too small to use");
SendRequest req = SendRequest.forTx(template);
req.coinSelector = AllowUnconfirmedCoinSelector.get();
editContractSendRequest(req);
req.shuffleOutputs = false; // TODO: Fix things so shuffling is usable.
req.aesKey = userKey;
req = clientChannelProperties.modifyContractSendRequest(req);
if (userKey != null) req.aesKey = userKey;
wallet.completeTx(req);
Coin multisigFee = req.tx.getFee();
multisigContract = req.tx;

View File

@ -21,6 +21,7 @@ import com.google.common.collect.Multimap;
import com.google.common.collect.MultimapBuilder;
import org.bitcoinj.core.*;
import org.bitcoinj.crypto.TransactionSignature;
import org.bitcoinj.protocols.channels.IPaymentChannelClient.ClientChannelProperties;
import org.bitcoinj.script.Script;
import org.bitcoinj.script.ScriptBuilder;
import org.bitcoinj.wallet.AllowUnconfirmedCoinSelector;
@ -99,7 +100,7 @@ public class PaymentChannelV2ClientState extends PaymentChannelClientState {
}
@Override
public synchronized void initiate(@Nullable KeyParameter userKey) throws ValueOutOfRangeException, InsufficientMoneyException {
public synchronized void initiate(@Nullable KeyParameter userKey, ClientChannelProperties clientChannelProperties) throws ValueOutOfRangeException, InsufficientMoneyException {
final NetworkParameters params = wallet.getParams();
Transaction template = new Transaction(params);
// There is also probably a change output, but we don't bother shuffling them as it's obvious from the
@ -113,9 +114,9 @@ public class PaymentChannelV2ClientState extends PaymentChannelClientState {
throw new ValueOutOfRangeException("totalValue too small to use");
SendRequest req = SendRequest.forTx(template);
req.coinSelector = AllowUnconfirmedCoinSelector.get();
editContractSendRequest(req);
req.shuffleOutputs = false; // TODO: Fix things so shuffling is usable.
req.aesKey = userKey;
req = clientChannelProperties.modifyContractSendRequest(req);
if (userKey != null) req.aesKey = userKey;
wallet.completeTx(req);
Coin multisigFee = req.tx.getFee();
contract = req.tx;

View File

@ -17,6 +17,7 @@
package org.bitcoinj.protocols.channels;
import org.bitcoinj.core.*;
import org.bitcoinj.protocols.channels.PaymentChannelClient.VersionSelector;
import org.bitcoinj.testing.TestWithWallet;
import org.bitcoinj.utils.Threading;
import org.bitcoinj.wallet.Wallet;
@ -48,6 +49,7 @@ import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicBoolean;
import static org.bitcoinj.core.Coin.*;
import static org.bitcoinj.protocols.channels.PaymentChannelClient.VersionSelector.*;
import static org.bitcoinj.protocols.channels.PaymentChannelCloseException.CloseReason;
import static org.bitcoinj.testing.FakeTxBuilder.createFakeBlock;
import static org.bitcoin.paymentchannel.Protos.TwoWayChannelMessage.MessageType;
@ -75,20 +77,26 @@ public class ChannelConnectionTest extends TestWithWallet {
* version of the channel.
*/
@Parameterized.Parameters(name = "{index}: ChannelConnectionTest({0})")
public static Collection<PaymentChannelClient.VersionSelector> data() {
public static Collection<PaymentChannelClient.DefaultClientChannelProperties> data() {
return Arrays.asList(
PaymentChannelClient.VersionSelector.VERSION_1,
PaymentChannelClient.VersionSelector.VERSION_2_ALLOW_1);
new PaymentChannelClient.DefaultClientChannelProperties() {
@Override
public VersionSelector versionSelector() { return VERSION_1;}
},
new PaymentChannelClient.DefaultClientChannelProperties() {
@Override
public VersionSelector versionSelector() { return VERSION_2_ALLOW_1;}
});
}
@Parameterized.Parameter
public PaymentChannelClient.VersionSelector versionSelector;
public IPaymentChannelClient.ClientChannelProperties clientChannelProperties;
/**
* Returns <code>true</code> if we are using a protocol version that requires the exchange of refunds.
*/
private boolean useRefunds() {
return versionSelector == PaymentChannelClient.VersionSelector.VERSION_1;
return clientChannelProperties.versionSelector() == VERSION_1;
}
/**
@ -96,7 +104,7 @@ public class ChannelConnectionTest extends TestWithWallet {
* @return
*/
private boolean isMultiSigContract() {
return versionSelector == PaymentChannelClient.VersionSelector.VERSION_1;
return clientChannelProperties.versionSelector() == VERSION_1;
}
@Override
@ -197,7 +205,7 @@ public class ChannelConnectionTest extends TestWithWallet {
server.bindAndStart(4243);
PaymentChannelClientConnection client = new PaymentChannelClientConnection(
new InetSocketAddress("localhost", 4243), 30, wallet, myKey, COIN, "", PaymentChannelClient.DEFAULT_TIME_WINDOW, userKeySetup, versionSelector);
new InetSocketAddress("localhost", 4243), 30, wallet, myKey, COIN, "", userKeySetup, clientChannelProperties);
// Wait for the multi-sig tx to be transmitted.
broadcastTxPause.release();
@ -280,7 +288,7 @@ public class ChannelConnectionTest extends TestWithWallet {
}
// Gives the server crap and checks proper error responses are sent.
ChannelTestUtils.RecordingPair pair = ChannelTestUtils.makeRecorders(serverWallet, mockBroadcaster);
PaymentChannelClient client = new PaymentChannelClient(wallet, myKey, COIN, Sha256Hash.ZERO_HASH, pair.clientRecorder, versionSelector);
PaymentChannelClient client = new PaymentChannelClient(wallet, myKey, COIN, Sha256Hash.ZERO_HASH, null, clientChannelProperties, pair.clientRecorder);
PaymentChannelServer server = pair.server;
server.connectionOpen();
client.connectionOpen();
@ -305,7 +313,7 @@ public class ChannelConnectionTest extends TestWithWallet {
public void testServerErrorHandling_killSocketOnClose() throws Exception {
// Make sure the server closes the socket on CLOSE
ChannelTestUtils.RecordingPair pair = ChannelTestUtils.makeRecorders(serverWallet, mockBroadcaster);
PaymentChannelClient client = new PaymentChannelClient(wallet, myKey, COIN, Sha256Hash.ZERO_HASH, pair.clientRecorder, versionSelector);
PaymentChannelClient client = new PaymentChannelClient(wallet, myKey, COIN, Sha256Hash.ZERO_HASH, null, clientChannelProperties, pair.clientRecorder);
PaymentChannelServer server = pair.server;
server.connectionOpen();
client.connectionOpen();
@ -323,7 +331,7 @@ public class ChannelConnectionTest extends TestWithWallet {
public void testServerErrorHandling_killSocketOnError() throws Exception {
// Make sure the server closes the socket on ERROR
ChannelTestUtils.RecordingPair pair = ChannelTestUtils.makeRecorders(serverWallet, mockBroadcaster);
PaymentChannelClient client = new PaymentChannelClient(wallet, myKey, COIN, Sha256Hash.ZERO_HASH, pair.clientRecorder, versionSelector);
PaymentChannelClient client = new PaymentChannelClient(wallet, myKey, COIN, Sha256Hash.ZERO_HASH, null, clientChannelProperties, pair.clientRecorder);
PaymentChannelServer server = pair.server;
server.connectionOpen();
client.connectionOpen();
@ -347,7 +355,7 @@ public class ChannelConnectionTest extends TestWithWallet {
// Open up a normal channel.
ChannelTestUtils.RecordingPair pair = ChannelTestUtils.makeRecorders(serverWallet, mockBroadcaster);
pair.server.connectionOpen();
PaymentChannelClient client = new PaymentChannelClient(wallet, myKey, COIN, someServerId, pair.clientRecorder, versionSelector);
PaymentChannelClient client = new PaymentChannelClient(wallet, myKey, COIN, someServerId, null, clientChannelProperties, pair.clientRecorder);
PaymentChannelServer server = pair.server;
client.connectionOpen();
server.receiveMessage(pair.clientRecorder.checkNextMsg(MessageType.CLIENT_VERSION));
@ -402,7 +410,7 @@ public class ChannelConnectionTest extends TestWithWallet {
// Open up a normal channel.
ChannelTestUtils.RecordingPair pair = ChannelTestUtils.makeRecorders(serverWallet, mockBroadcaster);
pair.server.connectionOpen();
PaymentChannelClient client = new PaymentChannelClient(wallet, myKey, COIN, someServerId, pair.clientRecorder, versionSelector);
PaymentChannelClient client = new PaymentChannelClient(wallet, myKey, COIN, someServerId, null, clientChannelProperties, pair.clientRecorder);
PaymentChannelServer server = pair.server;
client.connectionOpen();
server.receiveMessage(pair.clientRecorder.checkNextMsg(MessageType.CLIENT_VERSION));
@ -462,7 +470,7 @@ public class ChannelConnectionTest extends TestWithWallet {
(StoredPaymentChannelClientStates) wallet.getExtensions().get(StoredPaymentChannelClientStates.EXTENSION_ID);
pair = ChannelTestUtils.makeRecorders(serverWallet, mockBroadcaster);
client = new PaymentChannelClient(wallet, myKey, COIN, someServerId, pair.clientRecorder, versionSelector);
client = new PaymentChannelClient(wallet, myKey, COIN, someServerId, null, clientChannelProperties, pair.clientRecorder);
server = pair.server;
client.connectionOpen();
server.connectionOpen();
@ -489,7 +497,7 @@ public class ChannelConnectionTest extends TestWithWallet {
// Now open up a new client with the same id and make sure the server disconnects the previous client.
pair = ChannelTestUtils.makeRecorders(serverWallet, mockBroadcaster);
client = new PaymentChannelClient(wallet, myKey, COIN, someServerId, pair.clientRecorder, versionSelector);
client = new PaymentChannelClient(wallet, myKey, COIN, someServerId, null, clientChannelProperties, pair.clientRecorder);
server = pair.server;
client.connectionOpen();
server.connectionOpen();
@ -501,7 +509,7 @@ public class ChannelConnectionTest extends TestWithWallet {
}
// Make sure the server allows two simultaneous opens. It will close the first and allow resumption of the second.
pair = ChannelTestUtils.makeRecorders(serverWallet, mockBroadcaster);
client = new PaymentChannelClient(wallet, myKey, COIN, someServerId, pair.clientRecorder, versionSelector);
client = new PaymentChannelClient(wallet, myKey, COIN, someServerId, null, clientChannelProperties, pair.clientRecorder);
server = pair.server;
client.connectionOpen();
server.connectionOpen();
@ -585,7 +593,7 @@ public class ChannelConnectionTest extends TestWithWallet {
public void testClientUnknownVersion() throws Exception {
// Tests client rejects unknown version
ChannelTestUtils.RecordingPair pair = ChannelTestUtils.makeRecorders(serverWallet, mockBroadcaster);
PaymentChannelClient client = new PaymentChannelClient(wallet, myKey, COIN, Sha256Hash.ZERO_HASH, pair.clientRecorder, versionSelector);
PaymentChannelClient client = new PaymentChannelClient(wallet, myKey, COIN, Sha256Hash.ZERO_HASH, null, clientChannelProperties, pair.clientRecorder);
client.connectionOpen();
pair.clientRecorder.checkNextMsg(MessageType.CLIENT_VERSION);
client.receiveMessage(Protos.TwoWayChannelMessage.newBuilder()
@ -605,7 +613,7 @@ public class ChannelConnectionTest extends TestWithWallet {
// Tests that clients reject too large time windows
ChannelTestUtils.RecordingPair pair = ChannelTestUtils.makeRecorders(serverWallet, mockBroadcaster, 100);
PaymentChannelServer server = pair.server;
PaymentChannelClient client = new PaymentChannelClient(wallet, myKey, COIN, Sha256Hash.ZERO_HASH, pair.clientRecorder, versionSelector);
PaymentChannelClient client = new PaymentChannelClient(wallet, myKey, COIN, Sha256Hash.ZERO_HASH, null, clientChannelProperties, pair.clientRecorder);
client.connectionOpen();
server.connectionOpen();
server.receiveMessage(pair.clientRecorder.checkNextMsg(MessageType.CLIENT_VERSION));
@ -630,7 +638,7 @@ public class ChannelConnectionTest extends TestWithWallet {
public void testValuesAreRespected() throws Exception {
ChannelTestUtils.RecordingPair pair = ChannelTestUtils.makeRecorders(serverWallet, mockBroadcaster);
PaymentChannelServer server = pair.server;
PaymentChannelClient client = new PaymentChannelClient(wallet, myKey, COIN, Sha256Hash.ZERO_HASH, pair.clientRecorder, versionSelector);
PaymentChannelClient client = new PaymentChannelClient(wallet, myKey, COIN, Sha256Hash.ZERO_HASH, null, clientChannelProperties, pair.clientRecorder);
client.connectionOpen();
server.connectionOpen();
server.receiveMessage(pair.clientRecorder.checkNextMsg(MessageType.CLIENT_VERSION));
@ -656,7 +664,7 @@ public class ChannelConnectionTest extends TestWithWallet {
pair = ChannelTestUtils.makeRecorders(serverWallet, mockBroadcaster);
server = pair.server;
final Coin myValue = COIN.multiply(10);
client = new PaymentChannelClient(wallet, myKey, myValue, Sha256Hash.ZERO_HASH, pair.clientRecorder, versionSelector);
client = new PaymentChannelClient(wallet, myKey, myValue, Sha256Hash.ZERO_HASH, null, clientChannelProperties, pair.clientRecorder);
client.connectionOpen();
server.connectionOpen();
server.receiveMessage(pair.clientRecorder.checkNextMsg(MessageType.CLIENT_VERSION));
@ -684,7 +692,7 @@ public class ChannelConnectionTest extends TestWithWallet {
emptyWallet.freshReceiveKey();
ChannelTestUtils.RecordingPair pair = ChannelTestUtils.makeRecorders(serverWallet, mockBroadcaster);
PaymentChannelServer server = pair.server;
PaymentChannelClient client = new PaymentChannelClient(emptyWallet, myKey, COIN, Sha256Hash.ZERO_HASH, pair.clientRecorder, versionSelector);
PaymentChannelClient client = new PaymentChannelClient(emptyWallet, myKey, COIN, Sha256Hash.ZERO_HASH, null, clientChannelProperties, pair.clientRecorder);
client.connectionOpen();
server.connectionOpen();
server.receiveMessage(pair.clientRecorder.checkNextMsg(MessageType.CLIENT_VERSION));
@ -706,7 +714,7 @@ public class ChannelConnectionTest extends TestWithWallet {
public void testClientRefusesNonCanonicalKey() throws Exception {
ChannelTestUtils.RecordingPair pair = ChannelTestUtils.makeRecorders(serverWallet, mockBroadcaster);
PaymentChannelServer server = pair.server;
PaymentChannelClient client = new PaymentChannelClient(wallet, myKey, COIN, Sha256Hash.ZERO_HASH, pair.clientRecorder, versionSelector);
PaymentChannelClient client = new PaymentChannelClient(wallet, myKey, COIN, Sha256Hash.ZERO_HASH, null, clientChannelProperties, pair.clientRecorder);
client.connectionOpen();
server.connectionOpen();
server.receiveMessage(pair.clientRecorder.checkNextMsg(MessageType.CLIENT_VERSION));
@ -724,7 +732,7 @@ public class ChannelConnectionTest extends TestWithWallet {
public void testClientResumeNothing() throws Exception {
ChannelTestUtils.RecordingPair pair = ChannelTestUtils.makeRecorders(serverWallet, mockBroadcaster);
PaymentChannelServer server = pair.server;
PaymentChannelClient client = new PaymentChannelClient(wallet, myKey, COIN, Sha256Hash.ZERO_HASH, pair.clientRecorder, versionSelector);
PaymentChannelClient client = new PaymentChannelClient(wallet, myKey, COIN, Sha256Hash.ZERO_HASH, null, clientChannelProperties, pair.clientRecorder);
client.connectionOpen();
server.connectionOpen();
server.receiveMessage(pair.clientRecorder.checkNextMsg(MessageType.CLIENT_VERSION));
@ -738,7 +746,7 @@ public class ChannelConnectionTest extends TestWithWallet {
@Test
public void testClientRandomMessage() throws Exception {
ChannelTestUtils.RecordingPair pair = ChannelTestUtils.makeRecorders(serverWallet, mockBroadcaster);
PaymentChannelClient client = new PaymentChannelClient(wallet, myKey, COIN, Sha256Hash.ZERO_HASH, pair.clientRecorder, versionSelector);
PaymentChannelClient client = new PaymentChannelClient(wallet, myKey, COIN, Sha256Hash.ZERO_HASH, null, clientChannelProperties, pair.clientRecorder);
client.connectionOpen();
pair.clientRecorder.checkNextMsg(MessageType.CLIENT_VERSION);
@ -759,7 +767,7 @@ public class ChannelConnectionTest extends TestWithWallet {
Sha256Hash someServerId = Sha256Hash.ZERO_HASH;
ChannelTestUtils.RecordingPair pair = ChannelTestUtils.makeRecorders(serverWallet, mockBroadcaster);
pair.server.connectionOpen();
PaymentChannelClient client = new PaymentChannelClient(wallet, myKey, COIN, someServerId, pair.clientRecorder, versionSelector);
PaymentChannelClient client = new PaymentChannelClient(wallet, myKey, COIN, someServerId, null, clientChannelProperties, pair.clientRecorder);
PaymentChannelServer server = pair.server;
client.connectionOpen();
server.receiveMessage(pair.clientRecorder.checkNextMsg(MessageType.CLIENT_VERSION));
@ -792,7 +800,7 @@ public class ChannelConnectionTest extends TestWithWallet {
client.connectionClosed();
// Now try opening a new channel with the same server ID and verify the client asks for a new channel.
client = new PaymentChannelClient(wallet, myKey, COIN, someServerId, pair.clientRecorder, versionSelector);
client = new PaymentChannelClient(wallet, myKey, COIN, someServerId, null, clientChannelProperties, pair.clientRecorder);
client.connectionOpen();
Protos.TwoWayChannelMessage msg = pair.clientRecorder.checkNextMsg(MessageType.CLIENT_VERSION);
assertFalse(msg.getClientVersion().hasPreviousChannelContractHash());
@ -807,7 +815,7 @@ public class ChannelConnectionTest extends TestWithWallet {
Sha256Hash someServerId = Sha256Hash.ZERO_HASH;
ChannelTestUtils.RecordingPair pair = ChannelTestUtils.makeRecorders(serverWallet, mockBroadcaster);
pair.server.connectionOpen();
PaymentChannelClient client = new PaymentChannelClient(wallet, myKey, COIN, someServerId, pair.clientRecorder, versionSelector);
PaymentChannelClient client = new PaymentChannelClient(wallet, myKey, COIN, someServerId, null, clientChannelProperties, pair.clientRecorder);
PaymentChannelServer server = pair.server;
client.connectionOpen();
server.receiveMessage(pair.clientRecorder.checkNextMsg(MessageType.CLIENT_VERSION));
@ -861,7 +869,7 @@ public class ChannelConnectionTest extends TestWithWallet {
Sha256Hash someServerId = Sha256Hash.ZERO_HASH;
ChannelTestUtils.RecordingPair pair = ChannelTestUtils.makeRecorders(serverWallet, mockBroadcaster);
pair.server.connectionOpen();
PaymentChannelClient client = new PaymentChannelClient(wallet, myKey, COIN, someServerId, pair.clientRecorder, versionSelector);
PaymentChannelClient client = new PaymentChannelClient(wallet, myKey, COIN, someServerId, null, clientChannelProperties, pair.clientRecorder);
PaymentChannelServer server = pair.server;
client.connectionOpen();
final Protos.TwoWayChannelMessage msg = pair.clientRecorder.checkNextMsg(MessageType.CLIENT_VERSION);
@ -892,7 +900,7 @@ public class ChannelConnectionTest extends TestWithWallet {
Sha256Hash someServerId = Sha256Hash.ZERO_HASH;
ChannelTestUtils.RecordingPair pair = ChannelTestUtils.makeRecorders(serverWallet, mockBroadcaster);
pair.server.connectionOpen();
PaymentChannelClient client = new PaymentChannelClient(wallet, myKey, COIN, someServerId, pair.clientRecorder, versionSelector);
PaymentChannelClient client = new PaymentChannelClient(wallet, myKey, COIN, someServerId, null, clientChannelProperties, pair.clientRecorder);
PaymentChannelServer server = pair.server;
client.connectionOpen();
server.receiveMessage(pair.clientRecorder.checkNextMsg(MessageType.CLIENT_VERSION));

View File

@ -34,6 +34,9 @@ import java.util.HashMap;
import static org.bitcoin.paymentchannel.Protos.TwoWayChannelMessage;
import static org.bitcoin.paymentchannel.Protos.TwoWayChannelMessage.MessageType.*;
import static org.bitcoinj.protocols.channels.PaymentChannelClient.VersionSelector.VERSION_1;
import static org.bitcoinj.protocols.channels.PaymentChannelClient.VersionSelector.VERSION_2;
import static org.bitcoinj.protocols.channels.PaymentChannelClient.VersionSelector.VERSION_2_ALLOW_1;
import static org.easymock.EasyMock.capture;
import static org.easymock.EasyMock.createMock;
import static org.easymock.EasyMock.replay;
@ -55,16 +58,25 @@ public class PaymentChannelClientTest {
* version of the channel.
*/
@Parameterized.Parameters(name = "{index}: PaymentChannelClientTest({0})")
public static Collection<PaymentChannelClient.VersionSelector> data() {
public static Collection<PaymentChannelClient.DefaultClientChannelProperties> data() {
return Arrays.asList(
PaymentChannelClient.VersionSelector.VERSION_1,
PaymentChannelClient.VersionSelector.VERSION_2_ALLOW_1,
PaymentChannelClient.VersionSelector.VERSION_2
new PaymentChannelClient.DefaultClientChannelProperties() {
@Override
public PaymentChannelClient.VersionSelector versionSelector() { return VERSION_1;}
},
new PaymentChannelClient.DefaultClientChannelProperties() {
@Override
public PaymentChannelClient.VersionSelector versionSelector() { return VERSION_2_ALLOW_1;}
},
new PaymentChannelClient.DefaultClientChannelProperties() {
@Override
public PaymentChannelClient.VersionSelector versionSelector() { return VERSION_2;}
}
);
}
@Parameterized.Parameter
public PaymentChannelClient.VersionSelector versionSelector;
public IPaymentChannelClient.ClientChannelProperties clientChannelProperties;
@Before
public void before() {
@ -78,7 +90,7 @@ public class PaymentChannelClientTest {
@Test
public void shouldSendClientVersionOnChannelOpen() throws Exception {
PaymentChannelClient dut = new PaymentChannelClient(wallet, ecKey, maxValue, serverHash, connection, versionSelector);
PaymentChannelClient dut = new PaymentChannelClient(wallet, ecKey, maxValue, serverHash, null, clientChannelProperties, connection);
connection.sendToServer(capture(clientVersionCapture));
EasyMock.expect(wallet.getExtensions()).andReturn(new HashMap<String, WalletExtension>());
replay(connection, wallet);
@ -87,10 +99,20 @@ public class PaymentChannelClientTest {
}
@Test
public void shouldSendTimeWindowInClientVersion() throws Exception {
long timeWindow = 4000;
final long timeWindow = 4000;
KeyParameter userKey = null;
PaymentChannelClient dut =
new PaymentChannelClient(wallet, ecKey, maxValue, serverHash, timeWindow, userKey, connection, versionSelector);
new PaymentChannelClient(wallet, ecKey, maxValue, serverHash, userKey, new PaymentChannelClient.DefaultClientChannelProperties() {
@Override
public long timeWindow() {
return timeWindow;
}
@Override
public PaymentChannelClient.VersionSelector versionSelector() {
return clientChannelProperties.versionSelector();
}
}, connection);
connection.sendToServer(capture(clientVersionCapture));
EasyMock.expect(wallet.getExtensions()).andReturn(new HashMap<String, WalletExtension>());
replay(connection, wallet);
@ -104,7 +126,7 @@ public class PaymentChannelClientTest {
assertEquals("Wrong type " + type, CLIENT_VERSION, type);
final Protos.ClientVersion clientVersion = response.getClientVersion();
final int major = clientVersion.getMajor();
final int requestedVersion = versionSelector.getRequestedMajorVersion();
final int requestedVersion = clientChannelProperties.versionSelector().getRequestedMajorVersion();
assertEquals("Wrong major version " + major, requestedVersion, major);
final long actualTimeWindow = clientVersion.getTimeWindowSecs();
assertEquals("Wrong timeWindow " + actualTimeWindow, expectedTimeWindow, actualTimeWindow );

View File

@ -92,7 +92,16 @@ public class PaymentChannelServerTest {
connection.sendToClient(capture(initiateCapture));
replay(connection);
dut = new PaymentChannelServer(broadcaster, wallet, Coin.CENT, minTimeWindow, 40000, connection);
dut = new PaymentChannelServer(broadcaster, wallet, Coin.CENT, new PaymentChannelServer.DefaultServerChannelProperties() {
@Override
public long getMinTimeWindow() {
return minTimeWindow;
}
@Override
public long getMaxTimeWindow() {
return 40000;
}
}, connection);
dut.connectionOpen();
dut.receiveMessage(message);
@ -111,7 +120,14 @@ public class PaymentChannelServerTest {
connection.sendToClient(capture(initiateCapture));
replay(connection);
dut = new PaymentChannelServer(broadcaster, wallet, Coin.CENT, 20000, maxTimeWindow, connection);
dut = new PaymentChannelServer(broadcaster, wallet, Coin.CENT, new PaymentChannelServer.DefaultServerChannelProperties(){
@Override
public long getMaxTimeWindow() {
return maxTimeWindow;
}
@Override
public long getMinTimeWindow() { return 20000; }
}, connection);
dut.connectionOpen();
dut.receiveMessage(message);
@ -123,12 +139,24 @@ public class PaymentChannelServerTest {
@Test(expected = IllegalArgumentException.class)
public void shouldNotAllowTimeWindowLessThan2h() {
dut = new PaymentChannelServer(broadcaster, wallet, Coin.CENT, 7199, 40000, connection);
dut = new PaymentChannelServer(broadcaster, wallet, Coin.CENT, new PaymentChannelServer.DefaultServerChannelProperties(){
@Override
public long getMaxTimeWindow() { return 40000; }
@Override
public long getMinTimeWindow() {
return 7199;
}
}, connection);
}
@Test(expected = IllegalArgumentException.class)
public void shouldNotAllowNegativeTimeWindow() {
dut = new PaymentChannelServer(broadcaster, wallet, Coin.CENT, 40001, 40000, connection);
dut = new PaymentChannelServer(broadcaster, wallet, Coin.CENT, new PaymentChannelServer.DefaultServerChannelProperties(){
@Override
public long getMaxTimeWindow() { return 40000; }
@Override
public long getMinTimeWindow() { return 40001; }
}, connection);
}
@Test
@ -139,7 +167,12 @@ public class PaymentChannelServerTest {
replay(connection);
final int expire = 24 * 60 * 60 - 60; // This the default defined in paymentchannel.proto
dut = new PaymentChannelServer(broadcaster, wallet, Coin.CENT, expire, expire, connection);
dut = new PaymentChannelServer(broadcaster, wallet, Coin.CENT, new PaymentChannelServer.DefaultServerChannelProperties(){
@Override
public long getMaxTimeWindow() { return expire; }
@Override
public long getMinTimeWindow() { return expire; }
}, connection);
dut.connectionOpen();
long expectedExpire = Utils.currentTimeSeconds() + expire;
dut.receiveMessage(message);

View File

@ -817,25 +817,21 @@ public class PaymentChannelStateTest extends TestWithWallet {
switch (versionSelector) {
case VERSION_1:
clientState = new PaymentChannelV1ClientState(wallet, myKey, ECKey.fromPublicOnly(serverKey.getPubKey()), CENT, EXPIRE_TIME) {
@Override
protected void editContractSendRequest(SendRequest req) {
req.coinSelector = wallet.getCoinSelector();
}
};
clientState = new PaymentChannelV1ClientState(wallet, myKey, ECKey.fromPublicOnly(serverKey.getPubKey()), CENT, EXPIRE_TIME) ;
break;
case VERSION_2_ALLOW_1:
case VERSION_2:
clientState = new PaymentChannelV2ClientState(wallet, myKey, ECKey.fromPublicOnly(serverKey.getPubKey()), CENT, EXPIRE_TIME) {
@Override
protected void editContractSendRequest(SendRequest req) {
req.coinSelector = wallet.getCoinSelector();
}
};
clientState = new PaymentChannelV2ClientState(wallet, myKey, ECKey.fromPublicOnly(serverKey.getPubKey()), CENT, EXPIRE_TIME);
break;
}
assertEquals(PaymentChannelClientState.State.NEW, clientState.getState());
clientState.initiate();
clientState.initiate(null, new PaymentChannelClient.DefaultClientChannelProperties() {
@Override
public SendRequest modifyContractSendRequest(SendRequest sendRequest) {
sendRequest.coinSelector = wallet.getCoinSelector();
return sendRequest;
}
});
assertEquals(getInitialClientState(), clientState.getState());
if (useRefunds()) {

View File

@ -23,10 +23,7 @@ import joptsimple.OptionSpec;
import org.bitcoinj.core.*;
import org.bitcoinj.kits.WalletAppKit;
import org.bitcoinj.params.RegTestParams;
import org.bitcoinj.protocols.channels.PaymentChannelClient;
import org.bitcoinj.protocols.channels.PaymentChannelClientConnection;
import org.bitcoinj.protocols.channels.StoredPaymentChannelClientStates;
import org.bitcoinj.protocols.channels.ValueOutOfRangeException;
import org.bitcoinj.protocols.channels.*;
import org.bitcoinj.utils.BriefLogFormatter;
import org.bitcoinj.utils.Threading;
import org.bitcoinj.wallet.Wallet;
@ -70,14 +67,21 @@ public class ExamplePaymentChannelClient {
parser.printHelpOn(System.err);
return;
}
PaymentChannelClient.VersionSelector versionSelector = PaymentChannelClient.VersionSelector.VERSION_1;
IPaymentChannelClient.ClientChannelProperties clientChannelProperties = new PaymentChannelClient.DefaultClientChannelProperties(){
@Override
public PaymentChannelClient.VersionSelector versionSelector() { return PaymentChannelClient.VersionSelector.VERSION_1; }
};
if (opts.has("version")) {
switch (version.value(opts)) {
case 1:
versionSelector = PaymentChannelClient.VersionSelector.VERSION_1;
// Keep the default
break;
case 2:
versionSelector = PaymentChannelClient.VersionSelector.VERSION_2;
clientChannelProperties = new PaymentChannelClient.DefaultClientChannelProperties(){
@Override
public PaymentChannelClient.VersionSelector versionSelector() { return PaymentChannelClient.VersionSelector.VERSION_2; }
};
break;
default:
System.err.println("Invalid version - valid versions are 1, 2");
@ -85,7 +89,7 @@ public class ExamplePaymentChannelClient {
}
}
NetworkParameters params = net.value(opts).get();
new ExamplePaymentChannelClient().run(opts.nonOptionArguments().get(0), versionSelector, params);
new ExamplePaymentChannelClient().run(opts.nonOptionArguments().get(0), clientChannelProperties, params);
}
public ExamplePaymentChannelClient() {
@ -94,7 +98,7 @@ public class ExamplePaymentChannelClient {
params = RegTestParams.get();
}
public void run(final String host, PaymentChannelClient.VersionSelector versionSelector, final NetworkParameters params) throws Exception {
public void run(final String host, IPaymentChannelClient.ClientChannelProperties clientChannelProperties, final NetworkParameters params) 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.
@ -137,19 +141,19 @@ public class ExamplePaymentChannelClient {
// demonstrates resuming a channel that wasn't closed yet. It should close automatically once we run out
// of money on the channel.
log.info("Round one ...");
openAndSend(timeoutSeconds, server, channelID, 5, versionSelector);
openAndSend(timeoutSeconds, server, channelID, 5, clientChannelProperties);
log.info("Round two ...");
log.info(appKit.wallet().toString());
openAndSend(timeoutSeconds, server, channelID, 4, versionSelector); // 4 times because the opening of the channel made a payment.
openAndSend(timeoutSeconds, server, channelID, 4, clientChannelProperties); // 4 times because the opening of the channel made a payment.
log.info("Stopping ...");
appKit.stopAsync();
appKit.awaitTerminated();
}
private void openAndSend(int timeoutSecs, InetSocketAddress server, String channelID, final int times, PaymentChannelClient.VersionSelector versionSelector) throws IOException, ValueOutOfRangeException, InterruptedException {
private void openAndSend(int timeoutSecs, InetSocketAddress server, String channelID, final int times, IPaymentChannelClient.ClientChannelProperties clientChannelProperties) throws IOException, ValueOutOfRangeException, InterruptedException {
// Use protocol version 1 for simplicity
PaymentChannelClientConnection client = new PaymentChannelClientConnection(
server, timeoutSecs, appKit.wallet(), myKey, channelSize, channelID, versionSelector);
server, timeoutSecs, appKit.wallet(), myKey, channelSize, channelID, null, clientChannelProperties);
// Opening the channel requires talking to the server, so it's asynchronous.
final CountDownLatch latch = new CountDownLatch(1);
Futures.addCallback(client.getChannelOpenFuture(), new FutureCallback<PaymentChannelClientConnection>() {