3
0
mirror of https://github.com/Qortal/altcoinj.git synced 2025-02-07 14:54:15 +00:00

Payment channels: server closes/settles the channel automatically if the client has sent the last remaining money in it.

Also, throw an exception if the client tries to submit a  rolled back amount of money instead of silently ignoring it.
This commit is contained in:
Mike Hearn 2013-10-16 19:21:06 +02:00
parent 5b28091c9a
commit 32a823804c
5 changed files with 62 additions and 34 deletions

View File

@ -12,6 +12,8 @@ public class PaymentChannelCloseException extends Exception {
TIME_WINDOW_TOO_LARGE,
/** Generated by a 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,
// Values after here indicate its probably possible to try reopening channel again
@ -45,16 +47,17 @@ public class PaymentChannelCloseException extends Exception {
CONNECTION_CLOSED,
}
CloseReason error;
public CloseReason getCloseReason() {
return error;
}
private final CloseReason error;
public PaymentChannelCloseException(String message, CloseReason error) {
super(message);
this.error = error;
}
public CloseReason getCloseReason() {
return error;
}
public String toString() {
return "PaymentChannelCloseException for reason " + getCloseReason().toString();
}

View File

@ -87,7 +87,7 @@ public class PaymentChannelServer {
*/
public void paymentIncrease(BigInteger by, BigInteger to);
}
@GuardedBy("lock") private final ServerConnection conn;
private final ServerConnection conn;
// Used to keep track of whether or not the "socket" ie connection is open and we can generate messages
@GuardedBy("lock") private boolean connectionOpen = false;
@ -262,11 +262,17 @@ public class PaymentChannelServer {
Protos.UpdatePayment updatePayment = msg.getUpdatePayment();
BigInteger lastBestPayment = state.getBestValueToMe();
state.incrementPayment(BigInteger.valueOf(updatePayment.getClientChangeValue()), updatePayment.getSignature().toByteArray());
final BigInteger refundSize = BigInteger.valueOf(updatePayment.getClientChangeValue());
boolean stillUsable = state.incrementPayment(refundSize, updatePayment.getSignature().toByteArray());
BigInteger bestPaymentChange = state.getBestValueToMe().subtract(lastBestPayment);
if (bestPaymentChange.compareTo(BigInteger.ZERO) > 0)
conn.paymentIncrease(bestPaymentChange, state.getBestValueToMe());
if (!stillUsable) {
log.info("Channel is now fully exhausted, closing/initiating settlement");
settlePayment(CloseReason.CHANNEL_EXHAUSTED);
}
}
/**
@ -351,36 +357,44 @@ public class PaymentChannelServer {
@GuardedBy("lock")
private void receiveCloseMessage() throws ValueOutOfRangeException {
log.info("Got CLOSE message, closing channel");
connectionClosing = true;
if (state != null) {
Futures.addCallback(state.close(), new FutureCallback<Transaction>() {
@Override
public void onSuccess(Transaction result) {
// Send the successfully accepted transaction back to the client.
final Protos.TwoWayChannelMessage.Builder msg = Protos.TwoWayChannelMessage.newBuilder();
msg.setType(Protos.TwoWayChannelMessage.MessageType.CLOSE);
if (result != null) {
// Result can be null on various error paths, like if we never actually opened
// properly and so on.
msg.getCloseBuilder().setTx(ByteString.copyFrom(result.bitcoinSerialize()));
}
log.info("Sending CLOSE back with finalized broadcast contract.");
conn.sendToClient(msg.build());
// The client is expected to hang up the TCP connection after we send back the
// CLOSE message.
}
@Override
public void onFailure(Throwable t) {
log.error("Failed to broadcast close TX", t);
conn.destroyConnection(CloseReason.CLIENT_REQUESTED_CLOSE);
}
});
settlePayment(CloseReason.CLIENT_REQUESTED_CLOSE);
} else {
conn.destroyConnection(CloseReason.CLIENT_REQUESTED_CLOSE);
}
}
@GuardedBy("lock")
private void settlePayment(final CloseReason clientRequestedClose) throws ValueOutOfRangeException {
// Setting connectionClosing here prevents us from sending another CLOSE when state.close() calls
// close() on us here below via the stored channel state.
// TODO: Strongly separate the lifecycle of the payment channel from the TCP connection in these classes.
connectionClosing = true;
Futures.addCallback(state.close(), new FutureCallback<Transaction>() {
@Override
public void onSuccess(Transaction result) {
// Send the successfully accepted transaction back to the client.
final Protos.TwoWayChannelMessage.Builder msg = Protos.TwoWayChannelMessage.newBuilder();
msg.setType(Protos.TwoWayChannelMessage.MessageType.CLOSE);
if (result != null) {
// Result can be null on various error paths, like if we never actually opened
// properly and so on.
msg.getCloseBuilder().setTx(ByteString.copyFrom(result.bitcoinSerialize()));
log.info("Sending CLOSE back with finalized broadcast contract.");
} else {
log.info("Sending CLOSE back without finalized broadcast contract.");
}
conn.sendToClient(msg.build());
}
@Override
public void onFailure(Throwable t) {
log.error("Failed to broadcast close TX", t);
conn.destroyConnection(clientRequestedClose);
}
});
}
/**
* <p>Called when the connection terminates. Notifies the {@link StoredServerChannel} object that we can attempt to
* resume this channel in the future and stops generating messages for the client.</p>

View File

@ -281,8 +281,9 @@ public class PaymentChannelServerState {
* @param refundSize How many satoshis of the original contract are refunded to the client (the rest are ours)
* @param signatureBytes The new signature spending the multi-sig contract to a new payment transaction
* @throws VerificationException If the signature does not verify or size is out of range (incl being rejected by the network as dust).
* @return true if there is more value left on the channel, false if it is now fully used up.
*/
public synchronized void incrementPayment(BigInteger refundSize, byte[] signatureBytes) throws VerificationException, ValueOutOfRangeException {
public synchronized boolean incrementPayment(BigInteger refundSize, byte[] signatureBytes) throws VerificationException, ValueOutOfRangeException {
checkState(state == State.READY);
checkNotNull(refundSize);
checkNotNull(signatureBytes);
@ -296,7 +297,7 @@ public class PaymentChannelServerState {
if (newValueToMe.compareTo(BigInteger.ZERO) < 0)
throw new ValueOutOfRangeException("Attempt to refund more than the contract allows.");
if (newValueToMe.compareTo(bestValueToMe) < 0)
return;
throw new ValueOutOfRangeException("Attempt to roll back payment on the channel.");
// Get the wallet's copy of the multisigContract (ie with confidence information), if this is null, the wallet
// was not connected to the peergroup when the contract was broadcast (which may cause issues down the road, and
@ -334,6 +335,7 @@ public class PaymentChannelServerState {
bestValueToMe = newValueToMe;
bestValueSignature = signatureBytes;
updateChannelInWallet();
return !fullyUsedUp;
}
// Signs the first input of the transaction which must spend the multisig contract.

View File

@ -601,11 +601,16 @@ public class ChannelConnectionTest extends TestWithWallet {
pair.clientRecorder.checkOpened();
assertNull(pair.serverRecorder.q.poll());
assertNull(pair.clientRecorder.q.poll());
// Send the whole channel at once.
// Send the whole channel at once. The server will broadcast the final contract and close the channel for us.
client.incrementPayment(Utils.COIN);
broadcastTxPause.release();
server.receiveMessage(pair.clientRecorder.checkNextMsg(MessageType.UPDATE_PAYMENT));
broadcasts.take();
// The channel is now empty.
assertEquals(BigInteger.ZERO, client.state().getValueRefunded());
pair.serverRecorder.q.take(); // Take the BigInteger.
client.receiveMessage(pair.serverRecorder.checkNextMsg(MessageType.CLOSE));
assertEquals(CloseReason.SERVER_REQUESTED_CLOSE, pair.clientRecorder.q.take());
client.connectionClosed();
// Now try opening a new channel with the same server ID and verify the client asks for a new channel.

View File

@ -513,7 +513,11 @@ public class PaymentChannelStateTest extends TestWithWallet {
serverState.incrementPayment(halfCoin.subtract(totalPayment), signature2);
serverState.incrementPayment(halfCoin.subtract(totalPayment.subtract(size)), signature);
// Trying to take reduce the refund size fails.
try {
serverState.incrementPayment(halfCoin.subtract(totalPayment.subtract(size)), signature);
fail();
} catch (ValueOutOfRangeException e) {}
assertEquals(serverState.getBestValueToMe(), totalPayment);
try {