Compare commits

...

31 Commits
0.1.0 ... main

Author SHA1 Message Date
Jacek Sieka
5ccdeb46e0
Merge pull request #3 from logos-storage/stew-bump
chore: bump stew, fix import conflict
2025-12-11 09:43:43 +01:00
Jacek Sieka
fdcea5a2aa
try without maxtagged 2025-12-11 09:30:27 +01:00
Jacek Sieka
86d5601b4b
readd windows 2025-12-11 09:25:26 +01:00
Jacek Sieka
a15660f70f
chore: fix import conflict
There's a new byteutils in newer versions of stew - also, remove
upraises and disable windows testing which requires SSL library install
2025-12-10 22:06:29 +01:00
Jacek Sieka
309431a482
Merge pull request #2 from logos-storage/update-to-nim-2-x
Update to nim 2 x
2025-12-10 20:48:38 +01:00
Arnaud
1e7358c7c4
Fix tests 2025-02-14 11:01:51 +01:00
Arnaud
f924a32458
Fix typo 2025-02-14 10:59:08 +01:00
Arnaud
f3879ed666
Update dependencies and remove support to Nim 1.6 due to dependencies issue 2025-02-14 10:55:38 +01:00
Arnaud
9cbecdde20
Update to Nim 2.0.14 2025-01-07 11:37:03 +01:00
Arnaud
e3719433d5
Update versions 2024-12-18 15:28:59 +01:00
Arnaud
89052f638d
Update dependencies for Nim 2.x 2024-12-18 15:27:16 +01:00
zah
c13aba9942
Merge pull request #1 from yyoncho/main
Add setup files
2022-07-12 23:21:49 +03:00
Ivan Yonchovski
991d56f7d1 Add setup files 2022-07-12 15:48:13 +03:00
Mark Spanbroek
7047da19b1 Eliminate small chance of test failing
Test would sometimes fail because the example
seq would be empty in both cases.
2022-05-09 15:31:22 +02:00
Mark Spanbroek
6b4c455bf4 version 0.5.1 2022-05-09 10:46:45 +02:00
Mark Spanbroek
9611c36c78 Update to latest contractabi 2022-01-19 09:37:42 +01:00
Mark Spanbroek
ad95604c18 Add license 2022-01-10 11:22:17 +01:00
Mark Spanbroek
5977ea47e3 Update to latest version of nim-contract-abi
Encoding is rewritten to make use of the new tuple api.
2021-12-06 15:39:27 +01:00
Mark Spanbroek
39ef902177 Remove tests that are now in the contractabi module 2021-12-06 15:26:35 +01:00
Mark Spanbroek
213551c512 version 0.5.0 2021-11-25 10:07:47 +01:00
Mark Spanbroek
a8bd625e3f Use contractabi module
Which was extracted from this project.
2021-11-25 10:04:10 +01:00
Mark Spanbroek
9396fcb7d0 Remove workaround for CI issue 2021-05-10 09:24:03 +02:00
Mark Spanbroek
adcd534eb5 Update to version 0.9.1 of questionable 2021-05-10 09:12:46 +02:00
Mark Spanbroek
2a8e4e5bf4 Allow wallet to be used as a reference type 2021-04-19 16:14:27 +02:00
Mark Spanbroek
50d59ce48a Update to questionable 0.7.0 2021-04-19 16:12:52 +02:00
Mark Spanbroek
56f6fac21e Nim 1.4.6 2021-04-19 16:09:26 +02:00
Mark Spanbroek
b096093931 Update questionable to 0.6.3 2021-04-16 12:38:05 +02:00
Mark Spanbroek
85ffad6fb1 Update to latest version of questionable 2021-04-15 11:15:32 +02:00
Mark Spanbroek
76cc0a9bcc Wallet keeps track of nonces 2021-04-14 12:57:14 +02:00
Mark Spanbroek
04beab5b91 Simplify 2021-04-12 18:11:40 +02:00
Mark Spanbroek
2ac173f87c Update to questionable 0.5.0 2021-04-12 16:29:44 +02:00
32 changed files with 542 additions and 622 deletions

View File

@ -8,22 +8,15 @@ jobs:
strategy: strategy:
matrix: matrix:
os: [ubuntu-latest, macOS-latest, windows-latest] os: [ubuntu-latest, macOS-latest, windows-latest]
nim: [stable, 1.2.6] nim: [stable, 2.0.14]
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v6
- uses: iffy/install-nim@v3
with: with:
version: ${{ matrix.nim }} submodules: recursive
- uses: jiro4989/setup-nim-action@v2
# workaround for https://github.com/iffy/install-nim/issues/11 with:
- name: Workaround SSL error with choosenim nim-version: ${{matrix.nim}}
run: | repo-token: ${{ secrets.GITHUB_TOKEN }}
curl -fO https://curl.se/ca/cacert.pem
install cacert.pem ~/.nimble/bin
ls ~/.nimble/bin
shell: bash
if: runner.os == 'Windows'
- name: Build - name: Build
run: nimble install -y run: nimble install -y
- name: Test - name: Test

3
.gitignore vendored
View File

@ -1,3 +1,6 @@
* *
!*/ !*/
!*.* !*.*
nimble.develop
nimble.paths
nimbledeps

View File

@ -1 +1 @@
nim 1.4.4 nim 1.6.2

5
License.md Normal file
View File

