diff --git a/nimbus/db/state_db.nim b/nimbus/db/state_db.nim index 1f16d021d..146a86882 100644 --- a/nimbus/db/state_db.nim +++ b/nimbus/db/state_db.nim @@ -208,6 +208,10 @@ proc isDeadAccount*(db: AccountStateDB, address: EthAddress): bool = else: result = true +proc getCommittedStorage*(db: AccountStateDB, address: EthAddress, slot: UInt256): UInt256 = + discard + # TODO: stub + proc rootHash*(db: ReadOnlyStateDB): KeccakHash {.borrow.} proc getAccount*(db: ReadOnlyStateDB, address: EthAddress): Account {.borrow.} proc getCodeHash*(db: ReadOnlyStateDB, address: EthAddress): Hash256 {.borrow.} @@ -220,3 +224,4 @@ proc hasCodeOrNonce*(db: ReadOnlyStateDB, address: EthAddress): bool {.borrow.} proc accountExists*(db: ReadOnlyStateDB, address: EthAddress): bool {.borrow.} proc isDeadAccount*(db: ReadOnlyStateDB, address: EthAddress): bool {.borrow.} proc isEmptyAccount*(db: ReadOnlyStateDB, address: EthAddress): bool {.borrow.} +proc getCommittedStorage*(db: ReadOnlyStateDB, address: EthAddress, slot: UInt256): UInt256 {.borrow.} diff --git a/nimbus/p2p/executor.nim b/nimbus/p2p/executor.nim index 9ce778d2f..2bd729e70 100644 --- a/nimbus/p2p/executor.nim +++ b/nimbus/p2p/executor.nim @@ -113,7 +113,8 @@ const eth5, # FkTangerine eth5, # FkSpurious eth3, # FkByzantium - eth2 # FkConstantinople + eth2, # FkConstantinople + eth2 # FkIstanbul ] proc processBlock*(chainDB: BaseChainDB, header: BlockHeader, body: BlockBody, vmState: BaseVMState): ValidationResult = diff --git a/nimbus/transaction.nim b/nimbus/transaction.nim index 5c06d9f7c..b8a9549ce 100644 --- a/nimbus/transaction.nim +++ b/nimbus/transaction.nim @@ -12,19 +12,19 @@ import import eth/common/transaction as common_transaction export common_transaction -func intrinsicGas*(data: openarray[byte]): GasInt = - result = 21_000 # GasTransaction +func intrinsicGas*(data: openarray[byte], fork: Fork): GasInt = + result = gasFees[fork][GasTransaction] for i in data: if i == 0: - result += 4 # GasTXDataZero + result += gasFees[fork][GasTXDataZero] else: - result += 68 # GasTXDataNonZero + result += gasFees[fork][GasTXDataNonZero] proc intrinsicGas*(tx: Transaction, fork: Fork): GasInt = # Compute the baseline gas cost for this transaction. This is the amount # of gas needed to send this transaction (but that is not actually used # for computation) - result = tx.payload.intrinsicGas + result = tx.payload.intrinsicGas(fork) if tx.isContractCreation: result = result + gasFees[fork][GasTXCreate] diff --git a/nimbus/vm/blake2b_f.nim b/nimbus/vm/blake2b_f.nim new file mode 100644 index 000000000..a4c956922 --- /dev/null +++ b/nimbus/vm/blake2b_f.nim @@ -0,0 +1,141 @@ +import nimcrypto/utils + +# Blake2 `F` compression function +# taken from nimcrypto with modification + +# in nimcrypto, blake2 compression function `F` +# is hardcoded for blake2b and blake2s +# we need a generic `F` function with +# `rounds` parameter + +type + Blake2bContext = object + h: array[8, uint64] + t: array[2, uint64] + +const Sigma = [ + [0'u8, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15], + [14'u8, 10, 4, 8, 9, 15, 13, 6, 1, 12, 0, 2, 11, 7, 5, 3], + [11'u8, 8, 12, 0, 5, 2, 15, 13, 10, 14, 3, 6, 7, 1, 9, 4], + [7'u8, 9, 3, 1, 13, 12, 11, 14, 2, 6, 5, 10, 4, 0, 15, 8], + [9'u8, 0, 5, 7, 2, 4, 10, 15, 14, 1, 11, 12, 6, 8, 3, 13], + [2'u8, 12, 6, 10, 0, 11, 8, 3, 4, 13, 7, 5, 15, 14, 1, 9], + [12'u8, 5, 1, 15, 14, 13, 4, 10, 0, 7, 6, 3, 9, 2, 8, 11], + [13'u8, 11, 7, 14, 12, 1, 3, 9, 5, 0, 15, 4, 8, 6, 2, 10], + [6'u8, 15, 14, 9, 11, 3, 0, 8, 12, 2, 13, 7, 1, 4, 10, 5], + [10'u8, 2, 8, 4, 7, 6, 1, 5, 15, 11, 9, 14, 3, 12, 13, 0], + [0'u8, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15], + [14'u8, 10, 4, 8, 9, 15, 13, 6, 1, 12, 0, 2, 11, 7, 5, 3] +] + +const B2BIV = [ + 0x6A09E667F3BCC908'u64, 0xBB67AE8584CAA73B'u64, + 0x3C6EF372FE94F82B'u64, 0xA54FF53A5F1D36F1'u64, + 0x510E527FADE682D1'u64, 0x9B05688C2B3E6C1F'u64, + 0x1F83D9ABFB41BD6B'u64, 0x5BE0CD19137E2179'u64 +] + +template B2B_G(v, a, b, c, d, x, y: untyped) = + v[a] = v[a] + v[b] + x + v[d] = ROR(v[d] xor v[a], 32) + v[c] = v[c] + v[d] + v[b] = ROR(v[b] xor v[c], 24) + v[a] = v[a] + v[b] + y + v[d] = ROR(v[d] xor v[a], 16) + v[c] = v[c] + v[d] + v[b] = ROR(v[b] xor v[c], 63) + +template B2BROUND(v, m, n: untyped) = + B2B_G(v, 0, 4, 8, 12, m[Sigma[n][ 0]], m[Sigma[n][ 1]]) + B2B_G(v, 1, 5, 9, 13, m[Sigma[n][ 2]], m[Sigma[n][ 3]]) + B2B_G(v, 2, 6, 10, 14, m[Sigma[n][ 4]], m[Sigma[n][ 5]]) + B2B_G(v, 3, 7, 11, 15, m[Sigma[n][ 6]], m[Sigma[n][ 7]]) + B2B_G(v, 0, 5, 10, 15, m[Sigma[n][ 8]], m[Sigma[n][ 9]]) + B2B_G(v, 1, 6, 11, 12, m[Sigma[n][10]], m[Sigma[n][11]]) + B2B_G(v, 2, 7, 8, 13, m[Sigma[n][12]], m[Sigma[n][13]]) + B2B_G(v, 3, 4, 9, 14, m[Sigma[n][14]], m[Sigma[n][15]]) + +proc blake2Transform(ctx: var Blake2bContext, input: openArray[byte], last: bool, rounds: uint32) {.inline.} = + var v: array[16, uint64] + var m: array[16, uint64] + + v[0] = ctx.h[0]; v[1] = ctx.h[1] + v[2] = ctx.h[2]; v[3] = ctx.h[3] + v[4] = ctx.h[4]; v[5] = ctx.h[5] + v[6] = ctx.h[6]; v[7] = ctx.h[7] + v[8] = B2BIV[0]; v[9] = B2BIV[1] + v[10] = B2BIV[2]; v[11] = B2BIV[3] + v[12] = B2BIV[4]; v[13] = B2BIV[5] + v[14] = B2BIV[6]; v[15] = B2BIV[7] + + v[12] = v[12] xor ctx.t[0] + v[13] = v[13] xor ctx.t[1] + if last: + v[14] = not(v[14]) + + m[0] = leLoad64(input, 0); m[1] = leLoad64(input, 8) + m[2] = leLoad64(input, 16); m[3] = leLoad64(input, 24) + m[4] = leLoad64(input, 32); m[5] = leLoad64(input, 40) + m[6] = leLoad64(input, 48); m[7] = leLoad64(input, 56) + m[8] = leLoad64(input, 64); m[9] = leLoad64(input, 72) + m[10] = leLoad64(input, 80); m[11] = leLoad64(input, 88) + m[12] = leLoad64(input, 96); m[13] = leLoad64(input, 104) + m[14] = leLoad64(input, 112); m[15] = leLoad64(input, 120) + + for i in 0.. {value} ({currentValue})") + + if gasRefund > 0: + computation.gasMeter.refundGas(gasRefund) + + computation.vmState.mutateStateDB: + db.setStorage(computation.msg.storageAddress, slot, value) diff --git a/nimbus/vm/interpreter/vm_forks.nim b/nimbus/vm/interpreter/vm_forks.nim index 38303ed51..5b40ebb7d 100644 --- a/nimbus/vm/interpreter/vm_forks.nim +++ b/nimbus/vm/interpreter/vm_forks.nim @@ -16,7 +16,8 @@ type FkTangerine, FkSpurious, FkByzantium, - FkConstantinople + FkConstantinople, + FkIstanbul const forkBlocks*: array[Fork, BlockNumber] = [ @@ -27,7 +28,8 @@ const FkTangerine: 2_463_000.toBlockNumber, # 18/10/2016 17:19:31 FkSpurious: 2_675_000.toBlockNumber, # 22/11/2016 18:15:44 FkByzantium: 4_370_000.toBlockNumber, # 16/10/2017 09:22:11 - FkConstantinople: 7_280_000.toBlockNumber # 28/02/2019 07:52:04 + FkConstantinople: 7_280_000.toBlockNumber, # 28/02/2019 07:52:04 + FkIstanbul: 9_069_000.toBlockNumber ] proc toFork*(blockNumber: BlockNumber): Fork = @@ -47,7 +49,8 @@ proc toFork*(blockNumber: BlockNumber): Fork = elif blockNumber < forkBlocks[FkSpurious]: FkTangerine elif blockNumber < forkBlocks[FkByzantium]: FkSpurious elif blockNumber < forkBlocks[FkConstantinople]: FkByzantium - else: FkConstantinople + elif blockNumber < forkBlocks[FkIstanbul]: FkConstantinople + else: FkIstanbul proc `$`*(fork: Fork): string = case fork @@ -59,4 +62,4 @@ proc `$`*(fork: Fork): string = of FkSpurious: result = "Spurious Dragon" of FkByzantium: result = "Byzantium" of FkConstantinople: result = "Constantinople" - + of FkIstanbul: result = "Istanbul" diff --git a/nimbus/vm/interpreter_dispatch.nim b/nimbus/vm/interpreter_dispatch.nim index 857e400f6..3d3d2bece 100644 --- a/nimbus/vm/interpreter_dispatch.nim +++ b/nimbus/vm/interpreter_dispatch.nim @@ -215,6 +215,14 @@ proc genConstantinopleJumpTable(ops: array[Op, NimNode]): array[Op, NimNode] {.c let ConstantinopleOpDispatch {.compileTime.}: array[Op, NimNode] = genConstantinopleJumpTable(ByzantiumOpDispatch) +proc genIstanbulJumpTable(ops: array[Op, NimNode]): array[Op, NimNode] {.compileTime.} = + result = ops + result[ChainId] = newIdentNode "chainId" + result[SelfBalance] = newIdentNode "selfBalance" + result[SStore] = newIdentNode "sstoreEIP2200" + +let IstanbulOpDispatch {.compileTime.}: array[Op, NimNode] = genIstanbulJumpTable(ConstantinopleOpDispatch) + proc opTableToCaseStmt(opTable: array[Op, NimNode], computation: NimNode): NimNode = let instr = quote do: `computation`.instr @@ -288,6 +296,9 @@ macro genByzantiumDispatch(computation: BaseComputation): untyped = macro genConstantinopleDispatch(computation: BaseComputation): untyped = result = opTableToCaseStmt(ConstantinopleOpDispatch, computation) +macro genIstanbulDispatch(computation: BaseComputation): untyped = + result = opTableToCaseStmt(IstanbulOpDispatch, computation) + proc frontierVM(computation: BaseComputation) = genFrontierDispatch(computation) @@ -306,6 +317,9 @@ proc byzantiumVM(computation: BaseComputation) {.gcsafe.} = proc constantinopleVM(computation: BaseComputation) {.gcsafe.} = genConstantinopleDispatch(computation) +proc istanbulVM(computation: BaseComputation) {.gcsafe.} = + genIstanbulDispatch(computation) + proc selectVM(computation: BaseComputation, fork: Fork) {.gcsafe.} = # TODO: Optimise getting fork and updating opCodeExec only when necessary case fork @@ -317,10 +331,12 @@ proc selectVM(computation: BaseComputation, fork: Fork) {.gcsafe.} = computation.tangerineVM() of FkSpurious: computation.spuriousVM() - of FKByzantium: + of FkByzantium: computation.byzantiumVM() - else: + of FkConstantinople: computation.constantinopleVM() + else: + computation.istanbulVM() proc executeOpcodes(computation: BaseComputation) = let fork = computation.getFork diff --git a/nimbus/vm/precompiles.nim b/nimbus/vm/precompiles.nim index 90ab55243..818c75e76 100644 --- a/nimbus/vm/precompiles.nim +++ b/nimbus/vm/precompiles.nim @@ -1,7 +1,7 @@ import ../vm_types, interpreter/[gas_meter, gas_costs, utils/utils_numeric, vm_forks], ../errors, stint, eth/[keys, common], chronicles, tables, macros, - message, math, nimcrypto, bncurve/[fields, groups] + message, math, nimcrypto, bncurve/[fields, groups], blake2b_f type PrecompileAddresses* = enum @@ -10,11 +10,13 @@ type paSha256, paRipeMd160, paIdentity, - # Byzantium onward + # Byzantium and Constantinople paModExp, paEcAdd, paEcMul, - paPairing = 8 + paPairing, + # Istanbul + paBlake2bf = 9 proc getSignature(computation: BaseComputation): (array[32, byte], Signature) = # input is Hash, V, R, S @@ -235,8 +237,9 @@ proc modExp*(computation: BaseComputation) = else: raise newException(EVMError, "The Nimbus VM doesn't support modular exponentiation with numbers larger than uint8192") -proc bn256ecAdd*(computation: BaseComputation) = - computation.gasMeter.consumeGas(GasECAdd, reason = "ecAdd Precompile") +proc bn256ecAdd*(computation: BaseComputation, fork: Fork = FkByzantium) = + let gasFee = if fork < FkIstanbul: GasECAdd else: GasECAddIstanbul + computation.gasMeter.consumeGas(gasFee, reason = "ecAdd Precompile") var input: array[128, byte] @@ -255,8 +258,9 @@ proc bn256ecAdd*(computation: BaseComputation) = computation.rawOutput = @output -proc bn256ecMul*(computation: BaseComputation) = - computation.gasMeter.consumeGas(GasECMul, reason="ecMul Precompile") +proc bn256ecMul*(computation: BaseComputation, fork: Fork = FkByzantium) = + let gasFee = if fork < FkIstanbul: GasECMul else: GasECMulIstanbul + computation.gasMeter.consumeGas(gasFee, reason="ecMul Precompile") var input: array[96, byte] @@ -277,13 +281,16 @@ proc bn256ecMul*(computation: BaseComputation) = computation.rawOutput = @output -proc bn256ecPairing*(computation: BaseComputation) = +proc bn256ecPairing*(computation: BaseComputation, fork: Fork = FkByzantium) = let msglen = len(computation.msg.data) if msglen mod 192 != 0: raise newException(ValidationError, "Invalid input length") let numPoints = msglen div 192 - let gasFee = GasECPairingBase + numPoints * GasECPairingPerPoint + let gasFee = if fork < FkIstanbul: + GasECPairingBase + numPoints * GasECPairingPerPoint + else: + GasECPairingBaseIstanbul + numPoints * GasECPairingPerPointIstanbul computation.gasMeter.consumeGas(gasFee, reason="ecPairing Precompile") var output: array[32, byte] @@ -311,12 +318,31 @@ proc bn256ecPairing*(computation: BaseComputation) = computation.rawOutput = @output +proc blake2bf*(computation: BaseComputation) = + template input(): untyped = + computation.msg.data + + if len(input) == blake2FInputLength: + let gasFee = GasInt(beLoad32(input, 0)) + computation.gasMeter.consumeGas(gasFee, reason="ecPairing Precompile") + + var output: array[64, byte] + if not blake2b_F(input, output): + raise newException(ValidationError, "Blake2b F function invalid input") + else: + computation.rawOutput = @output + +proc getMaxPrecompileAddr(fork: Fork): PrecompileAddresses = + if fork < FkByzantium: paIdentity + elif fork < FkIstanbul: paPairing + else: PrecompileAddresses.high + proc execPrecompiles*(computation: BaseComputation, fork: Fork): bool {.inline.} = for i in 0..18: if computation.msg.codeAddress[i] != 0: return let lb = computation.msg.codeAddress[19] - let maxPrecompileAddr = if fork < FkByzantium: paIdentity else: PrecompileAddresses.high + let maxPrecompileAddr = getMaxPrecompileAddr(fork) if lb in PrecompileAddresses.low.byte .. maxPrecompileAddr.byte: result = true let precompile = PrecompileAddresses(lb) @@ -328,9 +354,10 @@ proc execPrecompiles*(computation: BaseComputation, fork: Fork): bool {.inline.} of paRipeMd160: ripeMd160(computation) of paIdentity: identity(computation) of paModExp: modExp(computation) - of paEcAdd: bn256ecAdd(computation) - of paEcMul: bn256ecMul(computation) - of paPairing: bn256ecPairing(computation) + of paEcAdd: bn256ecAdd(computation, fork) + of paEcMul: bn256ecMul(computation, fork) + of paPairing: bn256ecPairing(computation, fork) + of paBlake2bf: blake2bf(computation) except OutOfGas: let msg = getCurrentExceptionMsg() # cannot use setError here, cyclic dependency diff --git a/tests/test_precompiles.nim b/tests/test_precompiles.nim index 22ecc7c56..330b788e1 100644 --- a/tests/test_precompiles.nim +++ b/tests/test_precompiles.nim @@ -8,7 +8,8 @@ import unittest2, ../nimbus/vm/precompiles, json, stew/byteutils, test_helpers, ospaths, tables, strformat, strutils, eth/trie/db, eth/common, ../nimbus/db/[db_chain, state_db], - ../nimbus/[constants, vm_types, vm_state], ../nimbus/vm/[computation, message], macros + ../nimbus/[constants, vm_types, vm_state], ../nimbus/vm/[computation, message], macros, + ../nimbus/vm/blake2b_f proc initAddress(i: byte): EthAddress = result[19] = i @@ -51,6 +52,71 @@ proc testFixture(fixtures: JsonNode, testStatusIMPL: var TestStatus) = #raise newException(ValueError, "Unknown test vector '" & $label & "'") echo "Unknown test vector '" & $label & "'" +const blake2InputTests = [ + ( + input: "", + expected: "error", + name: "vector 0: empty input", + ), + ( + input: "00000c48c9bdf267e6096a3ba7ca8485ae67bb2bf894fe72f36e3cf1361d5f3af54fa5d182e6ad7f520e511f6c3e2b8c68059b6bbd41fbabd9831f79217e1319cde05b61626300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000300000000000000000000000000000001", + expected: "error", + name: "vector 1: less than 213 bytes input", + ), + ( + input: "000000000c48c9bdf267e6096a3ba7ca8485ae67bb2bf894fe72f36e3cf1361d5f3af54fa5d182e6ad7f520e511f6c3e2b8c68059b6bbd41fbabd9831f79217e1319cde05b61626300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000300000000000000000000000000000001", + expected: "error", + name: "vector 2: more than 213 bytes input", + ), + ( + input: "0000000c48c9bdf267e6096a3ba7ca8485ae67bb2bf894fe72f36e3cf1361d5f3af54fa5d182e6ad7f520e511f6c3e2b8c68059b6bbd41fbabd9831f79217e1319cde05b61626300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000300000000000000000000000000000002", + expected: "error", + name: "vector 3: malformed final block indicator flag", + ), + ( + input: "0000000048c9bdf267e6096a3ba7ca8485ae67bb2bf894fe72f36e3cf1361d5f3af54fa5d182e6ad7f520e511f6c3e2b8c68059b6bbd41fbabd9831f79217e1319cde05b61626300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000300000000000000000000000000000001", + expected: "08c9bcf367e6096a3ba7ca8485ae67bb2bf894fe72f36e3cf1361d5f3af54fa5d282e6ad7f520e511f6c3e2b8c68059b9442be0454267ce079217e1319cde05b", + name: "vector 4", + ), + ( + input: "0000000c48c9bdf267e6096a3ba7ca8485ae67bb2bf894fe72f36e3cf1361d5f3af54fa5d182e6ad7f520e511f6c3e2b8c68059b6bbd41fbabd9831f79217e1319cde05b61626300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000300000000000000000000000000000001", + expected: "ba80a53f981c4d0d6a2797b69f12f6e94c212f14685ac4b74b12bb6fdbffa2d17d87c5392aab792dc252d5de4533cc9518d38aa8dbf1925ab92386edd4009923", + name: "vector 5", + ), + ( + input: "0000000c48c9bdf267e6096a3ba7ca8485ae67bb2bf894fe72f36e3cf1361d5f3af54fa5d182e6ad7f520e511f6c3e2b8c68059b6bbd41fbabd9831f79217e1319cde05b61626300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000", + expected: "75ab69d3190a562c51aef8d88f1c2775876944407270c42c9844252c26d2875298743e7f6d5ea2f2d3e8d226039cd31b4e426ac4f2d3d666a610c2116fde4735", + name: "vector 6", + ), + ( + input: "0000000148c9bdf267e6096a3ba7ca8485ae67bb2bf894fe72f36e3cf1361d5f3af54fa5d182e6ad7f520e511f6c3e2b8c68059b6bbd41fbabd9831f79217e1319cde05b61626300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000300000000000000000000000000000001", + expected: "b63a380cb2897d521994a85234ee2c181b5f844d2c624c002677e9703449d2fba551b3a8333bcdf5f2f7e08993d53923de3d64fcc68c034e717b9293fed7a421", + name: "vector 7", + ), + ( + input: "007A120048c9bdf267e6096a3ba7ca8485ae67bb2bf894fe72f36e3cf1361d5f3af54fa5d182e6ad7f520e511f6c3e2b8c68059b6bbd41fbabd9831f79217e1319cde05b61626300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000300000000000000000000000000000001", + expected: "6d2ce9e534d50e18ff866ae92d70cceba79bbcd14c63819fe48752c8aca87a4bb7dcc230d22a4047f0486cfcfb50a17b24b2899eb8fca370f22240adb5170189", + name: "vector 8", + ), +] + proc precompilesMain*() = suite "Precompiles": jsonTest("PrecompileTests", testFixture) + + suite "blake2bf": + var output: array[64, byte] + var expectedOutput: array[64, byte] + for x in blake2InputTests: + test x.name: + let z = if x.input.len == 0: @[] else: hexToSeqByte(x.input) + let res = blake2b_F(z, output) + if x.expected == "error": + check res == false + else: + hexToByteArray(x.expected, expectedOutput) + check res == true + check expectedOutput == output + +when isMainModule: + precompilesMain()