From b797a53e324aad1ab37594af58947111763d6de8 Mon Sep 17 00:00:00 2001 From: Mark Spanbroek Date: Mon, 22 Mar 2021 14:04:28 +0100 Subject: [PATCH] Accepting payments --- nitro/wallet/balances.nim | 2 + nitro/wallet/signedstate.nim | 6 +++ nitro/wallet/wallet.nim | 74 +++++++++++++++++++++++++++++------- tests/nitro/examples.nim | 5 +++ tests/nitro/testWallet.nim | 60 +++++++++++++++++++++++++++++ 5 files changed, 134 insertions(+), 13 deletions(-) diff --git a/nitro/wallet/balances.nim b/nitro/wallet/balances.nim index 43603ec..01b6f01 100644 --- a/nitro/wallet/balances.nim +++ b/nitro/wallet/balances.nim @@ -5,6 +5,8 @@ import ../protocol include questionable/errorban +export tables + type Balances* = OrderedTable[Destination, UInt256] diff --git a/nitro/wallet/signedstate.nim b/nitro/wallet/signedstate.nim index ed776ff..265f08a 100644 --- a/nitro/wallet/signedstate.nim +++ b/nitro/wallet/signedstate.nim @@ -12,6 +12,12 @@ type func hasParticipant*(signed: SignedState, participant: EthAddress): bool = signed.state.channel.participants.contains(participant) +func isSignedBy*(signed: SignedState, account: EthAddress): bool = + for (signer, signature) in signed.signatures: + if signer == account and signature.verify(signed.state, signer): + return true + false + func verifySignatures*(signed: SignedState): bool = for (participant, signature) in signed.signatures: if not signed.hasParticipant(participant): diff --git a/nitro/wallet/wallet.nim b/nitro/wallet/wallet.nim index 8f5ad3f..4fb9f36 100644 --- a/nitro/wallet/wallet.nim +++ b/nitro/wallet/wallet.nim @@ -65,17 +65,17 @@ func acceptChannel*(wallet: var Wallet, signed: SignedState): ?!ChannelId = wallet.createChannel(signed).success -func state*(wallet: Wallet, channel: ChannelId): ?State = +func latestSignedState*(wallet: Wallet, channel: ChannelId): ?SignedState = try: - wallet.channels[channel].state.some + wallet.channels[channel].some except KeyError: - State.none + SignedState.none + +func state*(wallet: Wallet, channel: ChannelId): ?State = + wallet.latestSignedState(channel)?.state func signatures*(wallet: Wallet, channel: ChannelId): ?Signatures = - try: - wallet.channels[channel].signatures.some - except KeyError: - Signatures.none + wallet.latestSignedState(channel)?.signatures func signature*(wallet: Wallet, channel: ChannelId, @@ -86,17 +86,25 @@ func signature*(wallet: Wallet, return signature.some Signature.none +func balance(state: State, + asset: EthAddress, + destination: Destination): UInt256 = + if balances =? state.outcome.balances(asset): + try: + balances[destination] + except KeyError: + 0.u256 + else: + 0.u256 + func balance*(wallet: Wallet, channel: ChannelId, asset: EthAddress, destination: Destination): UInt256 = if state =? wallet.state(channel): - if balances =? state.outcome.balances(asset): - try: - return balances[destination] - except KeyError: - return 0.u256 - 0.u256 + state.balance(asset, destination) + else: + 0.u256 func balance*(wallet: Wallet, channel: ChannelId, @@ -104,6 +112,19 @@ func balance*(wallet: Wallet, address: EthAddress): UInt256 = wallet.balance(channel, asset, address.toDestination) +func total(state: State, asset: EthAddress): UInt256 = + var total: UInt256 + if balances =? state.outcome.balances(asset): + for amount in balances.values: + total += amount # TODO: overflow? + total + +func total(wallet: Wallet, channel: ChannelId, asset: EthAddress): UInt256 = + if state =? wallet.state(channel): + state.total(asset) + else: + 0.u256 + func pay*(wallet: var Wallet, channel: ChannelId, asset: EthAddress, @@ -129,3 +150,30 @@ func pay*(wallet: var Wallet, receiver: EthAddress, amount: UInt256): ?!SignedState = wallet.pay(channel, asset, receiver.toDestination, amount) + +func acceptPayment*(wallet: var Wallet, + channel: ChannelId, + asset: EthAddress, + sender: EthAddress, + payment: SignedState): ?!void = + if not wallet.channels.contains(channel): + return void.failure "unknown channel" + + if not (getChannelId(payment.state.channel) == channel): + return void.failure "payment does not match channel" + + let currentBalance = wallet.balance(channel, asset, wallet.address) + let futureBalance = payment.state.balance(asset, wallet.destination) + if futureBalance <= currentBalance: + return void.failure "payment should not decrease balance" + + let currentTotal = wallet.total(channel, asset) + let futureTotal = payment.state.total(asset) + if futureTotal != currentTotal: + return void.failure "total supply of asset should not change" + + if not payment.isSignedBy(sender): + return void.failure "missing signature on payment" + + wallet.channels[channel] = payment + ok() diff --git a/tests/nitro/examples.nim b/tests/nitro/examples.nim index 63cf49c..1b0ca68 100644 --- a/tests/nitro/examples.nim +++ b/tests/nitro/examples.nim @@ -78,3 +78,8 @@ proc example*(_: type State): State = appDefinition: EthAddress.example, appData: seq[byte].example ) + +proc example*(_: type Signature): Signature = + let key = PrivateKey.random + let state = State.example + key.sign(state) diff --git a/tests/nitro/testWallet.nim b/tests/nitro/testWallet.nim index 5b38281..d74532e 100644 --- a/tests/nitro/testWallet.nim +++ b/tests/nitro/testWallet.nim @@ -137,3 +137,63 @@ suite "wallet: making payments": channel = wallet.openLedgerChannel(hub, chainId, nonce, asset, 1.u256) check wallet.pay(channel, asset, hub, 1.u256).isOk check wallet.pay(channel, asset, hub, 1.u256).isErr + +suite "wallet: accepting payments": + + let payerKey, receiverKey = PrivateKey.random() + let asset = EthAddress.example + let chainId = UInt256.example + let nonce = UInt48.example + + var payer, receiver: Wallet + var channel: ChannelId + + setup: + payer = Wallet.init(payerKey) + receiver = Wallet.init(receiverKey) + channel = payer.openLedgerChannel( + receiver.address, chainId, nonce, asset, 100.u256) + let update = payer.latestSignedState(channel).get + discard receiver.acceptChannel(update) + + test "updates channel state": + let payment = payer.pay(channel, asset, receiver.address, 42.u256).get + check receiver.acceptPayment(channel, asset, payer.address, payment).isOk + check receiver.balance(channel, asset, receiver.address) == 42.u256 + + test "does not accept a decrease in receiver balance": + let payment1 = payer.pay(channel, asset, receiver.address, 10.u256).get + let payment2 = payer.pay(channel, asset, receiver.address, 10.u256).get + check receiver.acceptPayment(channel, asset, payer.address, payment1).isOk + check receiver.acceptPayment(channel, asset, payer.address, payment2).isOk + check receiver.acceptPayment(channel, asset, payer.address, payment1).isErr + check receiver.balance(channel, asset, receiver.address) == 20 + + test "does not accept a payment where the total supply of the asset changes": + var payment = payer.pay(channel, asset, receiver.address, 10.u256).get + var balances = payment.state.outcome.balances(asset).get + balances[payer.destination] += 10.u256 + payment.state.outcome.update(asset, balances) + check receiver.acceptPayment(channel, asset, payer.address, payment).isErr + + test "does not accept a payment without a signature": + var payment = payer.pay(channel, asset, receiver.address, 10.u256).get + payment.signatures = @[] + check receiver.acceptPayment(channel, asset, payer.address, payment).isErr + + test "does not accept a payment with an incorrect signature": + var payment = payer.pay(channel, asset, receiver.address, 10.u256).get + payment.signatures = @{payer.address: Signature.example} + check receiver.acceptPayment(channel, asset, payer.address, payment).isErr + + test "does not accept a payment for an unknown channel": + let newChannel = payer.openLedgerChannel( + receiver.address, chainId, nonce + 1, asset, 100.u256) + let payment = payer.pay(newChannel, asset, receiver.address, 10.u256).get + check receiver.acceptPayment(newChannel, asset, payer.address, payment).isErr + + test "does not accept a payment for a different channel": + let newChannel = payer.openLedgerChannel( + receiver.address, chainId, nonce + 1, asset, 100.u256) + let payment = payer.pay(newChannel, asset, receiver.address, 10.u256).get + check receiver.acceptPayment(channel, asset, payer.address, payment).isErr