diff --git a/nitro/channelupdate.nim b/nitro/channelupdate.nim new file mode 100644 index 0000000..249f67c --- /dev/null +++ b/nitro/channelupdate.nim @@ -0,0 +1,20 @@ +import ./basics +import ./protocol + +include questionable/errorban + +type + ChannelUpdate* = object + state*: State + signatures*: seq[(EthAddress, Signature)] + +proc participants*(update: ChannelUpdate): seq[EthAddress] = + update.state.channel.participants + +proc verifySignatures*(update: ChannelUpdate): bool = + for (participant, signature) in update.signatures: + if not update.participants.contains(participant): + return false + if not signature.verify(update.state, participant): + return false + true diff --git a/nitro/ledger.nim b/nitro/ledger.nim new file mode 100644 index 0000000..7131442 --- /dev/null +++ b/nitro/ledger.nim @@ -0,0 +1,20 @@ +import ./basics +import ./channelupdate +import ./protocol + +proc startLedger*(me: EthAddress, + hub: EthAddress, + chainId: UInt256, + nonce: UInt48, + asset: EthAddress, + amount: UInt256): ChannelUpdate = + ChannelUpdate( + state: State( + channel: ChannelDefinition( + chainId: chainId, + participants: @[me, hub], + nonce: nonce + ), + outcome: Outcome.init(asset, {me.toDestination: amount}) + ) + ) diff --git a/nitro/wallet.nim b/nitro/wallet.nim index 763647d..dccb26f 100644 --- a/nitro/wallet.nim +++ b/nitro/wallet.nim @@ -1,21 +1,21 @@ import ./basics import ./keys import ./protocol +import ./channelupdate +import ./ledger include questionable/errorban export basics export keys +export channelupdate type Wallet* = object key: PrivateKey channels*: seq[Channel] Channel* = object - latest*, upcoming*: ?ChannelUpdate - ChannelUpdate* = object - state*: State - signatures*: seq[(EthAddress, Signature)] + latest*: ChannelUpdate proc init*(_: type Wallet, key: PrivateKey): Wallet = result.key = key @@ -23,25 +23,31 @@ proc init*(_: type Wallet, key: PrivateKey): Wallet = proc address*(wallet: Wallet): EthAddress = wallet.key.toPublicKey.toAddress +proc sign(wallet: Wallet, update: ChannelUpdate): ChannelUpdate = + var signed = update + signed.signatures &= @{wallet.address: wallet.key.sign(update.state)} + signed + +proc createChannel(wallet: var Wallet, update: ChannelUpdate): Channel = + let signed = wallet.sign(update) + let channel = Channel(latest: signed) + wallet.channels.add(channel) + channel + proc openLedgerChannel*(wallet: var Wallet, hub: EthAddress, chainId: UInt256, nonce: UInt48, asset: EthAddress, amount: UInt256): Channel = - let state = State( - channel: ChannelDefinition( - chainId: chainId, - participants: @[wallet.address, hub], - nonce: nonce - ), - outcome: Outcome.init(asset, {wallet.address.toDestination: amount}) - ) - let channel = Channel( - upcoming: ChannelUpdate( - state: state, - signatures: @{wallet.address: wallet.key.sign(state)} - ).some - ) - wallet.channels.add(channel) - channel + let update = startLedger(wallet.address, hub, chainId, nonce, asset, amount) + wallet.createChannel(update) + +proc acceptChannel*(wallet: var Wallet, update: ChannelUpdate): ?!Channel = + if not update.participants.contains(wallet.address): + return Channel.failure "wallet owner is not a participant" + + if not verifySignatures(update): + return Channel.failure "incorrect signatures" + + wallet.createChannel(update).success diff --git a/tests/nitro/testWallet.nim b/tests/nitro/testWallet.nim index 6028a83..5753711 100644 --- a/tests/nitro/testWallet.nim +++ b/tests/nitro/testWallet.nim @@ -23,30 +23,62 @@ suite "wallet: opening ledger channel": wallet = Wallet.init(key) channel = wallet.openLedgerChannel(hub, chainId, nonce, asset, amount) - test "creates a new upcoming state": - check channel.latest.isNone - check channel.upcoming.isSome - test "sets correct channel definition": - let definition = channel.upcoming?.state?.channel - check definition?.chainId == chainId.some - check definition?.nonce == nonce.some - check definition?.participants == @[wallet.address, hub].some + let definition = channel.latest.state.channel + check definition.chainId == chainId + check definition.nonce == nonce + check definition.participants == @[wallet.address, hub] test "provides correct outcome": - let outcome = channel.upcoming?.state?.outcome + let outcome = channel.latest.state.outcome let destination = wallet.address.toDestination - check outcome == Outcome.init(asset, {destination: amount}).some + check outcome == Outcome.init(asset, {destination: amount}) test "signs the upcoming state": - let state = channel.upcoming?.state - let signatures = channel.upcoming?.signatures - check signatures == @{wallet.address: key.sign(state.get)}.some + let state = channel.latest.state + let signatures = channel.latest.signatures + check signatures == @{wallet.address: key.sign(state)} test "sets app definition and app data to zero": - check channel.upcoming?.state?.appDefinition == EthAddress.zero.some - check channel.upcoming?.state?.appData?.len == 0.some + check channel.latest.state.appDefinition == EthAddress.zero + check channel.latest.state.appData.len == 0 test "updates the list of channels": check wallet.channels == @[channel] +suite "wallet: accepting incoming channel": + + let key = PrivateKey.random() + var wallet: Wallet + var update: ChannelUpdate + + setup: + wallet = Wallet.init(key) + update = ChannelUpdate(state: State.example) + update.state.channel.participants &= @[wallet.address] + + test "returns the new channel instance": + let channel = wallet.acceptChannel(update).get + check channel.latest.state == update.state + + test "updates the list of channels": + let channel = wallet.acceptChannel(update).get + check wallet.channels == @[channel] + + test "signs the channel state": + let channel = wallet.acceptChannel(update).get + let expectedSignatures = @{wallet.address: key.sign(update.state)} + check channel.latest.signatures == expectedSignatures + + test "fails when wallet address is not a participant": + let wrongParticipants = seq[EthAddress].example + update.state.channel.participants = wrongParticipants + check wallet.acceptChannel(update).isErr + + test "fails when signatures are incorrect": + let otherKey = PrivateKey.random() + let otherWallet = Wallet.init(otherKey) + let wrongAddress = EthAddress.example + update.state.channel.participants &= @[otherWallet.address] + update.signatures = @{wrongAddress: otherKey.sign(update.state)} + check wallet.acceptChannel(update).isErr