Fix ABI encoding of Nitro state to match javascript implementation

- rewrite ABI encoding to properly handle dynamic tuples
- surround Nitro types with extra tuples to match javascript
- test with examples of encoding extracted from javascript
This commit is contained in:
Mark Spanbroek 2021-02-25 09:11:46 +01:00
parent 3867258fe6
commit 34072f4749
10 changed files with 382 additions and 261 deletions

View File

@ -3,104 +3,124 @@ import pkg/stint
import ./types
type
Abi* = object
AbiWriter* = object
bytes: seq[byte]
tuples: seq[Tuple]
AbiEncoder* = object
stack: seq[Tuple]
Tuple = object
start: int
bytes: seq[byte]
postponed: seq[Split]
dynamic: bool
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 write*[T](encoder: var AbiEncoder, value: T)
proc encode*[T](_: type AbiEncoder, value: T): seq[byte]
proc encode*[T](_: type Abi, value: T): seq[byte]
proc init*(_: type AbiEncoder): AbiEncoder =
AbiEncoder(stack: @[Tuple()])
proc pad(writer: var AbiWriter, len: int) =
proc append(tupl: var Tuple, bytes: openArray[byte]) =
tupl.bytes.add(bytes)
proc 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)
proc 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
proc append(encoder: var AbiEncoder, bytes: openArray[byte]) =
encoder.stack[^1].append(bytes)
proc postpone(encoder: var AbiEncoder, bytes: seq[byte]) =
if encoder.stack.len > 1:
encoder.stack[^1].postpone(bytes)
else:
encoder.stack[0].append(bytes)
proc setDynamic(encoder: var AbiEncoder) =
encoder.stack[^1].dynamic = true
proc startTuple*(encoder: var AbiEncoder) =
encoder.stack.add(Tuple())
proc encode(encoder: var AbiEncoder, tupl: Tuple) =
if tupl.dynamic:
encoder.postpone(tupl.finish())
encoder.setDynamic()
else:
encoder.append(tupl.finish())
proc finishTuple*(encoder: var AbiEncoder) =
encoder.encode(encoder.stack.pop())
proc pad(encoder: var AbiEncoder, len: int) =
let padlen = (32 - len mod 32) mod 32
for _ in 0..<padlen:
writer.bytes.add(0'u8)
encoder.append([0'u8])
proc padleft(writer: var AbiWriter, bytes: openArray[byte]) =
writer.pad(bytes.len)
writer.bytes.add(bytes)
proc padleft(encoder: var AbiEncoder, bytes: openArray[byte]) =
encoder.pad(bytes.len)
encoder.append(bytes)
proc padright(writer: var AbiWriter, bytes: openArray[byte]) =
writer.bytes.add(bytes)
writer.pad(bytes.len)
proc padright(encoder: var AbiEncoder, bytes: openArray[byte]) =
encoder.append(bytes)
encoder.pad(bytes.len)
proc write*(writer: var AbiWriter, value: SomeUnsignedInt | StUint) =
writer.padleft(value.toBytesBE)
proc encode(encoder: var AbiEncoder, value: SomeUnsignedInt | StUint) =
encoder.padleft(value.toBytesBE)
proc write*(writer: var AbiWriter, value: bool) =
writer.write(cast[uint8](value))
proc encode(encoder: var AbiEncoder, value: bool) =
encoder.encode(cast[uint8](value))
proc write*(writer: var AbiWriter, value: enum) =
writer.write(uint64(ord(value)))
proc encode(encoder: var AbiEncoder, value: enum) =
encoder.encode(uint64(ord(value)))
proc write*[I](writer: var AbiWriter, bytes: array[I, byte]) =
writer.padright(bytes)
proc encode[I](encoder: var AbiEncoder, bytes: array[I, byte]) =
encoder.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 encode(encoder: var AbiEncoder, bytes: seq[byte]) =
encoder.encode(bytes.len.uint64)
encoder.padright(bytes)
encoder.setDynamic()
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 encode(encoder: var AbiEncoder, address: EthAddress) =
encoder.padleft(address.toArray)
proc write*(writer: var AbiWriter, address: EthAddress) =
writer.padleft(address.toArray)
proc encode[I, T](encoder: var AbiEncoder, value: array[I, T]) =
encoder.startTuple()
for element in value:
encoder.write(element)
encoder.finishTuple()
proc startTuple*(writer: var AbiWriter) =
writer.tuples.add(Tuple(start: writer.bytes.len))
proc 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()
proc finishTuple*(writer: var AbiWriter) =
let tupl = writer.tuples.pop()
for split in tupl.postponed:
let offset = writer.bytes.len - tupl.start
writer.bytes[split.head] = Abi.encode(offset.uint64)
writer.bytes.add(split.tail)
proc write*[T](encoder: var AbiEncoder, value: T) =
var writer = AbiEncoder.init()
writer.encode(value)
encoder.encode(writer.stack[0])
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 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
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()
proc encode*[T](_: type AbiEncoder, value: T): seq[byte] =
var encoder = AbiEncoder.init()
encoder.write(value)
encoder.finish()

View File

@ -11,10 +11,10 @@ type
chainId*: UInt256
proc getChannelId*(channel: Channel): array[32, byte] =
var writer: AbiWriter
writer.startTuple()
writer.write(channel.chainId)
writer.write(channel.participants)
writer.write(channel.nonce)
writer.finishTuple()
keccak256.digest(writer.finish()).data
var encoder= AbiEncoder.init()
encoder.startTuple()
encoder.write(channel.chainId)
encoder.write(channel.participants)
encoder.write(channel.nonce)
encoder.finishTuple()
keccak256.digest(encoder.finish()).data

View File

@ -17,7 +17,7 @@ type
allocation*: Allocation
of guaranteeType:
guarantee*: Guarantee
Allocation* = seq[AllocationItem]
Allocation* = distinct seq[AllocationItem]
AllocationItem* = object
destination*: array[32, byte]
amount*: UInt256
@ -25,41 +25,47 @@ type
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 encode*(encoder: var AbiEncoder, guarantee: Guarantee) =
encoder.startTuple()
encoder.startTuple()
encoder.write(guarantee.targetChannelId)
encoder.write(guarantee.destinations)
encoder.finishTuple()
encoder.finishTuple()
proc write*(writer: var AbiWriter, guarantee: Guarantee) =
writer.startTuple()
writer.write(guarantee.targetChannelId)
writer.write(guarantee.destinations)
writer.finishTuple()
proc encode*(encoder: var AbiEncoder, item: AllocationItem) =
encoder.startTuple()
encoder.write(item.destination)
encoder.write(item.amount)
encoder.finishTuple()
proc write*(writer: var AbiWriter, item: AllocationItem) =
writer.startTuple()
writer.write(item.destination)
writer.write(item.amount)
writer.finishTuple()
proc encode*(encoder: var AbiEncoder, allocation: Allocation) =
encoder.startTuple()
encoder.write(seq[AllocationItem](allocation))
encoder.finishTuple()
proc write*(writer: var AbiWriter, assetOutcome: AssetOutcome) =
var content: AbiWriter
proc encode*(encoder: var AbiEncoder, assetOutcome: AssetOutcome) =
var content= AbiEncoder.init()
content.startTuple()
content.startTuple()
content.write(assetOutcome.kind)
case assetOutcome.kind:
of allocationType:
content.write(Abi.encode(assetOutcome.allocation))
content.write(AbiEncoder.encode(assetOutcome.allocation))
of guaranteeType:
content.write(Abi.encode(assetOutcome.guarantee))
content.write(AbiEncoder.encode(assetOutcome.guarantee))
content.finishTuple()
content.finishTuple()
writer.startTuple()
writer.write(assetOutcome.assetHolder)
writer.write(content.finish())
writer.finishTuple()
proc write*(writer: var AbiWriter, outcome: Outcome) =
writer.startTuple()
writer.write(seq[AssetOutcome](outcome))
writer.finishTuple()
encoder.startTuple()
encoder.write(assetOutcome.assetHolder)
encoder.write(content.finish())
encoder.finishTuple()
proc encode*(encoder: var AbiEncoder, outcome: Outcome) =
encoder.startTuple()
encoder.write(seq[AssetOutcome](outcome))
encoder.finishTuple()
proc hashOutcome*(outcome: Outcome): array[32, byte] =
keccak256.digest(Abi.encode(outcome)).data
keccak256.digest(AbiEncoder.encode(outcome)).data

View File

@ -38,26 +38,26 @@ proc fixedPart*(state: State): FixedPart =
proc variablePart*(state: State): VariablePart =
VariablePart(
outcome: Abi.encode(state.outcome),
outcome: AbiEncoder.encode(state.outcome),
appData: state.appData
)
proc hashAppPart*(state: State): array[32, byte] =
var writer: AbiWriter
writer.startTuple()
writer.write(state.challengeDuration)
writer.write(state.appDefinition)
writer.write(state.appData)
writer.finishTuple()
keccak256.digest(writer.finish).data
var encoder= AbiEncoder.init()
encoder.startTuple()
encoder.write(state.challengeDuration)
encoder.write(state.appDefinition)
encoder.write(state.appData)
encoder.finishTuple()
keccak256.digest(encoder.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
var encoder= AbiEncoder.init()
encoder.startTuple()
encoder.write(state.turnNum)
encoder.write(state.isFinal)
encoder.write(getChannelId(state.channel))
encoder.write(hashAppPart(state))
encoder.write(hashOutcome(state.outcome))
encoder.finishTuple()
keccak256.digest(encoder.finish).data

View File

@ -1,5 +1,6 @@
import std/math
import pkg/stint
import pkg/stew/byteutils
export stint
@ -10,4 +11,7 @@ type
proc toArray*(address: EthAddress): array[20, byte] =
array[20, byte](address)
proc fromHex*(_: type EthAddress, hex: string): EthAddress =
EthAddress(array[20, byte].fromHex(hex))
proc `==`*(a, b: EthAddress): bool {.borrow.}

View File

@ -46,6 +46,9 @@ proc example*(_: type Guarantee): Guarantee =
destinations: seq[array[32, byte]].example
)
proc example*(_: type Allocation): Allocation =
Allocation(seq[AllocationItem].example)
proc example*(_: type AssetOutcome): AssetOutcome =
let kind = rand(AssetOutcomeType.low..AssetOutcomeType.high)
case kind:

View File

@ -10,152 +10,153 @@ suite "ABI encoding":
newSeq[byte](amount)
test "encodes uint8":
check Abi.encode(42'u8) == 31.zeroes & 42'u8
check AbiEncoder.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
check AbiEncoder.encode(false) == 31.zeroes & 0'u8
check AbiEncoder.encode(true) == 31.zeroes & 1'u8
test "encodes uint16, 32, 64":
check Abi.encode(0xABCD'u16) ==
check AbiEncoder.encode(0xABCD'u16) ==
30.zeroes & 0xAB'u8 & 0xCD'u8
check Abi.encode(0x11223344'u32) ==
check AbiEncoder.encode(0x11223344'u32) ==
28.zeroes & 0x11'u8 & 0x22'u8 & 0x33'u8 & 0x44'u8
check Abi.encode(0x1122334455667788'u64) ==
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 Abi.encode(SomeRange(0x1122)) == 30.zeroes & 0x11'u8 & 0x22'u8
check AbiEncoder.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
check AbiEncoder.encode(one) == 31.zeroes & 1'u8
check AbiEncoder.encode(two) == 31.zeroes & 2'u8
test "encodes stints":
let uint256 = UInt256.example
check Abi.encode(uint256) == @(uint256.toBytesBE)
check AbiEncoder.encode(uint256) == @(uint256.toBytesBE)
let uint128 = UInt128.example
check Abi.encode(uint128) == 16.zeroes & @(uint128.toBytesBE)
check AbiEncoder.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
check AbiEncoder.encode(bytes3) == @bytes3 & 29.zeroes
let bytes32 = array[32, byte].example
check Abi.encode(bytes32) == @bytes32
check AbiEncoder.encode(bytes32) == @bytes32
let bytes33 = array[33, byte].example
check Abi.encode(bytes33) == @bytes33 & 31.zeroes
check AbiEncoder.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 bytes3len = AbiEncoder.encode(bytes3.len.uint64)
check AbiEncoder.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 bytes32len = AbiEncoder.encode(bytes32.len.uint64)
check AbiEncoder.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
let bytes33len = AbiEncoder.encode(bytes33.len.uint64)
check AbiEncoder.encode(bytes33) == bytes33len & bytes33 & 31.zeroes
test "encodes ethereum addresses":
let address = EthAddress.example
check Abi.encode(address) == 12.zeroes & @(address.toArray)
check AbiEncoder.encode(address) == 12.zeroes & @(address.toArray)
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(4 * 32'u8) & # offset from start of tuple
Abi.encode(c) &
Abi.encode(6 * 32'u8) & # offset from start of tuple
Abi.encode(b) &
Abi.encode(d)
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 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(6 * 32'u8) & # offset from start of tuple
Abi.encode(c) &
Abi.encode(2 * 32'u8) & # offset from start of tuple
Abi.encode(d) &
Abi.encode(b)
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: AbiWriter
var expected= AbiEncoder.init()
expected.startTuple()
expected.write(element1)
expected.write(element2)
expected.finishTuple()
check Abi.encode([element1, element2]) == expected.finish()
check AbiEncoder.encode([element1, element2]) == expected.finish()
test "encodes sequences":
let element1 = seq[byte].example
let element2 = seq[byte].example
var expected: AbiWriter
var expected= AbiEncoder.init()
expected.write(2'u8)
expected.startTuple()
expected.write(element1)
expected.write(element2)
expected.finishTuple()
check Abi.encode(@[element1, element2]) == expected.finish()
check AbiEncoder.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 from start of tuple
Abi.encode(s)
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 writer: AbiWriter
writer.startTuple()
writer.write(a)
writer.finishTuple()
check writer.finish() == Abi.encode(a)
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 writer: AbiWriter
writer.startTuple()
writer.write(a)
writer.finishTuple()
check writer.finish() ==
Abi.encode(32'u8) & # offset from start of tuple
Abi.encode(a)
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

@ -2,6 +2,7 @@ import std/unittest
import pkg/nitro/channel
import pkg/nitro/abi
import pkg/nimcrypto
import pkg/stew/byteutils
import ./examples
suite "channel":
@ -9,13 +10,25 @@ suite "channel":
let channel = Channel.example
test "calculates channel id":
var writer: AbiWriter
writer.startTuple()
writer.write(channel.chainId)
writer.write(channel.participants)
writer.write(channel.nonce)
writer.finishTuple()
let encoded = writer.finish()
var encoder= AbiEncoder.init()
encoder.startTuple()
encoder.write(channel.chainId)
encoder.write(channel.participants)
encoder.write(channel.nonce)
encoder.finishTuple()
let encoded = encoder.finish()
let hashed = keccak256.digest(encoded).data
check getChannelId(channel) == hashed
test "produces same id as javascript implementation":
let channel = Channel(
chainId: 9001.u256,
nonce: 1,
participants: @[
EthAddress.fromHex("24b905Dcc8A11C0FE57C2592f3D25f0447402C10")
]
)
let expected = array[32, byte].fromHex(
"4f8cce57e9fe88edaab05234972eaf0c2d183e4f6b175aff293375fbe4d5d7cc"
)
check getChannelId(channel) == expected

View File

@ -1,5 +1,6 @@
import std/unittest
import pkg/nimcrypto
import pkg/stew/byteutils
import pkg/nitro/outcome
import ./examples
@ -7,21 +8,31 @@ 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()
var encoder= AbiEncoder.init()
encoder.startTuple()
encoder.startTuple()
encoder.write(guarantee.targetChannelId)
encoder.write(guarantee.destinations)
encoder.finishTuple()
encoder.finishTuple()
check AbiEncoder.encode(guarantee) == encoder.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()
var encoder= AbiEncoder.init()
encoder.startTuple()
encoder.write(item.destination)
encoder.write(item.amount)
encoder.finishTuple()
check AbiEncoder.encode(item) == encoder.finish()
test "encodes allocation":
let allocation = Allocation.example
var encoder= AbiEncoder.init()
encoder.startTuple()
encoder.write(seq[AllocationItem](allocation))
encoder.finishTuple()
check AbiEncoder.encode(allocation) == encoder.finish()
test "encodes allocation outcome":
let assetOutcome = AssetOutcome(
@ -29,17 +40,19 @@ suite "outcome":
assetHolder: EthAddress.example,
allocation: Allocation.example
)
var content: AbiWriter
var content= AbiEncoder.init()
content.startTuple()
content.startTuple()
content.write(allocationType)
content.write(Abi.encode(assetOutcome.allocation))
content.write(AbiEncoder.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()
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":
let assetOutcome = AssetOutcome(
@ -47,28 +60,68 @@ suite "outcome":
assetHolder: EthAddress.example,
guarantee: Guarantee.example
)
var content: AbiWriter
var content= AbiEncoder.init()
content.startTuple()
content.startTuple()
content.write(guaranteeType)
content.write(Abi.encode(assetOutcome.guarantee))
content.write(AbiEncoder.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()
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":
let outcome = Outcome.example()
var writer: AbiWriter
writer.startTuple()
writer.write(seq[AssetOutcome](outcome))
writer.finishTuple()
check Abi.encode(outcome) == writer.finish()
var encoder= AbiEncoder.init()
encoder.startTuple()
encoder.write(seq[AssetOutcome](outcome))
encoder.finishTuple()
check AbiEncoder.encode(outcome) == encoder.finish()
test "hashes outcomes":
let outcome = Outcome.example
let encoded = Abi.encode(outcome)
let encoded = AbiEncoder.encode(outcome)
let hashed = keccak256.digest(encoded).data
check hashOutcome(outcome) == hashed
test "produces the same encoding as the javascript implementation":
let outcome = Outcome(@[
AssetOutcome(
kind: allocationType,
assetHolder: EthAddress.fromHex(
"1E90B49563da16D2537CA1Ddd9b1285279103D93"
),
allocation: Allocation(@[
AllocationItem(
destination: array[32, byte].fromHex(
"f1918e8562236eb17adc8502332f4c9c82bc14e19bfc0aa10ab674ff75b3d2f3"
),
amount: 0x05.u256
)
])
),
AssetOutcome(
kind: guaranteeType,
assetHolder: EthAddress.fromHex(
"1E90B49563da16D2537CA1Ddd9b1285279103D93"
),
guarantee: Guarantee(
targetChannelId: array[32, byte].fromHex(
"cac1bb71f0a97c8ac94ca9546b43178a9ad254c7b757ac07433aa6df35cd8089"
),
destinations: @[
array[32, byte].fromHex(
"f1918e8562236eb17adc8502332f4c9c82bc14e19bfc0aa10ab674ff75b3d2f3"
)
]
)
)
])
let expected = fromHex(
"53993a1bc1de832c2e04bd59491a18d43b6546ec5c611f13dc5dc56d678d228d"
)
check hashOutcome(outcome) == expected

View File

@ -1,5 +1,6 @@
import std/unittest
import pkg/nimcrypto
import pkg/stew/byteutils
import pkg/nitro
import pkg/nitro/state
import pkg/nitro/abi
@ -20,31 +21,51 @@ suite "state":
test "has a variable part":
check state.variablePart == VariablePart(
outcome: Abi.encode(state.outcome),
outcome: AbiEncoder.encode(state.outcome),
appData: state.appData
)
test "hashes app part of state":
var writer: AbiWriter
writer.startTuple()
writer.write(state.challengeDuration)
writer.write(state.appDefinition)
writer.write(state.appData)
writer.finishTuple()
let encoded = writer.finish()
var encoder= AbiEncoder.init()
encoder.startTuple()
encoder.write(state.challengeDuration)
encoder.write(state.appDefinition)
encoder.write(state.appData)
encoder.finishTuple()
let encoded = encoder.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()
var encoder= AbiEncoder.init()
encoder.startTuple()
encoder.write(state.turnNum)
encoder.write(state.isFinal)
encoder.write(getChannelId(state.channel))
encoder.write(hashAppPart(state))
encoder.write(hashOutcome(state.outcome))
encoder.finishTuple()
let encoded = encoder.finish()
let hashed = keccak256.digest(encoded).data
check hashState(state) == hashed
test "produces the same hash as the javascript implementation":
let state = State(
channel: Channel(
chainId: 0x1.u256,
nonce: 1,
participants: @[
EthAddress.fromHex("DBE821484648c73C1996Da25f2355342B9803eBD")
]
),
outcome: Outcome(@[]),
turnNum: 1,
isFinal: false,
appData: @[0'u8],
appDefinition: EthAddress.default,
challengeDuration: 5
)
let expected = array[32, byte].fromHex(
"8f515b04e6120bffadc159b5e117297bb7c135337d4ec9c0468bcf298292f46d"
)
check hashState(state) == expected