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:
parent
5b28091c9a
commit
32a823804c
@ -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();
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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.
|
||||
|
@ -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.
|
||||
|
@ -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 {
|
||||
|
Loading…
x
Reference in New Issue
Block a user