diff --git a/core/src/main/java/org/bitcoinj/protocols/channels/IPaymentChannelClient.java b/core/src/main/java/org/bitcoinj/protocols/channels/IPaymentChannelClient.java index 398cf2ce..7e788c11 100644 --- a/core/src/main/java/org/bitcoinj/protocols/channels/IPaymentChannelClient.java +++ b/core/src/main/java/org/bitcoinj/protocols/channels/IPaymentChannelClient.java @@ -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. diff --git a/core/src/main/java/org/bitcoinj/protocols/channels/PaymentChannelClient.java b/core/src/main/java/org/bitcoinj/protocols/channels/PaymentChannelClient.java index 3bb0ed1b..cefd6d41 100644 --- a/core/src/main/java/org/bitcoinj/protocols/channels/PaymentChannelClient.java +++ b/core/src/main/java/org/bitcoinj/protocols/channels/PaymentChannelClient.java @@ -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(); } diff --git a/core/src/main/java/org/bitcoinj/protocols/channels/PaymentChannelClientConnection.java b/core/src/main/java/org/bitcoinj/protocols/channels/PaymentChannelClientConnection.java index 43491ed8..25e92a34 100644 --- a/core/src/main/java/org/bitcoinj/protocols/channels/PaymentChannelClientConnection.java +++ b/core/src/main/java/org/bitcoinj/protocols/channels/PaymentChannelClientConnection.java @@ -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(new ProtobufConnection.Listener() { diff --git a/core/src/main/java/org/bitcoinj/protocols/channels/PaymentChannelClientState.java b/core/src/main/java/org/bitcoinj/protocols/channels/PaymentChannelClientState.java index dd4e0b6c..8054efc0 100644 --- a/core/src/main/java/org/bitcoinj/protocols/channels/PaymentChannelClientState.java +++ b/core/src/main/java/org/bitcoinj/protocols/channels/PaymentChannelClientState.java @@ -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.

* - *

To begin, the client calls {@link PaymentChannelV1ClientState#initiate()}, which moves the channel into state + *

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

*/ 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(); diff --git a/core/src/main/java/org/bitcoinj/protocols/channels/PaymentChannelServer.java b/core/src/main/java/org/bitcoinj/protocols/channels/PaymentChannelServer.java index 8a4c6e22..a733f4c0 100644 --- a/core/src/main/java/org/bitcoinj/protocols/channels/PaymentChannelServer.java +++ b/core/src/main/java/org/bitcoinj/protocols/channels/PaymentChannelServer.java @@ -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; + } + + } + } diff --git a/core/src/main/java/org/bitcoinj/protocols/channels/PaymentChannelV1ClientState.java b/core/src/main/java/org/bitcoinj/protocols/channels/PaymentChannelV1ClientState.java index 57d89887..f1d741a4 100644 --- a/core/src/main/java/org/bitcoinj/protocols/channels/PaymentChannelV1ClientState.java +++ b/core/src/main/java/org/bitcoinj/protocols/channels/PaymentChannelV1ClientState.java @@ -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; diff --git a/core/src/main/java/org/bitcoinj/protocols/channels/PaymentChannelV2ClientState.java b/core/src/main/java/org/bitcoinj/protocols/channels/PaymentChannelV2ClientState.java index c35470dd..193f19be 100644 --- a/core/src/main/java/org/bitcoinj/protocols/channels/PaymentChannelV2ClientState.java +++ b/core/src/main/java/org/bitcoinj/protocols/channels/PaymentChannelV2ClientState.java @@ -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; diff --git a/core/src/test/java/org/bitcoinj/protocols/channels/ChannelConnectionTest.java b/core/src/test/java/org/bitcoinj/protocols/channels/ChannelConnectionTest.java index ad909503..eb2d7f25 100644 --- a/core/src/test/java/org/bitcoinj/protocols/channels/ChannelConnectionTest.java +++ b/core/src/test/java/org/bitcoinj/protocols/channels/ChannelConnectionTest.java @@ -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 data() { + public static Collection 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 true 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)); diff --git a/core/src/test/java/org/bitcoinj/protocols/channels/PaymentChannelClientTest.java b/core/src/test/java/org/bitcoinj/protocols/channels/PaymentChannelClientTest.java index 999782cc..25d04acd 100644 --- a/core/src/test/java/org/bitcoinj/protocols/channels/PaymentChannelClientTest.java +++ b/core/src/test/java/org/bitcoinj/protocols/channels/PaymentChannelClientTest.java @@ -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 data() { + public static Collection 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()); 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()); 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 ); diff --git a/core/src/test/java/org/bitcoinj/protocols/channels/PaymentChannelServerTest.java b/core/src/test/java/org/bitcoinj/protocols/channels/PaymentChannelServerTest.java index de32ced1..5fd81725 100644 --- a/core/src/test/java/org/bitcoinj/protocols/channels/PaymentChannelServerTest.java +++ b/core/src/test/java/org/bitcoinj/protocols/channels/PaymentChannelServerTest.java @@ -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); diff --git a/core/src/test/java/org/bitcoinj/protocols/channels/PaymentChannelStateTest.java b/core/src/test/java/org/bitcoinj/protocols/channels/PaymentChannelStateTest.java index 708b472b..bce67c4e 100644 --- a/core/src/test/java/org/bitcoinj/protocols/channels/PaymentChannelStateTest.java +++ b/core/src/test/java/org/bitcoinj/protocols/channels/PaymentChannelStateTest.java @@ -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()) { diff --git a/examples/src/main/java/org/bitcoinj/examples/ExamplePaymentChannelClient.java b/examples/src/main/java/org/bitcoinj/examples/ExamplePaymentChannelClient.java index 68550ece..26fa87ee 100644 --- a/examples/src/main/java/org/bitcoinj/examples/ExamplePaymentChannelClient.java +++ b/examples/src/main/java/org/bitcoinj/examples/ExamplePaymentChannelClient.java @@ -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() {