Nitro state, including ABI encoding and hashing

This commit is contained in:
Mark Spanbroek 2021-02-22 15:32:48 +01:00
commit ab2a39a704
17 changed files with 636 additions and 0 deletions

5
.editorconfig Normal file
View File

@ -0,0 +1,5 @@
[*]
indent_style = space
insert_final_newline = true
indent_size = 2
trim_trailing_whitespace = true

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
*
!*/
!*.*

1
.tool-versions Normal file
View File

@ -0,0 +1 @@
nim 1.4.2

3
nitro.nim Normal file
View File

@ -0,0 +1,3 @@
import ./nitro/state
export state

9
nitro.nimble Normal file
View File

@ -0,0 +1,9 @@
version = "0.1.0"
author = "Nim Nitro developers"
license = "MIT"
description = "Nitro state channels"
requires "nim >= 1.2.6 & < 2.0.0"
requires "nimcrypto >= 0.5.4 & < 0.6.0"
requires "stint"
requires "stew"

101
nitro/abi.nim Normal file
View File

@ -0,0 +1,101 @@
import pkg/stew/endians2
import pkg/stint
type
Abi* = object
AbiWriter* = object
bytes: seq[byte]
tuples: seq[Tuple]
Tuple = object
postponed: seq[Split]
Split = object
head: Slice[int]
tail: seq[byte]
proc isStatic*(_: type Abi, t: type SomeUnsignedInt): bool = true
proc isStatic*(_: type Abi, t: type StUint): bool = true
proc isStatic*(_: type Abi, t: type bool): bool = true
proc isStatic*(_: type Abi, t: type enum): bool = true
proc isStatic*[T](_: type Abi, t: type seq[T]): bool = false
proc isStatic*[I, T](_: type Abi, t: type array[I, T]): bool = Abi.isStatic(T)
proc encode*[T](_: type Abi, value: T): seq[byte]
proc pad(writer: var AbiWriter, len: int) =
let padlen = (32 - len mod 32) mod 32
for _ in 0..<padlen:
writer.bytes.add(0'u8)
proc padleft(writer: var AbiWriter, bytes: openArray[byte]) =
writer.pad(bytes.len)
writer.bytes.add(bytes)
proc padright(writer: var AbiWriter, bytes: openArray[byte]) =
writer.bytes.add(bytes)
writer.pad(bytes.len)
proc write*(writer: var AbiWriter, value: SomeUnsignedInt | StUint) =
writer.padleft(value.toBytesBE)
proc write*(writer: var AbiWriter, value: bool) =
writer.write(cast[uint8](value))
proc write*(writer: var AbiWriter, value: enum) =
writer.write(uint64(ord(value)))
proc write*[I](writer: var AbiWriter, bytes: array[I, byte]) =
writer.padright(bytes)
proc writeLater[T](writer: var AbiWriter, value: T) =
var split: Split
split.head.a = writer.bytes.high + 1
writer.write(0'u64)
split.head.b = writer.bytes.high
split.tail = Abi.encode(value)
writer.tuples[^1].postponed.add(split)
proc write*(writer: var AbiWriter, bytes: seq[byte]) =
if writer.tuples.len == 0:
writer.write(bytes.len.uint64)
writer.padright(bytes)
else:
writer.writeLater(bytes)
proc startTuple*(writer: var AbiWriter) =
writer.tuples.add(Tuple())
proc finishTuple*(writer: var AbiWriter) =
let tupl = writer.tuples.pop()
for split in tupl.postponed:
let offset = writer.bytes.len - split.head.a
writer.bytes[split.head] = Abi.encode(offset.uint64)
writer.bytes.add(split.tail)
proc write*[I, T](writer: var AbiWriter, value: array[I, T]) =
if writer.tuples.len == 0 or Abi.isStatic(T):
writer.startTuple()
for element in value:
writer.write(element)
writer.finishTuple()
else:
writer.writeLater(value)
proc write*[T](writer: var AbiWriter, value: seq[T]) =
if writer.tuples.len == 0:
writer.write(value.len.uint64)
writer.startTuple()
for element in value:
writer.write(element)
writer.finishTuple()
else:
writer.writeLater(value)
proc finish*(writer: var AbiWriter): seq[byte] =
doAssert writer.tuples.len == 0, "not all tuples were finished"
doAssert writer.bytes.len mod 32 == 0, "encoding invariant broken"
writer.bytes
proc encode*[T](_: type Abi, value: T): seq[byte] =
var writer: AbiWriter
writer.write(value)
writer.finish()

18
nitro/channel.nim Normal file
View File

@ -0,0 +1,18 @@
import pkg/nimcrypto
import ./abi
import ./types
export types
type
Channel* = object
nonce*: UInt48
participants*: seq[EthAddress]
chainId*: UInt256
proc getChannelId*(channel: Channel): array[32, byte] =
var writer: AbiWriter
writer.write(channel.chainId)
writer.write(channel.participants)
writer.write(channel.nonce)
keccak256.digest(writer.finish()).data

60
nitro/outcome.nim Normal file
View File

@ -0,0 +1,60 @@
import pkg/nimcrypto
import ./abi
import ./types
export types
export abi
type
Outcome* = seq[AssetOutcome]
AssetOutcomeType* = enum
allocationType = 0
guaranteeType = 1
AssetOutcome* = object
assetHolder*: EthAddress
case kind*: AssetOutcomeType
of allocationType:
allocation*: Allocation
of guaranteeType:
guarantee*: Guarantee
Allocation* = seq[AllocationItem]
AllocationItem* = object
destination*: array[32, byte]
amount*: UInt256
Guarantee* = object
targetChannelId*: array[32, byte]
destinations*: seq[array[32, byte]]
proc isStatic*(_: type Abi, t: type AssetOutcome): bool = false
proc isStatic*(_: type Abi, t: type AllocationItem): bool = true
proc isStatic*(_: type Abi, t: type Guarantee): bool = false
proc write*(writer: var AbiWriter, guarantee: Guarantee) =
writer.startTuple()
writer.write(guarantee.targetChannelId)
writer.write(guarantee.destinations)
writer.finishTuple()
proc write*(writer: var AbiWriter, item: AllocationItem) =
writer.startTuple()
writer.write(item.destination)
writer.write(item.amount)
writer.finishTuple()
proc write*(writer: var AbiWriter, assetOutcome: AssetOutcome) =
var content: AbiWriter
content.startTuple()
content.write(assetOutcome.kind)
case assetOutcome.kind:
of allocationType:
content.write(Abi.encode(assetOutcome.allocation))
of guaranteeType:
content.write(Abi.encode(assetOutcome.guarantee))
content.finishTuple()
writer.startTuple()
writer.write(assetOutcome.assetHolder)
writer.write(content.finish())
writer.finishTuple()
proc hashOutcome*(outcome: Outcome): array[32, byte] =
keccak256.digest(Abi.encode(outcome)).data

61
nitro/state.nim Normal file
View File

@ -0,0 +1,61 @@
import pkg/nimcrypto
import ./types
import ./channel
import ./outcome
import ./abi
export types
export channel
export outcome
type
State* = object
turnNum*: UInt48
isFinal*: bool
channel*: Channel
challengeDuration*: UInt48
outcome*: Outcome
appDefinition*: EthAddress
appData*: seq[byte]
FixedPart* = object
chainId*: UInt256
participants*: seq[EthAddress]
channelNonce*: UInt48
appDefinition*: EthAddress
challengeDuration*: UInt48
VariablePart* = object
outcome*: seq[byte]
appdata*: seq[byte]
proc fixedPart*(state: State): FixedPart =
FixedPart(
chainId: state.channel.chainId,
participants: state.channel.participants,
channelNonce: state.channel.nonce,
appDefinition: state.appDefinition,
challengeDuration: state.challengeDuration
)
proc variablePart*(state: State): VariablePart =
VariablePart(
outcome: Abi.encode(state.outcome),
appData: state.appData
)
proc hashAppPart*(state: State): array[32, byte] =
var writer: AbiWriter
writer.write(state.challengeDuration)
writer.write(state.appDefinition)
writer.write(state.appData)
keccak256.digest(writer.finish).data
proc hashState*(state: State): array[32, byte] =
var writer: AbiWriter
writer.startTuple()
writer.write(state.turnNum)
writer.write(state.isFinal)
writer.write(getChannelId(state.channel))
writer.write(hashAppPart(state))
writer.write(hashOutcome(state.outcome))
writer.finishTuple()
keccak256.digest(writer.finish).data

8
nitro/types.nim Normal file
View File

@ -0,0 +1,8 @@
import std/math
import pkg/stint
export stint
type
UInt48* = range[0'u64..2'u64^48-1]
EthAddress* = array[20, byte]

71
tests/nitro/examples.nim Normal file
View File

@ -0,0 +1,71 @@
import std/random
import std/sequtils
import pkg/nitro
randomize()
proc example*(_: type bool): bool =
rand(0'u8..1'u8) == 1
proc example*[T: SomeInteger](_: type T): T =
rand(T)
proc example*[I: static int, T](_: type array[I, T]): array[I, T] =
for i in 0..<I:
result[i] = T.example
proc example*[T](_: type seq[T], len = 0..5): seq[T] =
let chosenlen = rand(len)
newSeqWith(chosenlen, T.example)
proc example*(_: type UInt256): UInt256 =
UInt256.fromBytes(array[32, byte].example)
proc example*(_: type UInt128): UInt128 =
UInt128.fromBytes(array[16, byte].example)
proc example*(_: type Channel): Channel =
Channel(
nonce: UInt48.example,
participants: seq[EthAddress].example(2..5),
chainId: UInt256.example
)
proc example*(_: type AllocationItem): AllocationItem =
AllocationItem(
destination: array[32, byte].example,
amount: UInt256.example
)
proc example*(_: type Guarantee): Guarantee =
Guarantee(
targetChannelId: array[32, byte].example,
destinations: seq[array[32, byte]].example
)
proc example*(_: type AssetOutcome): AssetOutcome =
let kind = rand(AssetOutcomeType.low..AssetOutcomeType.high)
case kind:
of allocationType:
AssetOutcome(
kind: allocationType,
assetHolder: EthAddress.example,
allocation: Allocation.example
)
of guaranteeType:
AssetOutcome(
kind: guaranteeType,
assetHolder: EthAddress.example,
guarantee: Guarantee.example
)
proc example*(_: type State): State =
State(
turnNum: UInt48.example,
isFinal: bool.example,
channel: Channel.example,
challengeDuration: UInt48.example,
outcome: Outcome.example,
appDefinition: EthAddress.example,
appData: seq[byte].example
)

1
tests/nitro/nim.cfg Normal file
View File

@ -0,0 +1 @@
--path:"../.."

156
tests/nitro/testAbi.nim Normal file
View File

@ -0,0 +1,156 @@
import std/unittest
import pkg/nitro/abi
import pkg/stint
import ./examples
suite "ABI encoding":
proc zeroes(amount: int): seq[byte] =
newSeq[byte](amount)
test "encodes uint8":
check Abi.encode(42'u8) == 31.zeroes & 42'u8
test "encodes booleans":
check Abi.encode(false) == 31.zeroes & 0'u8
check Abi.encode(true) == 31.zeroes & 1'u8
test "encodes uint16, 32, 64":
check Abi.encode(0xABCD'u16) ==
30.zeroes & 0xAB'u8 & 0xCD'u8
check Abi.encode(0x11223344'u32) ==
28.zeroes & 0x11'u8 & 0x22'u8 & 0x33'u8 & 0x44'u8
check Abi.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 Abi.encode(SomeRange(0x1122)) == 30.zeroes & 0x11'u8 & 0x22'u8
test "encodes enums":
type SomeEnum = enum
one = 1
two = 2
check Abi.encode(one) == 31.zeroes & 1'u8
check Abi.encode(two) == 31.zeroes & 2'u8
test "encodes stints":
let uint256 = UInt256.example
check Abi.encode(uint256) == @(uint256.toBytesBE)
let uint128 = UInt128.example
check Abi.encode(uint128) == 16.zeroes & @(uint128.toBytesBE)
test "encodes byte arrays":
let bytes3 = [1'u8, 2'u8, 3'u8]
check Abi.encode(bytes3) == @bytes3 & 29.zeroes
let bytes32 = array[32, byte].example
check Abi.encode(bytes32) == @bytes32
let bytes33 = array[33, byte].example
check Abi.encode(bytes33) == @bytes33 & 31.zeroes
test "encodes byte sequences":
let bytes3 = @[1'u8, 2'u8, 3'u8]
let bytes3len = Abi.encode(bytes3.len.uint64)
check Abi.encode(bytes3) == bytes3len & bytes3 & 29.zeroes
let bytes32 = @(array[32, byte].example)
let bytes32len = Abi.encode(bytes32.len.uint64)
check Abi.encode(bytes32) == bytes32len & bytes32
let bytes33 = @(array[33, byte].example)
let bytes33len = Abi.encode(bytes33.len.uint64)
check Abi.encode(bytes33) == bytes33len & bytes33 & 31.zeroes
test "encodes 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 writer: AbiWriter
writer.startTuple()
writer.write(a)
writer.write(b)
writer.write(c)
writer.write(d)
writer.finishTuple()
check writer.finish() ==
Abi.encode(a) &
Abi.encode(3 * 32'u8) & # offset in tuple
Abi.encode(c) &
Abi.encode(3 * 32'u8) & # offset in tuple
Abi.encode(b) &
Abi.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 writer: AbiWriter
writer.startTuple()
writer.write(a)
writer.write(b)
writer.startTuple()
writer.write(c)
writer.write(d)
writer.finishTuple()
writer.finishTuple()
check writer.finish() ==
Abi.encode(a) &
Abi.encode(5 * 32'u8) & # offset in tuple
Abi.encode(c) &
Abi.encode(1 * 32'u8) & # offset in tuple
Abi.encode(d) &
Abi.encode(b)
test "encodes arrays":
let element1 = seq[byte].example
let element2 = seq[byte].example
var expected: AbiWriter
expected.startTuple()
expected.write(element1)
expected.write(element2)
expected.finishTuple()
check Abi.encode([element1, element2]) == expected.finish()
test "encodes sequences":
let element1 = seq[byte].example
let element2 = seq[byte].example
var expected: AbiWriter
expected.write(2'u8)
expected.startTuple()
expected.write(element1)
expected.write(element2)
expected.finishTuple()
check Abi.encode(@[element1, element2]) == expected.finish()
test "encodes sequence as dynamic element":
let s = @[42.u256, 43.u256]
var writer: AbiWriter
writer.startTuple()
writer.write(s)
writer.finishTuple()
check writer.finish() ==
Abi.encode(32'u8) & # offset in tuple
Abi.encode(s)
test "encodes array of static elements as static element":
let a = [[42'u8], [43'u8]]
var writer: AbiWriter
writer.startTuple()
writer.write(a)
writer.finishTuple()
check writer.finish() == Abi.encode(a)
test "encodes array of dynamic elements as dynamic element":
let a = [@[42'u8], @[43'u8]]
var writer: AbiWriter
writer.startTuple()
writer.write(a)
writer.finishTuple()
check writer.finish() ==
Abi.encode(32'u8) & # offset in tuple
Abi.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

@ -0,0 +1,19 @@
import std/unittest
import pkg/nitro/channel
import pkg/nitro/abi
import pkg/nimcrypto
import ./examples
suite "channel":
let channel = Channel.example
test "calculates channel id":
var writer: AbiWriter
writer.write(channel.chainId)
writer.write(channel.participants)
writer.write(channel.nonce)
let encoded = writer.finish()
let hashed = keccak256.digest(encoded).data
check getChannelId(channel) == hashed

View File

@ -0,0 +1,66 @@
import std/unittest
import pkg/nimcrypto
import pkg/nitro/outcome
import ./examples
suite "outcome":
test "encodes guarantees":
let guarantee = Guarantee.example
var writer: AbiWriter
writer.startTuple()
writer.write(guarantee.targetChannelId)
writer.write(guarantee.destinations)
writer.finishTuple()
check Abi.encode(guarantee) == writer.finish()
test "encodes allocation items":
let item = AllocationItem.example
var writer: AbiWriter
writer.startTuple()
writer.write(item.destination)
writer.write(item.amount)
writer.finishTuple()
check Abi.encode(item) == writer.finish()
test "encodes allocation outcome":
let assetOutcome = AssetOutcome(
kind: allocationType,
assetHolder: EthAddress.example,
allocation: Allocation.example
)
var content: AbiWriter
content.startTuple()
content.write(allocationType)
content.write(Abi.encode(assetOutcome.allocation))
content.finishTuple()
var writer: AbiWriter
writer.startTuple()
writer.write(assetOutcome.assetHolder)
writer.write(content.finish())
writer.finishTuple()
check Abi.encode(assetOutcome) == writer.finish()
test "encodes guarantee outcome":
let assetOutcome = AssetOutcome(
kind: guaranteeType,
assetHolder: EthAddress.example,
guarantee: Guarantee.example
)
var content: AbiWriter
content.startTuple()
content.write(guaranteeType)
content.write(Abi.encode(assetOutcome.guarantee))
content.finishTuple()
var writer: AbiWriter
writer.startTuple()
writer.write(assetOutcome.assetHolder)
writer.write(content.finish())
writer.finishTuple()
check Abi.encode(assetOutcome) == writer.finish()
test "hashes outcomes":
let outcome = Outcome.example
let encoded = Abi.encode(outcome)
let hashed = keccak256.digest(encoded).data
check hashOutcome(outcome) == hashed

48
tests/nitro/testState.nim Normal file
View File

@ -0,0 +1,48 @@
import std/unittest
import pkg/nimcrypto
import pkg/nitro
import pkg/nitro/state
import pkg/nitro/abi
import ./examples
suite "state":
let state = State.example
test "has a fixed part":
check state.fixedPart == FixedPart(
chainId: state.channel.chainId,
participants: state.channel.participants,
channelNonce: state.channel.nonce,
appDefinition: state.appDefinition,
challengeDuration: state.challengeDuration
)
test "has a variable part":
check state.variablePart == VariablePart(
outcome: Abi.encode(state.outcome),
appData: state.appData
)
test "hashes app part of state":
var writer: AbiWriter
writer.write(state.challengeDuration)
writer.write(state.appDefinition)
writer.write(state.appData)
let encoded = writer.finish()
let hashed = keccak256.digest(encoded).data
check hashAppPart(state) == hashed
test "hashes state":
var writer: AbiWriter
writer.startTuple()
writer.write(state.turnNum)
writer.write(state.isFinal)
writer.write(getChannelId(state.channel))
writer.write(hashAppPart(state))
writer.write(hashOutcome(state.outcome))
writer.finishTuple()
let encoded = writer.finish()
let hashed = keccak256.digest(encoded).data
check hashState(state) == hashed

6
tests/testAll.nim Normal file
View File

@ -0,0 +1,6 @@
import ./nitro/testAbi
import ./nitro/testChannel
import ./nitro/testOutcome
import ./nitro/testState
{.warning[UnusedImport]: off.}