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

Payment channels: plumb through the actual amount of value sent on a channel, as it can sometimes be different to how much was requested.

This commit is contained in:
Mike Hearn 2013-10-09 18:15:04 +02:00
parent 6625c9a2cb
commit 4b48dbfda9
4 changed files with 54 additions and 34 deletions

View File

@ -214,8 +214,8 @@ public class PaymentChannelClient {
switch (msg.getType()) { switch (msg.getType()) {
case SERVER_VERSION: case SERVER_VERSION:
checkState(step == InitStep.WAITING_FOR_VERSION_NEGOTIATION && msg.hasServerVersion()); 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 // Server might send back a major version lower than our own if they want to fallback to a
// We can't handle that, so we just close the channel // lower version. We can't handle that, so we just close the channel.
if (msg.getServerVersion().getMajor() != 0) { if (msg.getServerVersion().getMajor() != 0) {
errorBuilder = Protos.Error.newBuilder() errorBuilder = Protos.Error.newBuilder()
.setCode(Protos.Error.ErrorCode.NO_ACCEPTABLE_VERSION); .setCode(Protos.Error.ErrorCode.NO_ACCEPTABLE_VERSION);
@ -243,6 +243,7 @@ public class PaymentChannelClient {
errorBuilder = Protos.Error.newBuilder() errorBuilder = Protos.Error.newBuilder()
.setCode(Protos.Error.ErrorCode.CHANNEL_VALUE_TOO_LARGE); .setCode(Protos.Error.ErrorCode.CHANNEL_VALUE_TOO_LARGE);
closeReason = CloseReason.SERVER_REQUESTED_TOO_MUCH_VALUE; closeReason = CloseReason.SERVER_REQUESTED_TOO_MUCH_VALUE;
log.error("Server requested too much value");
break; break;
} }
@ -411,29 +412,34 @@ public class PaymentChannelClient {
} }
/** /**
* Increments the total value which we pay the server. * Increments the total value which we pay the server. Note that the amount of money sent may not be the same as the
* amount of money actually requested. It can be larger if the amount left over in the channel would be too small to
* be accepted by the Bitcoin network. ValueOutOfRangeException will be thrown, however, if there's not enough money
* left in the channel to make the payment at all.
* *
* @param size How many satoshis to increment the payment by (note: not the new total). * @param size How many satoshis to increment the payment by (note: not the new total).
* @throws ValueOutOfRangeException If the size is negative or would pay more than this channel's total value * @throws ValueOutOfRangeException If the size is negative or would pay more than this channel's total value
* ({@link PaymentChannelClientConnection#state()}.getTotalValue()) * ({@link PaymentChannelClientConnection#state()}.getTotalValue())
* @throws IllegalStateException If the channel has been closed or is not yet open * @throws IllegalStateException If the channel has been closed or is not yet open
* (see {@link PaymentChannelClientConnection#getChannelOpenFuture()} for the second) * (see {@link PaymentChannelClientConnection#getChannelOpenFuture()} for the second)
* @return amount of money actually sent.
*/ */
public void incrementPayment(BigInteger size) throws ValueOutOfRangeException, IllegalStateException { public BigInteger incrementPayment(BigInteger size) throws ValueOutOfRangeException, IllegalStateException {
lock.lock(); lock.lock();
try { try {
if (state() == null || !connectionOpen || step != InitStep.CHANNEL_OPEN) if (state() == null || !connectionOpen || step != InitStep.CHANNEL_OPEN)
throw new IllegalStateException("Channel is not fully initialized/has already been closed"); throw new IllegalStateException("Channel is not fully initialized/has already been closed");
byte[] signature = state().incrementPaymentBy(size); PaymentChannelClientState.IncrementedPayment payment = state().incrementPaymentBy(size);
Protos.UpdatePayment.Builder updatePaymentBuilder = Protos.UpdatePayment.newBuilder() Protos.UpdatePayment.Builder updatePaymentBuilder = Protos.UpdatePayment.newBuilder()
.setSignature(ByteString.copyFrom(signature)) .setSignature(ByteString.copyFrom(payment.signature.encodeToBitcoin()))
.setClientChangeValue(state.getValueRefunded().longValue()); .setClientChangeValue(state.getValueRefunded().longValue());
conn.sendToServer(Protos.TwoWayChannelMessage.newBuilder() conn.sendToServer(Protos.TwoWayChannelMessage.newBuilder()
.setUpdatePayment(updatePaymentBuilder) .setUpdatePayment(updatePaymentBuilder)
.setType(Protos.TwoWayChannelMessage.MessageType.UPDATE_PAYMENT) .setType(Protos.TwoWayChannelMessage.MessageType.UPDATE_PAYMENT)
.build()); .build());
return payment.amount;
} finally { } finally {
lock.unlock(); lock.unlock();
} }

View File

@ -360,6 +360,12 @@ public class PaymentChannelClientState {
} }
} }
/** Container for a signature and an amount that was sent. */
public static class IncrementedPayment {
public TransactionSignature signature;
public BigInteger amount;
}
/** /**
* <p>Updates the outputs on the payment contract transaction and re-signs it. The state must be READY in order to * <p>Updates the outputs on the payment contract transaction and re-signs it. The state must be READY in order to
* call this method. The signature that is returned should be sent to the server so it has the ability to broadcast * call this method. The signature that is returned should be sent to the server so it has the ability to broadcast
@ -372,19 +378,23 @@ public class PaymentChannelClientState {
* {@link PaymentChannelClientState#getValueRefunded()}</p> * {@link PaymentChannelClientState#getValueRefunded()}</p>
* *
* @param size How many satoshis to increment the payment by (note: not the new total). * @param size How many satoshis to increment the payment by (note: not the new total).
* @throws ValueOutOfRangeException If size is negative or the new value being returned as change is smaller than * @throws ValueOutOfRangeException If size is negative or the channel does not have sufficient money in it to
* min nondust output size (including if the new total payment is larger than this * complete this payment.
* channel's totalValue)
*/ */
public synchronized byte[] incrementPaymentBy(BigInteger size) throws ValueOutOfRangeException { public synchronized IncrementedPayment incrementPaymentBy(BigInteger size) throws ValueOutOfRangeException {
checkState(state == State.READY); checkState(state == State.READY);
checkNotExpired(); checkNotExpired();
checkNotNull(size); // Validity of size will be checked by makeUnsignedChannelContract. checkNotNull(size); // Validity of size will be checked by makeUnsignedChannelContract.
if (size.compareTo(BigInteger.ZERO) < 0) if (size.compareTo(BigInteger.ZERO) < 0)
throw new ValueOutOfRangeException("Tried to decrement payment"); throw new ValueOutOfRangeException("Tried to decrement payment");
BigInteger newValueToMe = valueToMe.subtract(size); BigInteger newValueToMe = valueToMe.subtract(size);
if (Transaction.MIN_NONDUST_OUTPUT.compareTo(newValueToMe) > 0 && !newValueToMe.equals(BigInteger.ZERO)) if (newValueToMe.compareTo(Transaction.MIN_NONDUST_OUTPUT) < 0 && newValueToMe.compareTo(BigInteger.ZERO) > 0) {
throw new ValueOutOfRangeException("New value being sent back as change was smaller than minimum nondust output"); log.info("New value being sent back as change was smaller than minimum nondust output, sending all");
size = valueToMe;
newValueToMe = BigInteger.ZERO;
}
if (newValueToMe.compareTo(BigInteger.ZERO) < 0)
throw new ValueOutOfRangeException("Channel has too little money to pay " + size + " satoshis");
Transaction tx = makeUnsignedChannelContract(newValueToMe); Transaction tx = makeUnsignedChannelContract(newValueToMe);
log.info("Signing new payment tx {}", tx); log.info("Signing new payment tx {}", tx);
Transaction.SigHash mode; Transaction.SigHash mode;
@ -397,7 +407,10 @@ public class PaymentChannelClientState {
TransactionSignature sig = tx.calculateSignature(0, myKey, multisigScript, mode, true); TransactionSignature sig = tx.calculateSignature(0, myKey, multisigScript, mode, true);
valueToMe = newValueToMe; valueToMe = newValueToMe;
updateChannelInWallet(); updateChannelInWallet();
return sig.encodeToBitcoin(); IncrementedPayment payment = new IncrementedPayment();
payment.signature = sig;
payment.amount = size;
return payment;
} }
private synchronized void updateChannelInWallet() { private synchronized void updateChannelInWallet() {

View File

@ -180,7 +180,8 @@ public class PaymentChannelServer {
log.error(" ... but we do not have any stored channels! Resume failed."); log.error(" ... but we do not have any stored channels! Resume failed.");
} }
} }
log.info("Got initial version message, responding with VERSIONS and INITIATE"); log.info("Got initial version message, responding with VERSIONS and INITIATE: min value={}",
minAcceptedChannelSize.longValue());
myKey = new ECKey(); myKey = new ECKey();
wallet.addKey(myKey); wallet.addKey(myKey);

View File

@ -170,7 +170,7 @@ public class PaymentChannelStateTest extends TestWithWallet {
BigInteger size = halfCoin.divide(BigInteger.TEN).divide(BigInteger.TEN); BigInteger size = halfCoin.divide(BigInteger.TEN).divide(BigInteger.TEN);
BigInteger totalPayment = BigInteger.ZERO; BigInteger totalPayment = BigInteger.ZERO;
for (int i = 0; i < 4; i++) { for (int i = 0; i < 4; i++) {
byte[] signature = clientState.incrementPaymentBy(size); byte[] signature = clientState.incrementPaymentBy(size).signature.encodeToBitcoin();
totalPayment = totalPayment.add(size); totalPayment = totalPayment.add(size);
serverState.incrementPayment(halfCoin.subtract(totalPayment), signature); serverState.incrementPayment(halfCoin.subtract(totalPayment), signature);
} }
@ -178,7 +178,7 @@ public class PaymentChannelStateTest extends TestWithWallet {
// Now confirm the contract transaction and make sure payments still work // Now confirm the contract transaction and make sure payments still work
chain.add(makeSolvedTestBlock(blockStore.getChainHead().getHeader(), multisigContract)); chain.add(makeSolvedTestBlock(blockStore.getChainHead().getHeader(), multisigContract));
byte[] signature = clientState.incrementPaymentBy(size); byte[] signature = clientState.incrementPaymentBy(size).signature.encodeToBitcoin();
totalPayment = totalPayment.add(size); totalPayment = totalPayment.add(size);
serverState.incrementPayment(halfCoin.subtract(totalPayment), signature); serverState.incrementPayment(halfCoin.subtract(totalPayment), signature);
@ -273,7 +273,7 @@ public class PaymentChannelStateTest extends TestWithWallet {
// Pay a tiny bit // Pay a tiny bit
serverState.incrementPayment(Utils.CENT.divide(BigInteger.valueOf(2)).subtract(Utils.CENT.divide(BigInteger.TEN)), serverState.incrementPayment(Utils.CENT.divide(BigInteger.valueOf(2)).subtract(Utils.CENT.divide(BigInteger.TEN)),
clientState.incrementPaymentBy(Utils.CENT.divide(BigInteger.TEN))); clientState.incrementPaymentBy(Utils.CENT.divide(BigInteger.TEN)).signature.encodeToBitcoin());
// Advance time until our we get close enough to lock time that server should rebroadcast // Advance time until our we get close enough to lock time that server should rebroadcast
Utils.rollMockClock(60*60*22); Utils.rollMockClock(60*60*22);
@ -469,7 +469,7 @@ public class PaymentChannelStateTest extends TestWithWallet {
fail(); fail();
} catch (ValueOutOfRangeException e) {} } catch (ValueOutOfRangeException e) {}
byte[] signature = clientState.incrementPaymentBy(size); byte[] signature = clientState.incrementPaymentBy(size).signature.encodeToBitcoin();
totalPayment = totalPayment.add(size); totalPayment = totalPayment.add(size);
byte[] signatureCopy = Arrays.copyOf(signature, signature.length); byte[] signatureCopy = Arrays.copyOf(signature, signature.length);
@ -500,7 +500,7 @@ public class PaymentChannelStateTest extends TestWithWallet {
serverState.incrementPayment(halfCoin.subtract(totalPayment), signature); serverState.incrementPayment(halfCoin.subtract(totalPayment), signature);
// Pay the rest (signed with SIGHASH_NONE|SIGHASH_ANYONECANPAY) // Pay the rest (signed with SIGHASH_NONE|SIGHASH_ANYONECANPAY)
byte[] signature2 = clientState.incrementPaymentBy(halfCoin.subtract(totalPayment)); byte[] signature2 = clientState.incrementPaymentBy(halfCoin.subtract(totalPayment)).signature.encodeToBitcoin();
totalPayment = totalPayment.add(halfCoin.subtract(totalPayment)); totalPayment = totalPayment.add(halfCoin.subtract(totalPayment));
assertEquals(totalPayment, halfCoin); assertEquals(totalPayment, halfCoin);
@ -602,7 +602,7 @@ public class PaymentChannelStateTest extends TestWithWallet {
BigInteger totalPayment = BigInteger.ZERO; BigInteger totalPayment = BigInteger.ZERO;
// We can send as little as we want - its up to the server to get the fees right // We can send as little as we want - its up to the server to get the fees right
byte[] signature = clientState.incrementPaymentBy(BigInteger.ONE); byte[] signature = clientState.incrementPaymentBy(BigInteger.ONE).signature.encodeToBitcoin();
totalPayment = totalPayment.add(BigInteger.ONE); totalPayment = totalPayment.add(BigInteger.ONE);
serverState.incrementPayment(Utils.CENT.subtract(totalPayment), signature); serverState.incrementPayment(Utils.CENT.subtract(totalPayment), signature);
@ -612,21 +612,20 @@ public class PaymentChannelStateTest extends TestWithWallet {
fail(); fail();
} catch (ValueOutOfRangeException e) {} } catch (ValueOutOfRangeException e) {}
// We cannot, however, send just under the total value - our refund would make it unspendable // We cannot send just under the total value - our refund would make it unspendable. So the client
try { // will correct it for us to be larger than the requested amount, to make the change output zero.
clientState.incrementPaymentBy(Utils.CENT.subtract(Transaction.MIN_NONDUST_OUTPUT)); PaymentChannelClientState.IncrementedPayment payment =
fail(); clientState.incrementPaymentBy(Utils.CENT.subtract(Transaction.MIN_NONDUST_OUTPUT));
} catch (ValueOutOfRangeException e) {} assertEquals(Utils.CENT.subtract(BigInteger.ONE), payment.amount);
// The server also won't accept it if we do that totalPayment = totalPayment.add(payment.amount);
// The server also won't accept it if we do that.
try { try {
serverState.incrementPayment(Transaction.MIN_NONDUST_OUTPUT.subtract(BigInteger.ONE), signature); serverState.incrementPayment(Transaction.MIN_NONDUST_OUTPUT.subtract(BigInteger.ONE), signature);
fail(); fail();
} catch (ValueOutOfRangeException e) {} } catch (ValueOutOfRangeException e) {}
signature = clientState.incrementPaymentBy(Utils.CENT.subtract(BigInteger.ONE)); serverState.incrementPayment(Utils.CENT.subtract(totalPayment), payment.signature.encodeToBitcoin());
totalPayment = totalPayment.add(Utils.CENT.subtract(BigInteger.ONE));
assertEquals(totalPayment, Utils.CENT);
serverState.incrementPayment(Utils.CENT.subtract(totalPayment), signature);
// And close the channel. // And close the channel.
serverState.close(); serverState.close();
@ -686,7 +685,8 @@ public class PaymentChannelStateTest extends TestWithWallet {
assertEquals(PaymentChannelServerState.State.READY, serverState.getState()); assertEquals(PaymentChannelServerState.State.READY, serverState.getState());
// Both client and server are now in the ready state, split the channel in half // Both client and server are now in the ready state, split the channel in half
byte[] signature = clientState.incrementPaymentBy(Transaction.REFERENCE_DEFAULT_MIN_TX_FEE.subtract(BigInteger.ONE)); byte[] signature = clientState.incrementPaymentBy(Transaction.REFERENCE_DEFAULT_MIN_TX_FEE.subtract(BigInteger.ONE))
.signature.encodeToBitcoin();
BigInteger totalRefund = Utils.CENT.subtract(Transaction.REFERENCE_DEFAULT_MIN_TX_FEE.subtract(BigInteger.ONE)); BigInteger totalRefund = Utils.CENT.subtract(Transaction.REFERENCE_DEFAULT_MIN_TX_FEE.subtract(BigInteger.ONE));
serverState.incrementPayment(totalRefund, signature); serverState.incrementPayment(totalRefund, signature);
@ -711,7 +711,7 @@ public class PaymentChannelStateTest extends TestWithWallet {
assertTrue(e.getMessage().contains("more in fees")); assertTrue(e.getMessage().contains("more in fees"));
} }
signature = clientState.incrementPaymentBy(BigInteger.ONE.shiftLeft(1)); signature = clientState.incrementPaymentBy(BigInteger.ONE.shiftLeft(1)).signature.encodeToBitcoin();
totalRefund = totalRefund.subtract(BigInteger.ONE.shiftLeft(1)); totalRefund = totalRefund.subtract(BigInteger.ONE.shiftLeft(1));
serverState.incrementPayment(totalRefund, signature); serverState.incrementPayment(totalRefund, signature);
@ -783,7 +783,7 @@ public class PaymentChannelStateTest extends TestWithWallet {
BigInteger size = halfCoin.divide(BigInteger.TEN).divide(BigInteger.TEN); BigInteger size = halfCoin.divide(BigInteger.TEN).divide(BigInteger.TEN);
BigInteger totalPayment = BigInteger.ZERO; BigInteger totalPayment = BigInteger.ZERO;
for (int i = 0; i < 5; i++) { for (int i = 0; i < 5; i++) {
byte[] signature = clientState.incrementPaymentBy(size); byte[] signature = clientState.incrementPaymentBy(size).signature.encodeToBitcoin();
totalPayment = totalPayment.add(size); totalPayment = totalPayment.add(size);
serverState.incrementPayment(halfCoin.subtract(totalPayment), signature); serverState.incrementPayment(halfCoin.subtract(totalPayment), signature);
} }
@ -800,7 +800,7 @@ public class PaymentChannelStateTest extends TestWithWallet {
// Now if we try to spend again the server will reject it since it saw a double-spend // Now if we try to spend again the server will reject it since it saw a double-spend
try { try {
byte[] signature = clientState.incrementPaymentBy(size); byte[] signature = clientState.incrementPaymentBy(size).signature.encodeToBitcoin();
totalPayment = totalPayment.add(size); totalPayment = totalPayment.add(size);
serverState.incrementPayment(halfCoin.subtract(totalPayment), signature); serverState.incrementPayment(halfCoin.subtract(totalPayment), signature);
fail(); fail();