Serialization and Ed25519 #2
@@ -6,9 +6,12 @@ Thumbs.db
|
||||
.netrwhist
|
||||
.nvimlog
|
||||
.idea/
|
||||
.artifacts/
|
||||
*.iml
|
||||
.gradle/
|
||||
local.properties
|
||||
Captures/
|
||||
.externalNativeBuild/
|
||||
temp
|
||||
erl_crash.dump
|
||||
*.class
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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/"
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
@@ -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) {}
|
||||
}
|
||||
+1
@@ -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 {
|
||||
-8
@@ -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
|
||||
@@ -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.
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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}},
|
||||
Reference in New Issue
Block a user