Serialization and Ed25519 (#2)

finally...

Reviewed-on: #2
Co-authored-by: Craig Everett <zxq9@zxq9.com>
Co-committed-by: Craig Everett <zxq9@zxq9.com>
This commit was merged in pull request #2.
This commit is contained in:
2026-08-20 21:19:16 +09:00
committed by zxq9
parent ea90d9d3ab
commit 4019af58e9
24 changed files with 2532 additions and 536 deletions
+3
View File
@@ -6,9 +6,12 @@ Thumbs.db
.netrwhist
.nvimlog
.idea/
.artifacts/
*.iml
.gradle/
local.properties
Captures/
.externalNativeBuild/
temp
erl_crash.dump
*.class
+26
View File
@@ -2,6 +2,32 @@
Java libraries for the Gajumaru
## Purpose
gm-java provides the basic functionality required to interact with the [Gajumaru](https://gajumaru./io)
blockchain system. This includes serialization, function wrappers for Gajumaru node endpoint calls,
handling of GRIDS URLs, formatting of monetary values, any cryptographic functions not covered in
standard Java libraries, and some network functionality necessary to interact smoothly with
Gajumaru chains (Groot and AC networks).
This codebase may *not* include references to third-party libraries, may *not* permit un-zeroed
byte values to be left on the heap until the JVM eventually decides to garbage collect them,
and may *not* build up a big, gnarly, inheritance-heavy OOPsy object hierarchy.
Classes should generally be structured as `final` classes of functions that operate over data
that can be expressed as Java primitives, most commonly `byte[]` and some form of `int` or
occasionally `BigInteger`.
`BigInteger` is not permitted in cryptographic operations, as it leaves potentially sensitive
artifacts scattered all throughout the heap and can occasionally make the garbage collector
go bananas when performing heavy math operations. `BigInteger` is excellent, however, for things
like representing currency values in Pucks.
All forms of floating-point arithmetic are forbidden in all currency calculations. The Gajumaru
does not have floats, and they do not exist at all in the Sophia language.
## Terms
Copyright (c) 2026 QPQ AG <info@qpq.swiss>. All rights reserved.
+2 -3
View File
@@ -14,11 +14,10 @@ rm -f "$project_dir/Testinator.class"
# Build every .java file
find "$project_dir/src/main/java" -name "*.java" | xargs javac \
-source 21 \
-target 21 \
--release 21 \
-d "$project_dir/build/classes"
# Build the Testinator thingy
javac -cp "build/classes" -d build src/Testinator.java
javac -cp "build/classes" -d build test/Testinator.java
echo "Bytecode in $project_dir/build/classes/"
View File
-172
View File
@@ -1,172 +0,0 @@
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Base64;
import java.util.ArrayList;
import java.util.List;
import java.math.BigInteger;
import swiss.qpq.gajumaru.core.encoding.Base58;
import swiss.qpq.gajumaru.core.encoding.RLP;
import swiss.qpq.gajumaru.core.formatting.GajuFormat;
public class Testinator {
public static void main(String[] args) {
if (args.length != 2) {
System.out.println("Error: Provide the test suite name and the working dir path.");
System.exit(1);
}
try {
switch (args[0]) {
case "base64" -> {
System.out.print(base64(args[1]));
}
case "base58" -> {
System.out.print(base58(args[1]));
}
case "base58_check" -> {
System.out.print(base58_check(args[1]));
}
case "rlp" -> {
System.out.print(rlp(args[1]));
}
case "rlp_stream" -> {
System.out.print(rlp_stream(args[1]));
}
case "rlp_fail" -> {
System.out.print(rlp_fail(args[1]));
}
case "gaju_format" -> {
System.out.print(gaju_format(args[1]));
}
}
} catch (Exception e) {
System.err.println("Error: " + e.getMessage());
System.exit(1);
}
}
private static String base64(String workingPath) throws IOException {
Path testPath = Path.of(workingPath, "base64.test");
Path encPath = Path.of(workingPath, "base64.java.txt");
Path decPath = Path.of(workingPath, "base64.java.back");
byte[] rawB = Files.readAllBytes(testPath);
byte[] encB = Base64.getEncoder().encode(rawB);
Files.write(encPath, encB);
byte[] readE = Files.readAllBytes(encPath);
byte[] decB = Base64.getDecoder().decode(readE);
Files.write(decPath, decB);
return encPath.toString() + " " + decPath.toString();
}
private static String base58(String workingPath) throws IOException {
Path testPath = Path.of(workingPath, "base58.test");
Path encPath = Path.of(workingPath, "base58.java.txt");
Path decPath = Path.of(workingPath, "base58.java.back");
byte[] rawB = Files.readAllBytes(testPath);
String encS = Base58.encode(rawB);
Files.write(encPath, encS.getBytes());
byte[] readE = Files.readAllBytes(encPath);
String readS = new String(readE);
byte[] decB = Base58.decode(readS);
Files.write(decPath, decB);
return encPath.toString() + " " + decPath.toString();
}
private static String base58_check(String workingPath) throws IOException {
Path testPath = Path.of(workingPath, "base58_check.test");
Path encPath = Path.of(workingPath, "base58_check.java.txt");
Path decPath = Path.of(workingPath, "base58_check.java.back");
byte[] rawB = Files.readAllBytes(testPath);
String encS = Base58.checkEncode(rawB);
Files.write(encPath, encS.getBytes());
byte[] readE = Files.readAllBytes(encPath);
String readS = new String(readE);
byte[] decB = Base58.checkDecode(readS);
Files.write(decPath, decB);
return encPath.toString() + " " + decPath.toString();
}
private static String rlp(String workingPath) throws IOException {
Path testPath = Path.of(workingPath, "rlp.test");
Path resPath = Path.of(workingPath, "rlp.java.back");
byte[] encB = Files.readAllBytes(testPath);
RLP.RLP_Data decRLP = RLP.decode(encB);
RLP.RLP_List root = decRLP.asList();
RLP.RLP_List sub = root.getItems().get(1).asList();
byte[] anchor = sub.getItems().get(1).asItem().getBytes();
byte[] reEnc = RLP.encode(decRLP);
Files.write(resPath, reEnc);
return new String(anchor) + "|" + resPath.toString();
}
private static String rlp_stream(String workingPath) throws IOException {
Path testPath = Path.of(workingPath, "rlp_stream.test");
byte[] buffer = Files.readAllBytes(testPath);
List<String> hashes = new ArrayList<>();
int offset = 0;
while (offset < buffer.length) {
RLP.DecodeResult res = RLP.decodeFrom(buffer, offset);
hashes.add(Integer.toString(res.data().hashCode())); // Just to verify we got objects
offset += res.consumed();
}
return Integer.toString(hashes.size());
}
private static String rlp_fail(String workingPath) throws IOException {
Path testPath = Path.of(workingPath, "rlp_fail.test");
byte[] buffer = Files.readAllBytes(testPath);
try {
RLP.decode(buffer);
return "SUCCESS";
} catch (RLP.RLPException e) {
return "RLP_EXCEPTION: " + e.getMessage();
} catch (Exception e) {
return "OTHER_EXCEPTION: " + e.getClass().getSimpleName();
}
}
private static String gaju_format(String workingPath) throws IOException {
Path testPath = Path.of(workingPath, "gaju_format.test");
Path resPath = Path.of(workingPath, "gaju_format.java.txt");
List<String> lines = Files.readAllLines(testPath);
List<String> results = new ArrayList<>();
for (String line : lines) {
if (line.trim().isEmpty()) continue;
String[] p = line.split("\\|");
String op = p[0];
switch (op) {
case "amount" -> {
GajuFormat.Type style = GajuFormat.Type.valueOf(p[1]);
GajuFormat.Unit unit = GajuFormat.Unit.valueOf(p[2]);
char sep = p[3].charAt(0);
int span = Integer.parseInt(p[4]);
BigInteger val = new BigInteger(p[5]);
results.add(GajuFormat.amount(new GajuFormat.FormatSpec(style, unit, sep, span), val));
}
case "approx" -> {
GajuFormat.Type style = GajuFormat.Type.valueOf(p[1]);
GajuFormat.Unit unit = GajuFormat.Unit.valueOf(p[2]);
char sep = p[3].charAt(0);
int span = Integer.parseInt(p[4]);
BigInteger val = new BigInteger(p[5]);
int prec = Integer.parseInt(p[6]);
results.add(GajuFormat.approxAmount(new GajuFormat.FormatSpec(style, unit, sep, span), val, prec));
}
case "read" -> {
byte[] b = GajuFormat.read(p[1]);
results.add(new BigInteger(b).toString());
}
}
}
Files.write(resPath, results);
return resPath.toString();
}
}
-24
View File
@@ -1,24 +0,0 @@
# GM Java <-> Erlang Tests
The core Java libraries are all written as transform functions over data.
There is little point, therefore, in obsessing over "unit tests" and "regression testing" of Java code in Java when we have cannonical code in Erlang.
The purpose of this utility is to instead test the Java libraries agains the Erlang libraries directly.
## How to run
In the current directory simply run `zx runlocal` and the report map will be printed to the screen and random test data will be in the `temp/` directory.
To run a specific test suite run `zx runlocal [module names]`.
To list module names, run `zx runlocal list`.
## Where things are
The test cases are all generated in gmt.erl, generally based on randomized but valid data that are fed to reference Erlang implementations.
The input to and output from each test is recorded to disk in the `temp/` dir.
The inputs are then read in by equivalent Java functions and the output is then compared.
For a test case to pass the outputs must be identical.
-328
View File
@@ -1,328 +0,0 @@
%%% @doc
%%% Gajumaru Java Lib Tester: gmt
%%%
%%% This module provides an inter-language test suite for the Gajumaru core
%%% Java libraries. It tests the Java implementation against the canonical
%%% Erlang implementation by generating random test vectors and comparing
%%% the outputs.
%%% @end
-module(gmt).
-vsn("0.1.0").
-author("Craig Everett <craigeverett@qpq.swiss>").
-copyright("Craig Everett <craigeverett@qpq.swiss>").
-license("LGPL-3.0-or-later").
-export([start/1]).
%%% Logic
mods() ->
#{"base64" => fun base64/0,
"base58" => fun base58/0,
"base58_check" => fun base58_check/0,
"rlp" => fun rlp/0,
"rlp_stream" => fun rlp_stream/0,
"rlp_fail" => fun rlp_fail/0,
"gaju_format" => fun gaju_format/0}.
start([]) ->
Tests = mods(),
ok = run(Tests),
zx:silent_stop();
start(["list"]) ->
ok = io:format("Available tests:~n"),
ok = lists:foreach(fun display/1, maps:keys(mods())),
zx:silent_stop();
start(Mods) ->
Available = mods(),
Tests = maps:with(Mods, Available),
ok =
case maps:size(Tests) =:= length(Mods) of
true ->
run(Tests);
false ->
NotMods = lists:subtract(Mods, maps:keys(Available)),
ok = io:format("The following arguments are not testable module names:~n"),
lists:foreach(fun display/1, NotMods)
end,
zx:silent_stop().
display(Name) ->
io:format(" ~ts~n", [Name]).
run(Tests) ->
{ok, Cwd} = file:get_cwd(),
ok =
case filename:basename(Cwd) of
"test" -> file:set_cwd("..");
_ -> ok
end,
ok = clean(),
ok = build(),
Results = maps:map(fun run/2, Tests),
io:format("Results:~n ~tp~n", [Results]).
run(Name, Test) ->
ok = io:format("~nRunning: ~ts...~n", [Name]),
Test().
clean() ->
Temp = "test/temp",
lists:foreach(fun(D) -> ok = clean(D) end, [Temp]).
clean(Dir) ->
case file:del_dir_r(Dir) of
ok -> ok;
{error, enoent} -> ok;
Error -> Error
end.
build() ->
Out = os:cmd("bin/compile"),
io:format("Compile: ~ts", [Out]).
temp_dir() ->
{ok, Cwd} = file:get_cwd(),
filename:join(Cwd, "test/temp").
trim(S) ->
Unprintable = fun(C) -> C =< 32 end,
lists:reverse(lists:dropwhile(Unprintable, lists:reverse(lists:dropwhile(Unprintable, S)))).
%%% Test Modules
base64() ->
Temp = temp_dir(),
TestFile = filename:join(Temp, "base64.test"),
ConvFile = filename:join(Temp, "base64.erlang.txt"),
ok = filelib:ensure_dir(TestFile),
ok = file:write_file(TestFile, rand:bytes(rand:uniform(5000))),
{ok, B} = file:read_file(TestFile),
Base64 = base64:encode(B),
ok = file:write_file(ConvFile, Base64),
Run = "bin/run base64 " ++ Temp,
Out = trim(os:cmd(Run)),
[JEnc, JDec] = string:split(Out, " "),
{ok, EEncB} = file:read_file(ConvFile),
{ok, JEncB} = file:read_file(JEnc),
EHash = crypto:hash(sha512, EEncB),
JHash = crypto:hash(sha512, JEncB),
{ok, EDecB} = file:read_file(TestFile),
{ok, JDecB} = file:read_file(JDec),
EBinHash = crypto:hash(sha512, EDecB),
JBinHash = crypto:hash(sha512, JDecB),
EHash =:= JHash andalso EBinHash =:= JBinHash.
base58() ->
Temp = temp_dir(),
TestFile = filename:join(Temp, "base58.test"),
ConvFile = filename:join(Temp, "base58.erlang.txt"),
ok = filelib:ensure_dir(TestFile),
ok = file:write_file(TestFile, rand:bytes(rand:uniform(5000))),
{ok, B} = file:read_file(TestFile),
Base58 = base58:binary_to_base58(B),
ok = file:write_file(ConvFile, Base58),
Run = "bin/run base58 " ++ Temp,
Out = trim(os:cmd(Run)),
[JEnc, JDec] = string:split(Out, " "),
{ok, EEncB} = file:read_file(ConvFile),
{ok, JEncB} = file:read_file(JEnc),
EHash = crypto:hash(sha512, EEncB),
JHash = crypto:hash(sha512, JEncB),
{ok, EDecB} = file:read_file(TestFile),
{ok, JDecB} = file:read_file(JDec),
EBinHash = crypto:hash(sha512, EDecB),
JBinHash = crypto:hash(sha512, JDecB),
EHash =:= JHash andalso EBinHash =:= JBinHash.
base58_check() ->
Temp = temp_dir(),
TestFile = filename:join(Temp, "base58_check.test"),
ConvFile = filename:join(Temp, "base58_check.erlang.txt"),
ok = filelib:ensure_dir(TestFile),
ok = file:write_file(TestFile, rand:bytes(rand:uniform(5000))),
{ok, B} = file:read_file(TestFile),
Checksum = binary:part(crypto:hash(sha256, crypto:hash(sha256, B)), 0, 4),
Base58C = base58:binary_to_base58(<<B/binary, Checksum/binary>>),
ok = file:write_file(ConvFile, Base58C),
Run = "bin/run base58_check " ++ Temp,
Out = trim(os:cmd(Run)),
[JEnc, JDec] = string:split(Out, " "),
{ok, EEncB} = file:read_file(ConvFile),
{ok, JEncB} = file:read_file(JEnc),
EHash = crypto:hash(sha512, EEncB),
JHash = crypto:hash(sha512, JEncB),
{ok, EDecB} = file:read_file(TestFile),
{ok, JDecB} = file:read_file(JDec),
EBinHash = crypto:hash(sha512, EDecB),
JBinHash = crypto:hash(sha512, JDecB),
EHash =:= JHash andalso EBinHash =:= JBinHash.
rlp() ->
Temp = temp_dir(),
Anchor = <<"I thought what I'd do was, I'd pretend I was one of those deaf-mutes.">>,
Data =
[rand:bytes(rand:uniform(20)),
[rand:bytes(rand:uniform(20)),
Anchor,
rand:bytes(rand:uniform(20)),
rand:bytes(rand:uniform(5000))],
rand:bytes(rand:uniform(2000))],
RLP = gmser_rlp:encode(Data),
RLP_File = filename:join(Temp, "rlp.test"),
ok = filelib:ensure_dir(RLP_File),
ok = file:write_file(RLP_File, RLP),
Run = "bin/run rlp " ++ Temp,
Out = trim(os:cmd(Run)),
case string:split(Out, "|") of
[Found, JPath] ->
JPathTrimmed = trim(JPath),
{ok, EEncB} = file:read_file(RLP_File),
case file:read_file(JPathTrimmed) of
{ok, JEncB} ->
EHash = crypto:hash(sha512, EEncB),
JHash = crypto:hash(sha512, JEncB),
EHash =:= JHash andalso Found =:= unicode:characters_to_list(Anchor);
{error, R} ->
io:format("Failed to read RLP Java result: ~tp (Path: ~tp)~n", [R, JPathTrimmed]),
false
end;
_ ->
io:format("RLP output mismatch: ~tp~n", [Out]),
false
end.
rlp_stream() ->
Temp = temp_dir(),
TestFile = filename:join(Temp, "rlp_stream.test"),
ok = filelib:ensure_dir(TestFile),
Data = [rand:bytes(rand:uniform(100)) || _ <- lists:seq(1, 10)],
RLP = << <<(gmser_rlp:encode(D))/binary>> || D <- Data >>,
ok = file:write_file(TestFile, RLP),
Run = "bin/run rlp_stream " ++ Temp,
Out = trim(os:cmd(Run)),
Out =:= "10".
rlp_fail() ->
Temp = temp_dir(),
TestFile = filename:join(Temp, "rlp_fail.test"),
ok = filelib:ensure_dir(TestFile),
Data = [<<"item1">>, <<"item2">>],
FullRLP = gmser_rlp:encode(Data),
TruncRLP = binary:part(FullRLP, 0, byte_size(FullRLP) - 2),
ok = file:write_file(TestFile, TruncRLP),
Run = "bin/run rlp_fail " ++ Temp,
Out = trim(os:cmd(Run)),
string:prefix(Out, "RLP_EXCEPTION:") =/= nomatch.
gaju_format() ->
Temp = temp_dir(),
TestFile = filename:join(Temp, "gaju_format.test"),
ok = filelib:ensure_dir(TestFile),
Cases = [gen_case() || _ <- lists:seq(1, 100)],
Lines = [serialize_case(C) || C <- Cases],
ok = file:write_file(TestFile, unicode:characters_to_binary([[L, "\n"] || L <- Lines])),
Run = "bin/run gaju_format " ++ Temp,
RawOut = os:cmd(Run),
JavaResPath = trim(RawOut),
case file:read_file(JavaResPath) of
{ok, ResContent} ->
JavaResults = string:split(trim(unicode:characters_to_list(ResContent)), "\n", all),
length(Cases) =:= length(JavaResults) andalso compare_results(Cases, JavaResults);
{error, Reason} ->
Format = "Failed to read Java results from: ~tp (Reason: ~tp)~nRaw Output: ~ts~n",
io:format(Format, [JavaResPath, Reason, RawOut]),
false
end.
%%% Generatorators
gen_case() ->
Ops = [amount, approx, read],
Op = lists:nth(rand:uniform(length(Ops)), Ops),
gen_case(Op).
gen_case(read) ->
Pucks = random_pucks(12),
Style = random_style(),
{read, hz_format:amount(gaju, Style, Pucks), Pucks};
gen_case(amount) ->
Unit = random_unit(),
Pucks =
case Unit of
gaju -> random_pucks(12);
puck -> random_pucks(8)
end,
Style = random_style(),
Sep = lists:nth(rand:uniform(2), [$,, $_]),
Span = rand:uniform(4),
Amount = hz_format:amount(Unit, hz_style(Style, Sep, Span), Pucks),
{amount, Style, Unit, Sep, Span, Pucks, Amount};
gen_case(approx) ->
Pucks = random_pucks(12),
Sep = lists:nth(rand:uniform(2), [$,, $_]),
Span = rand:uniform(2) + 2,
Prec = rand:uniform(18),
Approx = hz_format:approx_amount({Sep, Span}, Prec, Pucks),
{approx, us, gaju, Sep, Span, Pucks, Prec, Approx}.
random_pucks(MaxBytes) ->
Bytes = rand:bytes(rand:uniform(MaxBytes)),
crypto:bytes_to_integer(Bytes).
random_style() ->
lists:nth(rand:uniform(4), [us, jp, metric, legacy]).
random_unit() ->
lists:nth(rand:uniform(2), [gaju, puck]).
hz_style(us, Sep, Span) -> {Sep, Span};
hz_style(Style, _, _) -> Style.
serialize_case({amount, Style, Unit, Sep, Span, Pucks, _}) ->
FStyle = string:uppercase(atom_to_list(Style)),
FUnit = string:uppercase(atom_to_list(Unit)),
Stuff = [FStyle, FUnit, Sep, Span, Pucks],
io_lib:format("amount|~ts|~ts|~c|~b|~b", Stuff);
serialize_case({approx, Style, Unit, Sep, Span, Pucks, Prec, _}) ->
FStyle = string:uppercase(atom_to_list(Style)),
FUnit = string:uppercase(atom_to_list(Unit)),
Stuff = [FStyle, FUnit, Sep, Span, Pucks, Prec],
io_lib:format("approx|~ts|~ts|~c|~b|~b|~b", Stuff);
serialize_case({read, Input, _}) ->
InputStr = unicode:characters_to_list(Input),
io_lib:format("read|~ts", [InputStr]).
compare_results([], []) ->
true;
compare_results([Case | Cases], [Result | Results]) ->
case check_case(Case, Result) of
true ->
compare_results(Cases, Results);
false ->
io:format("Parity failure!~nCase: ~tp~nJava Result: ~tp~n", [Case, Result]),
false
end.
check_case({amount, _, _, _, _, _, Expected}, Result) ->
flatten(Expected) =:= flatten(Result);
check_case({approx, _, _, _, _, _, _, Expected}, Result) ->
flatten(Expected) =:= flatten(Result);
check_case({read, _, Expected}, Result) ->
integer_to_list(Expected) =:= flatten(Result).
flatten(B) when is_binary(B) -> unicode:characters_to_list(B);
flatten(L) when is_list(L) -> unicode:characters_to_list(L).
@@ -0,0 +1,156 @@
/*
* Copyright (c) 2026 QPQ AG <info@qpq.swiss>. All rights reserved.
* Project: Gajumaru Core Java Libraries <gajumaru.io>
*
* This program is dual-licensed:
* 1) Under the GNU Affero General Public License as published by the Free
* Software Foundation, either version 3 of the License, or (at your option)
* any later version (AGPL-3.0-or-later).
*
* 2) Under a commercial/proprietary license available directly from QPQ AG.
* If you wish to use this software outside the strict constraints of the
* AGPLv3 (e.g. within a closed-source or proprietary product), you must
* purchase a commercial license from QPQ AG.
*
* Authors:
* - Craig Everett <craigeverett@qpq.swiss>
*
* SPDX-License-Identifier: AGPL-3.0-or-later OR LicenseRef-QPQ-Commercial
*/
package swiss.qpq.gajumaru.core.crypto;
import java.util.Arrays;
import swiss.qpq.gajumaru.core.tools.CryptoUtils;
// Pure Java implementation of BLAKE2b.
// Based on eblake2.erl.
public final class Blake2b {
private static final long[] IV = {
0x6a09e667f3bcc908L, 0xbb67ae8584caa73bL, 0x3c6ef372fe94f82bL, 0xa54ff53a5f1d36f1L,
0x510e527fade682d1L, 0x9b05688c2b3e6c1fL, 0x1f83d9abfb41bd6bL, 0x5be0cd19137e2179L
};
private static final byte[][] SIGMA = {
{ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15 },
{ 14, 10, 4, 8, 9, 15, 13, 6, 1, 12, 0, 2, 11, 7, 5, 3 },
{ 11, 8, 12, 0, 5, 2, 15, 13, 10, 14, 3, 6, 7, 1, 9, 4 },
{ 7, 9, 3, 1, 13, 12, 11, 14, 2, 6, 5, 10, 4, 0, 15, 8 },
{ 9, 0, 5, 7, 2, 4, 10, 15, 14, 1, 11, 12, 6, 8, 3, 13 },
{ 2, 12, 6, 10, 0, 11, 8, 3, 4, 13, 7, 5, 15, 14, 1, 9 },
{ 12, 5, 1, 15, 14, 13, 4, 10, 0, 7, 6, 3, 9, 2, 8, 11 },
{ 13, 11, 7, 14, 12, 1, 3, 9, 5, 0, 15, 4, 8, 6, 2, 10 },
{ 6, 15, 14, 9, 11, 3, 0, 8, 12, 2, 13, 7, 1, 4, 10, 5 },
{ 10, 2, 8, 4, 7, 6, 1, 5, 15, 11, 9, 14, 3, 12, 13, 0 },
{ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15 }, // Round 10: sigma[0]
{ 14, 10, 4, 8, 9, 15, 13, 6, 1, 12, 0, 2, 11, 7, 5, 3 } // Round 11: sigma[1]
};
private Blake2b() {}
public static byte[] hash(byte[] data) {
return hash(data, 32);
}
public static byte[] hash(byte[] data, int hashLen) {
long[] h = Arrays.copyOf(IV, 8);
h[0] ^= 0x01010000L ^ hashLen;
long t0 = 0;
long t1 = 0;
int offset = 0;
while (offset + 128 < data.length) {
t0 += 128;
if (t0 < 128) t1++; // Overflow
compress(h, data, offset, t0, t1, false);
offset += 128;
}
t0 += (data.length - offset);
if (t0 < (data.length - offset)) t1++;
byte[] lastBlock = new byte[128];
System.arraycopy(data, offset, lastBlock, 0, data.length - offset);
compress(h, lastBlock, 0, t0, t1, true);
byte[] fullHash = new byte[64];
for (int i = 0; i < 8; i++) {
writeLongLittleEndian(h[i], fullHash, i * 8);
}
byte[] result = Arrays.copyOf(fullHash, hashLen);
// Memory hygiene
Arrays.fill(h, 0L);
Arrays.fill(fullHash, (byte) 0);
Arrays.fill(lastBlock, (byte) 0);
return result;
}
private static void compress(long[] h, byte[] block, int offset, long t0, long t1, boolean isLast) {
long[] m = new long[16];
for (int i = 0; i < 16; i++) {
m[i] = readLongLittleEndian(block, offset + i * 8);
}
long[] v = new long[16];
System.arraycopy( h, 0, v, 0, 8);
System.arraycopy(IV, 0, v, 8, 8);
v[12] ^= t0;
v[13] ^= t1;
if (isLast) {
v[14] ^= 0xffffffffffffffffL;
}
for (int round = 0; round < 12; round++) {
byte[] s = SIGMA[round];
g(v, 0, 4, 8, 12, m[s[0]], m[s[1]]);
g(v, 1, 5, 9, 13, m[s[2]], m[s[3]]);
g(v, 2, 6, 10, 14, m[s[4]], m[s[5]]);
g(v, 3, 7, 11, 15, m[s[6]], m[s[7]]);
g(v, 0, 5, 10, 15, m[s[8]], m[s[9]]);
g(v, 1, 6, 11, 12, m[s[10]], m[s[11]]);
g(v, 2, 7, 8, 13, m[s[12]], m[s[13]]);
g(v, 3, 4, 9, 14, m[s[14]], m[s[15]]);
}
for (int i = 0; i < 8; i++) {
h[i] ^= v[i] ^ v[i + 8];
}
// Wiping local arrays
Arrays.fill(m, 0L);
Arrays.fill(v, 0L);
}
private static void g(long[] v, int a, int b, int c, int d, long x, long y) {
v[a] = v[a] + v[b] + x;
v[d] = Long.rotateRight(v[d] ^ v[a], 32);
v[c] = v[c] + v[d];
v[b] = Long.rotateRight(v[b] ^ v[c], 24);
v[a] = v[a] + v[b] + y;
v[d] = Long.rotateRight(v[d] ^ v[a], 16);
v[c] = v[c] + v[d];
v[b] = Long.rotateRight(v[b] ^ v[c], 63);
}
private static long readLongLittleEndian(byte[] b, int offset) {
long res = 0;
for (int i = 0; i < 8; i++) {
res |= ((long) (b[offset + i] & 0xFF)) << (i * 8);
}
return res;
}
private static void writeLongLittleEndian(long v, byte[] b, int offset) {
for (int i = 0; i < 8; i++) {
b[offset + i] = (byte) ((v >>> (i * 8)) & 0xFF);
}
}
}
@@ -0,0 +1,849 @@
/*
* Copyright (c) 2026 QPQ AG <info@qpq.swiss>. All rights reserved.
* Project: Gajumaru Core Java Libraries <gajumaru.io>
*
* This program is dual-licensed:
* 1) Under the GNU Affero General Public License as published by the Free
* Software Foundation, either version 3 of the License, or (at your option)
* any later version (AGPL-3.0-or-later).
*
* 2) Under a commercial/proprietary license available directly from QPQ AG.
* If you wish to use this software outside the strict constraints of the
* AGPLv3 (e.g. within a closed-source or proprietary product), you must
* purchase a commercial license from QPQ AG.
*
* Authors:
* - Craig Everett <craigeverett@qpq.swiss>
*
* SPDX-License-Identifier: AGPL-3.0-or-later OR LicenseRef-QPQ-Commercial
*/
package swiss.qpq.gajumaru.core.crypto;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import swiss.qpq.gajumaru.core.tools.CryptoUtils;
// Pure Java implementation of Ed25519 curve arithmetic.
// Uses high-performance Radix-2^25.5 field arithmetic.
// Verified against the canonical Erlang ec_utils.
public final class Ed25519 {
// Precomputed limbs for Ed25519 constants (Radix 2^25.5)
private static final long[] BX = {52811034L, 25909283L, 16144682L, 17082669L, 27570973L, 30858332L, 40966398L, 8378388L, 20764389L, 8758491L};
private static final long[] BY = {40265304L, 26843545L, 13421772L, 20132659L, 26843545L, 6710886L, 53687091L, 13421772L, 40265318L, 26843545L};
private static final long[] D = {56195235L, 13857412L, 51736253L, 6949390L, 114729L, 24766616L, 60832955L, 30306712L, 48412415L, 21499315L};
private static final long[] D2 = {45281625L, 27714825L, 36363642L, 13898781L, 229458L, 15978800L, 54557047L, 27058993L, 29715967L, 9444199L};
private static final long[] I = {34513072L, 25610706L, 9377949L, 3500415L, 12389472L, 33281959L, 41962654L, 31548777L, 326685L, 11406482L};
public static final class Ge {
public final long[] X = new long[10], Y = new long[10], Z = new long[10], T = new long[10];
public void wipe() {
CryptoUtils.wipe(X);
CryptoUtils.wipe(Y);
CryptoUtils.wipe(Z);
CryptoUtils.wipe(T);
}
}
public static final class Scratch {
public final long[]
a = new long[10], b = new long[10], c = new long[10], d = new long[10],
e = new long[10], f = new long[10], g = new long[10], h = new long[10],
tmp = new long[10], t19 = new long[19];
public final long[][] stack = new long[10][10];
public final Ge geTmp = new Ge();
public void wipe() {
CryptoUtils.wipe(a);
CryptoUtils.wipe(b);
CryptoUtils.wipe(c);
CryptoUtils.wipe(d);
CryptoUtils.wipe(e);
CryptoUtils.wipe(f);
CryptoUtils.wipe(g);
CryptoUtils.wipe(h);
CryptoUtils.wipe(tmp);
CryptoUtils.wipe(t19);
for (long[] s : stack) CryptoUtils.wipe(s);
geTmp.wipe();
}
}
private Ed25519() {
}
public static byte[] publicKey(byte[] seed) {
byte[] hash = sha512(seed);
byte[] s = Arrays.copyOfRange(hash, 0, 32);
s[0] &= 248;
s[31] &= 127;
s[31] |= 64;
Scratch sc = new Scratch();
Ge A_point = scalarMulBase(s, sc);
byte[] A = compress(A_point, sc);
A_point.wipe();
sc.wipe();
CryptoUtils.wipe(hash);
CryptoUtils.wipe(s);
return A;
}
public static byte[] sign(byte[] seed, byte[] message) {
byte[] hash = sha512(seed);
byte[] s = Arrays.copyOfRange(hash, 0, 32);
s[0] &= 248;
s[31] &= 127;
s[31] |= 64;
byte[] prefix = Arrays.copyOfRange(hash, 32, 64);
byte[] rHash = sha512(prefix, message);
reduceScalar(rHash);
byte[] r = Arrays.copyOfRange(rHash, 0, 32);
Scratch sc = new Scratch();
Ge R_point = scalarMulBase(r, sc);
byte[] R = compress(R_point, sc);
byte[] A = publicKey(seed);
byte[] kHash = sha512(R, A, message);
reduceScalar(kHash);
byte[] k = Arrays.copyOfRange(kHash, 0, 32);
byte[] S = new byte[32];
scalarMulAdd(S, k, s, r);
byte[] sig = new byte[64];
System.arraycopy(R, 0, sig, 0, 32);
System.arraycopy(S, 0, sig, 32, 32);
R_point.wipe();
sc.wipe();
CryptoUtils.wipe(hash);
CryptoUtils.wipe(s);
CryptoUtils.wipe(prefix);
CryptoUtils.wipe(rHash);
CryptoUtils.wipe(r);
CryptoUtils.wipe(kHash);
CryptoUtils.wipe(k);
CryptoUtils.wipe(A);
return sig;
}
public static boolean verify(byte[] publicKey, byte[] message, byte[] signature) {
if (signature.length != 64) return false;
byte[] R_bytes = Arrays.copyOfRange(signature, 0, 32);
byte[] S_bytes = Arrays.copyOfRange(signature, 32, 64);
if ((S_bytes[31] & 0xe0) != 0) return false;
Scratch sc = new Scratch();
Ge A = decompress(publicKey, sc);
if (A == null) return false;
Ge R = decompress(R_bytes, sc);
if (R == null) return false;
byte[] kHash = sha512(R_bytes, publicKey, message);
reduceScalar(kHash);
byte[] k = Arrays.copyOfRange(kHash, 0, 32);
Ge sB = scalarMulBase(S_bytes, sc);
Ge kA = scalarMul(A, k, sc);
Ge RHS = new Ge();
ge_add(RHS, R, kA, sc);
byte[] LHS_bytes = compress(sB, sc);
byte[] RHS_bytes = compress(RHS, sc);
boolean ok = Arrays.equals(LHS_bytes, RHS_bytes);
A.wipe();
R.wipe();
sB.wipe();
kA.wipe();
RHS.wipe();
sc.wipe();
CryptoUtils.wipe(kHash);
CryptoUtils.wipe(k);
return ok;
}
public static Ge scalarMulBase(byte[] scalar, Scratch s) {
Ge res = new Ge();
fe_0(res.X);
fe_1(res.Y);
fe_1(res.Z);
fe_0(res.T);
Ge q = new Ge();
fe_copy(q.X, BX);
fe_copy(q.Y, BY);
fe_1(q.Z);
fe_mul(q.T, BX, BY, s.t19);
for (int i = 0; i < 256; i++) {
int bit = ((scalar[i / 8] & 0xFF) >>> (i % 8)) & 1;
ge_add(s.geTmp, res, q, s);
ge_cmov(res, s.geTmp, bit);
ge_double(q, q, s);
}
q.wipe();
return res;
}
public static void ge_double_scalarmul_vartime(Ge r, byte[] s, Ge a, byte[] k, Scratch sc) {
Ge sB = scalarMulBase(s, sc);
Ge kA = scalarMul(a, k, sc);
fe_neg(kA.X, kA.X);
fe_neg(kA.T, kA.T);
ge_add(r, sB, kA, sc);
sB.wipe();
kA.wipe();
}
public static Ge scalarMul(Ge p, byte[] scalar, Scratch s) {
Ge res = new Ge();
fe_0(res.X);
fe_1(res.Y);
fe_1(res.Z);
fe_0(res.T);
Ge q = new Ge();
fe_copy(q.X, p.X);
fe_copy(q.Y, p.Y);
fe_copy(q.Z, p.Z);
fe_copy(q.T, p.T);
for (int i = 0; i < 256; i++) {
int bit = ((scalar[i / 8] & 0xFF) >>> (i % 8)) & 1;
ge_add(s.geTmp, res, q, s);
ge_cmov(res, s.geTmp, bit);
ge_double(q, q, s);
}
q.wipe();
return res;
}
public static void ge_add(Ge r, Ge p1, Ge p2, Scratch s) {
long[] yMinusX1 = s.stack[0], yPlusX1 = s.stack[1], yMinusX2 = s.stack[2], yPlusX2 = s.stack[3];
long[] A = s.stack[4], B = s.stack[5], C = s.stack[6], D = s.stack[7];
long[] E = s.stack[8], F = s.stack[9], G = s.a, H = s.b;
fe_sub(yMinusX1, p1.Y, p1.X);
fe_add(yPlusX1, p1.Y, p1.X);
fe_sub(yMinusX2, p2.Y, p2.X);
fe_add(yPlusX2, p2.Y, p2.X);
fe_mul(A, yMinusX1, yMinusX2, s.t19);
fe_mul(B, yPlusX1, yPlusX2, s.t19);
fe_mul(C, p1.T, p2.T, s.t19);
fe_mul(C, C, D2, s.t19);
fe_mul(D, p1.Z, p2.Z, s.t19);
fe_add(D, D, D);
fe_sub(E, B, A);
fe_sub(F, D, C);
fe_add(G, D, C);
fe_add(H, B, A);
fe_mul(r.X, E, F, s.t19);
fe_mul(r.Y, G, H, s.t19);
fe_mul(r.Z, F, G, s.t19);
fe_mul(r.T, E, H, s.t19);
}
public static void ge_double(Ge r, Ge p, Scratch s) {
long[] A = s.stack[0], B = s.stack[1], C = s.stack[2], D = s.stack[3];
long[] E = s.stack[4], F = s.stack[5], G = s.stack[6], H = s.stack[7];
fe_sq(A, p.X, s.t19);
fe_sq(B, p.Y, s.t19);
fe_sq(C, p.Z, s.t19);
fe_add(C, C, C);
fe_neg(D, A);
fe_add(E, p.X, p.Y);
fe_sq(E, E, s.t19);
fe_sub(E, E, A);
fe_sub(E, E, B);
fe_add(G, D, B);
fe_sub(F, G, C);
fe_sub(H, D, B);
fe_mul(r.X, E, F, s.t19);
fe_mul(r.Y, G, H, s.t19);
fe_mul(r.Z, F, G, s.t19);
fe_mul(r.T, E, H, s.t19);
}
private static void ge_cmov(Ge r, Ge p, int b) {
fe_cmov(r.X, p.X, b);
fe_cmov(r.Y, p.Y, b);
fe_cmov(r.Z, p.Z, b);
fe_cmov(r.T, p.T, b);
}
private static void fe_cmov(long[] r, long[] p, int b) {
long mask = -(long) b;
for (int i = 0; i < 10; i++) {
long x = mask & (r[i] ^ p[i]);
r[i] ^= x;
}
}
public static Ge decompress(byte[] b, Scratch s) {
if (b.length != 32) return null;
Ge p = new Ge();
fe_frombytes(p.Y, b);
fe_1(p.Z);
// u = y^2 - 1
fe_sq(s.a, p.Y, s.t19);
fe_sub(s.a, s.a, p.Z);
// v = dy^2 + 1
fe_sq(s.b, p.Y, s.t19);
fe_mul(s.b, s.b, D, s.t19);
fe_add(s.b, s.b, p.Z);
// check root of u/v
fe_invert(s.c, s.b, s);
fe_mul(s.c, s.a, s.c, s.t19); // x2 = u/v
fe_pow22523(s.a, s.c, s); // a = x2^((p-5)/8)
fe_mul(s.a, s.c, s.a, s.t19); // x = x2 * a = x2^((p+3)/8)
fe_sq(s.b, s.a, s.t19);
fe_sub(s.b, s.b, s.c); // x^2 - x2
if (fe_isnonzero(s.b)) {
fe_mul(s.a, s.a, I, s.t19);
fe_sq(s.b, s.a, s.t19);
fe_sub(s.b, s.b, s.c);
if (fe_isnonzero(s.b)) return null;
}
if (fe_isnegative(s.a) != ((b[31] >> 7) & 1)) fe_neg(p.X, s.a);
else fe_copy(p.X, s.a);
fe_mul(p.T, p.X, p.Y, s.t19);
return p;
}
public static byte[] compress(Ge p, Scratch s) {
fe_invert(s.a, p.Z, s);
fe_mul(s.b, p.X, s.a, s.t19);
fe_mul(s.c, p.Y, s.a, s.t19);
byte[] out = fe_contract(s.c);
if (fe_isnegative(s.b) == 1) out[31] |= (byte) 0x80;
return out;
}
private static void fe_0(long[] h) {
for (int i = 0; i < 10; i++) h[i] = 0;
}
private static void fe_1(long[] h) {
h[0] = 1;
for (int i = 1; i < 10; i++) h[i] = 0;
}
private static void fe_copy(long[] h, long[] f) {
System.arraycopy(f, 0, h, 0, 10);
}
private static void fe_add(long[] h, long[] f, long[] g) {
for (int i = 0; i < 10; i++) h[i] = f[i] + g[i];
}
private static void fe_sub(long[] h, long[] f, long[] g) {
for (int i = 0; i < 10; i++) h[i] = f[i] - g[i];
}
private static void fe_neg(long[] h, long[] f) {
for (int i = 0; i < 10; i++) h[i] = -f[i];
}
public static void fe_mul(long[] out, long[] f, long[] g, long[] t) {
t[0] = f[0] * g[0];
t[1] = f[0] * g[1] + f[1] * g[0];
t[2] = f[0] * g[2] + f[2] * g[0] + 2 * f[1] * g[1];
t[3] = f[0] * g[3] + f[3] * g[0] + f[1] * g[2] + f[2] * g[1];
t[4] = f[0] * g[4] + f[4] * g[0] + f[2] * g[2] + 2 * f[1] * g[3] + 2 * f[3] * g[1];
t[5] = f[0] * g[5] + f[5] * g[0] + f[1] * g[4] + f[4] * g[1] + f[2] * g[3] + f[3] * g[2];
t[6] = f[0] * g[6] + f[6] * g[0] + f[2] * g[4] + f[4] * g[2] + 2 * f[1] * g[5] + 2 * f[5] * g[1] + 2 * f[3] * g[3];
t[7] = f[0] * g[7] + f[7] * g[0] + f[1] * g[6] + f[6] * g[1] + f[2] * g[5] + f[5] * g[2] + f[3] * g[4] + f[4] * g[3];
t[8] = f[0] * g[8] + f[8] * g[0] + f[2] * g[6] + f[6] * g[2] + f[4] * g[4] + 2 * f[1] * g[7] + 2 * f[7] * g[1] + 2 * f[3] * g[5] + 2 * f[5] * g[3];
t[9] = f[0] * g[9] + f[9] * g[0] + f[1] * g[8] + f[8] * g[1] + f[2] * g[7] + f[7] * g[2] + f[3] * g[6] + f[6] * g[3] + f[4] * g[5] + f[5] * g[4];
t[10] = 2 * f[1] * g[9] + 2 * f[9] * g[1] + f[2] * g[8] + f[8] * g[2] + 2 * f[3] * g[7] + 2 * f[7] * g[3] + f[4] * g[6] + f[6] * g[4] + 2 * f[5] * g[5];
t[11] = f[2] * g[9] + f[9] * g[2] + f[3] * g[8] + f[8] * g[3] + f[4] * g[7] + f[7] * g[4] + f[5] * g[6] + f[6] * g[5];
t[12] = 2 * f[3] * g[9] + 2 * f[9] * g[3] + f[4] * g[8] + f[8] * g[4] + 2 * f[5] * g[7] + 2 * f[7] * g[5] + f[6] * g[6];
t[13] = f[4] * g[9] + f[9] * g[4] + f[5] * g[8] + f[8] * g[5] + f[6] * g[7] + f[7] * g[6];
t[14] = 2 * f[5] * g[9] + 2 * f[9] * g[5] + f[6] * g[8] + f[8] * g[6] + 2 * f[7] * g[7];
t[15] = f[6] * g[9] + f[9] * g[6] + f[7] * g[8] + f[8] * g[7];
t[16] = 2 * f[7] * g[9] + 2 * f[9] * g[7] + f[8] * g[8];
t[17] = f[8] * g[9] + f[9] * g[8];
t[18] = 2 * f[9] * g[9];
fe_reduce(out, t);
}
public static void fe_sq(long[] out, long[] f, long[] t) {
t[0] = f[0] * f[0];
t[1] = 2 * f[0] * f[1];
t[2] = 2 * f[0] * f[2] + 2 * f[1] * f[1];
t[3] = 2 * f[0] * f[3] + 2 * f[1] * f[2];
t[4] = 2 * f[0] * f[4] + 4 * f[1] * f[3] + f[2] * f[2];
t[5] = 2 * f[0] * f[5] + 2 * f[1] * f[4] + 2 * f[2] * f[3];
t[6] = 2 * f[0] * f[6] + 4 * f[1] * f[5] + 2 * f[2] * f[4] + 2 * f[3] * f[3];
t[7] = 2 * f[0] * f[7] + 2 * f[1] * f[6] + 2 * f[2] * f[5] + 2 * f[3] * f[4];
t[8] = 2 * f[0] * f[8] + 4 * f[1] * f[7] + 2 * f[2] * f[6] + 4 * f[3] * f[5] + f[4] * f[4];
t[9] = 2 * f[0] * f[9] + 2 * f[1] * f[8] + 2 * f[2] * f[7] + 2 * f[3] * f[6] + 2 * f[4] * f[5];
t[10] = 4 * f[1] * f[9] + 2 * f[2] * f[8] + 4 * f[3] * f[7] + 2 * f[4] * f[6] + 2 * f[5] * f[5];
t[11] = 2 * f[2] * f[9] + 2 * f[3] * f[8] + 2 * f[4] * f[7] + 2 * f[5] * f[6];
t[12] = 4 * f[3] * f[9] + 2 * f[4] * f[8] + 4 * f[5] * f[7] + f[6] * f[6];
t[13] = 2 * f[4] * f[9] + 2 * f[5] * f[8] + 2 * f[6] * f[7];
t[14] = 4 * f[5] * f[9] + 2 * f[6] * f[8] + 2 * f[7] * f[7];
t[15] = 2 * f[6] * f[9] + 2 * f[7] * f[8];
t[16] = 4 * f[7] * f[9] + f[8] * f[8];
t[17] = 2 * f[8] * f[9];
t[18] = 2 * f[9] * f[9];
fe_reduce(out, t);
}
public static void fe_reduce(long[] h, long[] t) {
t[0] += t[10] * 19;
t[1] += t[11] * 19;
t[2] += t[12] * 19;
t[3] += t[13] * 19;
t[4] += t[14] * 19;
t[5] += t[15] * 19;
t[6] += t[16] * 19;
t[7] += t[17] * 19;
t[8] += t[18] * 19;
for (int p = 0; p < 2; p++) {
for (int i = 0; i < 9; i++) {
long c = t[i] >> (i % 2 == 0 ? 26 : 25);
t[i] &= (i % 2 == 0 ? 0x3FFFFFFL : 0x1FFFFFFL);
t[i + 1] += c;
}
long c = t[9] >> 25;
t[9] &= 0x1FFFFFFL;
t[0] += c * 19;
}
for (int i = 0; i < 10; i++) h[i] = t[i];
}
public static void fe_frombytes(long[] h, byte[] s) {
for (int i = 0; i < 10; i++) h[i] = 0;
int bitIdx = 0;
for (int i = 0; i < 10; i++) {
int len = (i % 2 == 0 ? 26 : 25);
for (int b = 0; b < len; b++) {
if (bitIdx < 255) {
if ((((s[bitIdx / 8] & 0xFF) >> (bitIdx % 8)) & 1) == 1) h[i] |= (1L << b);
}
bitIdx++;
}
}
}
public static byte[] fe_contract(long[] h) {
long[] val = Arrays.copyOf(h, 10);
for (int p = 0; p < 2; p++) {
for (int i = 0; i < 9; i++) {
long c = val[i] >> (i % 2 == 0 ? 26 : 25);
val[i] &= (i % 2 == 0 ? 0x3FFFFFFL : 0x1FFFFFFL);
val[i + 1] += c;
}
long c = val[9] >> 25;
val[9] &= 0x1FFFFFFL;
val[0] += c * 19;
}
long mask = 1;
for (int i = 9; i >= 0; i--) {
long target = (i % 2 == 0 ? 0x3FFFFFFL : 0x1FFFFFFL);
if (i == 0) target -= 19;
if (val[i] < target) {
mask = 0;
break;
}
if (val[i] > target) break;
}
val[0] += mask * 19;
for (int i = 0; i < 9; i++) {
long c = val[i] >> (i % 2 == 0 ? 26 : 25);
val[i] &= (i % 2 == 0 ? 0x3FFFFFFL : 0x1FFFFFFL);
val[i + 1] += c;
}
val[9] -= mask * (1L << 25);
for (int i = 0; i < 9; i++) {
long c = val[i] >> (i % 2 == 0 ? 26 : 25);
val[i] &= (i % 2 == 0 ? 0x3FFFFFFL : 0x1FFFFFFL);
val[i + 1] += c;
}
val[0] += (val[9] >> 25) * 19;
val[9] &= 0x1FFFFFFL;
byte[] out = new byte[32];
int bitIdx = 0;
for (int i = 0; i < 10; i++) {
int len = (i % 2 == 0 ? 26 : 25);
for (int bit = 0; bit < len; bit++) {
if (bitIdx < 255) {
if (((val[i] >> bit) & 1) == 1) out[bitIdx / 8] |= (byte) (1 << (bitIdx % 8));
bitIdx++;
}
}
}
return out;
}
private static void fe_invert(long[] out, long[] z, Scratch s) {
long[] t0 = s.stack[0], t1 = s.stack[1], z2 = s.stack[2], z9 = s.stack[3], z11 = s.stack[4];
long[] z2_5_0 = s.stack[5], z2_10_0 = s.stack[6], z2_20_0 = s.stack[7], z2_50_0 = s.stack[8], z2_100_0 = s.stack[9];
fe_sq(z2, z, s.t19);
fe_sq(t1, z2, s.t19);
fe_sq(t0, t1, s.t19);
fe_mul(z9, t0, z, s.t19);
fe_mul(z11, z9, z2, s.t19);
fe_sq(t0, z11, s.t19);
fe_mul(z2_5_0, t0, z9, s.t19);
fe_sq(t0, z2_5_0, s.t19);
for (int i = 1; i < 5; i++) fe_sq(t0, t0, s.t19);
fe_mul(z2_10_0, t0, z2_5_0, s.t19);
fe_sq(t0, z2_10_0, s.t19);
for (int i = 1; i < 10; i++) fe_sq(t0, t0, s.t19);
fe_mul(z2_20_0, t0, z2_10_0, s.t19);
fe_sq(t0, z2_20_0, s.t19);
for (int i = 1; i < 20; i++) fe_sq(t0, t0, s.t19);
fe_mul(t0, t0, z2_20_0, s.t19);
fe_sq(t0, t0, s.t19);
for (int i = 1; i < 10; i++) fe_sq(t0, t0, s.t19);
fe_mul(z2_50_0, t0, z2_10_0, s.t19);
fe_sq(t0, z2_50_0, s.t19);
for (int i = 1; i < 50; i++) fe_sq(t0, t0, s.t19);
fe_mul(z2_100_0, t0, z2_50_0, s.t19);
fe_sq(t1, z2_100_0, s.t19);
for (int i = 1; i < 100; i++) fe_sq(t1, t1, s.t19);
fe_mul(t1, t1, z2_100_0, s.t19);
fe_sq(t0, t1, s.t19);
for (int i = 1; i < 50; i++) fe_sq(t0, t0, s.t19);
fe_mul(t0, t0, z2_50_0, s.t19);
fe_sq(t1, t0, s.t19);
for (int i = 1; i < 5; i++) fe_sq(t1, t1, s.t19);
fe_mul(out, t1, z11, s.t19);
}
private static void fe_pow22523(long[] out, long[] in, Scratch s) {
long[] t0 = s.stack[0], t1 = s.stack[1], z2 = s.stack[2], z9 = s.stack[3], z11 = s.stack[4];
long[] z2_5_0 = s.stack[5], z2_10_0 = s.stack[6], z2_20_0 = s.stack[7], z2_50_0 = s.stack[8], z2_100_0 = s.stack[9];
fe_sq(z2, in, s.t19);
fe_sq(t1, z2, s.t19);
fe_sq(t0, t1, s.t19);
fe_mul(z9, t0, in, s.t19);
fe_mul(z11, z9, z2, s.t19);
fe_sq(t0, z11, s.t19);
fe_mul(z2_5_0, t0, z9, s.t19);
fe_sq(t0, z2_5_0, s.t19);
for (int i = 1; i < 5; i++) fe_sq(t0, t0, s.t19);
fe_mul(z2_10_0, t0, z2_5_0, s.t19);
fe_sq(t0, z2_10_0, s.t19);
for (int i = 1; i < 10; i++) fe_sq(t0, t0, s.t19);
fe_mul(z2_20_0, t0, z2_10_0, s.t19);
fe_sq(t0, z2_20_0, s.t19);
for (int i = 1; i < 20; i++) fe_sq(t0, t0, s.t19);
fe_mul(t0, t0, z2_20_0, s.t19);
fe_sq(t0, t0, s.t19);
for (int i = 1; i < 10; i++) fe_sq(t0, t0, s.t19);
fe_mul(z2_50_0, t0, z2_10_0, s.t19);
fe_sq(t0, z2_50_0, s.t19);
for (int i = 1; i < 50; i++) fe_sq(t0, t0, s.t19);
fe_mul(z2_100_0, t0, z2_50_0, s.t19);
fe_sq(t1, z2_100_0, s.t19);
for (int i = 1; i < 100; i++) fe_sq(t1, t1, s.t19);
fe_mul(t1, t1, z2_100_0, s.t19);
fe_sq(t0, t1, s.t19);
for (int i = 1; i < 50; i++) fe_sq(t0, t0, s.t19);
fe_mul(t0, t0, z2_50_0, s.t19);
fe_sq(t0, t0, s.t19);
fe_sq(t0, t0, s.t19);
fe_mul(out, t0, in, s.t19);
}
private static boolean fe_isnonzero(long[] h) {
byte[] s = fe_contract(h);
for (byte b : s) if (b != 0) return true;
return false;
}
private static int fe_isnegative(long[] h) {
byte[] s = fe_contract(h);
return s[0] & 1;
}
public static void reduceScalar(byte[] s) {
long s0 = 2097151 & load3(s, 0);
long s1 = 2097151 & (load4(s, 2) >> 5);
long s2 = 2097151 & (load3(s, 5) >> 2);
long s3 = 2097151 & (load4(s, 7) >> 7);
long s4 = 2097151 & (load4(s, 10) >> 4);
long s5 = 2097151 & (load3(s, 13) >> 1);
long s6 = 2097151 & (load4(s, 15) >> 6);
long s7 = 2097151 & (load3(s, 18) >> 3);
long s8 = 2097151 & load3(s, 21);
long s9 = 2097151 & (load4(s, 23) >> 5);
long s10 = 2097151 & (load3(s, 26) >> 2);
long s11 = 2097151 & (load4(s, 28) >> 7);
long s12 = 2097151 & (load4(s, 31) >> 4);
long s13 = 2097151 & (load3(s, 34) >> 1);
long s14 = 2097151 & (load4(s, 36) >> 6);
long s15 = 2097151 & (load3(s, 39) >> 3);
long s16 = 2097151 & load3(s, 42);
long s17 = 2097151 & (load4(s, 44) >> 5);
long s18 = 2097151 & (load3(s, 47) >> 2);
long s19 = 2097151 & (load4(s, 49) >> 7);
long s20 = 2097151 & (load4(s, 52) >> 4);
long s21 = 2097151 & (load3(s, 55) >> 1);
long s22 = 2097151 & (load4(s, 57) >> 6);
long s23 = (load4(s, 60) >> 3);
s11 += s23 * 666643;
s12 += s23 * 470296;
s13 += s23 * 654183;
s14 -= s23 * 997805;
s15 += s23 * 136657;
s16 -= s23 * 683901;
s10 += s22 * 666643;
s11 += s22 * 470296;
s12 += s22 * 654183;
s13 -= s22 * 997805;
s14 += s22 * 136657;
s15 -= s22 * 683901;
s9 += s21 * 666643;
s10 += s21 * 470296;
s11 += s21 * 654183;
s12 -= s21 * 997805;
s13 += s21 * 136657;
s14 -= s21 * 683901;
s8 += s20 * 666643;
s9 += s20 * 470296;
s10 += s20 * 654183;
s11 -= s20 * 997805;
s12 += s20 * 136657;
s13 -= s20 * 683901;
s7 += s19 * 666643;
s8 += s19 * 470296;
s9 += s19 * 654183;
s10 -= s19 * 997805;
s11 += s19 * 136657;
s12 -= s19 * 683901;
s6 += s18 * 666643;
s7 += s18 * 470296;
s8 += s18 * 654183;
s9 -= s18 * 997805;
s10 += s18 * 136657;
s11 -= s18 * 683901;
long c6 = (s6 + (1 << 20)) >> 21;
s7 += c6;
s6 -= c6 << 21;
long c8 = (s8 + (1 << 20)) >> 21;
s9 += c8;
s8 -= c8 << 21;
long c10 = (s10 + (1 << 20)) >> 21;
s11 += c10;
s10 -= c10 << 21;
long c12 = (s12 + (1 << 20)) >> 21;
s13 += c12;
s12 -= c12 << 21;
long c14 = (s14 + (1 << 20)) >> 21;
s15 += c14;
s14 -= c14 << 21;
long c16 = (s16 + (1 << 20)) >> 21;
s17 += c16;
s16 -= c16 << 21;
long c7 = (s7 + (1 << 20)) >> 21;
s8 += c7;
s7 -= c7 << 21;
long c9 = (s9 + (1 << 20)) >> 21;
s10 += c9;
s9 -= c9 << 21;
long c11 = (s11 + (1 << 20)) >> 21;
s12 += c11;
s11 -= c11 << 21;
long c13 = (s13 + (1 << 20)) >> 21;
s14 += c13;
s13 -= c13 << 21;
long c15 = (s15 + (1 << 20)) >> 21;
s16 += c15;
s15 -= c15 << 21;
s5 += s17 * 666643;
s6 += s17 * 470296;
s7 += s17 * 654183;
s8 -= s17 * 997805;
s9 += s17 * 136657;
s10 -= s17 * 683901;
s4 += s16 * 666643;
s5 += s16 * 470296;
s6 += s16 * 654183;
s7 -= s16 * 997805;
s8 += s16 * 136657;
s9 -= s16 * 683901;
s3 += s15 * 666643;
s4 += s15 * 470296;
s5 += s15 * 654183;
s6 -= s15 * 997805;
s7 += s15 * 136657;
s8 -= s15 * 683901;
s2 += s14 * 666643;
s3 += s14 * 470296;
s4 += s14 * 654183;
s5 -= s14 * 997805;
s6 += s14 * 136657;
s7 -= s14 * 683901;
s1 += s13 * 666643;
s2 += s13 * 470296;
s3 += s13 * 654183;
s4 -= s13 * 997805;
s5 += s13 * 136657;
s6 -= s13 * 683901;
s0 += s12 * 666643;
s1 += s12 * 470296;
s2 += s12 * 654183;
s3 -= s12 * 997805;
s4 += s12 * 136657;
s5 -= s12 * 683901;
long cc0 = (s0 + (1 << 20)) >> 21;
s1 += cc0;
s0 -= cc0 << 21;
long cc2 = (s2 + (1 << 20)) >> 21;
s3 += cc2;
s2 -= cc2 << 21;
long cc4 = (s4 + (1 << 20)) >> 21;
s5 += cc4;
s4 -= cc4 << 21;
long cc6 = (s6 + (1 << 20)) >> 21;
s7 += cc6;
s6 -= cc6 << 21;
long cc8 = (s8 + (1 << 20)) >> 21;
s9 += cc8;
s8 -= cc8 << 21;
long cc10 = (s10 + (1 << 20)) >> 21;
s11 += cc10;
s10 -= cc10 << 21;
long cc1 = (s1 + (1 << 20)) >> 21;
s2 += cc1;
s1 -= cc1 << 21;
long cc3 = (s3 + (1 << 20)) >> 21;
s4 += cc3;
s3 -= cc3 << 21;
long cc5 = (s5 + (1 << 20)) >> 21;
s6 += cc5;
s5 -= cc5 << 21;
long cc7 = (s7 + (1 << 20)) >> 21;
s8 += cc7;
s7 -= cc7 << 21;
long cc9 = (s9 + (1 << 20)) >> 21;
s10 += cc9;
s9 -= cc9 << 21;
long cc11 = (s11 + (1 << 20)) >> 21;
long s12_2 = cc11;
s11 -= cc11 << 21;
s0 += s12_2 * 666643;
s1 += s12_2 * 470296;
s2 += s12_2 * 654183;
s3 -= s12_2 * 997805;
s4 += s12_2 * 136657;
s5 -= s12_2 * 683901;
long ccc0 = s0 >> 21;
s1 += ccc0;
s0 -= ccc0 << 21;
long ccc1 = s1 >> 21;
s2 += ccc1;
s1 -= ccc1 << 21;
long ccc2 = s2 >> 21;
s3 += ccc2;
s2 -= ccc2 << 21;
long ccc3 = s3 >> 21;
s4 += ccc3;
s3 -= ccc3 << 21;
long ccc4 = s4 >> 21;
s5 += ccc4;
s4 -= ccc4 << 21;
long ccc5 = s5 >> 21;
s6 += ccc5;
s5 -= ccc5 << 21;
long ccc6 = s6 >> 21;
s7 += ccc6;
s6 -= ccc6 << 21;
long ccc7 = s7 >> 21;
s8 += ccc7;
s7 -= ccc7 << 21;
long ccc8 = s8 >> 21;
s9 += ccc8;
s8 -= ccc8 << 21;
long ccc9 = s9 >> 21;
s10 += ccc9;
s9 -= ccc9 << 21;
long ccc10 = s10 >> 21;
s11 += ccc10;
s10 -= ccc10 << 21;
long ccc11 = s11 >> 21;
long s12_3 = ccc11;
s11 -= ccc11 << 21;
s0 += s12_3 * 666643;
s1 += s12_3 * 470296;
s2 += s12_3 * 654183;
s3 -= s12_3 * 997805;
s4 += s12_3 * 136657;
s5 -= s12_3 * 683901;
long sccc0 = s0 >> 21;
s1 += sccc0;
s0 -= sccc0 << 21;
long sccc1 = s1 >> 21;
s2 += sccc1;
s1 -= sccc1 << 21;
long sccc2 = s2 >> 21;
s3 += sccc2;
s2 -= sccc2 << 21;
long sccc3 = s3 >> 21;
s4 += sccc3;
s3 -= sccc3 << 21;
long sccc4 = s4 >> 21;
s5 += sccc4;
s4 -= sccc4 << 21;
long sccc5 = s5 >> 21;
s6 += sccc5;
s5 -= sccc5 << 21;
Arrays.fill(s, (byte) 0);
long[] resLimbs = {s0, s1, s2, s3, s4, s5, s6, s7, s8, s9, s10, s11};
int bitIdx = 0;
for (int limbIdx = 0; limbIdx < 12; limbIdx++) {
for (int i = 0; i < 21; i++) {
if (bitIdx < 256) {
if (((resLimbs[limbIdx] >> i) & 1) == 1)
s[bitIdx / 8] |= (byte) (1 << (bitIdx % 8));
bitIdx++;
}
}
}
}
private static long load3(byte[] in, int off) {
return (in[off] & 0xffL) | ((in[off + 1] & 0xffL) << 8) | ((in[off + 2] & 0xffL) << 16);
}
private static long load4(byte[] in, int off) {
return (in[off] & 0xffL) | ((in[off + 1] & 0xffL) << 8) | ((in[off + 2] & 0xffL) << 16) | ((in[off + 3] & 0xffL) << 24);
}
private static void scalarMulAdd(byte[] S, byte[] k, byte[] s, byte[] r) {
long[] res = new long[64];
for (int i = 0; i < 32; i++) {
for (int j = 0; j < 32; j++) res[i + j] += (k[i] & 0xFFL) * (s[j] & 0xFFL);
}
for (int i = 0; i < 32; i++) res[i] += (r[i] & 0xFFL);
byte[] buffer = new byte[64];
long carry = 0;
for (int i = 0; i < 63; i++) {
long val = res[i] + carry;
buffer[i] = (byte) val;
carry = val >>> 8;
}
buffer[63] = (byte) (res[63] + carry);
reduceScalar(buffer);
System.arraycopy(buffer, 0, S, 0, 32);
}
private static byte[] sha512(byte[]... parts) {
try {
MessageDigest md = MessageDigest.getInstance("SHA-512");
for (byte[] p : parts) md.update(p);
return md.digest();
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
}
}
@@ -0,0 +1,172 @@
/*
* Copyright (c) 2026 QPQ AG <info@qpq.swiss>. All rights reserved.
* Project: Gajumaru Core Java Libraries <gajumaru.io>
*
* This program is dual-licensed:
* 1) Under the GNU Affero General Public License as published by the Free
* Software Foundation, either version 3 of the License, or (at your option)
* any later version (AGPL-3.0-or-later).
*
* 2) Under a commercial/proprietary license available directly from QPQ AG.
* If you wish to use this software outside the strict constraints of the
* AGPLv3 (e.g. within a closed-source or proprietary product), you must
* purchase a commercial license from QPQ AG.
*
* Authors:
* - Craig Everett <craigeverett@qpq.swiss>
*
* SPDX-License-Identifier: AGPL-3.0-or-later OR LicenseRef-QPQ-Commercial
*/
package swiss.qpq.gajumaru.core.crypto;
import java.util.Arrays;
import swiss.qpq.gajumaru.core.tools.CryptoUtils;
// Pure Java implementation of Keccak-256 (Keccak[c=512] sponge).
// Based on sha3.erl.
public final class Keccak256 {
private static final int LANE_SIZE = 64;
private static final int STATE_SIZE = 25;
private static final int CAPACITY = 512;
private static final int BITRATE = 1600 - CAPACITY; // 1088 bits = 136 bytes
private static final int BITRATE_BYTES = BITRATE / 8;
private static final long[] ROUND_CONSTANTS = {
0x0000000000000001L, 0x0000000000008082L, 0x800000000000808AL, 0x8000000080008000L,
0x000000000000808BL, 0x0000000080000001L, 0x8000000080008081L, 0x8000000000008009L,
0x000000000000008AL, 0x0000000000000088L, 0x0000000080008009L, 0x000000008000000AL,
0x000000008000808BL, 0x800000000000008BL, 0x8000000000008089L, 0x8000000000008003L,
0x8000000000008002L, 0x8000000000000080L, 0x000000000000800AL, 0x800000008000000AL,
0x8000000080008081L, 0x8000000000008080L, 0x0000000080000001L, 0x8000000080008008L
};
private static final int[] ROTATION_OFFSETS = {
0, 1, 62, 28, 27,
36, 44, 6, 55, 20,
3, 10, 43, 25, 39,
41, 45, 15, 21, 8,
18, 2, 61, 56, 14
};
private Keccak256() {}
public static byte[] hash(byte[] data) {
long[] state = new long[STATE_SIZE];
// Padding: Keccak[c=512] uses 0x01 ... 0x80 (or just 0x01 for non-NIST Keccak)
// Erlang code uses: keccak(Capacity, Message, <<>>, OutputBitLength)
// Pad logic in Erlang: <<Msg/bitstring, Delimiter/bitstring, 1:1, 0:PadZeros, 1:1>>
// With Delimiter = <<>>, it becomes <<1:1, 0:PadZeros, 1:1>> (standard Keccak padding)
byte[] padded = pad(data, BITRATE_BYTES);
for (int i = 0; i < padded.length; i += BITRATE_BYTES) {
absorb(state, padded, i);
}
byte[] result = squeeze(state, 32);
// Memory hygiene
Arrays.fill(state, 0L);
CryptoUtils.wipe(padded);
return result;
}
private static byte[] pad(byte[] data, int rateBytes) {
int mLen = data.length;
int padLen = rateBytes - (mLen % rateBytes);
byte[] padded = new byte[mLen + padLen];
System.arraycopy(data, 0, padded, 0, mLen);
if (padLen == 1) {
padded[mLen] = (byte) 0x81;
} else {
padded[mLen] = (byte) 0x01;
padded[padded.length - 1] |= (byte) 0x80;
}
return padded;
}
private static void absorb(long[] state, byte[] data, int offset) {
for (int i = 0; i < BITRATE_BYTES / 8; i++) {
state[i] ^= readLongLittleEndian(data, offset + i * 8);
}
keccakF(state);
}
private static byte[] squeeze(long[] state, int len) {
byte[] result = new byte[len];
int count = 0;
while (count < len) {
for (int i = 0; i < BITRATE_BYTES / 8 && count < len; i++) {
writeLongLittleEndian(state[i], result, count);
count += 8;
}
if (count < len) {
keccakF(state);
}
}
return result;
}
private static void keccakF(long[] a) {
for (int round = 0; round < 24; round++) {
// Theta
long[] c = new long[5];
for (int x = 0; x < 5; x++) {
c[x] = a[x] ^ a[x + 5] ^ a[x + 10] ^ a[x + 15] ^ a[x + 20];
}
for (int x = 0; x < 5; x++) {
long d = c[(x + 4) % 5] ^ Long.rotateLeft(c[(x + 1) % 5], 1);
for (int y = 0; y < 5; y++) {
a[x + y * 5] ^= d;
}
}
// Rho and Pi
long[] nextA = new long[STATE_SIZE];
for (int x = 0; x < 5; x++) {
for (int y = 0; y < 5; y++) {
int index = x + y * 5;
nextA[y + ((2 * x + 3 * y) % 5) * 5] = Long.rotateLeft(a[index], ROTATION_OFFSETS[index]);
}
}
// Chi
for (int y = 0; y < 5; y++) {
long[] row = new long[5];
for (int x = 0; x < 5; x++) {
row[x] = nextA[x + y * 5];
}
for (int x = 0; x < 5; x++) {
nextA[x + y * 5] = row[x] ^ ((~row[(x + 1) % 5]) & row[(x + 2) % 5]);
}
}
// Iota
nextA[0] ^= ROUND_CONSTANTS[round];
System.arraycopy(nextA, 0, a, 0, STATE_SIZE);
}
}
private static long readLongLittleEndian(byte[] b, int offset) {
long res = 0;
for (int i = 0; i < 8; i++) {
res |= ((long) (b[offset + i] & 0xFF)) << (i * 8);
}
return res;
}
private static void writeLongLittleEndian(long v, byte[] b, int offset) {
for (int i = 0; i < 8; i++) {
b[offset + i] = (byte) ((v >>> (i * 8)) & 0xFF);
}
}
}
@@ -0,0 +1,107 @@
/*
* Copyright (c) 2026 QPQ AG <info@qpq.swiss>. All rights reserved.
* Project: Gajumaru Core Java Libraries <gajumaru.io>
*
* This program is dual-licensed:
* 1) Under the GNU Affero General Public License as published by the Free
* Software Foundation, either version 3 of the License, or (at your option)
* any later version (AGPL-3.0-or-later).
*
* 2) Under a commercial/proprietary license available directly from QPQ AG.
* If you wish to use this software outside the strict constraints of the
* AGPLv3 (e.g. within a closed-source or proprietary product), you must
* purchase a commercial license from QPQ AG.
*
* Authors:
* - Craig Everett <craigeverett@qpq.swiss>
*
* SPDX-License-Identifier: AGPL-3.0-or-later OR LicenseRef-QPQ-Commercial
*/
package swiss.qpq.gajumaru.core.data;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
// Port of gmser_id.erl.
// Represents a 33-byte identifier (1-byte tag + 32-byte value).
public final class Id {
public enum Tag {
ACCOUNT(1),
NAME(2),
COMMITMENT(3),
CONTRACT(5),
CHANNEL(6),
ASSOCIATE_CHAIN(7),
NATIVE_TOKEN(8),
ENTRY(9);
public final int value;
Tag(int value) {
this.value = value;
}
private static final Map<Integer, Tag> VALUE_MAP = new HashMap<>();
static {
for (Tag t : Tag.values()) {
VALUE_MAP.put(t.value, t);
}
}
public static Tag fromValue(int v) {
Tag t = VALUE_MAP.get(v);
if (t == null) throw new IllegalArgumentException("Unknown ID tag value: " + v);
return t;
}
}
private final Tag tag;
private final byte[] value;
public Id(Tag tag, byte[] value) {
if (value.length != 32) {
throw new IllegalArgumentException("ID value must be exactly 32 bytes");
}
this.tag = tag;
this.value = Arrays.copyOf(value, 32);
}
public Tag getTag() { return tag; }
public byte[] getValue() { return Arrays.copyOf(value, 32); }
public byte[] serialize() {
byte[] result = new byte[33];
result[0] = (byte) tag.value;
System.arraycopy(value, 0, result, 1, 32);
return result;
}
public static Id deserialize(byte[] data) {
if (data.length != 33) {
throw new IllegalArgumentException("Serialized ID must be exactly 33 bytes");
}
Tag tag = Tag.fromValue(data[0] & 0xFF);
byte[] value = Arrays.copyOfRange(data, 1, 33);
return new Id(tag, value);
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Id)) return false;
Id id = (Id) o;
return tag == id.tag && Arrays.equals(value, id.value);
}
@Override
public int hashCode() {
int result = tag.hashCode();
result = 31 * result + Arrays.hashCode(value);
return result;
}
}
@@ -0,0 +1,93 @@
/*
* Copyright (c) 2026 QPQ AG <info@qpq.swiss>. All rights reserved.
* Project: Gajumaru Core Java Libraries <gajumaru.io>
*
* This program is dual-licensed:
* 1) Under the GNU Affero General Public License as published by the Free
* Software Foundation, either version 3 of the License, or (at your option)
* any later version (AGPL-3.0-or-later).
*
* 2) Under a commercial/proprietary license available directly from QPQ AG.
* If you wish to use this software outside the strict constraints of the
* AGPLv3 (e.g. within a closed-source or proprietary product), you must
* purchase a commercial license from QPQ AG.
*
* Authors:
* - Craig Everett <craigeverett@qpq.swiss>
*
* SPDX-License-Identifier: AGPL-3.0-or-later OR LicenseRef-QPQ-Commercial
*/
package swiss.qpq.gajumaru.core.encoding;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
// Port of gmser_api_encoder.erl.
// Handles Gajumaru API encoding (prefixed Base58Check).
public final class ApiEncoder {
public enum Type {
ACCOUNT_PUBKEY("ak", 32),
ACCOUNT_SECKEY("sk", 32),
TX_HASH("th", 32),
CONTRACT_PUBKEY("ct", 32),
CHANNEL("ch", 32),
SIGNATURE("sg", 64),
KEY_BLOCK_HASH("kh", 32),
MICRO_BLOCK_HASH("mh", 32),
COMMITMENT("cm", 32),
PEER_PUBKEY("pp", 32),
NAME("nm", -1); // Variable size
public final String prefix;
public final int size;
Type(String prefix, int size) {
this.prefix = prefix;
this.size = size;
}
}
private static final Map<String, Type> PREFIX_MAP = new HashMap<>();
static {
for (Type t : Type.values()) {
PREFIX_MAP.put(t.prefix, t);
}
}
private ApiEncoder() {}
public static String encode(Type type, byte[] payload) {
if (type.size != -1 && payload.length != type.size) {
throw new IllegalArgumentException("Invalid payload size for " + type + ": " + payload.length);
}
return type.prefix + "_" + Base58.checkEncode(payload);
}
public static DecodeResult decode(String input) {
String[] parts = input.split("_");
if (parts.length != 2) {
throw new IllegalArgumentException("Invalid encoded string format");
}
Type type = PREFIX_MAP.get(parts[0]);
if (type == null) {
throw new IllegalArgumentException("Unknown prefix: " + parts[0]);
}
byte[] payload = Base58.checkDecode(parts[1]);
if (type.size != -1 && payload.length != type.size) {
throw new IllegalArgumentException("Invalid decoded payload size for " + type + ": " + payload.length);
}
return new DecodeResult(type, payload);
}
public record DecodeResult(Type type, byte[] payload) {}
}
@@ -24,6 +24,7 @@ import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
// Stateless Base58 and Base58Check implementation.
public final class Base58 {
@@ -26,10 +26,7 @@ import java.util.List;
public final class RLP {
// Integrated data models
// RLP only has two types: byte arrays and lists of lists of byte arrays
// TODO: Comment this better with references.
public static class RLPException extends RuntimeException {
public RLPException(String message) {
super(message);
@@ -110,12 +107,10 @@ public final class RLP {
int prefix = buffer[offset] & 0xFF;
// 1. Single byte [0x00, 0x7F] (itself)
if (prefix <= 0x7F) {
return new DecodeResult(new RLP_Item(new byte[] { (byte) prefix }), 1);
}
// 2. Short string [0x80, 0xB7] (length 0-55)
if (prefix <= 0xB7) {
int payloadLen = prefix - 0x80;
checkBounds(buffer, offset + 1, payloadLen);
@@ -123,7 +118,6 @@ public final class RLP {
return new DecodeResult(new RLP_Item(payload), 1 + payloadLen);
}
// 3. Long string [0xB8, 0xBF] (length > 55)
if (prefix <= 0xBF) {
int lenLen = prefix - 0xB7;
checkBounds(buffer, offset + 1, lenLen);
@@ -133,7 +127,6 @@ public final class RLP {
return new DecodeResult(new RLP_Item(payload), 1 + lenLen + payloadLen);
}
// 4. Short list [0xC0, 0xF7] (total length 0-55)
if (prefix <= 0xF7) {
int payloadLen = prefix - 0xC0;
checkBounds(buffer, offset + 1, payloadLen);
@@ -141,7 +134,6 @@ public final class RLP {
return new DecodeResult(list, 1 + payloadLen);
}
// 5. Long list [0xF8, 0xFF] (total length > 55)
int lenLen = prefix - 0xF7;
checkBounds(buffer, offset + 1, lenLen);
int payloadLen = bigEndianToInt(buffer, offset + 1, offset + 1 + lenLen);
@@ -0,0 +1,62 @@
/*
* Copyright (c) 2026 QPQ AG <info@qpq.swiss>. All rights reserved.
* Project: Gajumaru Core Java Libraries <gajumaru.io>
*
* This program is dual-licensed:
* 1) Under the GNU Affero General Public License as published by the Free
* Software Foundation, either version 3 of the License, or (at your option)
* any later version (AGPL-3.0-or-later).
*
* 2) Under a commercial/proprietary license available directly from QPQ AG.
* If you wish to use this software outside the strict constraints of the
* AGPLv3 (e.g. within a closed-source or proprietary product), you must
* purchase a commercial license from QPQ AG.
*
* Authors:
* - Craig Everett <craigeverett@qpq.swiss>
*
* SPDX-License-Identifier: AGPL-3.0-or-later OR LicenseRef-QPQ-Commercial
*/
package swiss.qpq.gajumaru.core.tools;
import java.util.Arrays;
// Utility class for cryptographic operations and memory hygiene.
public final class CryptoUtils {
private CryptoUtils() {}
// Wipes a byte array by filling it with zeros.
public static void wipe(byte[] data) {
if (data != null) {
Arrays.fill(data, (byte) 0);
}
}
public static void wipe(long[] data) {
if (data != null) {
Arrays.fill(data, 0L);
}
}
public static String binToHex(byte[] data) {
StringBuilder sb = new StringBuilder();
for (byte b : data) {
sb.append(String.format("%02x", b & 0xff));
}
return sb.toString();
}
public static byte[] hexToBin(String s) {
int len = s.length();
byte[] data = new byte[len / 2];
for (int i = 0; i < len; i += 2) {
data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4)
+ Character.digit(s.charAt(i+1), 16));
}
return data;
}
}
@@ -13,3 +13,5 @@ doc/erlang.png
rel/example_project
.concrete/DEV_MODE
.rebar
.idea
*.iml
+53
View File
@@ -0,0 +1,53 @@
# GM Java <-> Erlang Tests
The core Java libraries are all written as transform functions over data.
There is little point, therefore, in obsessing over "unit tests" and "regression testing" of
Java code in Java when we have cannonical code in Erlang.
The purpose of this utility is to instead test the Java libraries agains the Erlang libraries
directly.
## How to run
In the current directory simply run `zx runlocal` and the report map will be printed to the screen
and random test data will be in the `temp/` directory.
To run a specific test suite run `zx runlocal [module names]`.
To list module names, run `zx runlocal list`.
## Where things are
The test cases are all generated in gmt.erl, generally based on randomized but valid data that
are fed to reference Erlang implementations. Java code is built using plain old javac, and is
handled by the `gajumaru-core/bin/compile` and `gajumaru-core/bin/run` scripts.
The input to and output from each test is recorded to disk in the `temp/` dir.
The inputs are then read in by equivalent Java functions and the output is then compared.
For a test case to pass the outputs must be identical.
## The Erlang execution context
`zx` manages loading the execution context, so dependencies like gmserialization, ec_utils,
base58, etc. (all of which are targets for which is increasingly turning into a Java port)
are brought in dynamically and managed by `zx`. The easiest way to test a specific function
in Java against the Erlang implementation is to add a call to it in gmt.erl and a matching
call in `../src/Testinator.java`.
The dependencies that are brought in can be listed with `zx list deps`.
The package names follow the pattern `[realm]-[package_name]-[version]`
The location of the sources varies on different systems.
- Linux: `$HOME/zomp/lib/[realm]/[package_name]/[version]/src`
- MacOS: `$HOME/.zx/zomp/lib/[realm]/[package_name]/[version]/src`
- Windows: `%%LOCALAPPDATA%%/zomp/lib/[realm]/[package_name]/[version]/src`
So for example `otpr-ec_utils-1.0.0` has its sources at `~/zomp/lib/otpr/ec_utils/1.0.0/src/`
on a reasonable system.
+416
View File
@@ -0,0 +1,416 @@
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Base64;
import java.util.ArrayList;
import java.util.List;
import java.util.Arrays;
import java.math.BigInteger;
import swiss.qpq.gajumaru.core.encoding.Base58;
import swiss.qpq.gajumaru.core.encoding.RLP;
import swiss.qpq.gajumaru.core.encoding.ApiEncoder;
import swiss.qpq.gajumaru.core.formatting.GajuFormat;
import swiss.qpq.gajumaru.core.crypto.Keccak256;
import swiss.qpq.gajumaru.core.crypto.Blake2b;
import swiss.qpq.gajumaru.core.crypto.Ed25519;
import swiss.qpq.gajumaru.core.data.Id;
import swiss.qpq.gajumaru.core.tools.CryptoUtils;
public class Testinator {
public static void main(String[] args) {
if (args.length < 2) {
System.out.println("Error: Provide the test suite name and the required arguments.");
System.exit(1);
}
try {
switch (args[0]) {
case "base64" -> { System.out.print(base64(args[1])); }
case "base58" -> { System.out.print(base58(args[1])); }
case "base58_check" -> { System.out.print(base58_check(args[1])); }
case "rlp" -> { System.out.print(rlp(args[1])); }
case "rlp_stream" -> { System.out.print(rlp_stream(args[1])); }
case "rlp_fail" -> { System.out.print(rlp_fail(args[1])); }
case "gaju_format" -> { System.out.print(gaju_format(args[1])); }
case "keccak256" -> { System.out.print(keccak256(args[1])); }
case "blake2b" -> { System.out.print(blake2b(args[1])); }
case "ed25519" -> { System.out.print(ed25519(args[1])); }
case "ed25519_verify" -> { System.out.print(ed25519_verify(args[1])); }
case "api_encode" -> { System.out.print(api_encode(args[1])); }
case "id_serialization" -> { System.out.print(id_serialization(args[1])); }
case "fe_parity" -> { System.out.print(fe_parity(args[1])); }
case "reduce_parity" -> { System.out.print(reduce_parity(args[1])); }
case "smb_parity" -> { System.out.print(smb_parity(args[1])); }
case "femul_parity" -> { System.out.print(femul_parity(args[1])); }
case "frombytes_parity" -> { System.out.print(frombytes_parity(args[1])); }
case "ge_parity" -> { System.out.print(ge_parity(args[1], args[2])); }
case "keccak" -> {
byte[] in = CryptoUtils.hexToBin(args[1]);
System.out.println(CryptoUtils.binToHex(Keccak256.hash(in)));
}
case "blake2b_direct" -> {
byte[] in = CryptoUtils.hexToBin(args[1]);
System.out.println(CryptoUtils.binToHex(Blake2b.hash(in)));
}
case "ak_encode" -> {
byte[] in = CryptoUtils.hexToBin(args[1]);
System.out.println(ApiEncoder.encode(ApiEncoder.Type.ACCOUNT_PUBKEY, in));
}
case "ed25519_pub" -> {
byte[] seed = CryptoUtils.hexToBin(args[1]);
System.out.println(CryptoUtils.binToHex(Ed25519.publicKey(seed)));
}
case "id_serialize" -> {
int tag = Integer.parseInt(args[1]);
byte[] val = CryptoUtils.hexToBin(args[2]);
Id id = new Id(Id.Tag.fromValue(tag), val);
System.out.println(CryptoUtils.binToHex(id.serialize()));
}
default -> {
System.out.println("Error: Unknown test suite or command: " + args[0]);
System.exit(1);
}
}
} catch (Exception e) {
System.err.println("Error: " + e.getMessage());
e.printStackTrace();
System.exit(1);
}
}
private static String ge_parity(String aHex, String bHex) throws IOException {
byte[] a = CryptoUtils.hexToBin(aHex);
byte[] b = CryptoUtils.hexToBin(bHex);
Ed25519.Scratch sc = new Ed25519.Scratch();
Ed25519.Ge p1 = Ed25519.scalarMulBase(a, sc);
Ed25519.Ge p2 = Ed25519.scalarMulBase(b, sc);
Ed25519.Ge p3 = new Ed25519.Ge();
Ed25519.ge_add(p3, p1, p2, sc);
byte[] res = Ed25519.compress(p3, sc);
p1.wipe(); p2.wipe(); p3.wipe(); sc.wipe();
return CryptoUtils.binToHex(res) + "|||";
}
private static String frombytes_parity(String workingPath) throws IOException {
Path testPath = Path.of(workingPath, "frombytes.test");
Path resPath = Path.of(workingPath, "frombytes.java.txt");
List<String> lines = Files.readAllLines(testPath);
List<String> results = new ArrayList<>();
for (String hex : lines) {
if (hex.trim().isEmpty()) continue;
byte[] in = CryptoUtils.hexToBin(hex);
long[] h = new long[10];
Ed25519.fe_frombytes(h, in);
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 10; i++) {
if (i > 0) sb.append(",");
sb.append(h[i]);
}
results.add(sb.toString());
}
Files.write(resPath, results);
return resPath.toString();
}
private static String femul_parity(String workingPath) throws IOException {
Path testPath = Path.of(workingPath, "femul.test");
Path resPath = Path.of(workingPath, "femul.java.txt");
List<String> lines = Files.readAllLines(testPath);
List<String> results = new ArrayList<>();
for (String line : lines) {
if (line.trim().isEmpty()) continue;
String[] parts = line.split("\\|");
byte[] a = CryptoUtils.hexToBin(parts[0]);
byte[] b = CryptoUtils.hexToBin(parts[1]);
long[] ha = new long[10], hb = new long[10];
Ed25519.fe_frombytes(ha, a);
Ed25519.fe_frombytes(hb, b);
long[] hr = new long[10];
Ed25519.fe_mul(hr, ha, hb, new long[19]);
byte[] out = Ed25519.fe_contract(hr);
results.add(CryptoUtils.binToHex(out));
}
Files.write(resPath, results);
return resPath.toString();
}
private static String smb_parity(String workingPath) throws IOException {
Path testPath = Path.of(workingPath, "smb.test");
Path resPath = Path.of(workingPath, "smb.java.txt");
List<String> lines = Files.readAllLines(testPath);
List<String> results = new ArrayList<>();
for (String hex : lines) {
if (hex.trim().isEmpty()) continue;
byte[] in = CryptoUtils.hexToBin(hex);
Ed25519.Scratch sc = new Ed25519.Scratch();
Ed25519.Ge p = Ed25519.scalarMulBase(in, sc);
byte[] out = Ed25519.compress(p, sc);
p.wipe(); sc.wipe();
results.add(CryptoUtils.binToHex(out));
}
Files.write(resPath, results);
return resPath.toString();
}
private static String reduce_parity(String workingPath) throws IOException {
Path testPath = Path.of(workingPath, "reduce.test");
Path resPath = Path.of(workingPath, "reduce.java.txt");
List<String> lines = Files.readAllLines(testPath);
List<String> results = new ArrayList<>();
for (String hex : lines) {
if (hex.trim().isEmpty()) continue;
byte[] in = CryptoUtils.hexToBin(hex);
byte[] s = Arrays.copyOf(in, 64);
Ed25519.reduceScalar(s);
byte[] out = Arrays.copyOf(s, 32);
results.add(CryptoUtils.binToHex(out));
}
Files.write(resPath, results);
return resPath.toString();
}
private static String fe_parity(String workingPath) throws IOException {
Path testPath = Path.of(workingPath, "fe.test");
Path resPath = Path.of(workingPath, "fe.java.txt");
List<String> lines = Files.readAllLines(testPath);
List<String> results = new ArrayList<>();
for (String hex : lines) {
if (hex.trim().isEmpty()) continue;
byte[] in = CryptoUtils.hexToBin(hex);
long[] h = new long[10];
Ed25519.fe_frombytes(h, in);
byte[] out = Ed25519.fe_contract(h);
results.add(CryptoUtils.binToHex(out));
}
Files.write(resPath, results);
return resPath.toString();
}
private static String base64(String workingPath) throws IOException {
Path testPath = Path.of(workingPath, "base64.test");
Path encPath = Path.of(workingPath, "base64.java.txt");
Path decPath = Path.of(workingPath, "base64.java.back");
byte[] rawB = Files.readAllBytes(testPath);
byte[] encB = Base64.getEncoder().encode(rawB);
Files.write(encPath, encB);
byte[] readE = Files.readAllBytes(encPath);
byte[] decB = Base64.getDecoder().decode(readE);
Files.write(decPath, decB);
return encPath.toString() + " " + decPath.toString();
}
private static String base58(String workingPath) throws IOException {
Path testPath = Path.of(workingPath, "base58.test");
Path encPath = Path.of(workingPath, "base58.java.txt");
Path decPath = Path.of(workingPath, "base58.java.back");
byte[] rawB = Files.readAllBytes(testPath);
String encS = Base58.encode(rawB);
Files.write(encPath, encS.getBytes());
byte[] readE = Files.readAllBytes(encPath);
String readS = new String(readE);
byte[] decB = Base58.decode(readS);
Files.write(decPath, decB);
return encPath.toString() + " " + decPath.toString();
}
private static String base58_check(String workingPath) throws IOException {
Path testPath = Path.of(workingPath, "base58_check.test");
Path encPath = Path.of(workingPath, "base58_check.java.txt");
Path decPath = Path.of(workingPath, "base58_check.java.back");
byte[] rawB = Files.readAllBytes(testPath);
String encS = Base58.checkEncode(rawB);
Files.write(encPath, encS.getBytes());
byte[] readE = Files.readAllBytes(encPath);
String readS = new String(readE);
byte[] decB = Base58.checkDecode(readS);
Files.write(decPath, decB);
return encPath.toString() + " " + decPath.toString();
}
private static String rlp(String workingPath) throws IOException {
Path testPath = Path.of(workingPath, "rlp.test");
Path resPath = Path.of(workingPath, "rlp.java.back");
byte[] encB = Files.readAllBytes(testPath);
RLP.RLP_Data decRLP = RLP.decode(encB);
RLP.RLP_List root = decRLP.asList();
RLP.RLP_List sub = root.getItems().get(1).asList();
byte[] anchor = sub.getItems().get(1).asItem().getBytes();
byte[] reEnc = RLP.encode(decRLP);
Files.write(resPath, reEnc);
return new String(anchor) + "|" + resPath.toString();
}
private static String rlp_stream(String workingPath) throws IOException {
Path testPath = Path.of(workingPath, "rlp_stream.test");
byte[] buffer = Files.readAllBytes(testPath);
List<String> hashes = new ArrayList<>();
int offset = 0;
while (offset < buffer.length) {
RLP.DecodeResult res = RLP.decodeFrom(buffer, offset);
hashes.add(Integer.toString(res.data().hashCode())); // Just to verify we got objects
offset += res.consumed();
}
return Integer.toString(hashes.size());
}
private static String rlp_fail(String workingPath) throws IOException {
Path testPath = Path.of(workingPath, "rlp_fail.test");
byte[] buffer = Files.readAllBytes(testPath);
try {
RLP.decode(buffer);
return "SUCCESS";
} catch (RLP.RLPException e) {
return "RLP_EXCEPTION: " + e.getMessage();
} catch (Exception e) {
return "OTHER_EXCEPTION: " + e.getClass().getSimpleName();
}
}
private static String gaju_format(String workingPath) throws IOException {
Path testPath = Path.of(workingPath, "gaju_format.test");
Path resPath = Path.of(workingPath, "gaju_format.java.txt");
List<String> lines = Files.readAllLines(testPath);
List<String> results = new ArrayList<>();
for (String line : lines) {
if (line.trim().isEmpty()) continue;
String[] p = line.split("\\|");
String op = p[0];
switch (op) {
case "amount" -> {
GajuFormat.Type style = GajuFormat.Type.valueOf(p[1]);
GajuFormat.Unit unit = GajuFormat.Unit.valueOf(p[2]);
char sep = p[3].charAt(0);
int span = Integer.parseInt(p[4]);
BigInteger val = new BigInteger(p[5]);
results.add(GajuFormat.amount(new GajuFormat.FormatSpec(style, unit, sep, span), val));
}
case "approx" -> {
GajuFormat.Type style = GajuFormat.Type.valueOf(p[1]);
GajuFormat.Unit unit = GajuFormat.Unit.valueOf(p[2]);
char sep = p[3].charAt(0);
int span = Integer.parseInt(p[4]);
BigInteger val = new BigInteger(p[5]);
int prec = Integer.parseInt(p[6]);
results.add(GajuFormat.approxAmount(new GajuFormat.FormatSpec(style, unit, sep, span), val, prec));
}
case "read" -> {
byte[] b = GajuFormat.read(p[1]);
results.add(new BigInteger(b).toString());
}
}
}
Files.write(resPath, results);
return resPath.toString();
}
private static String keccak256(String workingPath) throws IOException {
Path testPath = Path.of(workingPath, "keccak256.test");
Path resPath = Path.of(workingPath, "keccak256.java.txt");
byte[] input = Files.readAllBytes(testPath);
byte[] hash = Keccak256.hash(input);
Files.write(resPath, CryptoUtils.binToHex(hash).getBytes());
return resPath.toString();
}
private static String blake2b(String workingPath) throws IOException {
Path testPath = Path.of(workingPath, "blake2b.test");
Path resPath = Path.of(workingPath, "blake2b.java.txt");
byte[] input = Files.readAllBytes(testPath);
byte[] hash = Blake2b.hash(input);
Files.write(resPath, CryptoUtils.binToHex(hash).getBytes());
return resPath.toString();
}
private static String ed25519(String workingPath) throws IOException {
Path testPath = Path.of(workingPath, "ed25519.test");
Path resPath = Path.of(workingPath, "ed25519.java.txt");
List<String> lines = Files.readAllLines(testPath);
List<String> results = new ArrayList<>();
for (String line : lines) {
if (line.trim().isEmpty()) continue;
String[] parts = line.split("\\|");
byte[] seed = CryptoUtils.hexToBin(parts[0]);
byte[] msg = CryptoUtils.hexToBin(parts[1]);
byte[] pub = Ed25519.publicKey(seed);
byte[] sig = Ed25519.sign(seed, msg);
boolean verify = Ed25519.verify(pub, msg, sig);
results.add(CryptoUtils.binToHex(pub) + "|" + CryptoUtils.binToHex(sig) + "|" + verify);
CryptoUtils.wipe(seed);
CryptoUtils.wipe(msg);
CryptoUtils.wipe(pub);
CryptoUtils.wipe(sig);
}
Files.write(resPath, results);
return resPath.toString();
}
private static String ed25519_verify(String workingPath) throws IOException {
Path seedPath = Path.of(workingPath, "ed25519_seed.test");
Path msgPath = Path.of(workingPath, "ed25519_msg.test");
byte[] seed = CryptoUtils.hexToBin(Files.readString(seedPath).trim());
byte[] message = Files.readAllBytes(msgPath);
byte[] pub = Ed25519.publicKey(seed);
byte[] sig = Ed25519.sign(seed, message);
boolean verify = Ed25519.verify(pub, message, sig);
return CryptoUtils.binToHex(pub) + "|" + CryptoUtils.binToHex(sig) + "|" + verify;
}
private static String api_encode(String workingPath) throws IOException {
Path testPath = Path.of(workingPath, "api_encode.test");
Path resPath = Path.of(workingPath, "api_encode.java.txt");
List<String> lines = Files.readAllLines(testPath);
List<String> results = new ArrayList<>();
for (String line : lines) {
if (line.trim().isEmpty()) continue;
String[] p = line.split("\\|");
ApiEncoder.Type type = ApiEncoder.Type.valueOf(p[0]);
byte[] payload = CryptoUtils.hexToBin(p[1]);
results.add(ApiEncoder.encode(type, payload));
if (type == ApiEncoder.Type.ACCOUNT_SECKEY) {
CryptoUtils.wipe(payload);
}
}
Files.write(resPath, results);
return resPath.toString();
}
private static String id_serialization(String workingPath) throws IOException {
Path testPath = Path.of(workingPath, "id_serialization.test");
Path resPath = Path.of(workingPath, "id_serialization.java.txt");
List<String> lines = Files.readAllLines(testPath);
List<String> results = new ArrayList<>();
for (String line : lines) {
if (line.trim().isEmpty()) continue;
String[] p = line.split("\\|");
Id.Tag tag = Id.Tag.fromValue(Integer.parseInt(p[0]));
byte[] val = CryptoUtils.hexToBin(p[1]);
Id id = new Id(tag, val);
results.add(CryptoUtils.binToHex(id.serialize()));
}
Files.write(resPath, results);
return resPath.toString();
}
}
+588
View File
@@ -0,0 +1,588 @@
%%% @doc
%%% Gajumaru Java Lib Tester: gmt
%%%
%%% This module provides an inter-language test suite for the Gajumaru core
%%% Java libraries. It tests the Java implementation against the canonical
%%% Erlang implementation by generating random test vectors and comparing
%%% the outputs.
%%% @end
-module(gmt).
-vsn("0.1.0").
-author("Craig Everett <craigeverett@qpq.swiss>").
-copyright("Craig Everett <craigeverett@qpq.swiss>").
-license("LGPL-3.0-or-later").
-export([start/1]).
%%% Logic
mods() ->
#{"base64" => fun base64/0,
"base58" => fun base58/0,
"base58_check" => fun base58_check/0,
"rlp" => fun rlp/0,
"rlp_stream" => fun rlp_stream/0,
"rlp_fail" => fun rlp_fail/0,
"gaju_format" => fun gaju_format/0,
"keccak256" => fun keccak256/0,
"blake2b" => fun blake2b/0,
"ed25519" => fun ed25519/0,
"api_encode" => fun api_encode/0,
"id_serialization" => fun id_serialization/0,
"fe_parity" => fun fe_parity/0,
"reduce_parity" => fun reduce_parity/0,
"smb_parity" => fun smb_parity/0,
"femul_parity" => fun femul_parity/0,
"frombytes_parity" => fun frombytes_parity/0,
"ge_parity" => fun ge_parity/0}.
start([]) ->
Tests = mods(),
ok = run(Tests),
zx:silent_stop();
start(["list"]) ->
ok = io:format("Available tests:~n"),
ok = lists:foreach(fun display/1, maps:keys(mods())),
zx:silent_stop();
start(Mods) ->
Available = mods(),
Tests = maps:with(Mods, Available),
ok =
case maps:size(Tests) =:= length(Mods) of
true ->
run(Tests);
false ->
NotMods = lists:subtract(Mods, maps:keys(Available)),
ok = io:format("The following arguments are not testable module names:~n"),
lists:foreach(fun display/1, NotMods)
end,
zx:silent_stop().
display(Name) ->
io:format(" ~ts~n", [Name]).
run(Tests) ->
{ok, Cwd} = file:get_cwd(),
ok =
case filename:basename(Cwd) of
"test" -> file:set_cwd("..");
_ -> ok
end,
ok = clean(),
ok = build(),
Results = maps:map(fun run/2, Tests),
io:format("~nFinal Results:~n ~tp~n", [Results]).
run(Name, Test) ->
ok = io:format("~nRunning: ~ts...~n", [Name]),
Test().
clean() ->
Temp = "test/temp",
lists:foreach(fun(D) -> ok = clean(D) end, [Temp]).
clean(Dir) ->
case file:del_dir_r(Dir) of
ok -> ok;
{error, enoent} -> ok;
Error -> Error
end.
build() ->
Out = os:cmd("bin/compile"),
io:format("Compile: ~ts", [Out]).
temp_dir() ->
{ok, Cwd} = file:get_cwd(),
filename:join(Cwd, "test/temp").
trim(S) ->
Unprintable = fun(C) -> C =< 32 end,
lists:reverse(lists:dropwhile(Unprintable, lists:reverse(lists:dropwhile(Unprintable, S)))).
%%% Test Modules
base64() ->
Temp = temp_dir(),
TestFile = filename:join(Temp, "base64.test"),
ConvFile = filename:join(Temp, "base64.erlang.txt"),
ok = filelib:ensure_dir(TestFile),
ok = file:write_file(TestFile, rand:bytes(rand:uniform(5000))),
{ok, B} = file:read_file(TestFile),
Base64 = base64:encode(B),
ok = file:write_file(ConvFile, Base64),
Run = "bin/run base64 " ++ Temp,
Out = trim(os:cmd(Run)),
[JEnc, JDec] = string:split(Out, " "),
{ok, EEncB} = file:read_file(ConvFile),
{ok, JEncB} = file:read_file(JEnc),
EHash = crypto:hash(sha512, EEncB),
JHash = crypto:hash(sha512, JEncB),
{ok, EDecB} = file:read_file(TestFile),
{ok, JDecB} = file:read_file(JDec),
EBinHash = crypto:hash(sha512, EDecB),
JBinHash = crypto:hash(sha512, JDecB),
EHash =:= JHash andalso EBinHash =:= JBinHash.
base58() ->
Temp = temp_dir(),
TestFile = filename:join(Temp, "base58.test"),
ConvFile = filename:join(Temp, "base58.erlang.txt"),
ok = filelib:ensure_dir(TestFile),
ok = file:write_file(TestFile, rand:bytes(rand:uniform(5000))),
{ok, B} = file:read_file(TestFile),
Base58 = base58:binary_to_base58(B),
ok = file:write_file(ConvFile, Base58),
Run = "bin/run base58 " ++ Temp,
Out = trim(os:cmd(Run)),
[JEnc, JDec] = string:split(Out, " "),
{ok, EEncB} = file:read_file(ConvFile),
{ok, JEncB} = file:read_file(JEnc),
EHash = crypto:hash(sha512, EEncB),
JHash = crypto:hash(sha512, JEncB),
{ok, EDecB} = file:read_file(TestFile),
{ok, JDecB} = file:read_file(JDec),
EBinHash = crypto:hash(sha512, EDecB),
JBinHash = crypto:hash(sha512, JDecB),
EHash =:= JHash andalso EBinHash =:= JBinHash.
base58_check() ->
Temp = temp_dir(),
TestFile = filename:join(Temp, "base58_check.test"),
ConvFile = filename:join(Temp, "base58_check.erlang.txt"),
ok = filelib:ensure_dir(TestFile),
ok = file:write_file(TestFile, rand:bytes(rand:uniform(5000))),
{ok, B} = file:read_file(TestFile),
Checksum = binary:part(crypto:hash(sha256, crypto:hash(sha256, B)), 0, 4),
Base58C = base58:binary_to_base58(<<B/binary, Checksum/binary>>),
ok = file:write_file(ConvFile, Base58C),
Run = "bin/run base58_check " ++ Temp,
Out = trim(os:cmd(Run)),
[JEnc, JDec] = string:split(Out, " "),
{ok, EEncB} = file:read_file(ConvFile),
{ok, JEncB} = file:read_file(JEnc),
EHash = crypto:hash(sha512, EEncB),
JHash = crypto:hash(sha512, JEncB),
{ok, EDecB} = file:read_file(TestFile),
{ok, JDecB} = file:read_file(JDec),
EBinHash = crypto:hash(sha512, EDecB),
JBinHash = crypto:hash(sha512, JDecB),
EHash =:= JHash andalso EBinHash =:= JBinHash.
rlp() ->
Temp = temp_dir(),
Anchor = <<"I thought what I'd do was, I'd pretend I was one of those deaf-mutes.">>,
Data =
[rand:bytes(rand:uniform(20)),
[rand:bytes(rand:uniform(20)),
Anchor,
rand:bytes(rand:uniform(20)),
rand:bytes(rand:uniform(5000))],
rand:bytes(rand:uniform(2000))],
RLP = gmser_rlp:encode(Data),
RLP_File = filename:join(Temp, "rlp.test"),
ok = filelib:ensure_dir(RLP_File),
ok = file:write_file(RLP_File, RLP),
Run = "bin/run rlp " ++ Temp,
Out = trim(os:cmd(Run)),
case string:split(Out, "|") of
[Found, JPath] ->
JPathTrimmed = trim(JPath),
{ok, EEncB} = file:read_file(RLP_File),
case file:read_file(JPathTrimmed) of
{ok, JEncB} ->
EHash = crypto:hash(sha512, EEncB),
JHash = crypto:hash(sha512, JEncB),
EHash =:= JHash andalso Found =:= unicode:characters_to_list(Anchor);
{error, R} ->
ok = io:format("Failed to read RLP Java result: ~tp (Path: ~tp)~n", [R, JPathTrimmed]),
false
end;
_ ->
ok = io:format("RLP output mismatch: ~tp~n", [Out]),
false
end.
rlp_stream() ->
Temp = temp_dir(),
TestFile = filename:join(Temp, "rlp_stream.test"),
ok = filelib:ensure_dir(TestFile),
Data = [rand:bytes(rand:uniform(100)) || _ <- lists:seq(1, 10)],
RLP = << <<(gmser_rlp:encode(D))/binary>> || D <- Data >>,
ok = file:write_file(TestFile, RLP),
Run = "bin/run rlp_stream " ++ Temp,
Out = trim(os:cmd(Run)),
Out =:= "10".
rlp_fail() ->
Temp = temp_dir(),
TestFile = filename:join(Temp, "rlp_fail.test"),
ok = filelib:ensure_dir(TestFile),
Data = [<<"item1">>, <<"item2">>],
FullRLP = gmser_rlp:encode(Data),
TruncRLP = binary:part(FullRLP, 0, byte_size(FullRLP) - 2),
ok = file:write_file(TestFile, TruncRLP),
Run = "bin/run rlp_fail " ++ Temp,
Out = trim(os:cmd(Run)),
string:prefix(Out, "RLP_EXCEPTION:") =/= nomatch.
gaju_format() ->
Temp = temp_dir(),
TestFile = filename:join(Temp, "gaju_format.test"),
ok = filelib:ensure_dir(TestFile),
Cases = [gen_case() || _ <- lists:seq(1, 100)],
Lines = [serialize_case(C) || C <- Cases],
ok = file:write_file(TestFile, unicode:characters_to_binary([[L, "\n"] || L <- Lines])),
Run = "bin/run gaju_format " ++ Temp,
RawOut = os:cmd(Run),
JavaResPath = trim(RawOut),
case file:read_file(JavaResPath) of
{ok, ResContent} ->
JavaResults = string:split(trim(unicode:characters_to_list(ResContent)), "\n", all),
length(Cases) =:= length(JavaResults) andalso compare_results(Cases, JavaResults);
{error, Reason} ->
Format = "Failed to read Java results from: ~tp (Reason: ~tp)~nRaw Output: ~ts~n",
io:format(Format, [JavaResPath, Reason, RawOut]),
false
end.
keccak256() ->
Temp = temp_dir(),
TestFile = filename:join(Temp, "keccak256.test"),
ResFile = filename:join(Temp, "keccak256.erlang.txt"),
Data = rand:bytes(rand:uniform(5000)),
ok = filelib:ensure_dir(TestFile),
ok = file:write_file(TestFile, Data),
Hash = sha3:hash(256, Data),
HexHash = bin_to_hex(Hash),
ok = file:write_file(ResFile, HexHash),
Run = "bin/run keccak256 " ++ Temp,
Out = trim(os:cmd(Run)),
{ok, JOut} = file:read_file(trim(Out)),
unicode:characters_to_list(JOut) =:= HexHash.
blake2b() ->
Temp = temp_dir(),
TestFile = filename:join(Temp, "blake2b.test"),
ResFile = filename:join(Temp, "blake2b.erlang.txt"),
Data = rand:bytes(rand:uniform(5000)),
ok = filelib:ensure_dir(TestFile),
ok = file:write_file(TestFile, Data),
{ok, Hash} = eblake2:blake2b(32, Data),
HexHash = bin_to_hex(Hash),
ok = file:write_file(ResFile, HexHash),
Run = "bin/run blake2b " ++ Temp,
Out = trim(os:cmd(Run)),
{ok, JOut} = file:read_file(trim(Out)),
unicode:characters_to_list(JOut) =:= HexHash.
ed25519() ->
Temp = temp_dir(),
TestFile = filename:join(Temp, "ed25519.test"),
ok = filelib:ensure_dir(TestFile),
Cases = [{rand:bytes(32), rand:bytes(rand:uniform(100))} || _ <- lists:seq(1, 20)],
ok = file:write_file(TestFile, [[bin_to_hex(S), "|", bin_to_hex(M), "\n"] || {S, M} <- Cases]),
Sequence =
fun({S, M}) ->
#{public := Pub} = ecu_eddsa:sign_seed_keypair(S),
Sig = ecu_eddsa:sign_detached(M, S),
V = ecu_eddsa:sign_verify_detached(Sig, M, Pub),
lists:flatten(io_lib:format("~s|~s|~p", [bin_to_hex(Pub), bin_to_hex(Sig), V]))
end,
Expected = lists:map(Sequence, Cases),
Run = "bin/run ed25519 " ++ Temp,
Out = trim(os:cmd(Run)),
{ok, JOutContent} = file:read_file(trim(Out)),
JResults = [trim(L) || L <- string:split(trim(unicode:characters_to_list(JOutContent)), "\n", all)],
same_same(Expected, JResults).
api_encode() ->
Temp = temp_dir(),
TestFile = filename:join(Temp, "api_encode.test"),
Types = [account_pubkey, account_seckey, tx_hash, contract_pubkey, signature, commitment, peer_pubkey],
Cases = [{T, rand:bytes(type_size(T))} || T <- Types],
Lines = [io_lib:format("~ts|~ts", [string:uppercase(atom_to_list(T)), bin_to_hex(B)]) || {T, B} <- Cases],
ok = filelib:ensure_dir(TestFile),
ok = file:write_file(TestFile, unicode:characters_to_binary([[L, "\n"] || L <- Lines])),
Expected = [flatten(gmser_api_encoder:encode(T, B)) || {T, B} <- Cases],
Run = "bin/run api_encode " ++ Temp,
Out = trim(os:cmd(Run)),
{ok, JOutContent} = file:read_file(trim(Out)),
JResults = string:split(trim(unicode:characters_to_list(JOutContent)), "\n", all),
same_same(Expected, JResults).
type_size(signature) -> 64;
type_size(_) -> 32.
id_serialization() ->
Temp = temp_dir(),
TestFile = filename:join(Temp, "id_serialization.test"),
Tags = [1, 2, 3, 5, 6, 7, 9],
Cases = [{Tag, rand:bytes(32)} || Tag <- Tags],
Lines = [io_lib:format("~b|~ts", [Tag, bin_to_hex(B)]) || {Tag, B} <- Cases],
ok = filelib:ensure_dir(TestFile),
ok = file:write_file(TestFile, unicode:characters_to_binary([[L, "\n"] || L <- Lines])),
Expected = [bin_to_hex(gmser_id:encode(gmser_id:decode(<<T:8, B/binary>>))) || {T, B} <- Cases],
Run = "bin/run id_serialization " ++ Temp,
Out = trim(os:cmd(Run)),
{ok, JOutContent} = file:read_file(trim(Out)),
JResults = string:split(trim(unicode:characters_to_list(JOutContent)), "\n", all),
same_same(Expected, JResults).
fe_parity() ->
Temp = temp_dir(),
TestFile = filename:join(Temp, "fe.test"),
ok = filelib:ensure_dir(TestFile),
% Mask to 255 bits to avoid bit-255 sign bit ambiguity in fe_frombytes
RandomLittleFingers =
fun() ->
<<B:256/little>> = rand:bytes(32),
bin_to_hex(<<(B band ((1 bsl 255) - 1)):256/little>>)
end,
Inputs = [RandomLittleFingers() || _ <- lists:seq(1, 100)],
ok = file:write_file(TestFile, unicode:characters_to_binary([[I, "\n"] || I <- Inputs])),
Run = "bin/run fe_parity " ++ Temp,
Out = trim(os:cmd(Run)),
{ok, JOutContent} = file:read_file(trim(Out)),
JResults = string:split(trim(unicode:characters_to_list(JOutContent)), "\n", all),
P = ecu_ed25519:p(),
Pee =
fun(I) ->
<<B:256/little>> = hex_to_bin(I),
bin_to_hex(pack_p(B rem P))
end,
Expected = lists:map(Pee, Inputs),
same_same(Expected, JResults).
reduce_parity() ->
Temp = temp_dir(),
TestFile = filename:join(Temp, "reduce.test"),
ok = filelib:ensure_dir(TestFile),
Inputs = [rand:bytes(64) || _ <- lists:seq(1, 100)],
ok = file:write_file(TestFile, unicode:characters_to_binary([[bin_to_hex(I), "\n"] || I <- Inputs])),
Run = "bin/run reduce_parity " ++ Temp,
Out = trim(os:cmd(Run)),
{ok, JOutContent} = file:read_file(trim(Out)),
JResults = string:split(trim(unicode:characters_to_list(JOutContent)), "\n", all),
Expected = [bin_to_hex(ecu_ed25519:scalar_reduce(I)) || I <- Inputs],
same_same(Expected, JResults).
smb_parity() ->
Temp = temp_dir(),
TestFile = filename:join(Temp, "smb.test"),
ok = filelib:ensure_dir(TestFile),
Inputs = [<<(rand:bytes(31))/binary, 0>> || _ <- lists:seq(1, 10)],
ok = file:write_file(TestFile, unicode:characters_to_binary([[bin_to_hex(I), "\n"] || I <- Inputs])),
Run = "bin/run smb_parity " ++ Temp,
Out = trim(os:cmd(Run)),
{ok, JOutContent} = file:read_file(trim(Out)),
JResults = string:split(trim(unicode:characters_to_list(JOutContent)), "\n", all),
Expected = [bin_to_hex(ecu_ed25519:compress(ecu_ed25519:scalar_mul_base_noclamp(I))) || I <- Inputs],
same_same(Expected, JResults).
femul_parity() ->
Temp = temp_dir(),
TestFile = filename:join(Temp, "femul.test"),
ok = filelib:ensure_dir(TestFile),
% Mask to 255 bits
Gen = fun() -> <<B:256/little>> = rand:bytes(32), << (B band ((1 bsl 255) - 1)):256/little >> end,
Inputs = [{Gen(), Gen()} || _ <- lists:seq(1, 10)],
Lines = [bin_to_hex(A) ++ "|" ++ bin_to_hex(B) || {A, B} <- Inputs],
ok = file:write_file(TestFile, unicode:characters_to_binary([[L, "\n"] || L <- Lines])),
Run = "bin/run femul_parity " ++ Temp,
Out = trim(os:cmd(Run)),
{ok, JOutContent} = file:read_file(trim(Out)),
JResults = string:split(trim(unicode:characters_to_list(JOutContent)), "\n", all),
ExpectedBin = [bin_to_hex(pack_p(ecu_ed25519:f_mul(binary:decode_unsigned(A, little), binary:decode_unsigned(B, little)))) || {A, B} <- Inputs],
same_same3(Inputs, ExpectedBin, JResults).
same_same3(AB, L1, L2) ->
same_same3(AB, L1, L2, true).
same_same3([_ | R1], [S | R2], [S | R3], Result) ->
same_same3(R1, R2, R3, Result);
same_same3([{A, B} | R1], [E | R2], [J | R3], _) ->
ok = io:format("A: ~ts~nB: ~ts~nE: ~ts~nJ: ~ts~n", [bin_to_hex(A), bin_to_hex(B), E, J]),
same_same3(R1, R2, R3, false);
same_same3([], [], [], Result) ->
Result.
same_same(L1, L2) ->
same_same(L1, L2, true).
same_same([S | R1], [S | R2], Result) ->
same_same(R1, R2, Result);
same_same([E | R1], [J | R2], _) ->
ok = io:format("Mismatch!\nE: ~ts\nJ: ~ts\n", [E, J]),
same_same(R1, R2, false);
same_same([], [], Result) ->
Result.
frombytes_parity() ->
Temp = temp_dir(),
TestFile = filename:join(Temp, "frombytes.test"),
ok = filelib:ensure_dir(TestFile),
Inputs = [rand:bytes(32) || _ <- lists:seq(1, 10)],
ok = file:write_file(TestFile, unicode:characters_to_binary([[bin_to_hex(I), "\n"] || I <- Inputs])),
Run = "bin/run frombytes_parity " ++ Temp,
Out = trim(os:cmd(Run)),
{ok, JOutContent} = file:read_file(trim(Out)),
JResults = string:split(trim(unicode:characters_to_list(JOutContent)), "\n", all),
Limb =
fun(I) ->
Val = binary:decode_unsigned(I, little),
Limbs =
fun
F(V, Idx) when Idx < 10 ->
Size =
case Idx rem 2 =:= 0 of
true -> 26;
false -> 25
end,
L = V band ((1 bsl Size) - 1),
[L | F(V bsr Size, Idx + 1)];
F(_, _) ->
[]
end,
string:join([integer_to_list(L) || L <- Limbs(Val, 0)], ",")
end,
Expected = lists:map(Limb, Inputs),
same_same(Expected, JResults).
ge_parity() ->
A_bin = <<1, 2, 3, 0:232>>,
B_bin = <<4, 5, 6, 0:232>>,
P1_erl = ecu_ed25519:scalar_mul_base_noclamp(A_bin),
P2_erl = ecu_ed25519:scalar_mul_base_noclamp(B_bin),
P3_erl = ecu_ed25519:p_add(P1_erl, P2_erl),
E_comp = bin_to_hex(ecu_ed25519:compress(P3_erl)),
Run = "bin/run ge_parity " ++ bin_to_hex(A_bin) ++ " " ++ bin_to_hex(B_bin),
Out = trim(os:cmd(Run)),
case string:split(Out, "|||") of
[JX | _] -> E_comp =:= trim(JX);
_ -> false
end.
pack_p(V) ->
P = (1 bsl 255) - 19,
V_pos = if V < 0 -> V + P; true -> V rem P end,
Enc = binary:encode_unsigned(V_pos, little),
Size = byte_size(Enc),
if Size < 32 -> <<Enc/binary, 0:(8*(32-Size))>>; true -> Enc end.
bin_to_hex(Bin) ->
lists:flatten([io_lib:format("~2.16.0b", [X]) || X <- binary_to_list(Bin)]).
hex_to_bin(S) ->
hex_to_bin(S, []).
hex_to_bin([], Acc) ->
list_to_binary(lists:reverse(Acc));
hex_to_bin([X,Y|T], Acc) ->
{ok, [V], []} = io_lib:fread("~16u", [X,Y]),
hex_to_bin(T, [V | Acc]).
%%% Generatorators
gen_case() ->
Ops = [amount, approx, read],
Op = lists:nth(rand:uniform(length(Ops)), Ops),
gen_case(Op).
gen_case(read) ->
Pucks = random_pucks(12),
Style = random_style(),
{read, hz_format:amount(gaju, Style, Pucks), Pucks};
gen_case(amount) ->
Unit = random_unit(),
Pucks =
case Unit of
gaju -> random_pucks(12);
puck -> random_pucks(8)
end,
Style = random_style(),
Sep = lists:nth(rand:uniform(2), [$,, $_]),
Span = rand:uniform(4),
Amount = hz_format:amount(Unit, hz_style(Style, Sep, Span), Pucks),
{amount, Style, Unit, Sep, Span, Pucks, Amount};
gen_case(approx) ->
Pucks = random_pucks(12),
Sep = lists:nth(rand:uniform(2), [$,, $_]),
Span = rand:uniform(2) + 2,
Prec = rand:uniform(18),
Approx = hz_format:approx_amount({Sep, Span}, Prec, Pucks),
{approx, us, gaju, Sep, Span, Pucks, Prec, Approx}.
random_pucks(MaxBytes) ->
Bytes = rand:bytes(rand:uniform(MaxBytes)),
crypto:bytes_to_integer(Bytes).
random_style() ->
lists:nth(rand:uniform(4), [us, jp, metric, legacy]).
random_unit() ->
lists:nth(rand:uniform(2), [gaju, puck]).
hz_style(us, Sep, Span) -> {Sep, Span};
hz_style(Style, _, _) -> Style.
serialize_case({amount, Style, Unit, Sep, Span, Pucks, _}) ->
FStyle = string:uppercase(atom_to_list(Style)),
FUnit = string:uppercase(atom_to_list(Unit)),
Stuff = [FStyle, FUnit, Sep, Span, Pucks],
io_lib:format("amount|~ts|~ts|~c|~b|~b", Stuff);
serialize_case({approx, Style, Unit, Sep, Span, Pucks, Prec, _}) ->
FStyle = string:uppercase(atom_to_list(Style)),
FUnit = string:uppercase(atom_to_list(Unit)),
Stuff = [FStyle, FUnit, Sep, Span, Pucks, Prec],
io_lib:format("approx|~ts|~ts|~c|~b|~b|~b", Stuff);
serialize_case({read, Input, _}) ->
InputStr = unicode:characters_to_list(Input),
io_lib:format("read|~ts", [InputStr]).
compare_results([], []) ->
true;
compare_results([Case | Cases], [Result | Results]) ->
case check_case(Case, Result) of
true ->
compare_results(Cases, Results);
false ->
io:format("Parity failure!~nCase: ~tp~nJava Result: ~tp~n", [Case, Result]),
false
end.
check_case({amount, _, _, _, _, _, Expected}, Result) ->
flatten(Expected) =:= flatten(Result);
check_case({approx, _, _, _, _, _, _, Expected}, Result) ->
flatten(Expected) =:= flatten(Result);
check_case({read, _, Expected}, Result) ->
integer_to_list(Expected) =:= flatten(Result).
flatten(B) when is_binary(B) -> unicode:characters_to_list(B);
flatten(L) when is_list(L) -> unicode:characters_to_list(L).
@@ -6,7 +6,8 @@
{author,"Craig Everett"}.
{desc,"An inter-language test suite for Gajumaru core libs"}.
{package_id,{"qpq","gm_libtester",{0,1,0}}}.
{deps,[{"otpr","hakuzaru",{0,9,1}},
{deps,[{"otpr","sha3",{0,1,4}},
{"otpr","hakuzaru",{0,9,1}},
{"otpr","getopt",{1,0,2}},
{"otpr","zj",{1,1,0}},
{"otpr","ec_utils",{1,0,0}},