mirror of
https://github.com/bitcoinj/bitcoinj.git
synced 2024-11-20 10:12:19 +01:00
Payment channel API. Negotiation af the channel duration.
1. Client requests a time window, in seconds relative to now. 2. Server suggests an expire time, absolute time in seconds 3. Client accepts or rejects the servers proposal. Note that the IPaymentChannelClient.ClinentConnection interface has a new method. This will break old implementations. Let the client request the duration of a payment channel. Server can set allowed time window.
This commit is contained in:
parent
f569e10c17
commit
63b4b179e0
@ -120,6 +120,15 @@ public interface IPaymentChannelClient {
|
||||
*/
|
||||
void destroyConnection(PaymentChannelCloseException.CloseReason reason);
|
||||
|
||||
|
||||
/**
|
||||
* <p>Queries if the expire time proposed by server is acceptable. If <code>false</code> is return the channel
|
||||
* will be closed with a {@link com.google.bitcoin.protocols.channels.PaymentChannelCloseException.CloseReason#TIME_WINDOW_UNACCEPTABLE}.</p>
|
||||
* @param expireTime The time, in seconds, when this channel will be closed by the server. Note this is in absolute time, i.e. seconds since 1970-01-01T00:00:00.
|
||||
* @return <code>true</code> if the proposed time is acceptable <code>false</code> otherwise.
|
||||
*/
|
||||
public boolean acceptExpireTime(long expireTime);
|
||||
|
||||
/**
|
||||
* <p>Indicates the channel has been successfully opened and
|
||||
* {@link com.google.bitcoin.protocols.channels.PaymentChannelClient#incrementPayment(Coin)}
|
||||
|
@ -29,6 +29,8 @@ import org.bitcoin.paymentchannel.Protos;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkNotNull;
|
||||
@ -50,6 +52,9 @@ import static com.google.common.base.Preconditions.checkState;
|
||||
*/
|
||||
public class PaymentChannelClient implements IPaymentChannelClient {
|
||||
private static final org.slf4j.Logger log = LoggerFactory.getLogger(PaymentChannelClient.class);
|
||||
private static final int CLIENT_MAJOR_VERSION = 1;
|
||||
public final int CLIENT_MINOR_VERSION = 0;
|
||||
private static final int SERVER_MAJOR_VERSION = 1;
|
||||
|
||||
protected final ReentrantLock lock = Threading.lock("channelclient");
|
||||
|
||||
@ -61,6 +66,8 @@ public class PaymentChannelClient implements IPaymentChannelClient {
|
||||
// The state object used to step through initialization and pay the server
|
||||
@GuardedBy("lock") private PaymentChannelClientState state;
|
||||
|
||||
public static final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ");
|
||||
|
||||
// The step we are at in initialization, this is partially duplicated in the state object
|
||||
private enum InitStep {
|
||||
WAITING_FOR_CONNECTION_OPEN,
|
||||
@ -88,25 +95,25 @@ public class PaymentChannelClient implements IPaymentChannelClient {
|
||||
|
||||
private Coin missing;
|
||||
|
||||
private final long timeWindow;
|
||||
|
||||
@GuardedBy("lock") private long minPayment;
|
||||
|
||||
@GuardedBy("lock") SettableFuture<PaymentIncrementAck> increasePaymentFuture;
|
||||
@GuardedBy("lock") Coin lastPaymentActualAmount;
|
||||
|
||||
/**
|
||||
* <p>The maximum amount of time for which we will accept the server locking up our funds for the multisig
|
||||
* <p>The default maximum amount of time for which we will accept the server locking up our funds for the multisig
|
||||
* contract.</p>
|
||||
*
|
||||
* <p>Note that though this is not final, it is in all caps because it should generally not be modified unless you
|
||||
* have some guarantee that the server will not request at least this (channels will fail if this is too small).</p>
|
||||
*
|
||||
* <p>24 hours is the default as it is expected that clients limit risk exposure by limiting channel size instead of
|
||||
* <p>24 hours less a minute is the default as it is expected that clients limit risk exposure by limiting channel size instead of
|
||||
* limiting lock time when dealing with potentially malicious servers.</p>
|
||||
*/
|
||||
public long MAX_TIME_WINDOW = 24*60*60;
|
||||
public static final long DEFAULT_TIME_WINDOW = 24*60*60-60;
|
||||
|
||||
/**
|
||||
* 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.
|
||||
@ -122,10 +129,35 @@ public class PaymentChannelClient implements IPaymentChannelClient {
|
||||
* the server)
|
||||
*/
|
||||
public PaymentChannelClient(Wallet wallet, ECKey myKey, Coin maxValue, Sha256Hash serverId, ClientConnection conn) {
|
||||
this(wallet,myKey,maxValue,serverId, DEFAULT_TIME_WINDOW, conn);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a new channel manager which waits for {@link PaymentChannelClient#connectionOpen()} before acting.
|
||||
*
|
||||
* @param wallet The wallet which will be paid from, and where completed transactions will be committed.
|
||||
* Must already have a {@link StoredPaymentChannelClientStates} object in its extensions set.
|
||||
* @param myKey A freshly generated keypair used for the multisig contract and refund output.
|
||||
* @param maxValue The maximum value the server is allowed to request that we lock into this channel until the
|
||||
* refund transaction unlocks. Note that if there is a previously open channel, the refund
|
||||
* transaction used in this channel may be larger than maxValue. Thus, maxValue is not a method for
|
||||
* limiting the amount payable through this channel.
|
||||
* @param serverId An arbitrary hash representing this channel. This must uniquely identify the server. If an
|
||||
* existing stored channel exists in the wallet's {@link StoredPaymentChannelClientStates}, then an
|
||||
* attempt will be made to resume that channel.
|
||||
* @param 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 com.google.bitcoin.protocols.channels.IPaymentChannelClient.ClientConnection#acceptExpireTime(long)}
|
||||
* @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, 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);
|
||||
}
|
||||
|
||||
@ -144,15 +176,13 @@ public class PaymentChannelClient implements IPaymentChannelClient {
|
||||
private CloseReason receiveInitiate(Protos.Initiate initiate, Coin contractValue, Protos.Error.Builder errorBuilder) throws VerificationException, InsufficientMoneyException {
|
||||
log.info("Got INITIATE message:\n{}", initiate.toString());
|
||||
|
||||
checkState(initiate.getExpireTimeSecs() > 0 && initiate.getMinAcceptedChannelSize() >= 0);
|
||||
final long expireTime = initiate.getExpireTimeSecs();
|
||||
checkState( expireTime >= 0 && initiate.getMinAcceptedChannelSize() >= 0);
|
||||
|
||||
final long MAX_EXPIRY_TIME = Utils.currentTimeSeconds() + MAX_TIME_WINDOW;
|
||||
if (initiate.getExpireTimeSecs() > MAX_EXPIRY_TIME) {
|
||||
log.error("Server expiry time was out of our allowed bounds: {} vs {}", initiate.getExpireTimeSecs(),
|
||||
MAX_EXPIRY_TIME);
|
||||
errorBuilder.setCode(Protos.Error.ErrorCode.TIME_WINDOW_TOO_LARGE);
|
||||
errorBuilder.setExpectedValue(MAX_EXPIRY_TIME);
|
||||
return CloseReason.TIME_WINDOW_TOO_LARGE;
|
||||
if (! conn.acceptExpireTime(expireTime)) {
|
||||
log.error("Server suggested expire time was out of our allowed bounds: {} ({} s)", dateFormat.format(new Date(expireTime * 1000)), expireTime);
|
||||
errorBuilder.setCode(Protos.Error.ErrorCode.TIME_WINDOW_UNACCEPTABLE);
|
||||
return CloseReason.TIME_WINDOW_UNACCEPTABLE;
|
||||
}
|
||||
|
||||
Coin minChannelSize = Coin.valueOf(initiate.getMinAcceptedChannelSize());
|
||||
@ -177,8 +207,7 @@ public class PaymentChannelClient implements IPaymentChannelClient {
|
||||
final byte[] pubKeyBytes = initiate.getMultisigKey().toByteArray();
|
||||
if (!ECKey.isPubKeyCanonical(pubKeyBytes))
|
||||
throw new VerificationException("Server gave us a non-canonical public key, protocol error.");
|
||||
state = new PaymentChannelClientState(wallet, myKey, ECKey.fromPublicOnly(pubKeyBytes),
|
||||
contractValue, initiate.getExpireTimeSecs());
|
||||
state = new PaymentChannelClientState(wallet, myKey, ECKey.fromPublicOnly(pubKeyBytes), contractValue, expireTime);
|
||||
try {
|
||||
state.initiate();
|
||||
} catch (ValueOutOfRangeException e) {
|
||||
@ -265,7 +294,7 @@ public class PaymentChannelClient implements IPaymentChannelClient {
|
||||
checkState(step == InitStep.WAITING_FOR_VERSION_NEGOTIATION && msg.hasServerVersion());
|
||||
// Server might send back a major version lower than our own if they want to fallback to a
|
||||
// lower version. We can't handle that, so we just close the channel.
|
||||
if (msg.getServerVersion().getMajor() != 1) {
|
||||
if (msg.getServerVersion().getMajor() != SERVER_MAJOR_VERSION) {
|
||||
errorBuilder = Protos.Error.newBuilder()
|
||||
.setCode(Protos.Error.ErrorCode.NO_ACCEPTABLE_VERSION);
|
||||
closeReason = CloseReason.NO_ACCEPTABLE_VERSION;
|
||||
@ -421,7 +450,9 @@ public class PaymentChannelClient implements IPaymentChannelClient {
|
||||
step = InitStep.WAITING_FOR_VERSION_NEGOTIATION;
|
||||
|
||||
Protos.ClientVersion.Builder versionNegotiationBuilder = Protos.ClientVersion.newBuilder()
|
||||
.setMajor(1).setMinor(0);
|
||||
.setMajor(CLIENT_MAJOR_VERSION)
|
||||
.setMinor(CLIENT_MINOR_VERSION)
|
||||
.setTimeWindowSecs(timeWindow);
|
||||
|
||||
if (storedChannel != null) {
|
||||
versionNegotiationBuilder.setPreviousChannelContractHash(ByteString.copyFrom(storedChannel.contract.getHash().getBytes()));
|
||||
|
@ -20,6 +20,7 @@ import com.google.bitcoin.core.Coin;
|
||||
import com.google.bitcoin.core.ECKey;
|
||||
import com.google.bitcoin.core.InsufficientMoneyException;
|
||||
import com.google.bitcoin.core.Sha256Hash;
|
||||
import com.google.bitcoin.core.Utils;
|
||||
import com.google.bitcoin.core.Wallet;
|
||||
import com.google.bitcoin.net.NioClient;
|
||||
import com.google.bitcoin.net.ProtobufParser;
|
||||
@ -44,7 +45,8 @@ public class PaymentChannelClientConnection {
|
||||
|
||||
/**
|
||||
* Attempts to open a new connection to and open a payment channel with the given host and port, blocking until the
|
||||
* connection is open
|
||||
* connection is open. The server is requested to keep the channel open for {@link com.google.bitcoin.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
|
||||
@ -63,9 +65,35 @@ 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.DEFAULT_TIME_WINDOW);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @param server The host/port pair where the server is listening.
|
||||
* @param timeoutSeconds The connection timeout and read timeout during initialization. This should be large enough
|
||||
* to accommodate ECDSA signature operations and network latency.
|
||||
* @param wallet The wallet which will be paid from, and where completed transactions will be committed.
|
||||
* Must already have a {@link StoredPaymentChannelClientStates} object in its extensions set.
|
||||
* @param myKey A freshly generated keypair used for the multisig contract and refund output.
|
||||
* @param maxValue The maximum value this channel is allowed to request
|
||||
* @param serverId A unique ID which is used to attempt reopening of an existing channel.
|
||||
* This must be unique to the server, and, if your application is exposing payment channels to some
|
||||
* API, this should also probably encompass some caller UID to avoid applications opening channels
|
||||
* which were created by others.
|
||||
* @param timeWindow The time in seconds, relative to now, on how long this channel should be kept open.
|
||||
*
|
||||
* @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) 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.create(serverId.getBytes()),
|
||||
channelClient = new PaymentChannelClient(wallet, myKey, maxValue, Sha256Hash.create(serverId.getBytes()), timeWindow,
|
||||
new PaymentChannelClient.ClientConnection() {
|
||||
@Override
|
||||
public void sendToServer(Protos.TwoWayChannelMessage msg) {
|
||||
@ -78,6 +106,11 @@ public class PaymentChannelClientConnection {
|
||||
wireParser.closeConnection();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean acceptExpireTime(long expireTime) {
|
||||
return expireTime <= (timeWindow + Utils.currentTimeSeconds() + 60); // One extra minute to compensate for time skew and latency
|
||||
}
|
||||
|
||||
@Override
|
||||
public void channelOpen(boolean wasInitiated) {
|
||||
wireParser.setSocketTimeout(0);
|
||||
|
@ -24,9 +24,9 @@ public class PaymentChannelCloseException extends Exception {
|
||||
public enum CloseReason {
|
||||
/** We could not find a version which was mutually acceptable with the client/server */
|
||||
NO_ACCEPTABLE_VERSION,
|
||||
/** Generated by a client when the server attempted to lock in our funds for an unacceptably long time */
|
||||
TIME_WINDOW_TOO_LARGE,
|
||||
/** Generated by a client when the server requested we lock up an unacceptably high value */
|
||||
/** Generated by the client when time window the suggested by the server is unacceptable */
|
||||
TIME_WINDOW_UNACCEPTABLE,
|
||||
/** Generated by the client when the server requested we lock up an unacceptably high value */
|
||||
SERVER_REQUESTED_TOO_MUCH_VALUE,
|
||||
/** Generated by the server when the client has used up all the value in the channel. */
|
||||
CHANNEL_EXHAUSTED,
|
||||
|
@ -46,6 +46,8 @@ public class PaymentChannelServer {
|
||||
private static final org.slf4j.Logger log = LoggerFactory.getLogger(PaymentChannelServer.class);
|
||||
|
||||
protected final ReentrantLock lock = Threading.lock("channelserver");
|
||||
public final int SERVER_MAJOR_VERSION = 1;
|
||||
public final int SERVER_MINOR_VERSION = 0;
|
||||
|
||||
// The step in the initialization process we are in, some of this is duplicated in the PaymentChannelServerState
|
||||
private enum InitStep {
|
||||
@ -129,17 +131,26 @@ public class PaymentChannelServer {
|
||||
// The time this channel expires (ie the refund transaction's locktime)
|
||||
@GuardedBy("lock") private long expireTime;
|
||||
|
||||
/**
|
||||
* <p>The amount of time we request the client lock in their funds.</p>
|
||||
*
|
||||
* <p>The value defaults to 24 hours - 60 seconds and should always be greater than 2 hours plus the amount of time
|
||||
* the channel is expected to be used and smaller than 24 hours minus the client <-> server latency minus some
|
||||
* factor to account for client clock inaccuracy.</p>
|
||||
*/
|
||||
public long timeWindow = 24*60*60 - 60;
|
||||
public static final long DEFAULT_MAX_TIME_WINDOW = 7 * 24 * 60 * 60;
|
||||
|
||||
/**
|
||||
* Creates a new server-side state manager which handles a single client connection.
|
||||
* Maximum channel duration, in seconds, that the client can request. Defaults to 1 week.
|
||||
* 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.
|
||||
*/
|
||||
protected final long maxTimeWindow;
|
||||
|
||||
public static final long DEFAULT_MIN_TIME_WINDOW = 4 * 60 * 60;
|
||||
public static final long HARD_MIN_TIME_WINDOW = -StoredPaymentChannelServerStates.CHANNEL_EXPIRE_OFFSET;
|
||||
/**
|
||||
* Minimum channel duration, in seconds, that the client can request. Should always be larger than than 2 hours, defaults to 4 hours
|
||||
*/
|
||||
protected final long minTimeWindow;
|
||||
|
||||
/**
|
||||
* Creates a new server-side state manager which handles a single client connection. The server will only accept
|
||||
* a channel with time window between 4 hours and 1 week. 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 broadcaster The PeerGroup on which transactions will be broadcast - should have multiple connections.
|
||||
* @param wallet The wallet which will be used to complete transactions.
|
||||
@ -154,10 +165,36 @@ 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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new server-side state manager which handles a single client connection.
|
||||
*
|
||||
* @param broadcaster The PeerGroup on which transactions will be broadcast - should have multiple connections.
|
||||
* @param wallet The wallet which will be used to complete transactions.
|
||||
* Unlike {@link PaymentChannelClient}, this does not have to already contain a StoredState manager
|
||||
* @param minAcceptedChannelSize The minimum value the client must lock into this channel. A value too large will be
|
||||
* rejected by clients, and a value too low will require excessive channel reopening
|
||||
* and may cause fees to be require to 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 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) {
|
||||
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.minAcceptedChannelSize = checkNotNull(minAcceptedChannelSize);
|
||||
this.conn = checkNotNull(conn);
|
||||
this.minTimeWindow = minTimeWindow;
|
||||
this.maxTimeWindow = maxTimeWindow;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -172,19 +209,21 @@ public class PaymentChannelServer {
|
||||
@GuardedBy("lock")
|
||||
private void receiveVersionMessage(Protos.TwoWayChannelMessage msg) throws VerificationException {
|
||||
checkState(step == InitStep.WAITING_ON_CLIENT_VERSION && msg.hasClientVersion());
|
||||
if (msg.getClientVersion().getMajor() != 1) {
|
||||
error("This server needs protocol v1", Protos.Error.ErrorCode.NO_ACCEPTABLE_VERSION,
|
||||
CloseReason.NO_ACCEPTABLE_VERSION);
|
||||
final Protos.ClientVersion clientVersion = msg.getClientVersion();
|
||||
final int major = clientVersion.getMajor();
|
||||
if (major != SERVER_MAJOR_VERSION) {
|
||||
error("This server needs protocol version " + SERVER_MAJOR_VERSION + " , client offered " + major,
|
||||
Protos.Error.ErrorCode.NO_ACCEPTABLE_VERSION, CloseReason.NO_ACCEPTABLE_VERSION);
|
||||
return;
|
||||
}
|
||||
|
||||
Protos.ServerVersion.Builder versionNegotiationBuilder = Protos.ServerVersion.newBuilder()
|
||||
.setMajor(1).setMinor(0);
|
||||
.setMajor(SERVER_MAJOR_VERSION).setMinor(SERVER_MINOR_VERSION);
|
||||
conn.sendToClient(Protos.TwoWayChannelMessage.newBuilder()
|
||||
.setType(Protos.TwoWayChannelMessage.MessageType.SERVER_VERSION)
|
||||
.setServerVersion(versionNegotiationBuilder)
|
||||
.build());
|
||||
ByteString reopenChannelContractHash = msg.getClientVersion().getPreviousChannelContractHash();
|
||||
ByteString reopenChannelContractHash = clientVersion.getPreviousChannelContractHash();
|
||||
if (reopenChannelContractHash != null && reopenChannelContractHash.size() == 32) {
|
||||
Sha256Hash contractHash = new Sha256Hash(reopenChannelContractHash.toByteArray());
|
||||
log.info("New client that wants to resume {}", contractHash);
|
||||
@ -221,7 +260,7 @@ public class PaymentChannelServer {
|
||||
myKey = new ECKey();
|
||||
wallet.freshReceiveKey();
|
||||
|
||||
expireTime = Utils.currentTimeSeconds() + timeWindow;
|
||||
expireTime = Utils.currentTimeSeconds() + truncateTimeWindow(clientVersion.getTimeWindowSecs());
|
||||
step = InitStep.WAITING_ON_UNSIGNED_REFUND;
|
||||
|
||||
Protos.Initiate.Builder initiateBuilder = Protos.Initiate.newBuilder()
|
||||
@ -236,6 +275,18 @@ public class PaymentChannelServer {
|
||||
.build());
|
||||
}
|
||||
|
||||
private long truncateTimeWindow(long timeWindow) {
|
||||
if (timeWindow < minTimeWindow) {
|
||||
log.info("client requested time window {} s to short, offering {} s", timeWindow, minTimeWindow);
|
||||
return minTimeWindow;
|
||||
}
|
||||
if (timeWindow > maxTimeWindow) {
|
||||
log.info("client requested time window {} s to long, offering {} s", timeWindow, minTimeWindow);
|
||||
return maxTimeWindow;
|
||||
}
|
||||
return timeWindow;
|
||||
}
|
||||
|
||||
@GuardedBy("lock")
|
||||
private void receiveRefundMessage(Protos.TwoWayChannelMessage msg) throws VerificationException {
|
||||
checkState(step == InitStep.WAITING_ON_UNSIGNED_REFUND && msg.hasProvideRefund());
|
||||
|
@ -2838,6 +2838,26 @@ public final class Protos {
|
||||
* </pre>
|
||||
*/
|
||||
com.google.protobuf.ByteString getPreviousChannelContractHash();
|
||||
|
||||
// optional uint64 time_window_secs = 4 [default = 86340];
|
||||
/**
|
||||
* <code>optional uint64 time_window_secs = 4 [default = 86340];</code>
|
||||
*
|
||||
* <pre>
|
||||
* How many seconds should the channel be open, only used when a new channel is created.
|
||||
* Defaults to 24 h minus 60 seconds, 24*60*60 - 60
|
||||
* </pre>
|
||||
*/
|
||||
boolean hasTimeWindowSecs();
|
||||
/**
|
||||
* <code>optional uint64 time_window_secs = 4 [default = 86340];</code>
|
||||
*
|
||||
* <pre>
|
||||
* How many seconds should the channel be open, only used when a new channel is created.
|
||||
* Defaults to 24 h minus 60 seconds, 24*60*60 - 60
|
||||
* </pre>
|
||||
*/
|
||||
long getTimeWindowSecs();
|
||||
}
|
||||
/**
|
||||
* Protobuf type {@code paymentchannels.ClientVersion}
|
||||
@ -2910,6 +2930,11 @@ public final class Protos {
|
||||
previousChannelContractHash_ = input.readBytes();
|
||||
break;
|
||||
}
|
||||
case 32: {
|
||||
bitField0_ |= 0x00000008;
|
||||
timeWindowSecs_ = input.readUInt64();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (com.google.protobuf.InvalidProtocolBufferException e) {
|
||||
@ -3012,10 +3037,37 @@ public final class Protos {
|
||||
return previousChannelContractHash_;
|
||||
}
|
||||
|
||||
// optional uint64 time_window_secs = 4 [default = 86340];
|
||||
public static final int TIME_WINDOW_SECS_FIELD_NUMBER = 4;
|
||||
private long timeWindowSecs_;
|
||||
/**
|
||||
* <code>optional uint64 time_window_secs = 4 [default = 86340];</code>
|
||||
*
|
||||
* <pre>
|
||||
* How many seconds should the channel be open, only used when a new channel is created.
|
||||
* Defaults to 24 h minus 60 seconds, 24*60*60 - 60
|
||||
* </pre>
|
||||
*/
|
||||
public boolean hasTimeWindowSecs() {
|
||||
return ((bitField0_ & 0x00000008) == 0x00000008);
|
||||
}
|
||||
/**
|
||||
* <code>optional uint64 time_window_secs = 4 [default = 86340];</code>
|
||||
*
|
||||
* <pre>
|
||||
* How many seconds should the channel be open, only used when a new channel is created.
|
||||
* Defaults to 24 h minus 60 seconds, 24*60*60 - 60
|
||||
* </pre>
|
||||
*/
|
||||
public long getTimeWindowSecs() {
|
||||
return timeWindowSecs_;
|
||||
}
|
||||
|
||||
private void initFields() {
|
||||
major_ = 0;
|
||||
minor_ = 0;
|
||||
previousChannelContractHash_ = com.google.protobuf.ByteString.EMPTY;
|
||||
timeWindowSecs_ = 86340L;
|
||||
}
|
||||
private byte memoizedIsInitialized = -1;
|
||||
public final boolean isInitialized() {
|
||||
@ -3042,6 +3094,9 @@ public final class Protos {
|
||||
if (((bitField0_ & 0x00000004) == 0x00000004)) {
|
||||
output.writeBytes(3, previousChannelContractHash_);
|
||||
}
|
||||
if (((bitField0_ & 0x00000008) == 0x00000008)) {
|
||||
output.writeUInt64(4, timeWindowSecs_);
|
||||
}
|
||||
getUnknownFields().writeTo(output);
|
||||
}
|
||||
|
||||
@ -3063,6 +3118,10 @@ public final class Protos {
|
||||
size += com.google.protobuf.CodedOutputStream
|
||||
.computeBytesSize(3, previousChannelContractHash_);
|
||||
}
|
||||
if (((bitField0_ & 0x00000008) == 0x00000008)) {
|
||||
size += com.google.protobuf.CodedOutputStream
|
||||
.computeUInt64Size(4, timeWindowSecs_);
|
||||
}
|
||||
size += getUnknownFields().getSerializedSize();
|
||||
memoizedSerializedSize = size;
|
||||
return size;
|
||||
@ -3190,6 +3249,8 @@ public final class Protos {
|
||||
bitField0_ = (bitField0_ & ~0x00000002);
|
||||
previousChannelContractHash_ = com.google.protobuf.ByteString.EMPTY;
|
||||
bitField0_ = (bitField0_ & ~0x00000004);
|
||||
timeWindowSecs_ = 86340L;
|
||||
bitField0_ = (bitField0_ & ~0x00000008);
|
||||
return this;
|
||||
}
|
||||
|
||||
@ -3230,6 +3291,10 @@ public final class Protos {
|
||||
to_bitField0_ |= 0x00000004;
|
||||
}
|
||||
result.previousChannelContractHash_ = previousChannelContractHash_;
|
||||
if (((from_bitField0_ & 0x00000008) == 0x00000008)) {
|
||||
to_bitField0_ |= 0x00000008;
|
||||
}
|
||||
result.timeWindowSecs_ = timeWindowSecs_;
|
||||
result.bitField0_ = to_bitField0_;
|
||||
onBuilt();
|
||||
return result;
|
||||
@ -3255,6 +3320,9 @@ public final class Protos {
|
||||
if (other.hasPreviousChannelContractHash()) {
|
||||
setPreviousChannelContractHash(other.getPreviousChannelContractHash());
|
||||
}
|
||||
if (other.hasTimeWindowSecs()) {
|
||||
setTimeWindowSecs(other.getTimeWindowSecs());
|
||||
}
|
||||
this.mergeUnknownFields(other.getUnknownFields());
|
||||
return this;
|
||||
}
|
||||
@ -3416,6 +3484,59 @@ public final class Protos {
|
||||
return this;
|
||||
}
|
||||
|
||||
// optional uint64 time_window_secs = 4 [default = 86340];
|
||||
private long timeWindowSecs_ = 86340L;
|
||||
/**
|
||||
* <code>optional uint64 time_window_secs = 4 [default = 86340];</code>
|
||||
*
|
||||
* <pre>
|
||||
* How many seconds should the channel be open, only used when a new channel is created.
|
||||
* Defaults to 24 h minus 60 seconds, 24*60*60 - 60
|
||||
* </pre>
|
||||
*/
|
||||
public boolean hasTimeWindowSecs() {
|
||||
return ((bitField0_ & 0x00000008) == 0x00000008);
|
||||
}
|
||||
/**
|
||||
* <code>optional uint64 time_window_secs = 4 [default = 86340];</code>
|
||||
*
|
||||
* <pre>
|
||||
* How many seconds should the channel be open, only used when a new channel is created.
|
||||
* Defaults to 24 h minus 60 seconds, 24*60*60 - 60
|
||||
* </pre>
|
||||
*/
|
||||
public long getTimeWindowSecs() {
|
||||
return timeWindowSecs_;
|
||||
}
|
||||
/**
|
||||
* <code>optional uint64 time_window_secs = 4 [default = 86340];</code>
|
||||
*
|
||||
* <pre>
|
||||
* How many seconds should the channel be open, only used when a new channel is created.
|
||||
* Defaults to 24 h minus 60 seconds, 24*60*60 - 60
|
||||
* </pre>
|
||||
*/
|
||||
public Builder setTimeWindowSecs(long value) {
|
||||
bitField0_ |= 0x00000008;
|
||||
timeWindowSecs_ = value;
|
||||
onChanged();
|
||||
return this;
|
||||
}
|
||||
/**
|
||||
* <code>optional uint64 time_window_secs = 4 [default = 86340];</code>
|
||||
*
|
||||
* <pre>
|
||||
* How many seconds should the channel be open, only used when a new channel is created.
|
||||
* Defaults to 24 h minus 60 seconds, 24*60*60 - 60
|
||||
* </pre>
|
||||
*/
|
||||
public Builder clearTimeWindowSecs() {
|
||||
bitField0_ = (bitField0_ & ~0x00000008);
|
||||
timeWindowSecs_ = 86340L;
|
||||
onChanged();
|
||||
return this;
|
||||
}
|
||||
|
||||
// @@protoc_insertion_point(builder_scope:paymentchannels.ClientVersion)
|
||||
}
|
||||
|
||||
@ -8567,14 +8688,14 @@ public final class Protos {
|
||||
*/
|
||||
BAD_TRANSACTION(3, 4),
|
||||
/**
|
||||
* <code>TIME_WINDOW_TOO_LARGE = 5;</code>
|
||||
* <code>TIME_WINDOW_UNACCEPTABLE = 5;</code>
|
||||
*
|
||||
* <pre>
|
||||
* (wrong inputs/outputs, sequence, lock time, signature,
|
||||
* etc)
|
||||
* </pre>
|
||||
*/
|
||||
TIME_WINDOW_TOO_LARGE(4, 5),
|
||||
TIME_WINDOW_UNACCEPTABLE(4, 5),
|
||||
/**
|
||||
* <code>CHANNEL_VALUE_TOO_LARGE = 6;</code>
|
||||
*
|
||||
@ -8630,14 +8751,14 @@ public final class Protos {
|
||||
*/
|
||||
public static final int BAD_TRANSACTION_VALUE = 4;
|
||||
/**
|
||||
* <code>TIME_WINDOW_TOO_LARGE = 5;</code>
|
||||
* <code>TIME_WINDOW_UNACCEPTABLE = 5;</code>
|
||||
*
|
||||
* <pre>
|
||||
* (wrong inputs/outputs, sequence, lock time, signature,
|
||||
* etc)
|
||||
* </pre>
|
||||
*/
|
||||
public static final int TIME_WINDOW_TOO_LARGE_VALUE = 5;
|
||||
public static final int TIME_WINDOW_UNACCEPTABLE_VALUE = 5;
|
||||
/**
|
||||
* <code>CHANNEL_VALUE_TOO_LARGE = 6;</code>
|
||||
*
|
||||
@ -8668,7 +8789,7 @@ public final class Protos {
|
||||
case 2: return SYNTAX_ERROR;
|
||||
case 3: return NO_ACCEPTABLE_VERSION;
|
||||
case 4: return BAD_TRANSACTION;
|
||||
case 5: return TIME_WINDOW_TOO_LARGE;
|
||||
case 5: return TIME_WINDOW_UNACCEPTABLE;
|
||||
case 6: return CHANNEL_VALUE_TOO_LARGE;
|
||||
case 7: return MIN_PAYMENT_TOO_LARGE;
|
||||
case 8: return OTHER;
|
||||
@ -9367,29 +9488,30 @@ public final class Protos {
|
||||
"\022\022\n\016PROVIDE_REFUND\020\004\022\021\n\rRETURN_REFUND\020\005\022" +
|
||||
"\024\n\020PROVIDE_CONTRACT\020\006\022\020\n\014CHANNEL_OPEN\020\007\022",
|
||||
"\022\n\016UPDATE_PAYMENT\020\010\022\017\n\013PAYMENT_ACK\020\013\022\t\n\005" +
|
||||
"CLOSE\020\t\022\t\n\005ERROR\020\n\"X\n\rClientVersion\022\r\n\005m" +
|
||||
"CLOSE\020\t\022\t\n\005ERROR\020\n\"y\n\rClientVersion\022\r\n\005m" +
|
||||
"ajor\030\001 \002(\005\022\020\n\005minor\030\002 \001(\005:\0010\022&\n\036previous" +
|
||||
"_channel_contract_hash\030\003 \001(\014\"0\n\rServerVe" +
|
||||
"rsion\022\r\n\005major\030\001 \002(\005\022\020\n\005minor\030\002 \001(\005:\0010\"r" +
|
||||
"\n\010Initiate\022\024\n\014multisig_key\030\001 \002(\014\022!\n\031min_" +
|
||||
"accepted_channel_size\030\002 \002(\004\022\030\n\020expire_ti" +
|
||||
"me_secs\030\003 \002(\004\022\023\n\013min_payment\030\004 \002(\004\"1\n\rPr" +
|
||||
"ovideRefund\022\024\n\014multisig_key\030\001 \002(\014\022\n\n\002tx\030" +
|
||||
"\002 \002(\014\"!\n\014ReturnRefund\022\021\n\tsignature\030\001 \002(\014",
|
||||
"\"V\n\017ProvideContract\022\n\n\002tx\030\001 \002(\014\0227\n\017initi" +
|
||||
"al_payment\030\002 \002(\0132\036.paymentchannels.Updat" +
|
||||
"ePayment\"M\n\rUpdatePayment\022\033\n\023client_chan" +
|
||||
"ge_value\030\001 \002(\004\022\021\n\tsignature\030\002 \002(\014\022\014\n\004inf" +
|
||||
"o\030\003 \001(\014\"\032\n\nPaymentAck\022\014\n\004info\030\001 \001(\014\"\030\n\nS" +
|
||||
"ettlement\022\n\n\002tx\030\003 \002(\014\"\246\002\n\005Error\0225\n\004code\030" +
|
||||
"\001 \001(\0162 .paymentchannels.Error.ErrorCode:" +
|
||||
"\005OTHER\022\023\n\013explanation\030\002 \001(\t\022\026\n\016expected_" +
|
||||
"value\030\003 \001(\004\"\270\001\n\tErrorCode\022\013\n\007TIMEOUT\020\001\022\020" +
|
||||
"\n\014SYNTAX_ERROR\020\002\022\031\n\025NO_ACCEPTABLE_VERSIO",
|
||||
"N\020\003\022\023\n\017BAD_TRANSACTION\020\004\022\031\n\025TIME_WINDOW_" +
|
||||
"TOO_LARGE\020\005\022\033\n\027CHANNEL_VALUE_TOO_LARGE\020\006" +
|
||||
"\022\031\n\025MIN_PAYMENT_TOO_LARGE\020\007\022\t\n\005OTHER\020\010B$" +
|
||||
"\n\032org.bitcoin.paymentchannelB\006Protos"
|
||||
"_channel_contract_hash\030\003 \001(\014\022\037\n\020time_win" +
|
||||
"dow_secs\030\004 \001(\004:\00586340\"0\n\rServerVersion\022\r" +
|
||||
"\n\005major\030\001 \002(\005\022\020\n\005minor\030\002 \001(\005:\0010\"r\n\010Initi" +
|
||||
"ate\022\024\n\014multisig_key\030\001 \002(\014\022!\n\031min_accepte" +
|
||||
"d_channel_size\030\002 \002(\004\022\030\n\020expire_time_secs" +
|
||||
"\030\003 \002(\004\022\023\n\013min_payment\030\004 \002(\004\"1\n\rProvideRe" +
|
||||
"fund\022\024\n\014multisig_key\030\001 \002(\014\022\n\n\002tx\030\002 \002(\014\"!",
|
||||
"\n\014ReturnRefund\022\021\n\tsignature\030\001 \002(\014\"V\n\017Pro" +
|
||||
"videContract\022\n\n\002tx\030\001 \002(\014\0227\n\017initial_paym" +
|
||||
"ent\030\002 \002(\0132\036.paymentchannels.UpdatePaymen" +
|
||||
"t\"M\n\rUpdatePayment\022\033\n\023client_change_valu" +
|
||||
"e\030\001 \002(\004\022\021\n\tsignature\030\002 \002(\014\022\014\n\004info\030\003 \001(\014" +
|
||||
"\"\032\n\nPaymentAck\022\014\n\004info\030\001 \001(\014\"\030\n\nSettleme" +
|
||||
"nt\022\n\n\002tx\030\003 \002(\014\"\251\002\n\005Error\0225\n\004code\030\001 \001(\0162 " +
|
||||
".paymentchannels.Error.ErrorCode:\005OTHER\022" +
|
||||
"\023\n\013explanation\030\002 \001(\t\022\026\n\016expected_value\030\003" +
|
||||
" \001(\004\"\273\001\n\tErrorCode\022\013\n\007TIMEOUT\020\001\022\020\n\014SYNTA",
|
||||
"X_ERROR\020\002\022\031\n\025NO_ACCEPTABLE_VERSION\020\003\022\023\n\017" +
|
||||
"BAD_TRANSACTION\020\004\022\034\n\030TIME_WINDOW_UNACCEP" +
|
||||
"TABLE\020\005\022\033\n\027CHANNEL_VALUE_TOO_LARGE\020\006\022\031\n\025" +
|
||||
"MIN_PAYMENT_TOO_LARGE\020\007\022\t\n\005OTHER\020\010B$\n\032or" +
|
||||
"g.bitcoin.paymentchannelB\006Protos"
|
||||
};
|
||||
com.google.protobuf.Descriptors.FileDescriptor.InternalDescriptorAssigner assigner =
|
||||
new com.google.protobuf.Descriptors.FileDescriptor.InternalDescriptorAssigner() {
|
||||
@ -9407,7 +9529,7 @@ public final class Protos {
|
||||
internal_static_paymentchannels_ClientVersion_fieldAccessorTable = new
|
||||
com.google.protobuf.GeneratedMessage.FieldAccessorTable(
|
||||
internal_static_paymentchannels_ClientVersion_descriptor,
|
||||
new java.lang.String[] { "Major", "Minor", "PreviousChannelContractHash", });
|
||||
new java.lang.String[] { "Major", "Minor", "PreviousChannelContractHash", "TimeWindowSecs", });
|
||||
internal_static_paymentchannels_ServerVersion_descriptor =
|
||||
getDescriptor().getMessageTypes().get(2);
|
||||
internal_static_paymentchannels_ServerVersion_fieldAccessorTable = new
|
||||
|
@ -100,6 +100,10 @@ message ClientVersion {
|
||||
// responds with a SERVER_VERSION and then immediately sends a CHANNEL_OPEN, it otherwise
|
||||
// follows SERVER_VERSION with an Initiate representing a new channel
|
||||
optional bytes previous_channel_contract_hash = 3;
|
||||
|
||||
// How many seconds should the channel be open, only used when a new channel is created.
|
||||
// Defaults to 24 h minus 60 seconds, 24*60*60 - 60
|
||||
optional uint64 time_window_secs = 4 [default = 86340];
|
||||
}
|
||||
|
||||
// Send by secondary to primary upon receiving the ClientVersion message. If it is willing to
|
||||
@ -232,18 +236,18 @@ message Settlement {
|
||||
// closing the socket (unless they just received an ERROR or a CLOSE)
|
||||
message Error {
|
||||
enum ErrorCode {
|
||||
TIMEOUT = 1; // Protocol timeout occurred (one party hung).
|
||||
SYNTAX_ERROR = 2; // Generic error indicating some message was not properly
|
||||
// formatted or was out of order.
|
||||
NO_ACCEPTABLE_VERSION = 3; // We don't speak the version the other side asked for.
|
||||
BAD_TRANSACTION = 4; // A provided transaction was not in the proper structure
|
||||
// (wrong inputs/outputs, sequence, lock time, signature,
|
||||
// etc)
|
||||
TIME_WINDOW_TOO_LARGE = 5; // The expire time specified by the secondary was too large
|
||||
// for the primary
|
||||
CHANNEL_VALUE_TOO_LARGE = 6; // The minimum channel value specified by the secondary was
|
||||
// too large for the primary
|
||||
MIN_PAYMENT_TOO_LARGE = 7; // The min "dust limit" specified by the server was too large for the client.
|
||||
TIMEOUT = 1; // Protocol timeout occurred (one party hung).
|
||||
SYNTAX_ERROR = 2; // Generic error indicating some message was not properly
|
||||
// formatted or was out of order.
|
||||
NO_ACCEPTABLE_VERSION = 3; // We don't speak the version the other side asked for.
|
||||
BAD_TRANSACTION = 4; // A provided transaction was not in the proper structure
|
||||
// (wrong inputs/outputs, sequence, lock time, signature,
|
||||
// etc)
|
||||
TIME_WINDOW_UNACCEPTABLE = 5; // The expire time specified by the secondary was unacceptable
|
||||
// for the primary
|
||||
CHANNEL_VALUE_TOO_LARGE = 6; // The minimum channel value specified by the secondary was
|
||||
// too large for the primary
|
||||
MIN_PAYMENT_TOO_LARGE = 7; // The min "dust limit" specified by the server was too large for the client.
|
||||
|
||||
OTHER = 8;
|
||||
};
|
||||
|
@ -42,6 +42,7 @@ import static org.junit.Assert.assertFalse;
|
||||
|
||||
@RunWith(value = Parameterized.class)
|
||||
public class NetworkAbstractionTests {
|
||||
private static final int CLIENT_MAJOR_VERSION = 1;
|
||||
private AtomicBoolean fail;
|
||||
private final int clientType;
|
||||
private final ClientConnectionManager channels;
|
||||
@ -433,14 +434,14 @@ public class NetworkAbstractionTests {
|
||||
Protos.TwoWayChannelMessage msg = Protos.TwoWayChannelMessage.newBuilder()
|
||||
.setType(Protos.TwoWayChannelMessage.MessageType.CHANNEL_OPEN)
|
||||
.setClientVersion(Protos.ClientVersion.newBuilder()
|
||||
.setMajor(1)
|
||||
.setMajor(CLIENT_MAJOR_VERSION)
|
||||
.setPreviousChannelContractHash(ByteString.copyFrom(new byte[0x10000 - 12])))
|
||||
.build();
|
||||
// Small message that fits in the buffer
|
||||
Protos.TwoWayChannelMessage msg2 = Protos.TwoWayChannelMessage.newBuilder()
|
||||
.setType(Protos.TwoWayChannelMessage.MessageType.CHANNEL_OPEN)
|
||||
.setClientVersion(Protos.ClientVersion.newBuilder()
|
||||
.setMajor(1)
|
||||
.setMajor(CLIENT_MAJOR_VERSION)
|
||||
.setPreviousChannelContractHash(ByteString.copyFrom(new byte[1])))
|
||||
.build();
|
||||
// Break up the message into chunks to simulate packet network (with strange MTUs...)
|
||||
@ -486,7 +487,7 @@ public class NetworkAbstractionTests {
|
||||
Protos.TwoWayChannelMessage msg5 = Protos.TwoWayChannelMessage.newBuilder()
|
||||
.setType(Protos.TwoWayChannelMessage.MessageType.CHANNEL_OPEN)
|
||||
.setClientVersion(Protos.ClientVersion.newBuilder()
|
||||
.setMajor(1)
|
||||
.setMajor(CLIENT_MAJOR_VERSION)
|
||||
.setPreviousChannelContractHash(ByteString.copyFrom(new byte[0x10000 - 11])))
|
||||
.build();
|
||||
try {
|
||||
|
@ -46,6 +46,7 @@ import static org.bitcoin.paymentchannel.Protos.TwoWayChannelMessage.MessageType
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
public class ChannelConnectionTest extends TestWithWallet {
|
||||
private static final int CLIENT_MAJOR_VERSION = 1;
|
||||
private Wallet serverWallet;
|
||||
private BlockChain serverChain;
|
||||
private AtomicBoolean fail;
|
||||
@ -321,7 +322,7 @@ public class ChannelConnectionTest extends TestWithWallet {
|
||||
.setType(MessageType.CLIENT_VERSION)
|
||||
.setClientVersion(Protos.ClientVersion.newBuilder()
|
||||
.setPreviousChannelContractHash(ByteString.copyFrom(Sha256Hash.create(new byte[]{0x03}).getBytes()))
|
||||
.setMajor(1).setMinor(42))
|
||||
.setMajor(CLIENT_MAJOR_VERSION).setMinor(42))
|
||||
.build());
|
||||
pair.serverRecorder.checkNextMsg(MessageType.SERVER_VERSION);
|
||||
pair.serverRecorder.checkNextMsg(MessageType.INITIATE);
|
||||
@ -382,7 +383,7 @@ public class ChannelConnectionTest extends TestWithWallet {
|
||||
.setType(MessageType.CLIENT_VERSION)
|
||||
.setClientVersion(Protos.ClientVersion.newBuilder()
|
||||
.setPreviousChannelContractHash(ByteString.copyFrom(contractHash.getBytes()))
|
||||
.setMajor(1).setMinor(42))
|
||||
.setMajor(CLIENT_MAJOR_VERSION).setMinor(42))
|
||||
.build());
|
||||
// We get the usual resume sequence.
|
||||
pair.serverRecorder.checkNextMsg(MessageType.SERVER_VERSION);
|
||||
@ -440,7 +441,7 @@ public class ChannelConnectionTest extends TestWithWallet {
|
||||
.setType(MessageType.CLIENT_VERSION)
|
||||
.setClientVersion(Protos.ClientVersion.newBuilder()
|
||||
.setPreviousChannelContractHash(ByteString.copyFrom(new byte[]{0x00, 0x01}))
|
||||
.setMajor(1).setMinor(42))
|
||||
.setMajor(CLIENT_MAJOR_VERSION).setMinor(42))
|
||||
.build());
|
||||
|
||||
srv.serverRecorder.checkNextMsg(MessageType.SERVER_VERSION);
|
||||
@ -456,7 +457,7 @@ public class ChannelConnectionTest extends TestWithWallet {
|
||||
client.connectionOpen();
|
||||
pair.clientRecorder.checkNextMsg(MessageType.CLIENT_VERSION);
|
||||
client.receiveMessage(Protos.TwoWayChannelMessage.newBuilder()
|
||||
.setServerVersion(Protos.ServerVersion.newBuilder().setMajor(2))
|
||||
.setServerVersion(Protos.ServerVersion.newBuilder().setMajor(-1))
|
||||
.setType(MessageType.SERVER_VERSION).build());
|
||||
pair.clientRecorder.checkNextMsg(MessageType.ERROR);
|
||||
assertEquals(CloseReason.NO_ACCEPTABLE_VERSION, pair.clientRecorder.q.take());
|
||||
@ -468,9 +469,9 @@ public class ChannelConnectionTest extends TestWithWallet {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testClientTimeWindowTooLarge() throws Exception {
|
||||
public void testClientTimeWindowUnacceptable() throws Exception {
|
||||
// Tests that clients reject too large time windows
|
||||
ChannelTestUtils.RecordingPair pair = ChannelTestUtils.makeRecorders(serverWallet, mockBroadcaster);
|
||||
ChannelTestUtils.RecordingPair pair = ChannelTestUtils.makeRecorders(serverWallet, mockBroadcaster, 100);
|
||||
PaymentChannelServer server = pair.server;
|
||||
PaymentChannelClient client = new PaymentChannelClient(wallet, myKey, COIN, Sha256Hash.ZERO_HASH, pair.clientRecorder);
|
||||
client.connectionOpen();
|
||||
@ -485,7 +486,7 @@ public class ChannelConnectionTest extends TestWithWallet {
|
||||
.setType(MessageType.INITIATE).build());
|
||||
|
||||
pair.clientRecorder.checkNextMsg(MessageType.ERROR);
|
||||
assertEquals(CloseReason.TIME_WINDOW_TOO_LARGE, pair.clientRecorder.q.take());
|
||||
assertEquals(CloseReason.TIME_WINDOW_UNACCEPTABLE, pair.clientRecorder.q.take());
|
||||
// Double-check that we cant do anything that requires an open channel
|
||||
try {
|
||||
client.incrementPayment(Coin.SATOSHI);
|
||||
|
@ -60,11 +60,17 @@ public class ChannelTestUtils {
|
||||
|
||||
public static class RecordingClientConnection implements PaymentChannelClient.ClientConnection {
|
||||
public BlockingQueue<Object> q = new LinkedBlockingQueue<Object>();
|
||||
final static int IGNORE_EXPIRE = -1;
|
||||
private final int maxExpireTime;
|
||||
|
||||
// An arbitrary sentinel object for equality testing.
|
||||
public static final Object CHANNEL_INITIATED = new Object();
|
||||
public static final Object CHANNEL_OPEN = new Object();
|
||||
|
||||
public RecordingClientConnection(int maxExpireTime) {
|
||||
this.maxExpireTime = maxExpireTime;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendToServer(Protos.TwoWayChannelMessage msg) {
|
||||
q.add(msg);
|
||||
@ -75,6 +81,11 @@ public class ChannelTestUtils {
|
||||
q.add(reason);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean acceptExpireTime(long expireTime) {
|
||||
return this.maxExpireTime == IGNORE_EXPIRE || expireTime <= maxExpireTime;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void channelOpen(boolean wasInitiated) {
|
||||
if (wasInitiated)
|
||||
@ -109,10 +120,13 @@ public class ChannelTestUtils {
|
||||
}
|
||||
|
||||
public static RecordingPair makeRecorders(final Wallet serverWallet, final TransactionBroadcaster mockBroadcaster) {
|
||||
return makeRecorders(serverWallet, mockBroadcaster, RecordingClientConnection.IGNORE_EXPIRE);
|
||||
}
|
||||
public static RecordingPair makeRecorders(final Wallet serverWallet, final TransactionBroadcaster mockBroadcaster, int maxExpireTime) {
|
||||
RecordingPair pair = new RecordingPair();
|
||||
pair.serverRecorder = new RecordingServerConnection();
|
||||
pair.server = new PaymentChannelServer(mockBroadcaster, serverWallet, Coin.COIN, pair.serverRecorder);
|
||||
pair.clientRecorder = new RecordingClientConnection();
|
||||
pair.clientRecorder = new RecordingClientConnection(maxExpireTime);
|
||||
return pair;
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,70 @@
|
||||
package com.google.bitcoin.protocols.channels;
|
||||
|
||||
import com.google.bitcoin.core.*;
|
||||
import org.bitcoin.paymentchannel.Protos;
|
||||
import org.easymock.Capture;
|
||||
import org.easymock.EasyMock;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.HashMap;
|
||||
|
||||
import static org.bitcoin.paymentchannel.Protos.TwoWayChannelMessage;
|
||||
import static org.bitcoin.paymentchannel.Protos.TwoWayChannelMessage.MessageType.*;
|
||||
import static org.easymock.EasyMock.capture;
|
||||
import static org.easymock.EasyMock.createMock;
|
||||
import static org.easymock.EasyMock.replay;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
public class PaymentChannelClientTest {
|
||||
|
||||
private static final int CLIENT_MAJOR_VERSION = 1;
|
||||
private Wallet wallet;
|
||||
private ECKey ecKey;
|
||||
private Sha256Hash serverHash;
|
||||
private IPaymentChannelClient.ClientConnection connection;
|
||||
public Coin maxValue;
|
||||
public Capture<TwoWayChannelMessage> clientVersionCapture;
|
||||
public int defaultTimeWindow = 86340;
|
||||
|
||||
@Before
|
||||
public void before() {
|
||||
wallet = createMock(Wallet.class);
|
||||
ecKey = createMock(ECKey.class);
|
||||
maxValue = Coin.COIN;
|
||||
serverHash = Sha256Hash.create("serverId".getBytes());
|
||||
connection = createMock(IPaymentChannelClient.ClientConnection.class);
|
||||
clientVersionCapture = new Capture<TwoWayChannelMessage>();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldSendClientVersionOnChannelOpen() throws Exception {
|
||||
PaymentChannelClient dut = new PaymentChannelClient(wallet, ecKey, maxValue, serverHash, connection);
|
||||
connection.sendToServer(capture(clientVersionCapture));
|
||||
EasyMock.expect(wallet.getExtensions()).andReturn(new HashMap<String, WalletExtension>());
|
||||
replay(connection, wallet);
|
||||
dut.connectionOpen();
|
||||
assertClientVersion(defaultTimeWindow);
|
||||
}
|
||||
@Test
|
||||
public void shouldSendTimeWindowInClientVersion() throws Exception {
|
||||
long timeWindow = 4000;
|
||||
PaymentChannelClient dut = new PaymentChannelClient(wallet, ecKey, maxValue, serverHash, timeWindow, connection);
|
||||
connection.sendToServer(capture(clientVersionCapture));
|
||||
EasyMock.expect(wallet.getExtensions()).andReturn(new HashMap<String, WalletExtension>());
|
||||
replay(connection, wallet);
|
||||
dut.connectionOpen();
|
||||
assertClientVersion(4000);
|
||||
}
|
||||
|
||||
private void assertClientVersion(long expectedTimeWindow) {
|
||||
final TwoWayChannelMessage response = clientVersionCapture.getValue();
|
||||
final TwoWayChannelMessage.MessageType type = response.getType();
|
||||
assertEquals("Wrong type " + type, CLIENT_VERSION, type);
|
||||
final Protos.ClientVersion clientVersion = response.getClientVersion();
|
||||
final int major = clientVersion.getMajor();
|
||||
assertEquals("Wrong major version " + major, CLIENT_MAJOR_VERSION, major);
|
||||
final long actualTimeWindow = clientVersion.getTimeWindowSecs();
|
||||
assertEquals("Wrong timeWindow " + actualTimeWindow, expectedTimeWindow, actualTimeWindow );
|
||||
}
|
||||
}
|
@ -0,0 +1,149 @@
|
||||
package com.google.bitcoin.protocols.channels;
|
||||
|
||||
import com.google.bitcoin.core.Coin;
|
||||
import com.google.bitcoin.core.TransactionBroadcaster;
|
||||
import com.google.bitcoin.core.Utils;
|
||||
import com.google.bitcoin.core.Wallet;
|
||||
import org.bitcoin.paymentchannel.Protos;
|
||||
import org.easymock.Capture;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
import static junit.framework.TestCase.assertTrue;
|
||||
import static org.bitcoin.paymentchannel.Protos.TwoWayChannelMessage;
|
||||
import static org.bitcoin.paymentchannel.Protos.TwoWayChannelMessage.MessageType;
|
||||
import static org.easymock.EasyMock.*;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
public class PaymentChannelServerTest {
|
||||
|
||||
private static final int CLIENT_MAJOR_VERSION = 1;
|
||||
private static final long SERVER_MAJOR_VERSION = 1;
|
||||
public Wallet wallet;
|
||||
public PaymentChannelServer.ServerConnection connection;
|
||||
public PaymentChannelServer dut;
|
||||
public Capture<? extends TwoWayChannelMessage> serverVersionCapture;
|
||||
private TransactionBroadcaster broadcaster;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
broadcaster = createMock(TransactionBroadcaster.class);
|
||||
wallet = createMock(Wallet.class);
|
||||
connection = createMock(PaymentChannelServer.ServerConnection.class);
|
||||
serverVersionCapture = new Capture<TwoWayChannelMessage>();
|
||||
connection.sendToClient(capture(serverVersionCapture));
|
||||
Utils.setMockClock();
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void shouldAcceptDefaultTimeWindow() {
|
||||
final TwoWayChannelMessage message = createClientVersionMessage();
|
||||
final Capture<TwoWayChannelMessage> initiateCapture = new Capture<TwoWayChannelMessage>();
|
||||
connection.sendToClient(capture(initiateCapture));
|
||||
replay(connection);
|
||||
|
||||
dut = new PaymentChannelServer(broadcaster, wallet, Coin.CENT, connection);
|
||||
|
||||
dut.connectionOpen();
|
||||
dut.receiveMessage(message);
|
||||
|
||||
long expectedExpire = Utils.currentTimeSeconds() + 24 * 60 * 60 - 60; // This the default defined in paymentchannel.proto
|
||||
assertServerVersion();
|
||||
assertExpireTime(expectedExpire, initiateCapture);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldTruncateTooSmallTimeWindow() {
|
||||
final int minTimeWindow = 20000;
|
||||
final int timeWindow = minTimeWindow - 1;
|
||||
final TwoWayChannelMessage message = createClientVersionMessage(timeWindow);
|
||||
final Capture<TwoWayChannelMessage> initiateCapture = new Capture<TwoWayChannelMessage>();
|
||||
connection.sendToClient(capture(initiateCapture));
|
||||
|
||||
replay(connection);
|
||||
dut = new PaymentChannelServer(broadcaster, wallet, Coin.CENT, minTimeWindow, 40000, connection);
|
||||
|
||||
dut.connectionOpen();
|
||||
dut.receiveMessage(message);
|
||||
|
||||
long expectedExpire = Utils.currentTimeSeconds() + minTimeWindow;
|
||||
assertServerVersion();
|
||||
assertExpireTime(expectedExpire, initiateCapture);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldTruncateTooLargeTimeWindow() {
|
||||
final int maxTimeWindow = 40000;
|
||||
final int timeWindow = maxTimeWindow + 1;
|
||||
final TwoWayChannelMessage message = createClientVersionMessage(timeWindow);
|
||||
final Capture<TwoWayChannelMessage> initiateCapture = new Capture<TwoWayChannelMessage>();
|
||||
connection.sendToClient(capture(initiateCapture));
|
||||
replay(connection);
|
||||
|
||||
dut = new PaymentChannelServer(broadcaster, wallet, Coin.CENT, 20000, maxTimeWindow, connection);
|
||||
|
||||
dut.connectionOpen();
|
||||
dut.receiveMessage(message);
|
||||
|
||||
long expectedExpire = Utils.currentTimeSeconds() + maxTimeWindow;
|
||||
assertServerVersion();
|
||||
assertExpireTime(expectedExpire, initiateCapture);
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void shouldNotAllowTimeWindowLessThan2h() {
|
||||
dut = new PaymentChannelServer(broadcaster, wallet, Coin.CENT, 7199, 40000, connection);
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void shouldNotAllowNegativeTimeWindow() {
|
||||
dut = new PaymentChannelServer(broadcaster, wallet, Coin.CENT, 40001, 40000, connection);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldAllowExactTimeWindow() {
|
||||
final TwoWayChannelMessage message = createClientVersionMessage();
|
||||
final Capture<TwoWayChannelMessage> initiateCapture = new Capture<TwoWayChannelMessage>();
|
||||
connection.sendToClient(capture(initiateCapture));
|
||||
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.connectionOpen();
|
||||
long expectedExpire = Utils.currentTimeSeconds() + expire;
|
||||
dut.receiveMessage(message);
|
||||
|
||||
assertServerVersion();
|
||||
assertExpireTime(expectedExpire, initiateCapture);
|
||||
}
|
||||
|
||||
private void assertServerVersion() {
|
||||
final TwoWayChannelMessage response = serverVersionCapture.getValue();
|
||||
final MessageType type = response.getType();
|
||||
assertEquals("Wrong type " + type, MessageType.SERVER_VERSION, type);
|
||||
final long major = response.getServerVersion().getMajor();
|
||||
assertEquals("Wrong major version", SERVER_MAJOR_VERSION, major);
|
||||
}
|
||||
|
||||
private void assertExpireTime(long expectedExpire, Capture<TwoWayChannelMessage> initiateCapture) {
|
||||
final TwoWayChannelMessage response = initiateCapture.getValue();
|
||||
final MessageType type = response.getType();
|
||||
assertEquals("Wrong type " + type, MessageType.INITIATE, type);
|
||||
final long actualExpire = response.getInitiate().getExpireTimeSecs();
|
||||
assertTrue("Expire time too small " + expectedExpire + " > " + actualExpire, expectedExpire <= actualExpire);
|
||||
assertTrue("Expire time too large " + expectedExpire + "<" + actualExpire, expectedExpire >= actualExpire);
|
||||
}
|
||||
|
||||
private TwoWayChannelMessage createClientVersionMessage() {
|
||||
final Protos.ClientVersion.Builder clientVersion = Protos.ClientVersion.newBuilder().setMajor(CLIENT_MAJOR_VERSION);
|
||||
return TwoWayChannelMessage.newBuilder().setType(MessageType.CLIENT_VERSION).setClientVersion(clientVersion).build();
|
||||
}
|
||||
|
||||
private TwoWayChannelMessage createClientVersionMessage(long timeWindow) {
|
||||
final Protos.ClientVersion.Builder clientVersion = Protos.ClientVersion.newBuilder().setMajor(CLIENT_MAJOR_VERSION);
|
||||
if (timeWindow > 0) clientVersion.setTimeWindowSecs(timeWindow);
|
||||
return TwoWayChannelMessage.newBuilder().setType(MessageType.CLIENT_VERSION).setClientVersion(clientVersion).build();
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in New Issue
Block a user