@ -0,0 +1,5 @@
Licensed and distributed under either of
[MIT license](http://opensource.org/licenses/MIT) or
[Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0)
at your option. These files may not be copied, modified, or distributed except
according to those terms.

View File

@ -10,3 +10,11 @@ References
- [statechannels.org](https://statechannels.org/) - [statechannels.org](https://statechannels.org/)
- [Nitro paper](https://magmo.com/nitro-protocol.pdf) - [Nitro paper](https://magmo.com/nitro-protocol.pdf)
- [Nitro smart contracts & Javascript code](https://github.com/statechannels/statechannels/tree/master/packages/nitro-protocol) - [Nitro smart contracts & Javascript code](https://github.com/statechannels/statechannels/tree/master/packages/nitro-protocol)
# Installation
To avoid conflicts with previous versions of `contractabi`, use the following command to install dependencies:
```bash
nimble install --maximumtaggedversions=2
```

5
config.nims Normal file
View File

@ -0,0 +1,5 @@
# begin Nimble config (version 2)
--noNimblePath
when withDir(thisDir(), system.fileExists("nimble.paths")):
include "nimble.paths"
# end Nimble config

101
nimble.lock Normal file
View File

@ -0,0 +1,101 @@
{
"version": 2,
"packages": {
"results": {
"version": "0.5.1",
"vcsRevision": "df8113dda4c2d74d460a8fa98252b0b771bf1f27",
"url": "https://github.com/arnetheduck/nim-results",
"downloadMethod": "git",
"dependencies": [],
"checksums": {
"sha1": "a9c011f74bc9ed5c91103917b9f382b12e82a9e7"
}
},
"unittest2": {
"version": "0.2.5",
"vcsRevision": "26f2ef3ae0ec72a2a75bfe557e02e88f6a31c189",
"url": "https://github.com/status-im/nim-unittest2",
"downloadMethod": "git",
"dependencies": [],
"checksums": {
"sha1": "02bb3751ba9ddc3c17bfd89f2e41cb6bfb8fc0c9"
}
},
"stew": {
"version": "0.4.2",
"vcsRevision": "b66168735d6f3841c5239c3169d3fe5fe98b1257",
"url": "https://github.com/status-im/nim-stew",
"downloadMethod": "git",
"dependencies": [
"results",
"unittest2"
],
"checksums": {
"sha1": "928e82cb8d2f554e8f10feb2349ee9c32fee3a8c"
}
},
"stint": {
"version": "0.8.2",
"vcsRevision": "470b7892561b5179ab20bd389a69217d6213fe58",
"url": "https://github.com/status-im/nim-stint",
"downloadMethod": "git",
"dependencies": [
"stew",
"unittest2"
],
"checksums": {
"sha1": "d8f871fd617e7857192d4609fe003b48942a8ae5"
}
},
"nimcrypto": {
"version": "0.6.4",
"vcsRevision": "721fb99ee099b632eb86dfad1f0d96ee87583774",
"url": "https://github.com/cheatfate/nimcrypto",
"downloadMethod": "git",
"dependencies": [],
"checksums": {
"sha1": "f9ab24fa940ed03d0fb09729a7303feb50b7eaec"
}
},
"questionable": {
"version": "0.10.15",
"vcsRevision": "82d90b67bcfb7f2e918b61dace2ff1a4ced60935",
"url": "https://github.com/codex-storage/questionable",
"downloadMethod": "git",
"dependencies": [],
"checksums": {
"sha1": "3238ff637c7b44d2fa8fcb839a8ded968e389de3"
}
},
"contractabi": {
"version": "0.7.3",
"vcsRevision": "0a7b4cecce725bcb11ad8648035a92704a8854d3",
"url": "https://github.com/status-im/nim-contract-abi",
"downloadMethod": "git",
"dependencies": [
"stint",
"stew",
"nimcrypto",
"questionable"
],
"checksums": {
"sha1": "1bb9af15f02a77b44af02ec94b0e392b1ec88438"
}
},
"secp256k1": {
"version": "0.6.0.3.2",
"vcsRevision": "b526c4b436809aa1cfe650026d796cf7b8328b91",
"url": "https://github.com/status-im/nim-secp256k1",
"downloadMethod": "git",
"dependencies": [
"stew",
"results",
"nimcrypto"
],
"checksums": {
"sha1": "e6e50bd4a29cb473b070eb5359d87d8946d96075"
}
}
},
"tasks": {}
}

View File

@ -1,12 +1,12 @@
version = "0.1.0" version = "0.6.1"
author = "Nim Nitro developers" author = "Nim Nitro developers"
license = "MIT" license = "MIT"
description = "Nitro state channels" description = "Nitro state channels"
requires "nim >= 1.2.6 & < 2.0.0" requires "nim >= 2.0.14 & < 3.0.0"
requires "nimcrypto >= 0.5.4 & < 0.6.0" requires "nimcrypto >= 0.6.0 & < 0.7.0"
requires "questionable >= 0.4.3 & < 0.5.0" requires "questionable >= 0.10.10 & < 0.11.0"
requires "upraises >= 0.1.0 & < 0.2.0" requires "contractabi >= 0.7.1 & < 0.8.0"
requires "secp256k1" requires "secp256k1 >= 0.6.0 & < 0.7.0"
requires "stint" requires "stint >= 0.8.0 & < 0.9.0"
requires "stew" requires "stew >= 0.2.0"

View File

@ -1,6 +1,5 @@
import pkg/questionable import pkg/questionable
import pkg/questionable/results import pkg/questionable/results
import pkg/upraises
import pkg/stint import pkg/stint
import ./basics/uint48 import ./basics/uint48
import ./basics/ethaddress import ./basics/ethaddress
@ -8,7 +7,6 @@ import ./basics/destination
export questionable export questionable
export results export results
export upraises
export stint export stint
export uint48 export uint48
export ethaddress export ethaddress

View File

@ -1,11 +1,10 @@
import std/hashes import std/hashes
import pkg/questionable import pkg/questionable
import pkg/questionable/results import pkg/questionable/results
import pkg/upraises
import pkg/stew/byteutils import pkg/stew/byteutils
import ./ethaddress import ./ethaddress
push: {.upraises:[].} {.push raises: [].}
type Destination* = distinct array[32, byte] type Destination* = distinct array[32, byte]

View File

@ -1,21 +1,12 @@
import pkg/questionable import std/hashes
import pkg/questionable/results import pkg/contractabi/address
import pkg/stew/byteutils
export questionable export address
type EthAddress* = distinct array[20, byte] type EthAddress* = Address
func zero*(_: type EthAddress): EthAddress = func zero*(_: type EthAddress): EthAddress =
EthAddress.default EthAddress.default
func toArray*(address: EthAddress): array[20, byte] = proc `hash`*(a: EthAddress): Hash =
array[20, byte](address) hash(a.toArray)
func `$`*(a: EthAddress): string =
a.toArray().toHex()
func parse*(_: type EthAddress, hex: string): ?EthAddress =
EthAddress(array[20, byte].fromHex(hex)).catch.option
proc `==`*(a, b: EthAddress): bool {.borrow.}

View File

@ -8,7 +8,7 @@ import ./wallet/signedstate
export signedstate export signedstate
push: {.upraises:[].} {.push raises: [].}
func `%`(value: Outcome | Allocation): JsonNode = func `%`(value: Outcome | Allocation): JsonNode =
type Base = distinctBase(typeof value) type Base = distinctBase(typeof value)
@ -37,7 +37,7 @@ func toJson*(payment: SignedState): string =
{.pop.} {.pop.}
push: {.upraises: [ValueError].} {.push raises: [ValueError].}
func expectKind(node: JsonNode, kind: JsonNodeKind) = func expectKind(node: JsonNode, kind: JsonNodeKind) =
if node.kind != kind: if node.kind != kind:
@ -46,27 +46,21 @@ func expectKind(node: JsonNode, kind: JsonNodeKind) =
func initFromJson*(bytes: var seq[byte], node: JsonNode, _: var string) = func initFromJson*(bytes: var seq[byte], node: JsonNode, _: var string) =
node.expectKind(JString) node.expectKind(JString)
let parsed = seq[byte].fromHex(node.getStr) without parsed =? seq[byte].fromHex(node.getStr):
if parsed.isOk:
bytes = parsed.get
else:
raise newException(ValueError, "invalid hex string") raise newException(ValueError, "invalid hex string")
bytes = parsed
func initFromJson*(address: var EthAddress, node: JsonNode, _: var string) = func initFromJson*(address: var EthAddress, node: JsonNode, _: var string) =
node.expectKind(JString) node.expectKind(JString)
let parsed = EthAddress.parse(node.getStr) without parsed =? EthAddress.init(node.getStr):
if parsed.isSome:
address = parsed.get
else:
raise newException(ValueError, "invalid ethereum address") raise newException(ValueError, "invalid ethereum address")
address = parsed
func initFromJson*(dest: var Destination, node: JsonNode, _: var string) = func initFromJson*(dest: var Destination, node: JsonNode, _: var string) =
node.expectKind(JString) node.expectKind(JString)
let parsed = Destination.parse(node.getStr) without parsed =? Destination.parse(node.getStr):
if parsed.isSome:
dest = parsed.get
else:
raise newException(ValueError, "invalid nitro destination") raise newException(ValueError, "invalid nitro destination")
dest = parsed
func initFromJson*(number: var UInt256, node: JsonNode, _: var string) = func initFromJson*(number: var UInt256, node: JsonNode, _: var string) =
node.expectKind(JString) node.expectKind(JString)
@ -74,15 +68,13 @@ func initFromJson*(number: var UInt256, node: JsonNode, _: var string) =
func initFromJson*(signature: var Signature, node: JsonNode, _: var string) = func initFromJson*(signature: var Signature, node: JsonNode, _: var string) =
node.expectKind(JString) node.expectKind(JString)
let parsed = Signature.parse(node.getStr) without parsed =? Signature.parse(node.getStr):
if parsed.isSome:
signature = parsed.get
else:
raise newException(ValueError, "invalid signature") raise newException(ValueError, "invalid signature")
signature = parsed
{.pop.} {.pop.}
push: {.upraises: [].} {.push raises: [].}
proc fromJson*(_: type SignedState, json: string): ?SignedState = proc fromJson*(_: type SignedState, json: string): ?SignedState =
try: try:

View File

@ -5,7 +5,7 @@ import ./basics
export basics export basics
export toPublicKey export toPublicKey
push: {.upraises:[].} {.push raises: [].}
type type
EthPrivateKey* = SkSecretKey EthPrivateKey* = SkSecretKey

View File

@ -1,132 +1,10 @@
import pkg/stew/endians2 import pkg/contractabi
import ../basics import ../basics
push: {.upraises:[].} {.push raises: [].}
export basics export basics
export contractabi
type func encode*(encoder: var AbiEncoder, destination: Destination) =
AbiEncoder* = object encoder.write(destination.toArray)
stack: seq[Tuple]
Tuple = object
bytes: seq[byte]
postponed: seq[Split]
dynamic: bool
Split = object
head: Slice[int]
tail: seq[byte]
func write*[T](encoder: var AbiEncoder, value: T)
func encode*[T](_: type AbiEncoder, value: T): seq[byte]
func init*(_: type AbiEncoder): AbiEncoder =
AbiEncoder(stack: @[Tuple()])
func append(tupl: var Tuple, bytes: openArray[byte]) =
tupl.bytes.add(bytes)
func postpone(tupl: var Tuple, bytes: seq[byte]) =
var split: Split
split.head.a = tupl.bytes.len
tupl.append(AbiEncoder.encode(0'u64))
split.head.b = tupl.bytes.high
split.tail = bytes
tupl.postponed.add(split)
func finish(tupl: Tuple): seq[byte] =
var bytes = tupl.bytes
for split in tupl.postponed:
let offset = bytes.len
bytes[split.head] = AbiEncoder.encode(offset.uint64)
bytes.add(split.tail)
bytes
func append(encoder: var AbiEncoder, bytes: openArray[byte]) =
encoder.stack[^1].append(bytes)
func postpone(encoder: var AbiEncoder, bytes: seq[byte]) =
if encoder.stack.len > 1:
encoder.stack[^1].postpone(bytes)
else:
encoder.stack[0].append(bytes)
func setDynamic(encoder: var AbiEncoder) =
encoder.stack[^1].dynamic = true
func startTuple*(encoder: var AbiEncoder) =
encoder.stack.add(Tuple())
func encode(encoder: var AbiEncoder, tupl: Tuple) =
if tupl.dynamic:
encoder.postpone(tupl.finish())
encoder.setDynamic()
else:
encoder.append(tupl.finish())
func finishTuple*(encoder: var AbiEncoder) =
encoder.encode(encoder.stack.pop())
func pad(encoder: var AbiEncoder, len: int) =
let padlen = (32 - len mod 32) mod 32
for _ in 0..<padlen:
encoder.append([0'u8])
func padleft(encoder: var AbiEncoder, bytes: openArray[byte]) =
encoder.pad(bytes.len)
encoder.append(bytes)
func padright(encoder: var AbiEncoder, bytes: openArray[byte]) =
encoder.append(bytes)
encoder.pad(bytes.len)
func encode(encoder: var AbiEncoder, value: SomeUnsignedInt | StUint) =
encoder.padleft(value.toBytesBE)
func encode(encoder: var AbiEncoder, value: bool) =
encoder.encode(cast[uint8](value))
func encode(encoder: var AbiEncoder, value: enum) =
encoder.encode(uint64(ord(value)))
func encode[I](encoder: var AbiEncoder, bytes: array[I, byte]) =
encoder.padright(bytes)
func encode(encoder: var AbiEncoder, bytes: seq[byte]) =
encoder.encode(bytes.len.uint64)
encoder.padright(bytes)
encoder.setDynamic()
func encode(encoder: var AbiEncoder, address: EthAddress) =
encoder.padleft(address.toArray)
func encode(encoder: var AbiEncoder, destination: Destination) =
encoder.encode(destination.toArray)
func encode[I, T](encoder: var AbiEncoder, value: array[I, T]) =
encoder.startTuple()
for element in value:
encoder.write(element)
encoder.finishTuple()
func encode[T](encoder: var AbiEncoder, value: seq[T]) =
encoder.encode(value.len.uint64)
encoder.startTuple()
for element in value:
encoder.write(element)
encoder.finishTuple()
encoder.setDynamic()
func write*[T](encoder: var AbiEncoder, value: T) =
var writer = AbiEncoder.init()
writer.encode(value)
encoder.encode(writer.stack[0])
func finish*(encoder: var AbiEncoder): seq[byte] =
doAssert encoder.stack.len == 1, "not all tuples were finished"
doAssert encoder.stack[0].bytes.len mod 32 == 0, "encoding invariant broken"
encoder.stack[0].bytes
func encode*[T](_: type AbiEncoder, value: T): seq[byte] =
var encoder = AbiEncoder.init()
encoder.write(value)
encoder.finish()

View File

@ -2,7 +2,7 @@ import pkg/nimcrypto
import ../basics import ../basics
import ./abi import ./abi
push: {.upraises:[].} {.push raises: [].}
export basics export basics
@ -13,10 +13,6 @@ type
chainId*: UInt256 chainId*: UInt256
func getChannelId*(channel: ChannelDefinition): Destination = func getChannelId*(channel: ChannelDefinition): Destination =
var encoder= AbiEncoder.init() let encoding = AbiEncoder.encode:
encoder.startTuple() (channel.chainId, channel.participants, channel.nonce)
encoder.write(channel.chainId) Destination(keccak256.digest(encoding).data)
encoder.write(channel.participants)
encoder.write(channel.nonce)
encoder.finishTuple()
Destination(keccak256.digest(encoder.finish()).data)

View File

@ -2,7 +2,7 @@ import pkg/nimcrypto
import ../basics import ../basics
import ./abi import ./abi
push: {.upraises:[].} {.push raises: [].}
export basics export basics
@ -52,46 +52,25 @@ func `==`*(a, b: AssetOutcome): bool =
proc `==`*(a, b: Outcome): bool {.borrow.} proc `==`*(a, b: Outcome): bool {.borrow.}
func encode*(encoder: var AbiEncoder, guarantee: Guarantee) = func encode*(encoder: var AbiEncoder, guarantee: Guarantee) =
encoder.startTuple() encoder.write:
encoder.startTuple() ( (guarantee.targetChannelId, guarantee.destinations), )
encoder.write(guarantee.targetChannelId)
encoder.write(guarantee.destinations)
encoder.finishTuple()
encoder.finishTuple()
func encode*(encoder: var AbiEncoder, item: AllocationItem) =
encoder.startTuple()
encoder.write(item.destination)
encoder.write(item.amount)
encoder.finishTuple()
func encode*(encoder: var AbiEncoder, allocation: Allocation) = func encode*(encoder: var AbiEncoder, allocation: Allocation) =
encoder.startTuple() encoder.write: (seq[AllocationItem](allocation),)
encoder.write(seq[AllocationItem](allocation))
encoder.finishTuple()
func encode*(encoder: var AbiEncoder, assetOutcome: AssetOutcome) = func encode*(encoder: var AbiEncoder, assetOutcome: AssetOutcome) =
var content= AbiEncoder.init() var content: seq[byte]
content.startTuple()
content.startTuple()
content.write(assetOutcome.kind)
case assetOutcome.kind: case assetOutcome.kind:
of allocationType: of allocationType:
content.write(AbiEncoder.encode(assetOutcome.allocation)) content = AbiEncoder.encode:
( (assetOutcome.kind, ABiEncoder.encode(assetOutcome.allocation)), )
of guaranteeType: of guaranteeType:
content.write(AbiEncoder.encode(assetOutcome.guarantee)) content = AbiEncoder.encode:
content.finishTuple() ( (assetOutcome.kind, AbiEncoder.encode(assetOutcome.guarantee)), )
content.finishTuple() encoder.write( (assetOutcome.assetHolder, content) )
encoder.startTuple()
encoder.write(assetOutcome.assetHolder)
encoder.write(content.finish())
encoder.finishTuple()
func encode*(encoder: var AbiEncoder, outcome: Outcome) = func encode*(encoder: var AbiEncoder, outcome: Outcome) =
encoder.startTuple() encoder.write: (seq[AssetOutcome](outcome),)
encoder.write(seq[AssetOutcome](outcome))
encoder.finishTuple()
func hashOutcome*(outcome: Outcome): array[32, byte] = func hashOutcome*(outcome: Outcome): array[32, byte] =
keccak256.digest(AbiEncoder.encode(outcome)).data keccak256.digest(AbiEncoder.encode(outcome)).data

View File

@ -1,11 +1,11 @@
import pkg/secp256k1 import pkg/secp256k1
import pkg/nimcrypto import pkg/nimcrypto/keccak
import pkg/stew/byteutils import pkg/stew/byteutils
import ../basics import ../basics
import ../keys import ../keys
import ./state import ./state
push: {.upraises:[].} {.push raises: [].}
export basics export basics
export keys export keys
@ -32,7 +32,7 @@ func recover(signature: Signature, hash: array[32, byte]): ?EthPublicKey =
func recover*(signature: Signature, state: State): ?EthAddress = func recover*(signature: Signature, state: State): ?EthAddress =
let hash = hashMessage(hashState(state)) let hash = hashMessage(hashState(state))
recover(signature, hash)?.toAddress recover(signature, hash).?toAddress
func verify*(signature: Signature, state: State, signer: EthAddress): bool = func verify*(signature: Signature, state: State, signer: EthAddress): bool =
recover(signature, state) == signer.some recover(signature, state) == signer.some
@ -43,8 +43,7 @@ func `$`*(signature: Signature): string =
bytes.toHex() bytes.toHex()
func parse*(_: type Signature, s: string): ?Signature = func parse*(_: type Signature, s: string): ?Signature =
let signature = catch: without var bytes =? array[65, byte].fromHex(s).catch:
var bytes = array[65, byte].fromHex(s) return Signature.none
bytes[64] = bytes[64] - 27 bytes[64] = bytes[64] - 27
SkRecoverableSignature.fromRaw(bytes).get() SkRecoverableSignature.fromRaw(bytes).option
signature.option

View File

@ -4,7 +4,7 @@ import ./channel
import ./outcome import ./outcome
import ./abi import ./abi
push: {.upraises:[].} {.push raises: [].}
export basics export basics
export channel export channel
@ -45,21 +45,17 @@ func variablePart*(state: State): VariablePart =
) )
func hashAppPart*(state: State): array[32, byte] = func hashAppPart*(state: State): array[32, byte] =
var encoder= AbiEncoder.init() let encoding = AbiEncoder.encode:
encoder.startTuple() (state.challengeDuration, state.appDefinition, state.appData)
encoder.write(state.challengeDuration) keccak256.digest(encoding).data
encoder.write(state.appDefinition)
encoder.write(state.appData)
encoder.finishTuple()
keccak256.digest(encoder.finish).data
func hashState*(state: State): array[32, byte] = func hashState*(state: State): array[32, byte] =
var encoder= AbiEncoder.init() let encoding = AbiEncoder.encode:
encoder.startTuple() (
encoder.write(state.turnNum) state.turnNum,
encoder.write(state.isFinal) state.isFinal,
encoder.write(getChannelId(state.channel)) getChannelId(state.channel),
encoder.write(hashAppPart(state)) hashAppPart(state),
encoder.write(hashOutcome(state.outcome)) hashOutcome(state.outcome)
encoder.finishTuple() )
keccak256.digest(encoder.finish).data keccak256.digest(encoding).data

View File

@ -3,7 +3,7 @@ import std/sequtils
import ../basics import ../basics
import ../protocol import ../protocol
push: {.upraises:[].} {.push raises: [].}
export tables export tables
@ -31,7 +31,7 @@ func move*(balances: var Balances,
amount: UInt256): ?!void = amount: UInt256): ?!void =
try: try:
if balances[source] < amount: if balances[source] < amount:
return void.failure "insufficient funds" return failure "insufficient funds"
balances[source] -= amount balances[source] -= amount
if (balances.contains(destination)): if (balances.contains(destination)):
@ -39,6 +39,6 @@ func move*(balances: var Balances,
else: else:
balances[destination] = amount balances[destination] = amount
ok() success()
except KeyError: except KeyError:
void.failure "no funds" failure "no funds"

46
nitro/wallet/deref.nim Normal file
View File

@ -0,0 +1,46 @@
import std/macros
func identName(identDefs: NimNode): NimNode =
identDefs.expectKind(nnkIdentDefs)
identDefs[0]
func identType(identDefs: NimNode): NimNode =
identDefs.expectKind(nnkIdentDefs)
identDefs[^2]
func `identType=`(identDefs: NimNode, identType: NimNode) =
identDefs.expectKind(nnkIdentDefs)
identDefs[^2] = identType
func insertRef(function: NimNode) =
function.expectKind(nnkFuncDef)
var paramType = function.params[1].identType
if paramType.kind == nnkVarTy:
paramType = paramType[0]
function.params[1].identType = newNimNode(nnkRefTy, paramType).add(paramType)
func paramNames(function: NimNode): seq[NimNode] =
function.expectKind(nnkFuncDef)
for i in 1..<function.params.len:
result.add(function.params[i].identName)
func insertDeref(params: var seq[NimNode]) =
params[0] = newNimNode(nnkBracketExpr, params[0]).add(params[0])
func derefOverload(function: NimNode): NimNode =
function.expectKind(nnkFuncDef)
var arguments = function.paramNames
arguments.insertDeref()
result = function.copyNimTree()
result.insertRef()
result.body = newCall(function.name, arguments)
macro deref*(function: untyped{nkFuncDef}): untyped =
## Creates an overload that dereferences the first argument of the function
## call. Roughly equivalent to the `implicitDeref` experimental feature of
## Nim.
let overload = derefOverload(function)
quote do:
`function`
`overload`

34
nitro/wallet/nonces.nim Normal file
View File

@ -0,0 +1,34 @@
import std/tables
import std/sets
import std/hashes
import ../basics
{.push raises: [].}
type
Nonces* = object
next: Table[NonceKey, UInt48]
NonceKey = object
chainId: UInt256
participants: HashSet[EthAddress]
func hash(key: NonceKey): Hash =
var h: Hash
h = h !& key.chainId.hash
h = h !& key.participants.hash
!$h
func key(chainId: UInt256, participants: openArray[EthAddress]): NonceKey =
NonceKey(chainId: chainId, participants: participants.toHashSet)
func getNonce*(nonces: var Nonces,
chainId: UInt256,
participants: varargs[EthAddress]): UInt48 =
nonces.next.?[key(chainId, participants)] |? 0
func incNonce*(nonces: var Nonces,
oldNonce: UInt48,
chainId: UInt256,
participants: varargs[EthAddress]) =
let next = max(oldNonce, nonces.getNonce(chainId, participants)) + 1
nonces.next[key(chainId, participants)] = next

View File

@ -1,7 +1,7 @@
import ../basics import ../basics
import ../protocol import ../protocol
push: {.upraises:[].} {.push raises: [].}
type type
SignedState* = object SignedState* = object

View File

@ -5,8 +5,10 @@ import ../protocol
import ./signedstate import ./signedstate
import ./ledger import ./ledger
import ./balances import ./balances
import ./nonces
import ./deref
push: {.upraises:[].} {.push raises: [].}
export basics export basics
export keys export keys
@ -17,21 +19,26 @@ type
Wallet* = object Wallet* = object
key: EthPrivateKey key: EthPrivateKey
channels: Table[ChannelId, SignedState] channels: Table[ChannelId, SignedState]
nonces: Nonces
WalletRef* = ref Wallet
ChannelId* = Destination ChannelId* = Destination
Payment* = tuple Payment* = tuple
destination: Destination destination: Destination
amount: UInt256 amount: UInt256
func init*(_: type Wallet, key: EthPrivateKey): Wallet = func init*(_: type Wallet, key: EthPrivateKey): Wallet =
result.key = key Wallet(key: key)
func publicKey*(wallet: Wallet): EthPublicKey = func new*(_: type WalletRef, key: EthPrivateKey): WalletRef =
WalletRef(key: key)
func publicKey*(wallet: Wallet): EthPublicKey {.deref.} =
wallet.key.toPublicKey wallet.key.toPublicKey
func address*(wallet: Wallet): EthAddress = func address*(wallet: Wallet): EthAddress {.deref.} =
wallet.publicKey.toAddress wallet.publicKey.toAddress
func destination*(wallet: Wallet): Destination = func destination*(wallet: Wallet): Destination {.deref.}=
wallet.address.toDestination wallet.address.toDestination
func sign(wallet: Wallet, state: SignedState): SignedState = func sign(wallet: Wallet, state: SignedState): SignedState =
@ -39,12 +46,17 @@ func sign(wallet: Wallet, state: SignedState): SignedState =
signed.signatures &= wallet.key.sign(state.state) signed.signatures &= wallet.key.sign(state.state)
signed signed
func incNonce(wallet: var Wallet, state: SignedState) =
let channel = state.state.channel
wallet.nonces.incNonce(channel.nonce, channel.chainId, channel.participants)
func createChannel(wallet: var Wallet, state: SignedState): ?!ChannelId = func createChannel(wallet: var Wallet, state: SignedState): ?!ChannelId =
let id = getChannelId(state.state.channel) let id = getChannelId(state.state.channel)
if wallet.channels.contains(id): if wallet.channels.contains(id):
return ChannelId.failure("channel with id " & $id & " already exists") return failure "channel with id " & $id & " already exists"
wallet.channels[id] = wallet.sign(state) wallet.channels[id] = wallet.sign(state)
ok id wallet.incNonce(state)
success id
func updateChannel(wallet: var Wallet, state: SignedState) = func updateChannel(wallet: var Wallet, state: SignedState) =
let signed = wallet.sign(state) let signed = wallet.sign(state)
@ -56,31 +68,43 @@ func openLedgerChannel*(wallet: var Wallet,
chainId: UInt256, chainId: UInt256,
nonce: UInt48, nonce: UInt48,
asset: EthAddress, asset: EthAddress,
amount: UInt256): ?!ChannelId = amount: UInt256): ?!ChannelId {.deref.} =
let state = startLedger(wallet.address, hub, chainId, nonce, asset, amount) let state = startLedger(wallet.address, hub, chainId, nonce, asset, amount)
wallet.createChannel(state) wallet.createChannel(state)
func acceptChannel*(wallet: var Wallet, signed: SignedState): ?!ChannelId = func openLedgerChannel*(wallet: var Wallet,
hub: EthAddress,
chainId: UInt256,
asset: EthAddress,
amount: UInt256): ?!ChannelId {.deref.} =
let nonce = wallet.nonces.getNonce(chainId, wallet.address, hub)
openLedgerChannel(wallet, hub, chainId, nonce, asset, amount)
func acceptChannel*(wallet: var Wallet,
signed: SignedState): ?!ChannelId {.deref.} =
if not signed.hasParticipant(wallet.address): if not signed.hasParticipant(wallet.address):
return ChannelId.failure "wallet owner is not a participant" return failure "wallet owner is not a participant"
if not verifySignatures(signed): if not verifySignatures(signed):
return ChannelId.failure "incorrect signatures" return failure "incorrect signatures"
wallet.createChannel(signed) wallet.createChannel(signed)
func latestSignedState*(wallet: Wallet, channel: ChannelId): ?SignedState = func latestSignedState*(wallet: Wallet,
wallet.channels?[channel] channel: ChannelId): ?SignedState {.deref.} =
wallet.channels.?[channel]
func state*(wallet: Wallet, channel: ChannelId): ?State = func state*(wallet: Wallet,
wallet.latestSignedState(channel)?.state channel: ChannelId): ?State {.deref.} =
wallet.latestSignedState(channel).?state
func signatures*(wallet: Wallet, channel: ChannelId): ?seq[Signature] = func signatures*(wallet: Wallet,
wallet.latestSignedState(channel)?.signatures channel: ChannelId): ?seq[Signature] {.deref.} =
wallet.latestSignedState(channel).?signatures
func signature*(wallet: Wallet, func signature*(wallet: Wallet,
channel: ChannelId, channel: ChannelId,
address: EthAddress): ?Signature = address: EthAddress): ?Signature {.deref.} =
if signed =? wallet.latestSignedState(channel): if signed =? wallet.latestSignedState(channel):
for signature in signed.signatures: for signature in signed.signatures:
if signer =? signature.recover(signed.state): if signer =? signature.recover(signed.state):
@ -91,30 +115,27 @@ func signature*(wallet: Wallet,
func balance(state: State, func balance(state: State,
asset: EthAddress, asset: EthAddress,
destination: Destination): UInt256 = destination: Destination): UInt256 =
if balances =? state.outcome.balances(asset): without balance =? state.outcome.balances(asset).?[destination]:
if balance =? (balances?[destination]): return 0.u256
balance balance
else:
0.u256
else:
0.u256
func balance*(wallet: Wallet, func balance*(wallet: Wallet,
channel: ChannelId, channel: ChannelId,
asset: EthAddress, asset: EthAddress,
destination: Destination): UInt256 = destination: Destination): UInt256 {.deref.} =
if state =? wallet.state(channel): without state =? wallet.state(channel):
state.balance(asset, destination) return 0.u256
else: state.balance(asset, destination)
0.u256
func balance*(wallet: Wallet, func balance*(wallet: Wallet,
channel: ChannelId, channel: ChannelId,
asset: EthAddress, asset: EthAddress,
address: EthAddress): UInt256 = address: EthAddress): UInt256 {.deref.} =
wallet.balance(channel, asset, address.toDestination) wallet.balance(channel, asset, address.toDestination)
func balance*(wallet: Wallet, channel: ChannelId, asset: EthAddress): UInt256 = func balance*(wallet: Wallet,
channel: ChannelId,
asset: EthAddress): UInt256 {.deref.} =
wallet.balance(channel, asset, wallet.address) wallet.balance(channel, asset, wallet.address)
func total(state: State, asset: EthAddress): UInt256 = func total(state: State, asset: EthAddress): UInt256 =
@ -125,65 +146,64 @@ func total(state: State, asset: EthAddress): UInt256 =
total total
func total(wallet: Wallet, channel: ChannelId, asset: EthAddress): UInt256 = func total(wallet: Wallet, channel: ChannelId, asset: EthAddress): UInt256 =
if state =? wallet.state(channel): without state =? wallet.state(channel):
state.total(asset) return 0.u256
else: state.total(asset)
0.u256
func pay*(wallet: var Wallet, func pay*(wallet: var Wallet,
channel: ChannelId, channel: ChannelId,
asset: EthAddress, asset: EthAddress,
receiver: Destination, receiver: Destination,
amount: UInt256): ?!SignedState = amount: UInt256): ?!SignedState {.deref.} =
if var state =? wallet.state(channel): without var state =? wallet.state(channel):
if var balances =? state.outcome.balances(asset): return failure "channel not found"
?balances.move(wallet.destination, receiver, amount)
state.outcome.update(asset, balances) without var balances =? state.outcome.balances(asset):
wallet.updateChannel(SignedState(state: state)) return failure "asset not found"
ok(wallet.channels?[channel].get)
else: ?balances.move(wallet.destination, receiver, amount)
SignedState.failure "asset not found" state.outcome.update(asset, balances)
else: wallet.updateChannel(SignedState(state: state))
SignedState.failure "channel not found" success !wallet.latestSignedState(channel)
func pay*(wallet: var Wallet, func pay*(wallet: var Wallet,
channel: ChannelId, channel: ChannelId,
asset: EthAddress, asset: EthAddress,
receiver: EthAddress, receiver: EthAddress,
amount: UInt256): ?!SignedState = amount: UInt256): ?!SignedState {.deref.} =
wallet.pay(channel, asset, receiver.toDestination, amount) wallet.pay(channel, asset, receiver.toDestination, amount)
func acceptPayment*(wallet: var Wallet, func acceptPayment*(wallet: var Wallet,
channel: ChannelId, channel: ChannelId,
asset: EthAddress, asset: EthAddress,
sender: EthAddress, sender: EthAddress,
payment: SignedState): ?!void = payment: SignedState): ?!void {.deref.} =
if not wallet.channels.contains(channel): if not wallet.channels.contains(channel):
return void.failure "unknown channel" return failure "unknown channel"
if not (getChannelId(payment.state.channel) == channel): if not (getChannelId(payment.state.channel) == channel):
return void.failure "payment does not match channel" return failure "payment does not match channel"
let currentBalance = wallet.balance(channel, asset) let currentBalance = wallet.balance(channel, asset)
let futureBalance = payment.state.balance(asset, wallet.destination) let futureBalance = payment.state.balance(asset, wallet.destination)
if futureBalance <= currentBalance: if futureBalance <= currentBalance:
return void.failure "payment should not decrease balance" return failure "payment should not decrease balance"
let currentTotal = wallet.total(channel, asset) let currentTotal = wallet.total(channel, asset)
let futureTotal = payment.state.total(asset) let futureTotal = payment.state.total(asset)
if futureTotal != currentTotal: if futureTotal != currentTotal:
return void.failure "total supply of asset should not change" return failure "total supply of asset should not change"
if not payment.isSignedBy(sender): if not payment.isSignedBy(sender):
return void.failure "missing signature on payment" return failure "missing signature on payment"
if updatedBalances =? payment.state.outcome.balances(asset): without updatedBalances =? payment.state.outcome.balances(asset):
var expectedState: State = wallet.channels?[channel]?.state.get return failure "payment misses balances for asset"
expectedState.outcome.update(asset, updatedBalances)
if payment.state != expectedState: var expectedState: State = !wallet.state(channel)
return void.failure "payment has unexpected changes in state" expectedState.outcome.update(asset, updatedBalances)
else: if payment.state != expectedState:
return void.failure "payment misses balances for asset" return failure "payment has unexpected changes in state"
wallet.channels[channel] = payment wallet.channels[channel] = payment
ok() success()

View File

@ -19,10 +19,10 @@ proc example*[T](_: type seq[T], len = 0..5): seq[T] =
newSeqWith(chosenlen, T.example) newSeqWith(chosenlen, T.example)
proc example*(_: type UInt256): UInt256 = proc example*(_: type UInt256): UInt256 =
UInt256.fromBytes(array[32, byte].example) UInt256.fromBytesBE(array[32, byte].example)
proc example*(_: type UInt128): UInt128 = proc example*(_: type UInt128): UInt128 =
UInt128.fromBytes(array[16, byte].example) UInt128.fromBytesBE(array[16, byte].example)
proc example*(_: type EthAddress): EthAddress = proc example*(_: type EthAddress): EthAddress =
EthAddress(array[20, byte].example) EthAddress(array[20, byte].example)

View File

@ -6,154 +6,11 @@ suite "ABI encoding":
proc zeroes(amount: int): seq[byte] = proc zeroes(amount: int): seq[byte] =
newSeq[byte](amount) newSeq[byte](amount)
test "encodes uint8":
check AbiEncoder.encode(42'u8) == 31.zeroes & 42'u8
test "encodes booleans":
check AbiEncoder.encode(false) == 31.zeroes & 0'u8
check AbiEncoder.encode(true) == 31.zeroes & 1'u8
test "encodes uint16, 32, 64":
check AbiEncoder.encode(0xABCD'u16) ==
30.zeroes & 0xAB'u8 & 0xCD'u8
check AbiEncoder.encode(0x11223344'u32) ==
28.zeroes & 0x11'u8 & 0x22'u8 & 0x33'u8 & 0x44'u8
check AbiEncoder.encode(0x1122334455667788'u64) ==
24.zeroes &
0x11'u8 & 0x22'u8 & 0x33'u8 & 0x44'u8 &
0x55'u8 & 0x66'u8 & 0x77'u8 & 0x88'u8
test "encodes ranges":
type SomeRange = range[0x0000'u16..0xAAAA'u16]
check AbiEncoder.encode(SomeRange(0x1122)) == 30.zeroes & 0x11'u8 & 0x22'u8
test "encodes enums":
type SomeEnum = enum
one = 1
two = 2
check AbiEncoder.encode(one) == 31.zeroes & 1'u8
check AbiEncoder.encode(two) == 31.zeroes & 2'u8
test "encodes stints":
let uint256 = UInt256.example
check AbiEncoder.encode(uint256) == @(uint256.toBytesBE)
let uint128 = UInt128.example
check AbiEncoder.encode(uint128) == 16.zeroes & @(uint128.toBytesBE)
test "encodes byte arrays":
let bytes3 = [1'u8, 2'u8, 3'u8]
check AbiEncoder.encode(bytes3) == @bytes3 & 29.zeroes
let bytes32 = array[32, byte].example
check AbiEncoder.encode(bytes32) == @bytes32
let bytes33 = array[33, byte].example
check AbiEncoder.encode(bytes33) == @bytes33 & 31.zeroes
test "encodes byte sequences":
let bytes3 = @[1'u8, 2'u8, 3'u8]
let bytes3len = AbiEncoder.encode(bytes3.len.uint64)
check AbiEncoder.encode(bytes3) == bytes3len & bytes3 & 29.zeroes
let bytes32 = @(array[32, byte].example)
let bytes32len = AbiEncoder.encode(bytes32.len.uint64)
check AbiEncoder.encode(bytes32) == bytes32len & bytes32
let bytes33 = @(array[33, byte].example)
let bytes33len = AbiEncoder.encode(bytes33.len.uint64)
check AbiEncoder.encode(bytes33) == bytes33len & bytes33 & 31.zeroes
test "encodes ethereum addresses": test "encodes ethereum addresses":
let address = EthAddress.example let address = EthAddress.example
check AbiEncoder.encode(address) == 12.zeroes & @(address.toArray) check AbiEncoder.encode(address) == 12.zeroes & @(address.toArray)
test "encodes tuples": test "encodes nitro destinations":
let a = true let destination = Destination.example
let b = @[1'u8, 2'u8, 3'u8] check:
let c = 0xAABBCCDD'u32 AbiEncoder.encode(destination) == AbiEncoder.encode(destination.toArray)
let d = @[4'u8, 5'u8, 6'u8]
var encoder= AbiEncoder.init()
encoder.startTuple()
encoder.write(a)
encoder.write(b)
encoder.write(c)
encoder.write(d)
encoder.finishTuple()
check encoder.finish() ==
AbiEncoder.encode(a) &
AbiEncoder.encode(4 * 32'u8) & # offset in tuple
AbiEncoder.encode(c) &
AbiEncoder.encode(6 * 32'u8) & # offset in tuple
AbiEncoder.encode(b) &
AbiEncoder.encode(d)
test "encodes nested tuples":
let a = true
let b = @[1'u8, 2'u8, 3'u8]
let c = 0xAABBCCDD'u32
let d = @[4'u8, 5'u8, 6'u8]
var encoder= AbiEncoder.init()
encoder.startTuple()
encoder.write(a)
encoder.write(b)
encoder.startTuple()
encoder.write(c)
encoder.write(d)
encoder.finishTuple()
encoder.finishTuple()
check encoder.finish() ==
AbiEncoder.encode(a) &
AbiEncoder.encode(3 * 32'u8) & # offset of b in outer tuple
AbiEncoder.encode(5 * 32'u8) & # offset of inner tuple in outer tuple
AbiEncoder.encode(b) &
AbiEncoder.encode(c) &
AbiEncoder.encode(2 * 32'u8) & # offset of d in inner tuple
AbiEncoder.encode(d)
test "encodes arrays":
let element1 = seq[byte].example
let element2 = seq[byte].example
var expected= AbiEncoder.init()
expected.startTuple()
expected.write(element1)
expected.write(element2)
expected.finishTuple()
check AbiEncoder.encode([element1, element2]) == expected.finish()
test "encodes sequences":
let element1 = seq[byte].example
let element2 = seq[byte].example
var expected= AbiEncoder.init()
expected.write(2'u8)
expected.startTuple()
expected.write(element1)
expected.write(element2)
expected.finishTuple()
check AbiEncoder.encode(@[element1, element2]) == expected.finish()
test "encodes sequence as dynamic element":
let s = @[42.u256, 43.u256]
var encoder= AbiEncoder.init()
encoder.startTuple()
encoder.write(s)
encoder.finishTuple()
check encoder.finish() ==
AbiEncoder.encode(32'u8) & # offset in tuple
AbiEncoder.encode(s)
test "encodes array of static elements as static element":
let a = [[42'u8], [43'u8]]
var encoder= AbiEncoder.init()
encoder.startTuple()
encoder.write(a)
encoder.finishTuple()
check encoder.finish() == AbiEncoder.encode(a)
test "encodes array of dynamic elements as dynamic element":
let a = [@[42'u8], @[43'u8]]
var encoder= AbiEncoder.init()
encoder.startTuple()
encoder.write(a)
encoder.finishTuple()
check encoder.finish() ==
AbiEncoder.encode(32'u8) & # offset in tuple
AbiEncoder.encode(a)
# https://medium.com/b2expand/abi-encoding-explanation-4f470927092d
# https://docs.soliditylang.org/en/v0.8.1/abi-spec.html#formal-specification-of-the-encoding

View File

@ -7,13 +7,8 @@ suite "channel definition":
let channel = ChannelDefinition.example let channel = ChannelDefinition.example
test "calculates channel id": test "calculates channel id":
var encoder= AbiEncoder.init() let encoded = AbiEncoder.encode:
encoder.startTuple() (channel.chainId, channel.participants, channel.nonce)
encoder.write(channel.chainId)
encoder.write(channel.participants)
encoder.write(channel.nonce)
encoder.finishTuple()
let encoded = encoder.finish()
let hashed = keccak256.digest(encoded).data let hashed = keccak256.digest(encoded).data
check getChannelId(channel) == Destination(hashed) check getChannelId(channel) == Destination(hashed)
@ -22,10 +17,10 @@ suite "channel definition":
chainId: 9001.u256, chainId: 9001.u256,
nonce: 1, nonce: 1,
participants: @[ participants: @[
EthAddress.parse("24b905Dcc8A11C0FE57C2592f3D25f0447402C10").get() !EthAddress.init("24b905Dcc8A11C0FE57C2592f3D25f0447402C10")
] ]
) )
let expected = Destination.parse( let expected = !Destination.parse(
"4f8cce57e9fe88edaab05234972eaf0c2d183e4f6b175aff293375fbe4d5d7cc" "4f8cce57e9fe88edaab05234972eaf0c2d183e4f6b175aff293375fbe4d5d7cc"
).get() )
check getChannelId(channel) == expected check getChannelId(channel) == expected

View File

@ -6,31 +6,20 @@ suite "outcome":
test "encodes guarantees": test "encodes guarantees":
let guarantee = Guarantee.example let guarantee = Guarantee.example
var encoder= AbiEncoder.init() let expected = AbiEncoder.encode:
encoder.startTuple() ((guarantee.targetChannelId, guarantee.destinations),)
encoder.startTuple() check AbiEncoder.encode(guarantee) == expected
encoder.write(guarantee.targetChannelId)
encoder.write(guarantee.destinations)
encoder.finishTuple()
encoder.finishTuple()
check AbiEncoder.encode(guarantee) == encoder.finish()
test "encodes allocation items": test "encodes allocation items":
let item = AllocationItem.example let item = AllocationItem.example
var encoder= AbiEncoder.init() let expected = AbiEncoder.encode: (item.destination, item.amount)
encoder.startTuple() check AbiEncoder.encode(item) == expected
encoder.write(item.destination)
encoder.write(item.amount)
encoder.finishTuple()
check AbiEncoder.encode(item) == encoder.finish()
test "encodes allocation": test "encodes allocation":
let allocation = Allocation.example let allocation = Allocation.example
var encoder= AbiEncoder.init() let expected = AbiEncoder.encode:
encoder.startTuple() (seq[AllocationItem](allocation),)
encoder.write(seq[AllocationItem](allocation)) check AbiEncoder.encode(allocation) == expected
encoder.finishTuple()
check AbiEncoder.encode(allocation) == encoder.finish()
test "encodes allocation outcome": test "encodes allocation outcome":
let assetOutcome = AssetOutcome( let assetOutcome = AssetOutcome(
@ -38,19 +27,11 @@ suite "outcome":
assetHolder: EthAddress.example, assetHolder: EthAddress.example,
allocation: Allocation.example allocation: Allocation.example
) )
var content= AbiEncoder.init() let content = AbiEncoder.encode:
content.startTuple() ((allocationType, AbiEncoder.encode(assetOutcome.allocation)),)
content.startTuple() let expected = AbiEncoder.encode:
content.write(allocationType) (assetOutcome.assetHolder, content)
content.write(AbiEncoder.encode(assetOutcome.allocation)) check AbiEncoder.encode(assetOutcome) == expected
content.finishTuple()
content.finishTuple()
var encoder= AbiEncoder.init()
encoder.startTuple()
encoder.write(assetOutcome.assetHolder)
encoder.write(content.finish())
encoder.finishTuple()
check AbiEncoder.encode(assetOutcome) == encoder.finish()
test "encodes guarantee outcome": test "encodes guarantee outcome":
let assetOutcome = AssetOutcome( let assetOutcome = AssetOutcome(
@ -58,27 +39,17 @@ suite "outcome":
assetHolder: EthAddress.example, assetHolder: EthAddress.example,
guarantee: Guarantee.example guarantee: Guarantee.example
) )
var content= AbiEncoder.init() let content = AbiEncoder.encode:
content.startTuple() ((guaranteeType, AbiEncoder.encode(assetOutcome.guarantee)),)
content.startTuple() let expected = AbiEncoder.encode:
content.write(guaranteeType) (assetOutcome.assetHolder, content)
content.write(AbiEncoder.encode(assetOutcome.guarantee)) check AbiEncoder.encode(assetOutcome) == expected
content.finishTuple()
content.finishTuple()
var encoder= AbiEncoder.init()
encoder.startTuple()
encoder.write(assetOutcome.assetHolder)
encoder.write(content.finish())
encoder.finishTuple()
check AbiEncoder.encode(assetOutcome) == encoder.finish()
test "encodes outcomes": test "encodes outcomes":
let outcome = Outcome.example() let outcome = Outcome.example()
var encoder= AbiEncoder.init() let expected = AbiEncoder.encode:
encoder.startTuple() (seq[AssetOutcome](outcome),)
encoder.write(seq[AssetOutcome](outcome)) check AbiEncoder.encode(outcome) == expected
encoder.finishTuple()
check AbiEncoder.encode(outcome) == encoder.finish()
test "hashes outcomes": test "hashes outcomes":
let outcome = Outcome.example let outcome = Outcome.example
@ -90,31 +61,31 @@ suite "outcome":
let outcome = Outcome(@[ let outcome = Outcome(@[
AssetOutcome( AssetOutcome(
kind: allocationType, kind: allocationType,
assetHolder: EthAddress.parse( assetHolder: !EthAddress.init(
"1E90B49563da16D2537CA1Ddd9b1285279103D93" "1E90B49563da16D2537CA1Ddd9b1285279103D93"
).get(), ),
allocation: Allocation(@[ allocation: Allocation(@[
( (
destination: Destination.parse( destination: !Destination.parse(
"f1918e8562236eb17adc8502332f4c9c82bc14e19bfc0aa10ab674ff75b3d2f3" "f1918e8562236eb17adc8502332f4c9c82bc14e19bfc0aa10ab674ff75b3d2f3"
).get(), ),
amount: 0x05.u256 amount: 0x05.u256
) )
]) ])
), ),
AssetOutcome( AssetOutcome(
kind: guaranteeType, kind: guaranteeType,
assetHolder: EthAddress.parse( assetHolder: !EthAddress.init(
"1E90B49563da16D2537CA1Ddd9b1285279103D93" "1E90B49563da16D2537CA1Ddd9b1285279103D93"
).get(), ),
guarantee: Guarantee( guarantee: Guarantee(
targetChannelId: Destination.parse( targetChannelId: !Destination.parse(
"cac1bb71f0a97c8ac94ca9546b43178a9ad254c7b757ac07433aa6df35cd8089" "cac1bb71f0a97c8ac94ca9546b43178a9ad254c7b757ac07433aa6df35cd8089"
).get(), ),
destinations: @[ destinations: @[
Destination.parse( !Destination.parse(
"f1918e8562236eb17adc8502332f4c9c82bc14e19bfc0aa10ab674ff75b3d2f3" "f1918e8562236eb17adc8502332f4c9c82bc14e19bfc0aa10ab674ff75b3d2f3"
).get() )
] ]
) )
) )

View File

@ -40,7 +40,7 @@ suite "signature":
chainId: 0x1.u256, chainId: 0x1.u256,
nonce: 1, nonce: 1,
participants: @[ participants: @[
EthAddress.parse("0x8a64E10FF40Bc9C90EA5750313dB5e036495c10E").get() !EthAddress.init("0x8a64E10FF40Bc9C90EA5750313dB5e036495c10E")
] ]
), ),
outcome: Outcome(@[]), outcome: Outcome(@[]),
@ -50,12 +50,12 @@ suite "signature":
appDefinition: EthAddress.default, appDefinition: EthAddress.default,
challengeDuration: 5 challengeDuration: 5
) )
let seckey = EthPrivateKey.parse( let seckey = !EthPrivateKey.parse(
"41b0f5f91967dded8af487277874f95116094cc6004ac2b2169b5b6a87608f3e" "41b0f5f91967dded8af487277874f95116094cc6004ac2b2169b5b6a87608f3e"
).get() )
let expected = Signature.parse( let expected = !Signature.parse(
"9b966cf0065586d59c8b9eb475ac763c96ad8316b81061238f32968a631f9e21" & "9b966cf0065586d59c8b9eb475ac763c96ad8316b81061238f32968a631f9e21" &
"251363c193c78c89b3eb2fec23f0ea5c3c72acff7d1f27430cfb84b9da9831fb" & "251363c193c78c89b3eb2fec23f0ea5c3c72acff7d1f27430cfb84b9da9831fb" &
"1c" "1c"
).get() )
check seckey.sign(state) == expected check seckey.sign(state) == expected

View File

@ -23,26 +23,20 @@ suite "state":
) )
test "hashes app part of state": test "hashes app part of state":
var encoder= AbiEncoder.init() let encoded = AbiEncoder.encode:
encoder.startTuple() (state.challengeDuration, state.appDefinition, state.appData)
encoder.write(state.challengeDuration)
encoder.write(state.appDefinition)
encoder.write(state.appData)
encoder.finishTuple()
let encoded = encoder.finish()
let hashed = keccak256.digest(encoded).data let hashed = keccak256.digest(encoded).data
check hashAppPart(state) == hashed check hashAppPart(state) == hashed
test "hashes state": test "hashes state":
var encoder= AbiEncoder.init() let encoded = AbiEncoder.encode:
encoder.startTuple() (
encoder.write(state.turnNum) state.turnNum,
encoder.write(state.isFinal) state.isFinal,
encoder.write(getChannelId(state.channel)) getChannelId(state.channel),
encoder.write(hashAppPart(state)) hashAppPart(state),
encoder.write(hashOutcome(state.outcome)) hashOutcome(state.outcome)
encoder.finishTuple() )
let encoded = encoder.finish()
let hashed = keccak256.digest(encoded).data let hashed = keccak256.digest(encoded).data
check hashState(state) == hashed check hashState(state) == hashed
@ -52,7 +46,7 @@ suite "state":
chainId: 0x1.u256, chainId: 0x1.u256,
nonce: 1, nonce: 1,
participants: @[ participants: @[
EthAddress.parse("DBE821484648c73C1996Da25f2355342B9803eBD").get() !EthAddress.init("DBE821484648c73C1996Da25f2355342B9803eBD")
] ]
), ),
outcome: Outcome(@[]), outcome: Outcome(@[]),

View File

@ -23,29 +23,35 @@ suite "wallet: opening ledger channel":
setup: setup:
wallet = Wallet.init(key) wallet = Wallet.init(key)
channel = wallet.openLedgerChannel(hub, chainId, nonce, asset, amount).get channel = !wallet.openLedgerChannel(hub, chainId, nonce, asset, amount)
test "sets correct channel definition": test "sets correct channel definition":
let definition = wallet.state(channel).get.channel let definition = (!wallet.state(channel)).channel
check definition.chainId == chainId check definition.chainId == chainId
check definition.nonce == nonce check definition.nonce == nonce
check definition.participants == @[wallet.address, hub] check definition.participants == @[wallet.address, hub]
test "uses consecutive nonces when none is provided":
channel = !wallet.openLedgerChannel(hub, chainId, asset, amount)
check (!wallet.state(channel)).channel.nonce == nonce + 1
channel = !wallet.openLedgerChannel(hub, chainId, asset, amount)
check (!wallet.state(channel)).channel.nonce == nonce + 2
test "provides correct outcome": test "provides correct outcome":
let outcome = wallet.state(channel).get.outcome let outcome = (!wallet.state(channel)).outcome
check outcome == Outcome.init(asset, {wallet.destination: amount}) check outcome == Outcome.init(asset, {wallet.destination: amount})
test "signs the state": test "signs the state":
let state = wallet.state(channel).get let state = !wallet.state(channel)
let signatures = wallet.signatures(channel).get let signatures = !wallet.signatures(channel)
check signatures == @[key.sign(state)] check signatures == @[key.sign(state)]
test "sets app definition and app data to zero": test "sets app definition and app data to zero":
check wallet.state(channel).get.appDefinition == EthAddress.zero check (!wallet.state(channel)).appDefinition == EthAddress.zero
check wallet.state(channel).get.appData.len == 0 check (!wallet.state(channel)).appData.len == 0
test "does not allow opening a channel that already exists": test "does not allow opening a channel that already exists":
check wallet.openLedgerChannel(hub, chainId, nonce, asset, amount).isErr check wallet.openLedgerChannel(hub, chainId, nonce, asset, amount).isFailure
suite "wallet: accepting incoming channel": suite "wallet: accepting incoming channel":
@ -59,26 +65,26 @@ suite "wallet: accepting incoming channel":
signed.state.channel.participants &= @[wallet.address] signed.state.channel.participants &= @[wallet.address]
test "returns the new channel id": test "returns the new channel id":
let channel = wallet.acceptChannel(signed).get let channel = !wallet.acceptChannel(signed)
check wallet.state(channel).get == signed.state check !wallet.state(channel) == signed.state
test "signs the channel state": test "signs the channel state":
let channel = wallet.acceptChannel(signed).get let channel = !wallet.acceptChannel(signed)
let expectedSignatures = @[key.sign(signed.state)] let expectedSignatures = @[key.sign(signed.state)]
check wallet.signatures(channel).get == expectedSignatures check !wallet.signatures(channel) == expectedSignatures
test "fails when wallet address is not a participant": test "fails when wallet address is not a participant":
let wrongParticipants = seq[EthAddress].example let wrongParticipants = seq[EthAddress].example
signed.state.channel.participants = wrongParticipants signed.state.channel.participants = wrongParticipants
check wallet.acceptChannel(signed).isErr check wallet.acceptChannel(signed).isFailure
test "fails when signatures are incorrect": test "fails when signatures are incorrect":
signed.signatures = @[key.sign(State.example)] signed.signatures = @[key.sign(State.example)]
check wallet.acceptChannel(signed).isErr check wallet.acceptChannel(signed).isFailure
test "fails when channel with this id already exists": test "fails when channel with this id already exists":
check wallet.acceptChannel(signed).isOk check wallet.acceptChannel(signed).isSuccess
check wallet.acceptChannel(signed).isErr check wallet.acceptChannel(signed).isFailure
suite "wallet: making payments": suite "wallet: making payments":
@ -93,54 +99,54 @@ suite "wallet: making payments":
test "paying updates the channel state": test "paying updates the channel state":
wallet = Wallet.init(key) wallet = Wallet.init(key)
channel = wallet.openLedgerChannel(hub, chainId, nonce, asset, 100.u256).get channel = !wallet.openLedgerChannel(hub, chainId, nonce, asset, 100.u256)
check wallet.pay(channel, asset, hub, 1.u256).isOk check wallet.pay(channel, asset, hub, 1.u256).isSuccess
check wallet.balance(channel, asset) == 99.u256 check wallet.balance(channel, asset) == 99.u256
check wallet.balance(channel, asset, hub) == 1.u256 check wallet.balance(channel, asset, hub) == 1.u256
check wallet.pay(channel, asset, hub, 2.u256).isOk check wallet.pay(channel, asset, hub, 2.u256).isSuccess
check wallet.balance(channel, asset) == 97.u256 check wallet.balance(channel, asset) == 97.u256
check wallet.balance(channel, asset, hub) == 3.u256 check wallet.balance(channel, asset, hub) == 3.u256
test "paying updates signatures": test "paying updates signatures":
wallet = Wallet.init(key) wallet = Wallet.init(key)
channel = wallet.openLedgerChannel(hub, chainId, nonce, asset, 100.u256).get channel = !wallet.openLedgerChannel(hub, chainId, nonce, asset, 100.u256)
check wallet.pay(channel, asset, hub, 1.u256).isOk check wallet.pay(channel, asset, hub, 1.u256).isSuccess
let expectedSignature = key.sign(wallet.state(channel).get) let expectedSignature = key.sign(!wallet.state(channel))
check wallet.signature(channel, wallet.address) == expectedSignature.some check wallet.signature(channel, wallet.address) == expectedSignature.some
test "pay returns the updated signed state": test "pay returns the updated signed state":
wallet = Wallet.init(key) wallet = Wallet.init(key)
channel = wallet.openLedgerChannel(hub, chainId, nonce, asset, 42.u256).get channel = !wallet.openLedgerChannel(hub, chainId, nonce, asset, 42.u256)
let updated = wallet.pay(channel, asset, hub, 1.u256).option let updated = wallet.pay(channel, asset, hub, 1.u256).option
check updated?.state == wallet.state(channel) check updated.?state == wallet.state(channel)
check updated?.signatures == wallet.signatures(channel) check updated.?signatures == wallet.signatures(channel)
test "payment fails when channel not found": test "payment fails when channel not found":
wallet = Wallet.init(key) wallet = Wallet.init(key)
check wallet.pay(channel, asset, hub, 1.u256).isErr check wallet.pay(channel, asset, hub, 1.u256).isFailure
test "payment fails when asset not found": test "payment fails when asset not found":
wallet = Wallet.init(key) wallet = Wallet.init(key)
var state = State.example var state = State.example
state.channel.participants &= wallet.address state.channel.participants &= wallet.address
channel = wallet.acceptChannel(SignedState(state: state)).get channel = !wallet.acceptChannel(SignedState(state: state))
check wallet.pay(channel, asset, hub, 1.u256).isErr check wallet.pay(channel, asset, hub, 1.u256).isFailure
test "payment fails when payer has no allocation": test "payment fails when payer has no allocation":
wallet = Wallet.init(key) wallet = Wallet.init(key)
var state: State var state: State
state.channel = ChannelDefinition(participants: @[wallet.address]) state.channel = ChannelDefinition(participants: @[wallet.address])
state.outcome = Outcome.init(asset, @[]) state.outcome = Outcome.init(asset, @[])
channel = wallet.acceptChannel(SignedState(state: state)).get channel = !wallet.acceptChannel(SignedState(state: state))
check wallet.pay(channel, asset, hub, 1.u256).isErr check wallet.pay(channel, asset, hub, 1.u256).isFailure
test "payment fails when payer has insufficient funds": test "payment fails when payer has insufficient funds":
wallet = Wallet.init(key) wallet = Wallet.init(key)
channel = wallet.openLedgerChannel(hub, chainId, nonce, asset, 1.u256).get 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).isSuccess
check wallet.pay(channel, asset, hub, 1.u256).isErr check wallet.pay(channel, asset, hub, 1.u256).isFailure
suite "wallet: accepting payments": suite "wallet: accepting payments":
@ -155,55 +161,71 @@ suite "wallet: accepting payments":
setup: setup:
payer = Wallet.init(payerKey) payer = Wallet.init(payerKey)
receiver = Wallet.init(receiverKey) receiver = Wallet.init(receiverKey)
channel = payer.openLedgerChannel( channel = !payer.openLedgerChannel(
receiver.address, chainId, nonce, asset, 100.u256).get receiver.address, chainId, nonce, asset, 100.u256)
let update = payer.latestSignedState(channel).get let update = !payer.latestSignedState(channel)
discard receiver.acceptChannel(update) discard receiver.acceptChannel(update)
test "updates channel state": test "updates channel state":
let payment = payer.pay(channel, asset, receiver.address, 42.u256).get let payment = !payer.pay(channel, asset, receiver.address, 42.u256)
check receiver.acceptPayment(channel, asset, payer.address, payment).isOk check receiver.acceptPayment(channel, asset, payer.address, payment).isSuccess
check receiver.balance(channel, asset) == 42.u256 check receiver.balance(channel, asset) == 42.u256
test "fails when receiver balance is decreased": test "fails when receiver balance is decreased":
let payment1 = payer.pay(channel, asset, receiver.address, 10.u256).get let payment1 = !payer.pay(channel, asset, receiver.address, 10.u256)
let payment2 = payer.pay(channel, asset, receiver.address, 10.u256).get let payment2 = !payer.pay(channel, asset, receiver.address, 10.u256)
check receiver.acceptPayment(channel, asset, payer.address, payment1).isOk check receiver.acceptPayment(channel, asset, payer.address, payment1).isSuccess
check receiver.acceptPayment(channel, asset, payer.address, payment2).isOk check receiver.acceptPayment(channel, asset, payer.address, payment2).isSuccess
check receiver.acceptPayment(channel, asset, payer.address, payment1).isErr check receiver.acceptPayment(channel, asset, payer.address, payment1).isFailure
check receiver.balance(channel, asset) == 20 check receiver.balance(channel, asset) == 20
test "fails when the total supply of the asset changes": test "fails when the total supply of the asset changes":
var payment = payer.pay(channel, asset, receiver.address, 10.u256).get var payment = !payer.pay(channel, asset, receiver.address, 10.u256)
var balances = payment.state.outcome.balances(asset).get var balances = !payment.state.outcome.balances(asset)
balances[payer.destination] += 10.u256 balances[payer.destination] += 10.u256
payment.state.outcome.update(asset, balances) payment.state.outcome.update(asset, balances)
check receiver.acceptPayment(channel, asset, payer.address, payment).isErr check receiver.acceptPayment(channel, asset, payer.address, payment).isFailure
test "fails without a signature": test "fails without a signature":
var payment = payer.pay(channel, asset, receiver.address, 10.u256).get var payment = !payer.pay(channel, asset, receiver.address, 10.u256)
payment.signatures = @[] payment.signatures = @[]
check receiver.acceptPayment(channel, asset, payer.address, payment).isErr check receiver.acceptPayment(channel, asset, payer.address, payment).isFailure
test "fails with an incorrect signature": test "fails with an incorrect signature":
var payment = payer.pay(channel, asset, receiver.address, 10.u256).get var payment = !payer.pay(channel, asset, receiver.address, 10.u256)
payment.signatures = @[Signature.example] payment.signatures = @[Signature.example]
check receiver.acceptPayment(channel, asset, payer.address, payment).isErr check receiver.acceptPayment(channel, asset, payer.address, payment).isFailure
test "fails when channel is unknown": test "fails when channel is unknown":
let newChannel = payer.openLedgerChannel( let newChannel = !payer.openLedgerChannel(
receiver.address, chainId, nonce + 1, asset, 100.u256).get receiver.address, chainId, nonce + 1, asset, 100.u256)
let payment = payer.pay(newChannel, asset, receiver.address, 10.u256).get let payment = !payer.pay(newChannel, asset, receiver.address, 10.u256)
check receiver.acceptPayment(newChannel, asset, payer.address, payment).isErr check receiver.acceptPayment(newChannel, asset, payer.address, payment).isFailure
test "fails when payment does not match channel": test "fails when payment does not match channel":
let newChannel = payer.openLedgerChannel( let newChannel = !payer.openLedgerChannel(
receiver.address, chainId, nonce + 1, asset, 100.u256).get receiver.address, chainId, nonce + 1, asset, 100.u256)
let payment = payer.pay(newChannel, asset, receiver.address, 10.u256).get let payment = !payer.pay(newChannel, asset, receiver.address, 10.u256)
check receiver.acceptPayment(channel, asset, payer.address, payment).isErr check receiver.acceptPayment(channel, asset, payer.address, payment).isFailure
test "fails when state is updated in unrelated areas": test "fails when state is updated in unrelated areas":
var payment = payer.pay(channel, asset, receiver.address, 10.u256).get var payment = !payer.pay(channel, asset, receiver.address, 10.u256)
payment.state.appDefinition = EthAddress.example payment.state.appDefinition = EthAddress.example
payment.signatures = @[payerKey.sign(payment.state)] payment.signatures = @[payerKey.sign(payment.state)]
check receiver.acceptPayment(channel, asset, payer.address, payment).isErr check receiver.acceptPayment(channel, asset, payer.address, payment).isFailure
suite "wallet reference type":
let asset = EthAddress.example
let amount = 42.u256
let chainId = UInt256.example
test "wallet can also be used as a reference type":
let wallet1 = WalletRef.new(EthPrivateKey.random())
let wallet2 = WalletRef.new(EthPrivateKey.random())
let address1 = wallet1.address
let address2 = wallet2.address
let channel = !wallet1.openLedgerChannel(address2, chainId, asset, amount)
check !wallet2.acceptChannel(!wallet1.latestSignedState(channel)) == channel
let payment = !wallet1.pay(channel, asset, address2, amount)
check wallet2.acceptPayment(channel, asset, address1, payment).isSuccess

View File

@ -0,0 +1,37 @@
import ../basics
import pkg/nitro/wallet/nonces
suite "nonces":
let chainId = UInt256.example
let participants = seq[EthAddress].example(1..5)
var nonces: Nonces
setup:
nonces = Nonces()
test "nonces start at 0":
check nonces.getNonce(chainId, participants) == 0
test "nonces increase by 1":
nonces.incNonce(0, chainId, participants)
check nonces.getNonce(chainId, participants) == 1
nonces.incNonce(1, chainId, participants)
check nonces.getNonce(chainId, participants) == 2
test "nonces do not decrease":
nonces.incNonce(100, chainId, participants)
check nonces.getNonce(chainId, participants) == 101
nonces.incNonce(0, chainId, participants)
check nonces.getNonce(chainId, participants) == 102
test "nonces are different when participants differ":
let otherParticipants = seq[EthAddress].example(1..5)
nonces.incNonce(0, chainId, participants)
check nonces.getNonce(chainId, otherParticipants) == 0
test "nonces are different when chain ids differ":
let otherChainId = UInt256.example
nonces.incNonce(0, chainId, participants)
check nonces.getNonce(otherChainId, participants) == 0

View File

@ -3,6 +3,7 @@ import ./nitro/protocol/testChannel
import ./nitro/protocol/testOutcome import ./nitro/protocol/testOutcome
import ./nitro/protocol/testState import ./nitro/protocol/testState
import ./nitro/protocol/testSignature import ./nitro/protocol/testSignature
import ./nitro/wallet/testNonces
import ./nitro/testWallet import ./nitro/testWallet
import ./nitro/testJson import ./nitro/testJson