Compare commits
25
Commits
uw-asn1
...
64c592285a
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
64c592285a | ||
|
|
954d8760ac | ||
|
|
20e462ec03 | ||
|
|
11516b1cca | ||
|
|
c1c13213c1 | ||
|
|
5407431566 | ||
|
|
ea7a013abe | ||
|
|
ddc811b63d | ||
|
|
c21f399b69 | ||
|
|
97bb833d35 | ||
|
|
77dea4f3cf | ||
|
|
7493b473a7 | ||
|
|
3d3e90a6c6 | ||
|
|
a4ca21593b | ||
|
|
b751b46edc | ||
|
|
b95ba8a88f | ||
|
|
1de3608c9b | ||
|
|
4019af58e9 | ||
|
|
ea90d9d3ab | ||
|
|
78b5fb1512 | ||
|
|
f99e081160 | ||
|
|
11cdfc4569 | ||
|
|
8cce885721 | ||
|
|
8cff34abb4 | ||
|
|
366c6157af |
@@ -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,69 +0,0 @@
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Base64;
|
||||
|
||||
import swiss.qpq.gajumaru.core.encoding.Base58;
|
||||
|
||||
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 "rlp" -> {
|
||||
System.out.print(rlp(args[1]));
|
||||
}
|
||||
}
|
||||
} catch (IOException 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[] rawBytes = Files.readAllBytes(testPath);
|
||||
byte[] encBytes = Base64.getEncoder().encode(rawBytes);
|
||||
Files.write(encPath, encBytes);
|
||||
byte[] readEncBytes = Files.readAllBytes(encPath);
|
||||
byte[] decBytes = Base64.getDecoder().decode(readEncBytes);
|
||||
Files.write(decPath, decBytes);
|
||||
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[] rawBytes = Files.readAllBytes(testPath);
|
||||
String encString = Base58.encode(rawBytes);
|
||||
Files.write(encPath, encString.getBytes());
|
||||
byte[] readEncBytes = Files.readAllBytes(encPath);
|
||||
String readEncString = new String(readEncBytes);
|
||||
byte[] decBytes = Base58.decode(readEncString);
|
||||
Files.write(decPath, decBytes);
|
||||
return encPath.toString() + " " + decPath.toString();
|
||||
}
|
||||
|
||||
private static String rlp(String workingPath) throws IOException {
|
||||
Path testPath = Path.of(workingPath, "rlp.test");
|
||||
Path encPath = Path.of(workingPath, "rlp.java.back");
|
||||
byte[] encBytes = Files.readAllBytes(testPath);
|
||||
RLP_Data decRLP = RLP.decode(encBytes),
|
||||
|
||||
Files.write(decPath, decBytes);
|
||||
return encPath.toString() + " " + decPath.toString();
|
||||
}
|
||||
}
|
||||
@@ -1,191 +0,0 @@
|
||||
/*
|
||||
* 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.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
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 abstract static class RLP_Data {}
|
||||
|
||||
public static final class RLP_Item extends RLP_Data {
|
||||
public final byte[] bytes;
|
||||
public RLP_Item(byte[] bytes) {
|
||||
this.bytes = bytes != null ? bytes : new byte[0];
|
||||
}
|
||||
}
|
||||
|
||||
public static final class RLP_List extends RLP_Data {
|
||||
public final List<RLP_Data> items;
|
||||
public RLP_List(List<RLP_Data> items) {
|
||||
this.items = items != null ? items : new ArrayList<>();
|
||||
}
|
||||
}
|
||||
|
||||
private RLP() {}
|
||||
|
||||
public static byte[] encode(RLP_Data data) {
|
||||
if (data instanceof RLP_Item item) {
|
||||
return encodeItem(item.bytes);
|
||||
} else if (data instanceof RLP_List list) {
|
||||
return encodeList(list.items);
|
||||
}
|
||||
throw new IllegalArgumentException("Unsupported RLP type");
|
||||
}
|
||||
|
||||
private static byte[] encodeItem(byte[] bytes) {
|
||||
if (bytes.length == 1 && (bytes[0] & 0xFF) <= 0x7F) {
|
||||
return bytes;
|
||||
}
|
||||
return prefixData(bytes, 0x80, 0xB7);
|
||||
}
|
||||
|
||||
private static byte[] encodeList(List<RLP_Data> items) {
|
||||
if (items.isEmpty()) {
|
||||
return new byte[] { (byte) 0xC0 };
|
||||
}
|
||||
|
||||
// Pass 1: Encode all sub-elements and compute exact combined byte length
|
||||
byte[][] encodedChildren = new byte[items.size()][];
|
||||
int totalPayloadLength = 0;
|
||||
for (int i = 0; i < items.size(); i++) {
|
||||
encodedChildren[i] = encode(items.get(i));
|
||||
totalPayloadLength += encodedChildren[i].length;
|
||||
}
|
||||
|
||||
// Pass 2: Generate the structural frame list marker
|
||||
byte[] prefix = prefixLength(totalPayloadLength, 0xC0, 0xF7);
|
||||
|
||||
// Pass 3: Flatten all segments directly into a single linear allocation
|
||||
byte[] result = new byte[prefix.length + totalPayloadLength];
|
||||
System.arraycopy(prefix, 0, result, 0, prefix.length);
|
||||
|
||||
int writePtr = prefix.length;
|
||||
for (byte[] child : encodedChildren) {
|
||||
System.arraycopy(child, 0, result, writePtr, child.length);
|
||||
writePtr += child.length;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static byte[] prefixData(byte[] payload, int shortOffset, int longOffset) {
|
||||
byte[] prefix = prefixLength(payload.length, shortOffset, longOffset);
|
||||
byte[] result = new byte[prefix.length + payload.length];
|
||||
System.arraycopy(prefix, 0, result, 0, prefix.length);
|
||||
System.arraycopy(payload, 0, result, prefix.length, payload.length);
|
||||
return result;
|
||||
}
|
||||
|
||||
private static byte[] prefixLength(int length, int shortOffset, int longOffset) {
|
||||
if (length <= 55) {
|
||||
return new byte[] { (byte) (shortOffset + length) };
|
||||
}
|
||||
byte[] lengthBytes = intToBigEndian(length);
|
||||
byte[] prefix = new byte[1 + lengthBytes.length];
|
||||
prefix[0] = (byte) (longOffset + lengthBytes.length);
|
||||
System.arraycopy(lengthBytes, 0, prefix, 1, lengthBytes.length);
|
||||
return prefix;
|
||||
}
|
||||
|
||||
private static byte[] intToBigEndian(int val) {
|
||||
if (val == 0) return new byte[0];
|
||||
int size = (Integer.numberOfLeadingZeros(val) == 32) ? 1 : (32 - Integer.numberOfLeadingZeros(val) + 7) / 8;
|
||||
byte[] sigBytes = new byte[size];
|
||||
for (int i = size - 1; i >= 0; i--) {
|
||||
sigBytes[i] = (byte) (val & 0xFF);
|
||||
val >>>= 8;
|
||||
}
|
||||
return sigBytes;
|
||||
}
|
||||
|
||||
|
||||
public static RLP_Data decode(byte[] bytes) {
|
||||
if (bytes == null || bytes.length == 0) {
|
||||
return new RLP_Item(new byte[0]);
|
||||
}
|
||||
return decodeRange(bytes, 0, bytes.length);
|
||||
}
|
||||
|
||||
private static RLP_Data decodeRange(byte[] bytes, int start, int end) {
|
||||
int prefix = bytes[start] & 0xFF;
|
||||
|
||||
if (prefix <= 0x7F) {
|
||||
return new RLP_Item(new byte[] { (byte) prefix });
|
||||
}
|
||||
if (prefix <= 0xB7) {
|
||||
return new RLP_Item(Arrays.copyOfRange(bytes, start + 1, start + 1 + (prefix - 0x80)));
|
||||
}
|
||||
if (prefix <= 0xBF) {
|
||||
int lenLen = prefix - 0xB7;
|
||||
int len = bigEndianToInt(bytes, start + 1, start + 1 + lenLen);
|
||||
return new RLP_Item(Arrays.copyOfRange(bytes, start + 1 + lenLen, start + 1 + lenLen + len));
|
||||
}
|
||||
if (prefix <= 0xF7) {
|
||||
int listLen = prefix - 0xC0;
|
||||
return parseListSequence(bytes, start + 1, start + 1 + listLen);
|
||||
}
|
||||
|
||||
int lenLen = prefix - 0xF7;
|
||||
int listLen = bigEndianToInt(bytes, start + 1, start + 1 + lenLen);
|
||||
return parseListSequence(bytes, start + 1 + lenLen, start + 1 + lenLen + listLen);
|
||||
}
|
||||
|
||||
private static RLP_List parseListSequence(byte[] bytes, int cursor, int limit) {
|
||||
List<RLP_Data> elements = new ArrayList<>();
|
||||
while (cursor < limit) {
|
||||
int itemStart = cursor;
|
||||
int prefix = bytes[cursor] & 0xFF;
|
||||
int elementTotalSize;
|
||||
|
||||
if (prefix <= 0x7F) {
|
||||
elementTotalSize = 1;
|
||||
} else if (prefix <= 0xB7) {
|
||||
elementTotalSize = 1 + (prefix - 0x80);
|
||||
} else if (prefix <= 0xBF) {
|
||||
int lenLen = prefix - 0xB7;
|
||||
elementTotalSize = 1 + lenLen + bigEndianToInt(bytes, itemStart + 1, itemStart + 1 + lenLen);
|
||||
} else if (prefix <= 0xF7) {
|
||||
elementTotalSize = 1 + (prefix - 0xC0);
|
||||
} else {
|
||||
int lenLen = prefix - 0xF7;
|
||||
elementTotalSize = 1 + lenLen + bigEndianToInt(bytes, itemStart + 1, itemStart + 1 + lenLen);
|
||||
}
|
||||
|
||||
elements.add(decodeRange(bytes, itemStart, itemStart + elementTotalSize));
|
||||
cursor += elementTotalSize;
|
||||
}
|
||||
return new RLP_List(elements);
|
||||
}
|
||||
|
||||
private static int bigEndianToInt(byte[] bytes, int start, int end) {
|
||||
int result = 0;
|
||||
for (int i = start; i < end; i++) {
|
||||
result = (result << 8) | (bytes[i] & 0xFF);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -1,15 +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`.
|
||||
@@ -1,135 +0,0 @@
|
||||
%%% @doc
|
||||
%%% Gajumaru Java Lib Tester: gmt
|
||||
%%% @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]).
|
||||
|
||||
|
||||
mods() ->
|
||||
#{"base64" => fun base64/0,
|
||||
"base58" => fun base58/0,
|
||||
"rlp" => fun rlp/0}.
|
||||
|
||||
|
||||
-spec start(ArgV) -> ok
|
||||
when ArgV :: [string()].
|
||||
|
||||
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) ->
|
||||
Tests = maps:with(Mods, mods()),
|
||||
ok =
|
||||
case maps:size(Tests) =:= length(Mods) of
|
||||
true ->
|
||||
run(Tests);
|
||||
false ->
|
||||
NotMods = lists:subtract(Mods, maps:keys(Tests)),
|
||||
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) ->
|
||||
BaseDir = filename:dirname(zx:get_home()),
|
||||
ok = file:set_cwd(BaseDir),
|
||||
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 = temp_dir(),
|
||||
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() ->
|
||||
"test/temp".
|
||||
|
||||
base64() ->
|
||||
TestFile = filename:join(temp_dir(), "base64.test"),
|
||||
ConvFile = filename:join(temp_dir(), "base64.erlang.txt"),
|
||||
ok = filelib:ensure_dir(TestFile),
|
||||
ok = file:write_file(TestFile, rand:bytes(rand:uniform(5000))),
|
||||
{ok, Bytes} = file:read_file(TestFile),
|
||||
Base64 = base64:encode(Bytes),
|
||||
ok = file:write_file(ConvFile, Base64),
|
||||
Run = unicode:characters_to_list(["bin/run base64 ", temp_dir()]),
|
||||
[JavaEncPath, JavaDecPath] = string:split(os:cmd(Run), " "),
|
||||
{ok, E_Enc_Bytes} = file:read_file(ConvFile),
|
||||
{ok, J_Enc_Bytes} = file:read_file(JavaEncPath),
|
||||
E_Hash = crypto:hash(sha512, E_Enc_Bytes),
|
||||
J_Hash = crypto:hash(sha512, J_Enc_Bytes),
|
||||
{ok, E_Dec_Bytes} = file:read_file(TestFile),
|
||||
{ok, J_Dec_Bytes} = file:read_file(JavaDecPath),
|
||||
E_BinHash = crypto:hash(sha512, E_Dec_Bytes),
|
||||
J_BinHash = crypto:hash(sha512, J_Dec_Bytes),
|
||||
E_Hash =:= J_Hash andalso E_BinHash =:= J_BinHash.
|
||||
|
||||
base58() ->
|
||||
TestFile = filename:join(temp_dir(), "base58.test"),
|
||||
ConvFile = filename:join(temp_dir(), "base58.erlang.txt"),
|
||||
ok = filelib:ensure_dir(TestFile),
|
||||
ok = file:write_file(TestFile, rand:bytes(rand:uniform(5000))),
|
||||
{ok, Bytes} = file:read_file(TestFile),
|
||||
Base58 = base58:binary_to_base58(Bytes),
|
||||
ok = file:write_file(ConvFile, Base58),
|
||||
Run = unicode:characters_to_list(["bin/run base58 ", temp_dir()]),
|
||||
[JavaEncPath, JavaDecPath] = string:split(os:cmd(Run), " "),
|
||||
{ok, E_Enc_Bytes} = file:read_file(ConvFile),
|
||||
{ok, J_Enc_Bytes} = file:read_file(JavaEncPath),
|
||||
E_Hash = crypto:hash(sha512, E_Enc_Bytes),
|
||||
J_Hash = crypto:hash(sha512, J_Enc_Bytes),
|
||||
{ok, E_Dec_Bytes} = file:read_file(TestFile),
|
||||
{ok, J_Dec_Bytes} = file:read_file(JavaDecPath),
|
||||
E_BinHash = crypto:hash(sha512, E_Dec_Bytes),
|
||||
J_BinHash = crypto:hash(sha512, J_Dec_Bytes),
|
||||
E_Hash =:= J_Hash andalso E_BinHash =:= J_BinHash.
|
||||
|
||||
rlp() ->
|
||||
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_dir(), "rlp.test"),
|
||||
ok = file:write_file(RLP_File, RLP),
|
||||
Run = unicode:characters_to_list(["bin/run rlp ", temp_dir()]),
|
||||
[Found, JavaEncPath] = string:split(os:cmd(Run), " "),
|
||||
{ok, E_Enc_Bytes} = file:read_file(RLP_File),
|
||||
{ok, J_Enc_Bytes} = file:read_file(JavaEncPath),
|
||||
E_Hash = crypto:hash(sha512, E_Enc_Bytes),
|
||||
J_Hash = crypto:hash(sha512, J_Enc_Bytes),
|
||||
E_Hash =:= J_Hash andalso Found =:= Anchor.
|
||||
@@ -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,934 @@
|
||||
/*
|
||||
* 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 exquisitely annoying Radix-2^25.5 field arithmetic. Never do this if you can avoid it.
|
||||
// Verified against the canonical Erlang ec_utils (see test/README.md for how).
|
||||
//
|
||||
// SAFETY NOTE:
|
||||
// Despite the effort at zeroing sensitive memory in this module, Java heap memory does not
|
||||
// provide reliable secret-memory erasure or even really permit it. If protection against
|
||||
// copies of secret material in managed memory is a requirement of the deployment environment,
|
||||
// don't use this implementation; use explicitly managed native memory instead.
|
||||
//
|
||||
// This implementation is intended for isolated environments where the Java heap is an
|
||||
// acceptable trust boundary. It is not intended to provide secure-memory guarantees
|
||||
// against a hostile or inspecting runtime.
|
||||
//
|
||||
// Android is one such platform where a vendor-provided C implementation of Ed25519
|
||||
// interacting over a JNI boundary with off-heap memory (via java.nio.ByteBuffer)
|
||||
// and aggressive memory sanitation is necessary.
|
||||
//
|
||||
// WARNING:
|
||||
// Do NOT compile this code with compiler optimizations turned on. It will remove the memory
|
||||
// protection functionality entirely as it is "no impact" code from the perspective of
|
||||
// performance optimization. Configure your build system to deliberately turn off optimizing
|
||||
// profilers for this module. You will need to do the same for any C code you might interface
|
||||
// with over JNI as well. Constant time execution, memory wiping, etc. are all viewed as
|
||||
// no-impact busywork or pessimization from the perspective of a performance profiler.
|
||||
//
|
||||
// References:
|
||||
// I don't even know where to start with references, but the tink-java library and pretty much
|
||||
// everything (and everyone!) referenced on the lib25519 page deserves a mention.
|
||||
// Of special mention, of course, is SUPERCOP.
|
||||
//
|
||||
// tink-java: https://github.com/tink-crypto/tink-java
|
||||
// lib25519 : https://lib25519.cr.yp.to/people.html
|
||||
// SUPERCOP : https://bench.cr.yp.to/supercop.html
|
||||
|
||||
// TODO: Craig 2026-08-21
|
||||
// I don't like the allocation of Scratch and Ge all over the place.
|
||||
// If someone were to apply this library to a high-throughput system, with many threads
|
||||
// signing stuff willy-nilly, then intense GC pressure could result simply because of
|
||||
// all the dead (and zeroed) Scratch and Ge space left littered throughout the dead heap
|
||||
// awaiting GC. What I want to do instead is provide a separate call path that allows the
|
||||
// current mechanism to work as well as a slightly lower-level call path where the caller
|
||||
// can provide a pre-allocated space by reference so if a high-throughput system is using
|
||||
// lots of worker threads and really pressuring the system, the caller can pre-allocate the
|
||||
// needed GC and Scratch space themselves once per thread.
|
||||
//
|
||||
// This should be pretty easy.
|
||||
// I just don't want to look at this module for at least a few days.
|
||||
|
||||
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};
|
||||
|
||||
// Group order L (little-endian)
|
||||
private static final byte[] L = {
|
||||
(byte) 0xed, (byte) 0xd3, (byte) 0xf5, (byte) 0x5c, (byte) 0x1a, (byte) 0x63, (byte) 0x12, (byte) 0x58,
|
||||
(byte) 0xd6, (byte) 0x9c, (byte) 0xf7, (byte) 0xa2, (byte) 0xde, (byte) 0xf9, (byte) 0xde, (byte) 0x14,
|
||||
(byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
|
||||
(byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x10
|
||||
};
|
||||
|
||||
// Field prime P = 2^255 - 19 (little-endian, 255 bits used)
|
||||
private static final byte[] P = {
|
||||
(byte) 0xed, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff,
|
||||
(byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff,
|
||||
(byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff,
|
||||
(byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x7f
|
||||
};
|
||||
|
||||
public static final class Ge {
|
||||
public final long[] X = new long[10], Y = new long[10], Z = new long[10], T = new long[10];
|
||||
|
||||
// Internal: Wipes the coordinates.
|
||||
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();
|
||||
|
||||
// Internal: Wipes the scratch space.
|
||||
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 (!isLessThan(S_bytes, L)) 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;
|
||||
}
|
||||
|
||||
// NOTE:
|
||||
// Internal curve and field arithmetic machinery.
|
||||
// These are public only for parity testing against reference implementations.
|
||||
|
||||
// Internal: Scalar multiplication by base point.
|
||||
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;
|
||||
}
|
||||
|
||||
// Internal: General scalar multiplication.
|
||||
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;
|
||||
}
|
||||
|
||||
// Internal: Point addition.
|
||||
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);
|
||||
}
|
||||
|
||||
// Internal: Point doubling.
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
// Internal: Point decompression.
|
||||
public static Ge decompress(byte[] b, Scratch s) {
|
||||
if (b.length != 32) return null;
|
||||
|
||||
// Ensure y is canonical (y < P)
|
||||
byte[] y_check = Arrays.copyOf(b, 32);
|
||||
y_check[31] &= 0x7F;
|
||||
if (!isLessThan(y_check, P)) 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;
|
||||
}
|
||||
|
||||
// Internal: Point compression.
|
||||
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];
|
||||
}
|
||||
|
||||
private static boolean isLessThan(byte[] a, byte[] b) {
|
||||
for (int i = 31; i >= 0; i--) {
|
||||
int ai = a[i] & 0xff;
|
||||
int bi = b[i] & 0xff;
|
||||
if (ai < bi) return true;
|
||||
if (ai > bi) return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Internal: Field multiplication.
|
||||
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);
|
||||
}
|
||||
|
||||
/// Internal: Field squaring.
|
||||
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);
|
||||
}
|
||||
|
||||
// Internal: Field reduction.
|
||||
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];
|
||||
}
|
||||
|
||||
// Internal: Load field element from bytes.
|
||||
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++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Internal: Contract field element to bytes.
|
||||
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;
|
||||
}
|
||||
|
||||
// Internal: Scalar reduction.
|
||||
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.crypto;
|
||||
|
||||
import java.security.SecureRandom;
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.SecretKey;
|
||||
import javax.crypto.spec.GCMParameterSpec;
|
||||
|
||||
|
||||
/**
|
||||
* Vault provides secure AES/GCM encryption and decryption with memory hygiene.
|
||||
*/
|
||||
public final class Vault {
|
||||
|
||||
private static final String AES_GCM = "AES/GCM/NoPadding";
|
||||
private static final int GCM_TAG_LENGTH = 128; // Bits
|
||||
private static final int IV_LENGTH = 12; // Bytes
|
||||
|
||||
private Vault() {}
|
||||
|
||||
/**
|
||||
* Holds the results of an encryption operation.
|
||||
*/
|
||||
public static final class Ciphertext {
|
||||
private final byte[] iv;
|
||||
private final byte[] data;
|
||||
|
||||
public Ciphertext(byte[] iv, byte[] data) {
|
||||
this.iv = iv;
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
public byte[] getIv() { return iv; }
|
||||
public byte[] getData() { return data; }
|
||||
}
|
||||
|
||||
/**
|
||||
* Encrypts plaintext using AES/GCM.
|
||||
* Manually generates the IV to ensure deterministic tag length and cross-provider consistency.
|
||||
*/
|
||||
public static Ciphertext encrypt(SecretKey key, byte[] plaintext) throws Exception {
|
||||
try {
|
||||
byte[] iv = new byte[IV_LENGTH];
|
||||
new SecureRandom().nextBytes(iv);
|
||||
|
||||
Cipher cipher = Cipher.getInstance(AES_GCM);
|
||||
GCMParameterSpec spec = new GCMParameterSpec(GCM_TAG_LENGTH, iv);
|
||||
cipher.init(Cipher.ENCRYPT_MODE, key, spec);
|
||||
|
||||
byte[] ciphertext = cipher.doFinal(plaintext);
|
||||
return new Ciphertext(iv, ciphertext);
|
||||
} catch (Exception e) {
|
||||
String msg = e.getMessage();
|
||||
if (msg == null) msg = e.toString();
|
||||
throw new Exception("Vault.encrypt failed: " + msg, e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Encrypts plaintext using AES/GCM with a specific IV.
|
||||
*/
|
||||
public static byte[] encrypt(SecretKey key, byte[] iv, byte[] plaintext) throws Exception {
|
||||
try {
|
||||
Cipher cipher = Cipher.getInstance(AES_GCM);
|
||||
GCMParameterSpec spec = new GCMParameterSpec(GCM_TAG_LENGTH, iv);
|
||||
cipher.init(Cipher.ENCRYPT_MODE, key, spec);
|
||||
return cipher.doFinal(plaintext);
|
||||
} catch (Exception e) {
|
||||
throw new Exception("Vault.encrypt(iv) failed: " + e.getClass().getName() + ": " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrypts ciphertext using AES/GCM.
|
||||
*/
|
||||
public static byte[] decrypt(SecretKey key, byte[] iv, byte[] ciphertext) throws Exception {
|
||||
try {
|
||||
Cipher cipher = Cipher.getInstance(AES_GCM);
|
||||
GCMParameterSpec spec = new GCMParameterSpec(GCM_TAG_LENGTH, iv);
|
||||
cipher.init(Cipher.DECRYPT_MODE, key, spec);
|
||||
return cipher.doFinal(ciphertext);
|
||||
} catch (Exception e) {
|
||||
String msg = e.getMessage();
|
||||
if (msg == null) msg = e.toString();
|
||||
throw new Exception("Vault.decrypt failed: " + msg, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
// Yes, this is kind of ridiculous, but yay OOP!
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (!(o instanceof Id id)) return false;
|
||||
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,69 @@
|
||||
/*
|
||||
* 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.ArrayList;
|
||||
import java.util.List;
|
||||
import swiss.qpq.gajumaru.core.encoding.ChainObjects;
|
||||
import swiss.qpq.gajumaru.core.encoding.RLP.RLP_Data;
|
||||
import swiss.qpq.gajumaru.core.encoding.RLP.RLP_Item;
|
||||
import swiss.qpq.gajumaru.core.encoding.RLP.RLP_List;
|
||||
|
||||
// SignedTx represents a signed Gajumaru TX
|
||||
// Port of signed_tx in gmser_chain_objects.erl.
|
||||
|
||||
public record SignedTx(
|
||||
List<byte[]> signatures,
|
||||
byte[] transaction
|
||||
) {
|
||||
|
||||
private static final int VSN = 1;
|
||||
|
||||
public byte[] serialize() {
|
||||
List<RLP_Data> fields = new ArrayList<>();
|
||||
|
||||
List<RLP_Data> sigs = new ArrayList<>();
|
||||
for (byte[] sig : signatures) {
|
||||
sigs.add(new RLP_Item(sig));
|
||||
}
|
||||
fields.add(new RLP_List(sigs));
|
||||
fields.add(new RLP_Item(transaction));
|
||||
|
||||
return ChainObjects.serialize(ChainObjects.TAG_SIGNED_TX, VSN, fields);
|
||||
}
|
||||
|
||||
public static SignedTx deserialize(byte[] data) {
|
||||
ChainObjects.SerializationResult res = ChainObjects.deserialize(data);
|
||||
if (res.tag() != ChainObjects.TAG_SIGNED_TX) {
|
||||
// Yeah, we can't actually avoid throw. It's disgusting. I don't like it.
|
||||
throw new IllegalArgumentException("Invalid tag for SignedTx: " + res.tag());
|
||||
}
|
||||
|
||||
List<RLP_Data> f = res.fields();
|
||||
|
||||
List<byte[]> signatures = new ArrayList<>();
|
||||
for (RLP_Data sig : f.get(0).asList().items) {
|
||||
signatures.add(sig.asItem().bytes);
|
||||
}
|
||||
|
||||
return new SignedTx(signatures, f.get(1).asItem().bytes);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
/*
|
||||
* 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.math.BigInteger;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import swiss.qpq.gajumaru.core.encoding.ChainObjects;
|
||||
import swiss.qpq.gajumaru.core.encoding.RLP.RLP_Data;
|
||||
import swiss.qpq.gajumaru.core.encoding.RLP.RLP_Item;
|
||||
|
||||
|
||||
// As one might imagine, SpendTx represents a Gajumaru SpendTx
|
||||
// Port of spend_tx in gmser_chain_objects.erl.
|
||||
|
||||
public record SpendTx(
|
||||
Id senderId,
|
||||
Id recipientId,
|
||||
BigInteger amount,
|
||||
BigInteger gasPrice,
|
||||
BigInteger gas,
|
||||
long ttl,
|
||||
long nonce,
|
||||
byte[] payload
|
||||
) {
|
||||
|
||||
private static final int VSN = 1;
|
||||
|
||||
public byte[] serialize() {
|
||||
List<RLP_Data> fields = new ArrayList<>();
|
||||
fields.add(ChainObjects.encodeId(senderId));
|
||||
fields.add(ChainObjects.encodeId(recipientId));
|
||||
fields.add(ChainObjects.encodeInt(amount));
|
||||
fields.add(ChainObjects.encodeInt(gasPrice));
|
||||
fields.add(ChainObjects.encodeInt(gas));
|
||||
fields.add(ChainObjects.encodeInt(ttl));
|
||||
fields.add(ChainObjects.encodeInt(nonce));
|
||||
fields.add(new RLP_Item(payload != null ? payload : new byte[0]));
|
||||
|
||||
return ChainObjects.serialize(ChainObjects.TAG_SPEND_TX, VSN, fields);
|
||||
}
|
||||
|
||||
public static SpendTx deserialize(byte[] data) {
|
||||
ChainObjects.SerializationResult res = ChainObjects.deserialize(data);
|
||||
if (res.tag() != ChainObjects.TAG_SPEND_TX) {
|
||||
throw new IllegalArgumentException("Invalid tag for SpendTx: " + res.tag());
|
||||
}
|
||||
|
||||
List<RLP_Data> f = res.fields();
|
||||
return new SpendTx(
|
||||
ChainObjects.decodeId(f.get(0)),
|
||||
ChainObjects.decodeId(f.get(1)),
|
||||
ChainObjects.decodeBigInt(f.get(2)),
|
||||
ChainObjects.decodeBigInt(f.get(3)),
|
||||
ChainObjects.decodeBigInt(f.get(4)),
|
||||
ChainObjects.decodeLong(f.get(5)),
|
||||
ChainObjects.decodeLong(f.get(6)),
|
||||
f.get(7).asItem().bytes
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
/*
|
||||
* 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.util.Arrays;
|
||||
import java.util.Base64;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import swiss.qpq.gajumaru.core.tools.CryptoUtils;
|
||||
|
||||
|
||||
// Port of gmser_api_encoder.erl.
|
||||
// Handles Gajumaru API encoding (prefixed Base58Check or Base64Check).
|
||||
//
|
||||
// This module will have to stay in step with gmserialization and will therefore be updated
|
||||
// from time to time. It is important to keep this as clean as possible for our future selves
|
||||
// to be able to read and understand. Alignment actually does matter, even if it isn't the
|
||||
// way "real" Java people do things.
|
||||
|
||||
public final class ApiEncoder {
|
||||
|
||||
public enum Encoding { BASE58, BASE64 }
|
||||
|
||||
public enum Type {
|
||||
KEY_BLOCK_HASH ("kh", 32, Encoding.BASE58),
|
||||
MICRO_BLOCK_HASH ("mh", 32, Encoding.BASE58),
|
||||
BLOCK_POF_HASH ("bf", 32, Encoding.BASE58),
|
||||
BLOCK_TX_HASH ("bx", 32, Encoding.BASE58),
|
||||
BLOCK_STATE_HASH ("bs", 32, Encoding.BASE58),
|
||||
BLOCK_WITNESS_HASH ("ws", 32, Encoding.BASE58),
|
||||
CHANNEL ("ch", 32, Encoding.BASE58),
|
||||
CONTRACT_PUBKEY ("ct", 32, Encoding.BASE58),
|
||||
CONTRACT_BYTEARRAY ("cb", -1, Encoding.BASE64),
|
||||
CONTRACT_STORE_KEY ("ck", -1, Encoding.BASE64),
|
||||
CONTRACT_STORE_VALUE("cv", -1, Encoding.BASE64),
|
||||
CONTRACT_SOURCE ("cx", -1, Encoding.BASE64),
|
||||
TRANSACTION ("tx", -1, Encoding.BASE64),
|
||||
TX_HASH ("th", 32, Encoding.BASE58),
|
||||
ACCOUNT_PUBKEY ("ak", 32, Encoding.BASE58),
|
||||
ACCOUNT_SECKEY ("sk", 32, Encoding.BASE58),
|
||||
ASSOCIATE_CHAIN ("ac", 32, Encoding.BASE58),
|
||||
SIGNATURE ("sg", 64, Encoding.BASE58),
|
||||
COMMITMENT ("cm", 32, Encoding.BASE58),
|
||||
PEER_PUBKEY ("pp", 32, Encoding.BASE58),
|
||||
NAME ("nm", -1, Encoding.BASE58),
|
||||
NATIVE_TOKEN ("nt", 32, Encoding.BASE58),
|
||||
STATE ("st", 32, Encoding.BASE64),
|
||||
POI ("pi", -1, Encoding.BASE64),
|
||||
STATE_TREES ("ss", -1, Encoding.BASE64),
|
||||
CALL_STATE_TREE ("cs", -1, Encoding.BASE64),
|
||||
MP_TREE_HASH ("mt", 32, Encoding.BASE58),
|
||||
HASH ("hs", 32, Encoding.BASE58),
|
||||
ENTRY ("en", -1, Encoding.BASE64),
|
||||
BYTEARRAY ("ba", -1, Encoding.BASE64);
|
||||
|
||||
public final String prefix;
|
||||
public final int size;
|
||||
public final Encoding encoding;
|
||||
|
||||
Type(String prefix, int size, Encoding encoding) {
|
||||
this.prefix = prefix;
|
||||
this.size = size;
|
||||
this.encoding = encoding;
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
String encoded;
|
||||
if (type.encoding == Encoding.BASE58) {
|
||||
encoded = Base58.checkEncode(payload);
|
||||
} else {
|
||||
encoded = base64CheckEncode(payload);
|
||||
}
|
||||
|
||||
return type.prefix + "_" + encoded;
|
||||
}
|
||||
|
||||
public static DecodeResult decode(String input) {
|
||||
int splitIdx = input.indexOf('_');
|
||||
if (splitIdx == -1) {
|
||||
throw new IllegalArgumentException("Invalid encoded string format (missing underscore)");
|
||||
}
|
||||
|
||||
String prefix = input.substring(0, splitIdx);
|
||||
String encoded = input.substring(splitIdx + 1);
|
||||
|
||||
Type type = PREFIX_MAP.get(prefix);
|
||||
if (type == null) {
|
||||
throw new IllegalArgumentException("Unknown prefix: " + prefix);
|
||||
}
|
||||
|
||||
byte[] payload;
|
||||
if (type.encoding == Encoding.BASE58) {
|
||||
payload = Base58.checkDecode(encoded);
|
||||
} else {
|
||||
payload = base64CheckDecode(encoded);
|
||||
}
|
||||
|
||||
if (type.size != -1 && payload.length != type.size) {
|
||||
throw new IllegalArgumentException("Invalid decoded payload size for " + type + ": " + payload.length);
|
||||
}
|
||||
|
||||
return new DecodeResult(type, payload);
|
||||
}
|
||||
|
||||
private static String base64CheckEncode(byte[] input) {
|
||||
byte[] checksum = CryptoUtils.doubleSha256(input);
|
||||
byte[] combined = new byte[input.length + 4];
|
||||
System.arraycopy(input, 0, combined, 0, input.length);
|
||||
System.arraycopy(checksum, 0, combined, input.length, 4);
|
||||
return Base64.getEncoder().encodeToString(combined);
|
||||
}
|
||||
|
||||
private static byte[] base64CheckDecode(String input) {
|
||||
byte[] decoded = Base64.getDecoder().decode(input);
|
||||
if (decoded.length < 4) {
|
||||
throw new IllegalArgumentException("Base64Check input too short");
|
||||
}
|
||||
|
||||
byte[] data = Arrays.copyOfRange(decoded, 0, decoded.length - 4);
|
||||
byte[] actual = Arrays.copyOfRange(decoded, decoded.length - 4, decoded.length);
|
||||
byte[] expected = CryptoUtils.doubleSha256(data);
|
||||
|
||||
for (int i = 0; i < 4; i++) {
|
||||
if (actual[i] != expected[i]) {
|
||||
throw new IllegalArgumentException("Base64Check checksum mismatch");
|
||||
}
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
public record DecodeResult(Type type, byte[] payload) {}
|
||||
}
|
||||
+43
-20
@@ -21,8 +21,11 @@
|
||||
package swiss.qpq.gajumaru.core.encoding;
|
||||
|
||||
import java.util.Arrays;
|
||||
import swiss.qpq.gajumaru.core.tools.CryptoUtils;
|
||||
|
||||
|
||||
// Stateless Base58 and Base58Check implementation.
|
||||
|
||||
// Stateless Base58 implementation.
|
||||
public final class Base58 {
|
||||
|
||||
private static final char[] ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz".toCharArray();
|
||||
@@ -52,25 +55,20 @@ public final class Base58 {
|
||||
char[] encoded = new char[temp.length * 2];
|
||||
int outputStart = encoded.length;
|
||||
|
||||
// Treat 'temp' as one giant number. Process until the entire array is reduced to zeros.
|
||||
// i indicates the position of the most significant leading zero.
|
||||
int i = zeros;
|
||||
while (i < temp.length) {
|
||||
int remainder = 0;
|
||||
|
||||
// Perform long division from left to right across the active bytes
|
||||
for (int j = i; j < temp.length; j++) {
|
||||
int currentByte = temp[j] & 0xFF; // Safe unsigned byte conversion
|
||||
int totalValue = (remainder * 256) + currentByte;
|
||||
|
||||
temp[j] = (byte) (totalValue / 58); // Mutate array with the quotient
|
||||
remainder = totalValue % 58; // Carry the remainder to the next byte
|
||||
temp[j] = (byte) (totalValue / 58);
|
||||
remainder = totalValue % 58;
|
||||
}
|
||||
|
||||
// The final remainder of this pass is our next Base58 digit character
|
||||
encoded[--outputStart] = ALPHABET[remainder];
|
||||
|
||||
// Advance the pointer forward if the current head byte has been ground down to 0
|
||||
if (temp[i] == 0) {
|
||||
i++;
|
||||
}
|
||||
@@ -94,19 +92,16 @@ public final class Base58 {
|
||||
return new byte[0];
|
||||
}
|
||||
|
||||
// Convert the string into numeric index offsets
|
||||
byte[] input58 = new byte[input.length()];
|
||||
for (int i = 0; i < input.length(); i++) {
|
||||
char c = input.charAt(i);
|
||||
int digit = (c < 128) ? INDEXES[c] : -1;
|
||||
if (digit < 0) {
|
||||
// I picked the wrong week to stop drinking...
|
||||
throw new IllegalArgumentException("Illegal Base58 character encountered: " + c);
|
||||
throw new IllegalArgumentException(String.format("Illegal Base58 character '%c' at index %d", c, i));
|
||||
}
|
||||
input58[i] = (byte) digit;
|
||||
}
|
||||
|
||||
// Count leading zeros to reconstruct leading 0 bytes
|
||||
int zeros = 0;
|
||||
while (zeros < input58.length && input58[zeros] == 0) {
|
||||
zeros++;
|
||||
@@ -120,33 +115,61 @@ public final class Base58 {
|
||||
while (i < input58.length) {
|
||||
int remainder = 0;
|
||||
|
||||
// Long division from left to right across the remaining numeric character indices
|
||||
for (int j = i; j < input58.length; j++) {
|
||||
int currentBase58Digit = input58[j] & 0xFF;
|
||||
int totalValue = (remainder * 58) + currentBase58Digit; // Shift base by 58
|
||||
|
||||
input58[j] = (byte) (totalValue / 256); // Mutate array with the base-256 quotient
|
||||
remainder = totalValue % 256; // Carry the byte remainder forward
|
||||
input58[j] = (byte) (totalValue / 256);
|
||||
remainder = totalValue % 256;
|
||||
}
|
||||
|
||||
// The remainder of this pass is the next raw base-256 byte payload
|
||||
decoded[--outputStart] = (byte) remainder;
|
||||
|
||||
// Advance past this leading index if its value has been exhausted or is 0
|
||||
while (i < input58.length && input58[i] == 0) {
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Strip extra leading zero allocations from the worst-case boundary buffer
|
||||
while (outputStart < decoded.length && decoded[outputStart] == 0) {
|
||||
outputStart++;
|
||||
}
|
||||
|
||||
// Re-inject the necessary leading padding zeros
|
||||
byte[] result = new byte[decoded.length - outputStart + zeros];
|
||||
System.arraycopy(decoded, outputStart, result, zeros, decoded.length - outputStart);
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
// Base58Check
|
||||
|
||||
//Encodes bytes with a 4-byte double-SHA256 checksum.
|
||||
public static String checkEncode(byte[] input) {
|
||||
byte[] checksum = CryptoUtils.doubleSha256(input);
|
||||
byte[] combined = new byte[input.length + 4];
|
||||
System.arraycopy(input, 0, combined, 0, input.length);
|
||||
System.arraycopy(checksum, 0, combined, input.length, 4);
|
||||
return encode(combined);
|
||||
}
|
||||
|
||||
// Decodes a Base58Check string and validates the checksum.
|
||||
public static byte[] checkDecode(String input) throws IllegalArgumentException {
|
||||
byte[] decoded = decode(input);
|
||||
if (decoded.length < 4) {
|
||||
throw new IllegalArgumentException("Base58Check input too short");
|
||||
}
|
||||
|
||||
byte[] data = Arrays.copyOfRange(decoded, 0, decoded.length - 4);
|
||||
byte[] actual = Arrays.copyOfRange(decoded, decoded.length - 4, decoded.length);
|
||||
byte[] expected = CryptoUtils.doubleSha256(data);
|
||||
|
||||
for (int i = 0; i < 4; i++) {
|
||||
if (actual[i] != expected[i]) {
|
||||
throw new IllegalArgumentException("Base58Check checksum mismatch");
|
||||
}
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
|
||||
// Internal Utilities
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
/*
|
||||
* 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.math.BigInteger;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import swiss.qpq.gajumaru.core.data.Id;
|
||||
import swiss.qpq.gajumaru.core.encoding.RLP.RLP_Data;
|
||||
import swiss.qpq.gajumaru.core.encoding.RLP.RLP_Item;
|
||||
import swiss.qpq.gajumaru.core.encoding.RLP.RLP_List;
|
||||
|
||||
// ChainObjects provides tagged serialization for Gajumaru objects.
|
||||
// Port of gmser_chain_objects.erl and gmserialization.erl.
|
||||
//
|
||||
// NOTE: Craig 2026-08-21
|
||||
// It may be possible to consolidate this a bit further. Something that has always bothered me
|
||||
// about the Gajumaru support libs is how much jumping between modules is often required. I'll
|
||||
// eventually restructure that to be a bit more simple, but if we can start these libs off in
|
||||
// that direction, that would be a tiny win, especially given how opaque Java can be. That
|
||||
// opaqueness is also wy I'm staying away from class hierarchies here as much as possible.
|
||||
|
||||
public final class ChainObjects {
|
||||
|
||||
public static final int TAG_ACCOUNT = 10;
|
||||
public static final int TAG_SIGNED_TX = 11;
|
||||
public static final int TAG_SPEND_TX = 12;
|
||||
public static final int TAG_CONTRACT_CREATE_TX = 42;
|
||||
public static final int TAG_CONTRACT_CALL_TX = 43;
|
||||
public static final int TAG_ORACLE_REGISTER_TX = 22; // Check exact value
|
||||
// ... add more as needed from gmser_chain_objects.erl
|
||||
|
||||
private ChainObjects() {}
|
||||
|
||||
public static byte[] serialize(int tag, int vsn, List<RLP_Data> fields) {
|
||||
List<RLP_Data> fullList = new ArrayList<>();
|
||||
fullList.add(encodeInt(tag));
|
||||
fullList.add(encodeInt(vsn));
|
||||
fullList.addAll(fields);
|
||||
return RLP.encode(new RLP_List(fullList));
|
||||
}
|
||||
|
||||
public static SerializationResult deserialize(byte[] data) {
|
||||
RLP_Data rlp = RLP.decode(data);
|
||||
if (!(rlp instanceof RLP_List list)) {
|
||||
throw new RLP.RLPException("Expected RLP list for ChainObject");
|
||||
}
|
||||
|
||||
List<RLP_Data> items = list.items;
|
||||
if (items.size() < 2) {
|
||||
throw new RLP.RLPException("ChainObject list too short (missing tag/vsn)");
|
||||
}
|
||||
|
||||
int tag = decodeInt(items.get(0));
|
||||
int vsn = decodeInt(items.get(1));
|
||||
List<RLP_Data> fields = items.subList(2, items.size());
|
||||
|
||||
return new SerializationResult(tag, vsn, fields);
|
||||
}
|
||||
|
||||
public static RLP_Item encodeId(Id id) {
|
||||
return new RLP_Item(id.serialize());
|
||||
}
|
||||
|
||||
public static Id decodeId(RLP_Data data) {
|
||||
return Id.deserialize(data.asItem().bytes);
|
||||
}
|
||||
|
||||
public record SerializationResult(int tag, int vsn, List<RLP_Data> fields) {}
|
||||
|
||||
// Helper to encode integers for RLP (big-endian, no leading zeros)
|
||||
public static RLP_Item encodeInt(long val) {
|
||||
return encodeInt(BigInteger.valueOf(val));
|
||||
}
|
||||
|
||||
public static RLP_Item encodeInt(BigInteger val) {
|
||||
if (val.equals(BigInteger.ZERO)) return new RLP_Item(new byte[0]);
|
||||
byte[] bytes = val.toByteArray();
|
||||
// Remove leading zero byte if present (BigInteger adds one if top bit is set because reasons)
|
||||
if (bytes.length > 1 && bytes[0] == 0) {
|
||||
bytes = java.util.Arrays.copyOfRange(bytes, 1, bytes.length);
|
||||
}
|
||||
return new RLP_Item(bytes);
|
||||
}
|
||||
|
||||
public static int decodeInt(RLP_Data data) {
|
||||
return decodeBigInt(data).intValue();
|
||||
}
|
||||
|
||||
public static long decodeLong(RLP_Data data) {
|
||||
return decodeBigInt(data).longValue();
|
||||
}
|
||||
|
||||
public static BigInteger decodeBigInt(RLP_Data data) {
|
||||
byte[] bytes = data.asItem().bytes;
|
||||
if (bytes.length == 0) return BigInteger.ZERO;
|
||||
return new BigInteger(1, bytes);
|
||||
}
|
||||
}
|
||||
@@ -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.encoding;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
// Gf256 implements finite field arithmetic over GF(256).
|
||||
// Ported from gf256.erl.
|
||||
|
||||
public final class Gf256 {
|
||||
|
||||
private final int[] exp;
|
||||
private final int[] log;
|
||||
|
||||
public Gf256(int primeModulus) {
|
||||
this.exp = new int[512];
|
||||
this.log = new int[256];
|
||||
int x = 1;
|
||||
for (int i = 0; i < 255; i++) {
|
||||
exp[i] = x;
|
||||
exp[i + 255] = x;
|
||||
log[x] = i;
|
||||
x <<= 1;
|
||||
if (x > 255) {
|
||||
x ^= primeModulus;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public int add(int a, int b) {
|
||||
return a ^ b;
|
||||
}
|
||||
|
||||
public int multiply(int a, int b) {
|
||||
if (a == 0 || b == 0) return 0;
|
||||
return exp[log[a] + log[b]];
|
||||
}
|
||||
|
||||
public int inverse(int x) {
|
||||
if (x == 0) throw new ArithmeticException("Division by zero");
|
||||
return exp[255 - log[x]];
|
||||
}
|
||||
|
||||
public int exponent(int i) {
|
||||
return exp[i % 255];
|
||||
}
|
||||
|
||||
public int[] monomialProduct(int[] poly, int coeff, int degree) {
|
||||
if (coeff == 0) return new int[]{0};
|
||||
int[] result = new int[poly.length + degree];
|
||||
for (int i = 0; i < poly.length; i++) {
|
||||
result[i] = multiply(poly[i], coeff);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public int[] polynomialProduct(int[] p1, int[] p2) {
|
||||
if (isZero(p1) || isZero(p2)) return new int[]{0};
|
||||
int[] result = new int[p1.length + p2.length - 1];
|
||||
for (int i = 0; i < p1.length; i++) {
|
||||
if (p1[i] == 0) continue;
|
||||
for (int j = 0; j < p2.length; j++) {
|
||||
result[i + j] ^= multiply(p1[i], p2[j]);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public int[] divide(int[] a, int[] b) {
|
||||
if (isZero(b)) throw new ArithmeticException("Division by zero");
|
||||
int inv = inverse(b[0]);
|
||||
int[] r = Arrays.copyOf(a, a.length);
|
||||
|
||||
int steps = a.length - b.length + 1;
|
||||
for (int i = 0; i < steps; i++) {
|
||||
if (r[i] == 0) continue;
|
||||
int scale = multiply(r[i], inv);
|
||||
for (int j = 0; j < b.length; j++) {
|
||||
r[i + j] ^= multiply(b[j], scale);
|
||||
}
|
||||
}
|
||||
// Remainder is the last b.length - 1 elements
|
||||
return Arrays.copyOfRange(r, a.length - b.length + 1, a.length);
|
||||
}
|
||||
|
||||
private boolean isZero(int[] poly) {
|
||||
return poly.length == 0 || (poly.length == 1 && poly[0] == 0);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,594 @@
|
||||
/*
|
||||
* 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.nio.charset.StandardCharsets;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
// Mnemonic provides Gajumaru-style mnemonic phrase encoding and decoding.
|
||||
// Port of hz_key_master.erl.
|
||||
//
|
||||
// Uses raw bit manipulation on byte[] to avoid BigInteger artifacts on the heap.
|
||||
// Returns byte[][] for phrases to allow for explicit memory scrubbing.
|
||||
|
||||
public final class Mnemonic {
|
||||
|
||||
private static final int DICT_SIZE = 4096;
|
||||
private static final int WIDTH = 12; // bits per word
|
||||
private static final int MAX_DATA_CHUNKS = 22; // For 256-bit seed
|
||||
|
||||
public static final String[] WORDS = {
|
||||
"aardvark", "abacus", "abalone", "abandon", "abbey", "abdomen", "abduct", "abhor", "abide",
|
||||
"ability", "able", "abnormal", "aboard", "abolish", "abort", "above", "abrasive", "abridged",
|
||||
"abroad", "abrupt", "abscond", "absence", "absinthe", "absorb", "abstract", "absurd", "abundant",
|
||||
"abuser", "abyss", "acacia", "academic", "accent", "accident", "acclaim", "account", "accredit",
|
||||
"accuse", "acetone", "achieve", "acid", "acne", "acolyte", "acoustic", "acquire", "acreage",
|
||||
"acrobat", "acrylic", "activity", "actor", "actress", "actual", "acuity", "acumen", "acute",
|
||||
"adage", "adamant", "adapt", "addendum", "addict", "address", "adequate", "adhesive", "adjacent",
|
||||
"adjoin", "adjust", "admiral", "admonish", "adobe", "adoption", "adorn", "adrenal", "adult",
|
||||
"advance", "advert", "advisor", "advocate", "aerobic", "affair", "affirm", "afflict", "afford",
|
||||
"affront", "afraid", "africa", "again", "aged", "agency", "agile", "agitate", "agnostic", "agony",
|
||||
"agrarian", "agree", "ahead", "ailment", "aimless", "airbrush", "airdrop", "airfare", "airline",
|
||||
"airmail", "airport", "airship", "airtight", "aisle", "ajar", "alarm", "albacore", "albino",
|
||||
"album", "alchemy", "alcove", "alert", "alfalfa", "algae", "algebra", "alias", "alibi", "alien",
|
||||
"alimony", "alive", "alkaline", "alley", "allied", "allocate", "allure", "almanac", "almond",
|
||||
"alone", "aloof", "alpaca", "alpha", "alpine", "alto", "altruism", "aluminum", "amateur", "amaze",
|
||||
"amber", "ambition", "ambrosia", "ambush", "amend", "amethyst", "amicable", "amiss", "ammonia",
|
||||
"amnesia", "amoeba", "amorous", "amount", "amperage", "amplify", "amputate", "amulet", "amuse",
|
||||
"anaconda", "anagram", "analysis", "anarchy", "anatomy", "ancestor", "anchor", "ancient",
|
||||
"android", "anecdote", "anemic", "aneurism", "angel", "angler", "angry", "anguish", "animal",
|
||||
"ankle", "annex", "announce", "annual", "anoint", "anomaly", "anorexia", "another", "answer",
|
||||
"antacid", "antenna", "anthem", "antique", "antonym", "anvil", "anxiety", "aorta", "apart",
|
||||
"apathy", "aperture", "apex", "aphid", "apogee", "apology", "apostle", "apparel", "appear",
|
||||
"apple", "appoint", "approve", "apricot", "apron", "aptitude", "aquatic", "arachnid", "arcade",
|
||||
"archery", "arctic", "ardent", "arduous", "arena", "argue", "argyle", "arise", "armada",
|
||||
"armchair", "armor", "armpit", "army", "aroma", "arouse", "arrange", "arrest", "arrive", "arrow",
|
||||
"arsenal", "arson", "artefact", "artist", "artwork", "asbestos", "ascend", "ascot", "ashamed",
|
||||
"ashtray", "ashy", "asia", "askew", "aspect", "asphalt", "aspirin", "assassin", "asset", "assist",
|
||||
"assorted", "assume", "asteroid", "asthma", "astound", "astride", "astute", "asylum", "atheism",
|
||||
"athlete", "atlas", "atoll", "atom", "atrium", "atrocity", "attack", "attend", "attic", "attorney",
|
||||
"attract", "auburn", "auction", "audacity", "audit", "augment", "august", "aura", "aurora",
|
||||
"austere", "author", "autopsy", "autumn", "avail", "avarice", "avatar", "avenger", "average",
|
||||
"aviation", "avocado", "avoid", "await", "awake", "award", "awesome", "awkward", "awning", "awry",
|
||||
"axe", "axiom", "axis", "azalea", "azimuth", "azure", "baboon", "babysit", "bachelor", "backhand",
|
||||
"bacon", "bacteria", "badge", "baffle", "bagel", "baggage", "bagpipe", "bailiff", "bakery",
|
||||
"balance", "balcony", "balder", "ballroom", "balmy", "baloney", "bamboo", "banana", "bandage",
|
||||
"bane", "bangle", "banister", "banjo", "banknote", "banner", "banquet", "banshee", "baptize",
|
||||
"barbecue", "barefoot", "bargain", "baritone", "bark", "barley", "barman", "barnyard", "baroness",
|
||||
"barrel", "basalt", "baseball", "bashful", "basic", "basket", "bassinet", "baste", "bathe",
|
||||
"battle", "bay", "bayonet", "bazooka", "beaker", "beam", "beanbag", "beard", "beastly", "beaten",
|
||||
"beauty", "because", "beckon", "become", "bedbug", "bedroom", "bedsore", "beefy", "beehive",
|
||||
"beeline", "beeswax", "beetle", "befall", "before", "befuddle", "beggar", "begin", "begrudge",
|
||||
"beguile", "behavior", "behead", "behind", "behold", "beige", "believe", "bellhop", "belong",
|
||||
"bemuse", "bench", "benefit", "benign", "bequeath", "berate", "beret", "berserk", "beseech",
|
||||
"beside", "bespoke", "best", "beta", "betray", "between", "beverage", "beware", "bewilder",
|
||||
"beyond", "biased", "bible", "bicep", "bicker", "bicycle", "bidding", "bifocal", "biggest",
|
||||
"bigmouth", "bigotry", "bigwig", "bike", "bikini", "billfold", "binary", "binder", "bingo",
|
||||
"binomial", "biology", "bionic", "biopsy", "bipedal", "birch", "birdcage", "birthday", "biscuit",
|
||||
"bisect", "bisque", "bistro", "bitter", "bizarre", "blackout", "blade", "blamed", "blanket",
|
||||
"blast", "blatant", "blazer", "bleak", "bleed", "blemish", "bless", "blimp", "blind", "blister",
|
||||
"blitz", "blizzard", "bloated", "blob", "blockade", "blogger", "blonde", "blooper", "blossom",
|
||||
"blouse", "blowgun", "blubber", "bludgeon", "bluejay", "bluffer", "blunder", "blur", "blustery",
|
||||
"boastful", "boater", "bobcat", "bobsled", "bobtail", "bodega", "bodice", "bodywork", "bogey",
|
||||
"bohemian", "boil", "boldface", "bolt", "bombard", "bonanza", "bond", "boneless", "bonfire",
|
||||
"bonnet", "bonsai", "bonus", "boogie", "book", "boost", "bootleg", "borax", "bordered", "boredom",
|
||||
"borrow", "bosom", "boss", "bosun", "botanist", "botched", "bother", "bottom", "botulism",
|
||||
"boulder", "boundary", "bouquet", "bourbon", "boutique", "bovine", "bowler", "boxer", "boycott",
|
||||
"boyhood", "bracelet", "brag", "braille", "bramble", "branch", "brass", "bratty", "brave",
|
||||
"brawler", "brazen", "breathe", "breeze", "brethren", "brevity", "brewery", "briar", "bribery",
|
||||
"brick", "bright", "brim", "brine", "brisket", "brittle", "broach", "broccoli", "broiler",
|
||||
"broken", "bronze", "broom", "browse", "bruise", "brunt", "brute", "bubble", "buckle", "buddy",
|
||||
"budget", "buffet", "builder", "bulb", "bulge", "bulimia", "bulky", "bulletin", "bummed", "bumpy",
|
||||
"bundle", "bungalow", "bunion", "bunker", "buoyant", "burden", "bureau", "burger", "burial",
|
||||
"burlap", "burnout", "blurp", "burrito", "burst", "bushel", "business", "bustle", "busy", "butane",
|
||||
"butcher", "butler", "button", "buxom", "buyer", "buyout", "buzz", "bylaw", "bypass", "cabaret",
|
||||
"cabbage", "cabin", "cactus", "cadaver", "cadet", "caffeine", "caftan", "cage", "cairn", "cajole",
|
||||
"calamity", "calcium", "calendar", "calico", "callous", "calm", "calorie", "calypso", "camera",
|
||||
"campus", "canal", "cancel", "candy", "canine", "cannibal", "canoe", "canteen", "canvas", "canyon",
|
||||
"capacity", "capital", "capsule", "capture", "caramel", "carbon", "carcass", "card", "careful",
|
||||
"cargo", "caribou", "carnival", "carousel", "carry", "carsick", "cartoon", "carve", "cascade",
|
||||
"cashew", "casino", "cassette", "castle", "casual", "catalog", "catcher", "category", "catnap",
|
||||
"catwalk", "cauldron", "causeway", "caution", "cavalry", "caveman", "cavity", "ceiling", "celery",
|
||||
"celibate", "cellmate", "cement", "censored", "center", "ceramic", "ceremony", "certain", "cesar",
|
||||
"cesium", "cesspool", "chaff", "chagrin", "chair", "chalice", "champion", "change", "chaotic",
|
||||
"chapter", "charity", "chase", "chat", "cheap", "checkers", "cheddar", "cheese", "chemical",
|
||||
"cherry", "chestnut", "chevron", "chew", "chicken", "chief", "chiffon", "child", "chimney",
|
||||
"china", "chipmunk", "chirp", "chisel", "chive", "chlorine", "choice", "choke", "cholera", "chomp",
|
||||
"choppy", "chorus", "chowder", "chronic", "chubby", "chuckle", "chug", "chummy", "chunk", "churn",
|
||||
"chutney", "cicada", "cider", "cigar", "cilantro", "cinema", "cinnamon", "cipher", "circle",
|
||||
"cistern", "citadel", "citizen", "citrus", "city", "civil", "claim", "clammy", "clang", "clarify",
|
||||
"class", "clatter", "clavicle", "clay", "cleanup", "cleft", "clemency", "clench", "clerk",
|
||||
"clever", "client", "cliff", "climate", "clinic", "clipped", "cloaked", "clock", "clog",
|
||||
"cloister", "closet", "clothing", "cloudy", "clove", "clown", "clubfoot", "clueless", "clump",
|
||||
"clunky", "cluster", "clutch", "coach", "coastal", "coated", "cobalt", "cobbler", "cobra",
|
||||
"cobweb", "coccyx", "cocktail", "coconut", "code", "coerce", "coffee", "cognac", "coherent",
|
||||
"cohort", "coiled", "coin", "colander", "colder", "coleslaw", "coliseum", "collect", "color",
|
||||
"column", "comatose", "combine", "comedy", "comfort", "comic", "common", "company", "comrade",
|
||||
"concert", "conduct", "confirm", "congress", "conical", "conjoin", "connect", "conquer", "consume",
|
||||
"control", "convince", "cookbook", "cool", "copper", "copy", "corduroy", "corner", "coronary",
|
||||
"corporal", "correct", "corset", "cortex", "cosmetic", "cosplay", "costume", "cotton", "cougar",
|
||||
"counter", "coupon", "courier", "cousin", "cover", "cowardly", "cowboy", "cowlick", "coyote",
|
||||
"crabby", "crackle", "cradle", "craft", "cram", "crane", "crater", "craving", "crawl", "crayon",
|
||||
"crazy", "creamy", "credit", "creep", "cremate", "crescent", "crevice", "cricket", "criminal",
|
||||
"cringe", "crisis", "critical", "croak", "crochet", "crooked", "crop", "croquet", "crossbow",
|
||||
"crouch", "crowd", "crucial", "cruel", "cruiser", "crumble", "crunch", "crush", "crux", "cryptic",
|
||||
"crystal", "cube", "cuckoo", "cucumber", "cuddle", "cudgel", "cuff", "cuisine", "culinary",
|
||||
"culprit", "cultural", "culvert", "cumin", "cumulus", "cunning", "cupboard", "cupcake", "cupid",
|
||||
"curator", "curb", "curfew", "curled", "currency", "cursive", "curtsy", "curvy", "cushion", "cuss",
|
||||
"custody", "cutback", "cutest", "cuticle", "cutlery", "cutout", "cycle", "cylinder", "cynic",
|
||||
"cypress", "cyst", "dabble", "daffodil", "dainty", "daiquiri", "daisy", "damage", "damsel",
|
||||
"dance", "dandruff", "danger", "dapper", "darkness", "darling", "dart", "dash", "database",
|
||||
"dateline", "daughter", "daunting", "dawdle", "daybreak", "daydream", "daylight", "dazed",
|
||||
"dazzle", "deadline", "dealer", "dean", "deathbed", "debate", "debrief", "debtor", "debut",
|
||||
"decade", "deceased", "decision", "deck", "declare", "decorate", "decrease", "dedicate", "deduct",
|
||||
"deed", "deepest", "deface", "defense", "define", "deflate", "deformed", "defraud", "deft",
|
||||
"defuse", "degree", "deity", "dejected", "delay", "delegate", "deliver", "delta", "delusion",
|
||||
"delve", "demand", "demeanor", "demise", "democrat", "demure", "denial", "denounce", "density",
|
||||
"dentist", "deny", "depart", "depend", "depict", "deploy", "deposit", "depress", "depth", "deputy",
|
||||
"derail", "derby", "derelict", "derive", "describe", "deserter", "desire", "desktop", "desolate",
|
||||
"despair", "destroy", "detach", "detect", "detour", "devalue", "develop", "device", "devote",
|
||||
"dewdrop", "diabetic", "diagnose", "dialogue", "diamond", "diaper", "diary", "diatribe", "dicey",
|
||||
"dictate", "diesel", "diet", "differ", "digest", "digital", "dignity", "digress", "dilemma",
|
||||
"diligent", "dilute", "diminish", "dimmer", "dimpled", "dingy", "dinner", "dinosaur", "diorama",
|
||||
"diploma", "direct", "dirty", "disabled", "disburse", "disco", "disdain", "disease", "disguise",
|
||||
"dishevel", "dismal", "dispense", "disrupt", "dissuade", "distance", "dive", "divide", "divorce",
|
||||
"divulge", "dizzy", "docility", "dockyard", "doctor", "document", "dodge", "dodo", "dogged",
|
||||
"doghouse", "dogmatic", "doldrums", "doll", "dolphin", "domain", "domestic", "dominant", "donate",
|
||||
"donkey", "doomsday", "door", "dorky", "dorm", "dorsal", "dosage", "dossier", "dotted", "doubt",
|
||||
"doughnut", "downtown", "dowry", "dozen", "draftee", "dragon", "drainage", "dramatic", "drapery",
|
||||
"drastic", "draw", "dream", "dredge", "dress", "dribble", "dried", "drift", "drink", "driveway",
|
||||
"drizzle", "drool", "droplet", "drought", "drove", "drowsy", "drudgery", "drug", "druid",
|
||||
"drummer", "drywall", "dubious", "duckling", "dugout", "duke", "dumbbell", "dumpster", "dungeon",
|
||||
"duo", "duplex", "duration", "dust", "dutchess", "dutiful", "duty", "duvet", "dwarf", "dwell",
|
||||
"dwindle", "dynamic", "dyslexia", "eager", "eagle", "eardrum", "earl", "earmark", "earner",
|
||||
"earphone", "earring", "earshot", "earth", "earwig", "easel", "eastward", "easy", "eatery", "ebb",
|
||||
"ebony", "echo", "eclectic", "eclipse", "ecology", "economy", "ecstasy", "edge", "edible",
|
||||
"edifice", "editor", "educate", "eel", "eery", "effigy", "effort", "eggnog", "eggplant",
|
||||
"eggshell", "ego", "elapse", "elastic", "elated", "elbow", "elder", "election", "elegance",
|
||||
"element", "elephant", "elevator", "eligible", "elite", "elixir", "ellipsis", "elm", "elongate",
|
||||
"elope", "eloquent", "elude", "elusive", "emaciate", "email", "emanate", "embark", "embezzle",
|
||||
"emblem", "embody", "embrace", "emerald", "emigrant", "eminent", "emission", "emoji", "emotion",
|
||||
"empathy", "emperor", "emphasis", "employer", "empower", "empty", "emu", "emulate", "enamel",
|
||||
"enchant", "enclose", "encoder", "encrypt", "encumber", "endeavor", "endless", "endorse", "endure",
|
||||
"enemy", "energize", "engage", "engine", "engraver", "engulf", "enhance", "enigma", "enjoy",
|
||||
"enlist", "enmity", "enormity", "enraged", "enrich", "enroll", "ensemble", "ensnare", "entangle",
|
||||
"enthrone", "entire", "entrance", "entwine", "envelope", "envision", "envy", "enzyme", "epic",
|
||||
"epidemic", "epigram", "epilepsy", "episode", "epitaph", "epoch", "epoxy", "equation", "equinox",
|
||||
"eraser", "erect", "erode", "errand", "error", "erupt", "escape", "escort", "escrow", "esoteric",
|
||||
"espresso", "essay", "essence", "estate", "esteem", "estimate", "estrange", "estuary", "eternal",
|
||||
"ethereal", "ethical", "ethnic", "eulogy", "euphoric", "eureka", "euro", "evade", "evaluate",
|
||||
"evasion", "event", "evict", "evidence", "evil", "evoke", "evolve", "exact", "exalted", "example",
|
||||
"excavate", "excerpt", "exchange", "excite", "exclude", "excrete", "excuse", "execute", "exempt",
|
||||
"exercise", "exhaust", "exhibit", "exhume", "exile", "exist", "exodus", "exorcist", "exotic",
|
||||
"expand", "expert", "expire", "explain", "expose", "express", "extend", "extinct", "extort",
|
||||
"extra", "eyeball", "eyeglass", "eyelash", "fabled", "fabric", "fabulous", "facade", "facelift",
|
||||
"facility", "fact", "faculty", "faded", "failure", "fainter", "fairy", "faith", "fake", "falcon",
|
||||
"fallout", "false", "famished", "famous", "fanatic", "fanboy", "fancy", "fanfare", "fang",
|
||||
"fantasy", "farewell", "farmer", "farther", "fashion", "fasten", "fatal", "fathom", "fatigue",
|
||||
"fatty", "faucet", "fault", "favorite", "fax", "fealty", "fearless", "feast", "feature", "federal",
|
||||
"fedora", "fee", "feeble", "feedback", "feeler", "feign", "feisty", "feline", "felon", "feminine",
|
||||
"femur", "fence", "feral", "fern", "ferocity", "ferret", "fertile", "fervent", "festival", "fetch",
|
||||
"fetid", "feud", "fever", "fiasco", "fiber", "fiction", "fiddler", "fidelity", "fidget",
|
||||
"fiendish", "fiery", "fiesta", "figure", "filament", "filch", "filet", "filigree", "filling",
|
||||
"filter", "finance", "fine", "finger", "finish", "firewood", "firm", "first", "fiscal", "fishery",
|
||||
"fissure", "fitful", "fixate", "fizz", "fjord", "flabby", "flag", "flail", "flaky", "flame",
|
||||
"flannel", "flapjack", "flash", "flatten", "flaunt", "flavor", "flawless", "fleece", "fleshy",
|
||||
"flexible", "flick", "flight", "flimsy", "fling", "flip", "flirt", "float", "floor", "floppy",
|
||||
"floral", "floss", "flotsam", "flourish", "flower", "fluent", "fluff", "fluid", "flummox", "flunk",
|
||||
"fluoride", "flurry", "flusher", "flute", "flux", "flypaper", "flywheel", "foam", "focus",
|
||||
"fodder", "fog", "fold", "foliage", "folklore", "follow", "fondue", "font", "foolish", "football",
|
||||
"forager", "forbid", "force", "forecast", "forfeit", "forget", "forklift", "forlorn", "formal",
|
||||
"forsake", "fortune", "forum", "forward", "fossil", "fought", "foul", "founder", "foxhound",
|
||||
"foxtrot", "foxy", "foyer", "fraction", "fragment", "frailty", "frantic", "fraught", "freaky",
|
||||
"freckled", "freeway", "freight", "frenzy", "frequent", "freshman", "fret", "fridge", "friend",
|
||||
"frighten", "fringed", "frisky", "fritter", "frizzy", "frock", "frolic", "frontier", "frost",
|
||||
"froth", "frown", "frozen", "fructose", "frugal", "fruit", "fugitive", "fulcrum", "fulfill",
|
||||
"fullback", "fumigate", "fund", "funeral", "fungus", "funny", "furious", "furl", "furnace",
|
||||
"furrow", "furthest", "fuselage", "fusion", "fussy", "futile", "futon", "future", "fuzzy",
|
||||
"gadget", "galaxy", "gallery", "gambler", "game", "gangster", "gap", "garage", "garden", "gargle",
|
||||
"garish", "garlic", "garment", "garnet", "garrison", "garter", "gaseous", "gaslight", "gasoline",
|
||||
"gastric", "gatepost", "gather", "gaudy", "gauge", "gauntlet", "gavel", "gawk", "gazette",
|
||||
"gearbox", "gecko", "geeky", "geezer", "gelatin", "gemstone", "general", "genius", "genre",
|
||||
"gentle", "genuine", "geology", "geometry", "gerbil", "germ", "gesture", "getaway", "geyser",
|
||||
"ghastly", "ghetto", "ghost", "ghoul", "giddy", "gifted", "gigantic", "giggle", "gilded",
|
||||
"gimmick", "giraffe", "girdle", "girlish", "girth", "gist", "gizmo", "gizzard", "glacier", "glad",
|
||||
"glamour", "glance", "glare", "glassy", "glaucoma", "glazed", "gleam", "gleeful", "glen", "glib",
|
||||
"glide", "glimpse", "glint", "glitter", "globe", "gloom", "glory", "glossary", "gloved", "glow",
|
||||
"glucose", "glue", "gluttony", "glycerin", "glyph", "gnarled", "gnash", "gnaw", "gnome", "goalie",
|
||||
"goatee", "goblet", "goddess", "goldfish", "golfer", "gondola", "good", "gooey", "goose", "gopher",
|
||||
"gorge", "gorilla", "gosling", "gospel", "gossip", "gothic", "gourmet", "govern", "gown",
|
||||
"grabber", "gracious", "graduate", "graffiti", "grainy", "grammar", "grant", "grape", "grasp",
|
||||
"grateful", "gravity", "gray", "greasy", "green", "gremlin", "grew", "gridlock", "grief", "grill",
|
||||
"grimace", "grin", "gristle", "gritty", "grizzly", "grocery", "groggy", "grommet", "groove",
|
||||
"gross", "grotto", "grout", "grovel", "grownup", "grub", "gruesome", "gruff", "grumpy", "grungy",
|
||||
"gryphon", "guard", "guava", "guess", "guidance", "guilty", "guitar", "gullible", "gulp", "gumbo",
|
||||
"gumdrop", "gumption", "gunsmith", "gurney", "guru", "gusty", "gut", "guttural", "gym", "gymnast",
|
||||
"gypsy", "gyration", "gyro", "habit", "hacksaw", "haggler", "haiku", "haircut", "halberd", "half",
|
||||
"halibut", "hallway", "halogen", "halter", "hamlet", "hammer", "hamster", "handrail", "hangover",
|
||||
"happy", "harass", "harbor", "hardwood", "harmonic", "harness", "harpoon", "harsh", "harvest",
|
||||
"hashtag", "hassle", "hatchet", "hateful", "hatred", "hauler", "haunch", "havoc", "haystack",
|
||||
"haywire", "hazard", "hazelnut", "hazmat", "headset", "health", "hearsay", "heat", "heavy",
|
||||
"heckler", "hectic", "hedgehog", "hedonism", "heedful", "hegemony", "height", "heinous",
|
||||
"heirloom", "heliport", "hello", "helmet", "helpful", "hemlock", "hen", "henchman", "henna",
|
||||
"herald", "herbal", "heresy", "heritage", "hernia", "heron", "herring", "hesitate", "hexagon",
|
||||
"hiatus", "hibiscus", "hiccup", "hickory", "hidden", "hideaway", "highway", "hijacker", "hiker",
|
||||
"hilarity", "hill", "hinge", "hint", "hip", "hippo", "history", "hither", "hoagie", "hoarder",
|
||||
"hoax", "hobby", "hobo", "hockey", "hoedown", "hoggish", "hoist", "holiday", "holler", "hologram",
|
||||
"holster", "holy", "homage", "home", "homicide", "homonym", "honeydew", "honk", "honor", "hoodie",
|
||||
"hookworm", "hooligan", "hoop", "hooray", "hopeful", "horizon", "hormone", "hornet", "horror",
|
||||
"horseman", "hospital", "hostess", "hotel", "hourly", "housing", "howdy", "howitzer", "hub",
|
||||
"hubcap", "hubris", "huge", "hula", "human", "humble", "humdrum", "humidity", "hummus", "humpback",
|
||||
"hungry", "hunter", "hurdle", "hurry", "hurt", "husband", "hush", "husky", "hustler", "hyacinth",
|
||||
"hybrid", "hydrate", "hyena", "hygiene", "hyper", "hyphen", "hypnosis", "hysteria", "iceberg",
|
||||
"icicle", "icon", "icy", "idea", "identify", "ideology", "idiot", "idler", "idolize", "idyllic",
|
||||
"igloo", "ignite", "ignore", "iguana", "illicit", "illusion", "imagery", "imbibe", "imbue",
|
||||
"imitate", "immense", "immolate", "immune", "impasse", "impede", "implode", "impostor", "impress",
|
||||
"impunity", "inbox", "incense", "inch", "incision", "include", "income", "incubate", "index",
|
||||
"indicate", "industry", "inept", "inertia", "infant", "inferno", "infinity", "inflict", "info",
|
||||
"infrared", "ingest", "ingot", "inhale", "inherit", "inhibit", "initial", "inject", "injure",
|
||||
"inkwell", "inlet", "inmate", "innocent", "innuendo", "input", "inquiry", "insane", "insert",
|
||||
"insignia", "insomnia", "inspect", "instruct", "insult", "intact", "interest", "intimate",
|
||||
"intruder", "invasion", "investor", "invite", "invoke", "iodine", "ionize", "iris", "irksome",
|
||||
"ironwork", "irritate", "island", "isolate", "isotope", "issue", "itchy", "item", "iterate",
|
||||
"ivory", "jabber", "jackal", "jade", "jagged", "jaguar", "jailer", "jamboree", "janitor", "jargon",
|
||||
"jasmine", "jaundice", "javelin", "jaw", "jawbone", "jaywalk", "jazz", "jealousy", "jeans",
|
||||
"jeopardy", "jerky", "jersey", "jester", "jet", "jettison", "jewelry", "jiffy", "jigsaw", "jinx",
|
||||
"jittery", "jock", "jogger", "joint", "joke", "jolly", "jostle", "journey", "joust", "joy",
|
||||
"joyride", "joystick", "jubilant", "judge", "judicial", "judo", "jug", "juggler", "juicy",
|
||||
"jujitsu", "jukebox", "jump", "junction", "junior", "junk", "juror", "jury", "justify", "juvenile",
|
||||
"kabob", "kale", "kangaroo", "karaoke", "karma", "kayak", "kazoo", "keen", "keepsake", "keg",
|
||||
"kennel", "keratin", "kerchief", "kerosene", "kestrel", "ketchup", "keyboard", "keyhole",
|
||||
"keynote", "keystone", "keyword", "khaki", "kick", "kidder", "kidney", "killjoy", "kilogram",
|
||||
"kilt", "kimono", "kind", "kinetic", "kinfolk", "kingdom", "kinsman", "kiosk", "kiss", "kitchen",
|
||||
"kite", "kitten", "kiwi", "klutzy", "knapsack", "knee", "knife", "knitted", "knockout", "know",
|
||||
"knuckle", "koala", "kumquat", "lab", "label", "labor", "lacerate", "lackey", "lacquer",
|
||||
"lacrosse", "lactose", "ladder", "ladybug", "laggard", "lagoon", "lair", "lament", "laminate",
|
||||
"lamp", "lancer", "landlord", "language", "lanky", "lantern", "lanyard", "laptop", "larceny",
|
||||
"large", "larvae", "larynx", "lasagna", "lasso", "latex", "latitude", "latrine", "lattice",
|
||||
"laugh", "laundry", "laureate", "lava", "lavender", "lavish", "lawful", "lawn", "lawyer",
|
||||
"laxative", "layaway", "layout", "lazy", "leader", "leaflet", "league", "leaky", "leap", "learn",
|
||||
"leash", "leathery", "lecture", "ledger", "leeway", "lefty", "legacy", "legend", "legible",
|
||||
"legume", "legwork", "leisure", "lemming", "lemonade", "lend", "length", "leniency", "lentil",
|
||||
"leopard", "leprosy", "lesion", "lesser", "lethargy", "letter", "level", "levitate", "levy",
|
||||
"lexicon", "liaison", "liberty", "library", "license", "lichen", "lieu", "lifespan", "lift",
|
||||
"ligament", "lighter", "likable", "lilac", "limbo", "lime", "limit", "linchpin", "linen",
|
||||
"linoleum", "linseed", "lion", "lipid", "lipstick", "liquid", "lisp", "listen", "literary",
|
||||
"lithium", "litigate", "little", "livery", "lizard", "llama", "loaded", "loaf", "loaner", "loathe",
|
||||
"lobbyist", "lobotomy", "lobster", "location", "lockjaw", "locust", "logic", "logo", "loiterer",
|
||||
"lollipop", "lonesome", "long", "lookout", "loopy", "looter", "lopsided", "lordship", "loser",
|
||||
"lotion", "lottery", "lotus", "loud", "lounge", "lousy", "lovely", "lowland", "loyalty", "lozenge",
|
||||
"lucid", "luck", "luggage", "lukewarm", "lullaby", "lumber", "luminous", "lunar", "lunch", "lure",
|
||||
"luxury", "lychee", "lymph", "lyric", "macaroni", "machine", "macro", "mad", "maestro", "magazine",
|
||||
"magenta", "maggot", "magic", "magma", "magnet", "mahogany", "maiden", "mailman", "maimed",
|
||||
"maintain", "majestic", "majority", "makeup", "malaria", "malice", "mallard", "malted", "malware",
|
||||
"mammal", "manager", "mandolin", "maneuver", "mango", "manhole", "manicure", "mankind", "manly",
|
||||
"manpower", "mansion", "mantra", "manual", "maple", "marathon", "marble", "march", "mare",
|
||||
"margin", "marine", "market", "marmot", "maroon", "marriage", "marshal", "martini", "marvel",
|
||||
"mascara", "mashup", "masked", "mason", "massive", "master", "matador", "matchbox", "material",
|
||||
"math", "matrix", "mattress", "maturity", "maverick", "maximum", "mayhem", "mayor", "meadow",
|
||||
"meander", "measure", "meatball", "mechanic", "medalist", "medical", "medley", "meeting",
|
||||
"megabyte", "melanoma", "mellow", "melody", "melt", "member", "meme", "memory", "menace", "mental",
|
||||
"menu", "meow", "merchant", "merge", "merit", "mermaid", "mesh", "message", "metallic", "meteor",
|
||||
"method", "metric", "miasma", "microbe", "midair", "midday", "midnight", "midpoint", "midriff",
|
||||
"midst", "midterm", "midwife", "midyear", "mighty", "migraine", "mild", "mileage", "military",
|
||||
"milkman", "million", "mimic", "mimosa", "minced", "mindful", "mineral", "minister", "minnow",
|
||||
"minstrel", "minty", "minute", "miracle", "mirror", "mischief", "misery", "misfit", "mishap",
|
||||
"missile", "mistake", "mitosis", "mitt", "mixture", "mnemonic", "mobile", "moccasin", "mocha",
|
||||
"mockery", "modern", "modify", "module", "mogul", "moisture", "molasses", "moldy", "molecule",
|
||||
"mollusk", "molten", "moment", "momma", "monarch", "money", "mongoose", "monitor", "monocle",
|
||||
"monster", "month", "monument", "moocher", "moon", "mop", "moped", "moral", "morgue", "morning",
|
||||
"morocco", "morphine", "morsel", "mortgage", "mosaic", "mosquito", "mossy", "motherly", "motivate",
|
||||
"motley", "motor", "motto", "mountain", "mourn", "mouse", "mouthful", "move", "mucky", "mucus",
|
||||
"muffin", "mugger", "mulch", "mullet", "multiply", "mummify", "mundane", "murmur", "muscle",
|
||||
"museum", "mushroom", "musical", "musket", "mustang", "mutant", "mute", "mutilate", "mutter",
|
||||
"mutual", "myopia", "myriad", "mystery", "mythical", "nacho", "nail", "naive", "naked", "namesake",
|
||||
"nanny", "napkin", "narcotic", "narrator", "narwhal", "nasal", "nasty", "national", "nature",
|
||||
"naughty", "nausea", "nautical", "navigate", "nearest", "nebula", "necklace", "nectar", "needle",
|
||||
"negative", "neglect", "neighbor", "nemesis", "neon", "neoprene", "nephew", "nepotism", "nerd",
|
||||
"nerve", "nestle", "network", "neuron", "neutral", "newborn", "newlywed", "newscast", "newton",
|
||||
"next", "nexus", "niche", "nickel", "nicotine", "niece", "nifty", "nightcap", "nihilist", "nimble",
|
||||
"ninja", "nippy", "nirvana", "nitpick", "nitrogen", "nitwit", "noble", "nobody", "nocturne",
|
||||
"noise", "nomad", "nominee", "nonsense", "noodle", "normalcy", "north", "nosedive", "nostril",
|
||||
"nosy", "notary", "notch", "notepad", "notice", "noun", "nourish", "novel", "noxious", "nozzle",
|
||||
"nuance", "nuclear", "nugget", "nuisance", "nullify", "numb", "numeral", "nunnery", "nurse",
|
||||
"nurture", "nutmeg", "nutrient", "nutshell", "nutty", "nylon", "oak", "oasis", "oatmeal",
|
||||
"obedient", "obelisk", "obese", "obey", "obituary", "object", "oblige", "oblong", "oboe",
|
||||
"obscure", "observe", "obsidian", "obsolete", "obstacle", "obtain", "obtuse", "obvious",
|
||||
"occasion", "occupant", "ocean", "ocelot", "octave", "octopus", "ocular", "oddity", "odometer",
|
||||
"odyssey", "offer", "offhand", "office", "offload", "offshore", "often", "ogre", "oilskin",
|
||||
"ointment", "okay", "oligarch", "omelet", "omit", "omnivore", "oncoming", "onion", "online",
|
||||
"onset", "onward", "onyx", "ooze", "opaque", "opera", "opinion", "oppose", "oppress", "option",
|
||||
"opulent", "oracle", "orange", "orbit", "orchard", "order", "ordinary", "oregano", "organize",
|
||||
"original", "ornament", "orphan", "orthodox", "osmosis", "ostrich", "otter", "ottoman", "ounce",
|
||||
"outage", "outburst", "outcome", "outdoor", "outfit", "outgrow", "outhouse", "outlaw", "output",
|
||||
"outrun", "outside", "outtake", "outward", "oval", "oven", "overall", "owl", "owner", "oxford",
|
||||
"oxygen", "oxymoron", "oyster", "ozone", "pacific", "package", "paddle", "padlock", "pageant",
|
||||
"pagoda", "painful", "pajamas", "palpable", "palsy", "paltry", "pamphlet", "panacea", "pancake",
|
||||
"panda", "panel", "panic", "panorama", "panther", "papaya", "paper", "paprika", "papyrus",
|
||||
"parade", "parcel", "pardon", "parental", "pariah", "parka", "parlor", "parody", "parrot",
|
||||
"parsley", "particle", "passport", "pasta", "patchy", "patent", "pathway", "patio", "patrol",
|
||||
"pattern", "pave", "pavilion", "pawnshop", "payday", "payload", "peaceful", "peanut", "pearly",
|
||||
"peasant", "pebble", "pecan", "pectoral", "peculiar", "pedantic", "peddler", "pedestal",
|
||||
"pedicure", "peerless", "pelican", "pellet", "penalty", "pencil", "pendant", "penguin", "penny",
|
||||
"pensive", "pentagon", "pepper", "percent", "perfect", "period", "perjury", "perk", "permit",
|
||||
"perplex", "person", "perturb", "peruse", "perverse", "pesky", "petal", "petition", "petrify",
|
||||
"petunia", "pewter", "phalanx", "phantom", "pharmacy", "phlegm", "phobia", "phoenix", "phonetic",
|
||||
"photo", "phrase", "physical", "piano", "pickup", "picnic", "picture", "pierce", "piety", "pigeon",
|
||||
"piglet", "pigment", "pile", "pilfer", "pilgrim", "pillow", "pinball", "pincer", "pinhole", "pink",
|
||||
"pinnacle", "pinpoint", "pinto", "pioneer", "pious", "pirate", "piston", "pitch", "pitfall",
|
||||
"pitiful", "pitted", "pivot", "pixel", "pixie", "pizza", "placard", "plan", "plaque", "plasma",
|
||||
"platform", "playlist", "plaza", "plead", "pledge", "plenty", "plethora", "pliable", "plot",
|
||||
"plumage", "plunge", "plural", "plushy", "plywood", "poacher", "pockmark", "podcast", "podiatry",
|
||||
"poem", "poignant", "pointy", "poison", "poker", "polarize", "polished", "polka", "pollen", "polo",
|
||||
"polygon", "pomade", "pompom", "poncho", "pontoon", "ponytail", "pooch", "popcorn", "popular",
|
||||
"porous", "porridge", "portion", "positive", "possible", "post", "potato", "potency", "pothole",
|
||||
"potluck", "poultry", "poverty", "powder", "powerful", "pox", "practice", "prairie", "praline",
|
||||
"prance", "prawn", "prayer", "preach", "precise", "predator", "prefer", "preheat", "prelude",
|
||||
"premium", "prepare", "prequel", "pressure", "pretty", "prevent", "price", "priest", "primary",
|
||||
"print", "priority", "prisoner", "privacy", "problem", "process", "produce", "profile", "program",
|
||||
"prohibit", "project", "prologue", "promise", "pronoun", "proof", "property", "prorate",
|
||||
"prospect", "protest", "proud", "provide", "prowl", "proxy", "prudish", "prune", "psalm", "pseudo",
|
||||
"psychic", "puberty", "public", "pucker", "pudding", "pudgy", "puffy", "pugilist", "pulley",
|
||||
"pulpit", "pulse", "puma", "pummel", "pumpkin", "puncture", "pundit", "pungent", "punish", "puny",
|
||||
"pupil", "puppy", "purchase", "puree", "purity", "purple", "pursuit", "purveyor", "pushup",
|
||||
"putrid", "putt", "puzzle", "pyramid", "pyre", "python", "quadrant", "quagmire", "quail", "quake",
|
||||
"quality", "quantity", "quarter", "quasar", "queasy", "queen", "quell", "quench", "query", "quest",
|
||||
"quick", "quiet", "quilted", "quirky", "quitter", "quiver", "quiz", "quote", "rabbit", "rabies",
|
||||
"raccoon", "race", "radar", "radio", "radon", "rafter", "raggedy", "ragtag", "raider", "railroad",
|
||||
"rainbow", "raisin", "rally", "rampage", "ramrod", "rancher", "random", "ransack", "rapid",
|
||||
"rarity", "rascal", "raspy", "rat", "ration", "raunchy", "ravage", "raven", "ravioli", "razor",
|
||||
"reaction", "ready", "realize", "reaper", "rebel", "rebound", "rebuttal", "receiver", "recharge",
|
||||
"recipe", "reckless", "recliner", "recorder", "recruit", "rectify", "recycle", "redeemer",
|
||||
"redhead", "redneck", "reduce", "redwood", "reef", "referee", "refinery", "reflect", "reform",
|
||||
"refrain", "refugee", "regalia", "regency", "reggae", "region", "regret", "regular", "rehab",
|
||||
"rehearse", "reject", "rejoice", "relaxed", "relevant", "relic", "reload", "remark", "remedy",
|
||||
"reminder", "remnant", "removal", "render", "renegade", "renovate", "rent", "repair", "repeater",
|
||||
"replica", "report", "reprisal", "reptile", "repute", "requiem", "rescue", "resemble", "resident",
|
||||
"resonate", "response", "restroom", "result", "retailer", "retina", "retrieve", "reunion",
|
||||
"reveler", "revive", "revolt", "reward", "rhapsody", "rhetoric", "rhino", "rhodium", "rhombus",
|
||||
"rhubarb", "rhythm", "rib", "ribbon", "rich", "rickshaw", "ricochet", "riddle", "ride", "ridicule",
|
||||
"riff", "rifle", "rightful", "rigid", "ringtone", "rinse", "riot", "ripple", "risk", "ritual",
|
||||
"ritzy", "rival", "river", "roadwork", "roar", "roast", "robbery", "robe", "robin", "robot",
|
||||
"robust", "rocky", "rodeo", "roguish", "romantic", "romp", "roofing", "rookie", "roommate",
|
||||
"rosemary", "roster", "rotate", "rotten", "rouge", "roulette", "round", "routine", "rowboat",
|
||||
"royal", "rubber", "rubric", "rucksack", "rudder", "rueful", "ruffian", "rugby", "rugged", "ruin",
|
||||
"ruler", "ruminate", "rummage", "rumor", "rumple", "runner", "runoff", "runway", "rupture",
|
||||
"rural", "rusted", "ruthless", "saber", "sabotage", "sadistic", "sadness", "safari", "safe",
|
||||
"saffron", "saga", "said", "sailor", "saint", "salary", "salesman", "saliva", "salon", "salsa",
|
||||
"salt", "salute", "salvage", "sampler", "samurai", "sanctum", "sandwich", "sanguine", "sanitize",
|
||||
"sapling", "sapphire", "sarcasm", "sardine", "sassy", "satchel", "satisfy", "saturate", "sauce",
|
||||
"sauna", "savanna", "savior", "savory", "savvy", "sawdust", "sawhorse", "sawmill", "scab",
|
||||
"scaffold", "scale", "scamper", "scandal", "scapula", "scared", "scatter", "scavenge", "scenery",
|
||||
"scepter", "scheme", "schism", "schnapps", "scholar", "science", "scimitar", "scissor", "scoff",
|
||||
"scold", "scooter", "scope", "scorpion", "scotch", "scout", "scowl", "scramble", "screen",
|
||||
"script", "scroll", "scrub", "scuba", "scuffed", "sculpt", "scumbag", "scurry", "scuttle",
|
||||
"scythe", "seabird", "seafood", "sealant", "seamless", "seaplane", "search", "season", "seaweed",
|
||||
"secluded", "second", "secret", "section", "security", "sedan", "sediment", "seek", "seepage",
|
||||
"seesaw", "seethe", "segment", "seismic", "seizure", "seldom", "select", "self", "sellout",
|
||||
"seltzer", "semantic", "semester", "seminar", "senator", "senior", "sensory", "sentence", "sepia",
|
||||
"sepsis", "sequence", "serenade", "serfdom", "sergeant", "serious", "serpent", "serrated", "serum",
|
||||
"service", "sesame", "session", "setback", "settle", "setup", "severity", "sewage", "sewing",
|
||||
"sextant", "shabby", "shackle", "shadow", "shaft", "shaggy", "shaky", "shallow", "shame", "sharp",
|
||||
"shawl", "sheathe", "sheet", "shelter", "shepherd", "sheriff", "shield", "shifty", "shimmy",
|
||||
"shinbone", "shipyard", "shiver", "shock", "shoelace", "shop", "short", "shotgun", "shoulder",
|
||||
"shove", "showoff", "shrapnel", "shred", "shrine", "shroud", "shrug", "shudder", "shuffle",
|
||||
"shutdown", "sibling", "sickle", "sidewalk", "siege", "sierra", "signal", "silent", "silicone",
|
||||
"silk", "silly", "silver", "similar", "simple", "simulate", "since", "sinew", "single", "sinkhole",
|
||||
"sinus", "siphon", "siren", "sirloin", "sister", "sitcom", "size", "sizzle", "skate", "skeleton",
|
||||
"skeptic", "sketch", "skewer", "skier", "skillet", "skimmed", "skin", "skirt", "skittish", "skull",
|
||||
"skunk", "skydive", "skyline", "skyward", "slacker", "slalom", "slammer", "slant", "slather",
|
||||
"sleaze", "sled", "sleepy", "slender", "sleuth", "slice", "slim", "slinky", "slipper", "slobber",
|
||||
"slogan", "sloped", "sloth", "slouch", "sluggish", "slurp", "slush", "smart", "smash", "smear",
|
||||
"smell", "smile", "smith", "smoke", "smolder", "smooth", "smother", "smudge", "smug", "snack",
|
||||
"snag", "snake", "snapshot", "snarl", "snazzy", "sneaker", "sneeze", "snicker", "snide", "sniff",
|
||||
"snip", "snobbish", "snooze", "snore", "snot", "snowball", "snuggle", "soaked", "soap", "sob",
|
||||
"soccer", "society", "socket", "soda", "sodium", "soft", "soggy", "solar", "soldier", "solemn",
|
||||
"solid", "soloist", "solstice", "solution", "solve", "sombrero", "somebody", "sonata", "songbird",
|
||||
"sonic", "sooner", "soothe", "soprano", "sorbet", "sorcerer", "sorority", "sortie", "soulmate",
|
||||
"source", "south", "souvenir", "soybean", "space", "spandex", "spark", "spasm", "spatula", "spawn",
|
||||
"speak", "special", "speech", "spend", "spew", "sphere", "sphinx", "spicy", "spider", "spiffy",
|
||||
"spigot", "spill", "spine", "spirit", "spit", "splash", "spleen", "splint", "splotchy", "splurge",
|
||||
"spoil", "sponsor", "spoon", "sporty", "spotter", "spouse", "spray", "spreader", "sprinkle",
|
||||
"sprout", "spruce", "spud", "spunky", "spurn", "spy", "spyglass", "square", "squeeze", "squirrel",
|
||||
"sriracha", "stable", "staccato", "stadium", "stage", "stairway", "stalker", "stamp", "standard",
|
||||
"stapler", "starve", "station", "staunch", "stay", "steady", "steer", "stellar", "stencil",
|
||||
"stereo", "steward", "stick", "stifle", "stigma", "stilt", "stimulus", "stingray", "stipend",
|
||||
"stir", "stockade", "stoic", "stolen", "stomach", "stone", "stool", "stopper", "storm", "stow",
|
||||
"strategy", "street", "strike", "strong", "struggle", "stub", "stucco", "student", "stuff",
|
||||
"stumble", "stunt", "stupor", "sturgeon", "stutter", "stylus", "stymie", "suave", "subdue",
|
||||
"subject", "sublime", "submit", "subplot", "subsidy", "subtitle", "suburbia", "subvert", "subway",
|
||||
"success", "sudden", "sudsy", "suffer", "sugar", "suggest", "suitable", "sulfur", "sullen",
|
||||
"sultan", "summer", "sumo", "sunburn", "sundial", "sunken", "sunlight", "sunroof", "sunset",
|
||||
"superior", "support", "supreme", "surface", "surgery", "surmount", "surname", "surprise",
|
||||
"surround", "survive", "sushi", "suspect", "sustain", "swaddle", "swagger", "swampy", "swan",
|
||||
"swarm", "swath", "sweater", "sweeper", "swerve", "swift", "swimmer", "swindler", "swipe",
|
||||
"switch", "swivel", "swollen", "swoop", "sworn", "sycamore", "syllabus", "symbolic", "symmetry",
|
||||
"sympathy", "synapse", "sync", "syndrome", "synergy", "synopsis", "syntax", "syringe", "syrup",
|
||||
"system", "tablet", "taboo", "tacit", "tackle", "taco", "tactile", "tadpole", "taffy", "tag",
|
||||
"tailpipe", "takeout", "talent", "talisman", "tall", "tamper", "tandem", "tangy", "tankard",
|
||||
"tanned", "tantrum", "tapestry", "tapioca", "tardy", "target", "tariff", "tarmac", "tarnish",
|
||||
"tarp", "tarrier", "tartar", "task", "tassel", "tasteful", "tattoo", "taunt", "tavern", "taxation",
|
||||
"taxi", "teacher", "teal", "teamwork", "teapot", "teardrop", "teaser", "techno", "tedium",
|
||||
"teenager", "teeth", "telecast", "teller", "temple", "tenant", "tendency", "tennis", "tenor",
|
||||
"tension", "tentacle", "tenure", "tepid", "tequila", "terminal", "terrain", "terse", "tertiary",
|
||||
"testify", "tetanus", "tether", "texture", "thank", "thatch", "thaw", "theater", "theft", "theme",
|
||||
"theory", "therapy", "thesis", "thick", "thigh", "thimble", "thinner", "thirst", "thistle",
|
||||
"thorn", "thought", "thrall", "threat", "thrive", "throaty", "thrum", "thud", "thumb", "thunder",
|
||||
"thwart", "thyroid", "tiara", "tibia", "ticket", "tidal", "tidbit", "tidy", "tiger", "tight",
|
||||
"timber", "timeline", "timid", "tinfoil", "tinker", "tinsel", "tinted", "tipsy", "tiptoe",
|
||||
"tirade", "tired", "titanium", "title", "toad", "toaster", "tobacco", "toboggan", "today",
|
||||
"toddler", "toenail", "tofu", "together", "toggle", "toilet", "token", "tolerate", "tomato",
|
||||
"tomb", "tomcat", "tomorrow", "tonality", "toned", "tongs", "tonight", "tonnage", "tonsil",
|
||||
"toolbox", "tooth", "topaz", "topology", "topple", "topsoil", "torch", "torment", "tornado",
|
||||
"torpedo", "torque", "torrid", "torso", "torture", "torus", "total", "tote", "toucan", "tourist",
|
||||
"toward", "tower", "township", "toxic", "track", "trader", "traffic", "tragic", "train",
|
||||
"trample", "transfer", "trapeze", "trash", "trauma", "traveler", "treaty", "trek", "tremble",
|
||||
"trend", "trespass", "trial", "tribe", "tricycle", "trident", "trigger", "trilogy", "trinket",
|
||||
"trip", "triumph", "trivia", "trod", "troll", "trooper", "tropic", "trouble", "truant", "trucker",
|
||||
"trudge", "truffle", "trumpet", "trunk", "trust", "truth", "try", "tsunami", "tuba", "tubby",
|
||||
"tubular", "tuft", "tugboat", "tuition", "tulip", "tumbler", "tummy", "tumult", "tundra", "tuner",
|
||||
"tungsten", "tunic", "tunnel", "turbine", "turf", "turkey", "turmoil", "turnip", "turret",
|
||||
"turtle", "tussle", "tutor", "tutu", "tuxedo", "tweak", "tweet", "twerp", "twice", "twiddle",
|
||||
"twilight", "twin", "twirl", "twist", "tycoon", "type", "typhoon", "typical", "tyrant", "ubiquity",
|
||||
"ukulele", "ulcer", "ulterior", "ultimate", "ultra", "umbrella", "umpire", "uncle", "uncouth",
|
||||
"under", "undo", "undulate", "unicycle", "uniform", "unique", "unisex", "unite", "universe",
|
||||
"unkempt", "until", "unwieldy", "upbeat", "upchuck", "upcoming", "updraft", "upfront", "upgrade",
|
||||
"upheaval", "uphill", "upkeep", "uplift", "upload", "upper", "upright", "uproar", "upscale",
|
||||
"upset", "upstream", "uptake", "uptown", "upward", "uranium", "urban", "urchin", "urge", "useful",
|
||||
"useless", "username", "usher", "usual", "usurper", "utensil", "utility", "utmost", "utopia",
|
||||
"uvula", "vacant", "vaccine", "vacuum", "vagabond", "vagrant", "vague", "valet", "valid",
|
||||
"valuable", "valve", "vampire", "vandal", "vanguard", "vanish", "vanquish", "vapor", "variety",
|
||||
"varsity", "vascular", "vassal", "vast", "vector", "veer", "vegan", "veggie", "vehement",
|
||||
"vehicle", "velocity", "velvet", "vendor", "veneer", "vengeful", "venison", "venom", "venture",
|
||||
"venue", "veranda", "verb", "verdict", "verify", "vermin", "version", "vertebra", "vessel",
|
||||
"vestige", "veteran", "veto", "viable", "viaduct", "vibrant", "vicinity", "victim", "video",
|
||||
"view", "vigilant", "vigorous", "village", "vinegar", "vintage", "vinyl", "violence", "virtual",
|
||||
"virus", "visa", "visceral", "visitor", "visor", "visual", "vital", "vitriol", "vivid", "vocal",
|
||||
"voice", "volatile", "volcano", "volition", "volley", "voltage", "volume", "voodoo", "voter",
|
||||
"voucher", "vowel", "voyage", "vulture", "wacky", "wafer", "waft", "waggle", "wagon", "waitress",
|
||||
"waiver", "walkway", "wallet", "walnut", "walrus", "waltz", "wanderer", "wardrobe", "warhorse",
|
||||
"warlord", "warmth", "warped", "warrior", "warthog", "washroom", "wasp", "wasted", "watch",
|
||||
"waterbed", "wavy", "waxed", "wayward", "weaken", "wealthy", "weapon", "weary", "weather",
|
||||
"weaver", "webbed", "webcam", "website", "wedding", "weedy", "weekend", "weep", "weird", "welcome",
|
||||
"welfare", "western", "wetland", "whack", "whaler", "wharf", "wheeze", "whelp", "whiff", "whim",
|
||||
"whinny", "whiplash", "whirl", "whisper", "white", "wicked", "widow", "wife", "wig", "wildfire",
|
||||
"willowy", "wimpy", "windpipe", "winery", "wingtip", "winter", "wiper", "wireless", "wiry",
|
||||
"wisdom", "wish", "wistful", "withdraw", "witness", "witty", "wizardry", "wobble", "woe", "wolf",
|
||||
"woman", "wombat", "wonder", "woodwork", "woolen", "woozy", "work", "world", "wormhole", "worry",
|
||||
"worship", "worthy", "wounded", "wraith", "wrangle", "wrapper", "wreath", "wreckage", "wrench",
|
||||
"wrestle", "wretched", "wriggle", "wrinkle", "wrist", "written", "wrong", "xenon", "yacht", "yam",
|
||||
"yank", "yarn", "year", "yelp", "yeoman", "yes", "yeti", "yield", "yodel", "yoga", "yogurt",
|
||||
"yolk", "young", "yuletide", "yuppie", "zany", "zealot", "zebra", "zenith", "zeppelin", "zero",
|
||||
"zesty", "zinc", "zipper", "zodiac", "zombie", "zoom", "zucchini", "zygote"
|
||||
};
|
||||
|
||||
private static final Map<String, Integer> WORD_MAP = new HashMap<>(DICT_SIZE);
|
||||
static {
|
||||
for (int i = 0; i < WORDS.length; i++) {
|
||||
WORD_MAP.put(WORDS[i], i);
|
||||
}
|
||||
}
|
||||
|
||||
private Mnemonic() {}
|
||||
|
||||
public static byte[][] encode(byte[] seed) {
|
||||
if (seed.length != 32) {
|
||||
throw new IllegalArgumentException("Seed must be 32 bytes");
|
||||
}
|
||||
|
||||
int[] allChunks = new int[MAX_DATA_CHUNKS];
|
||||
for (int i = 0; i < MAX_DATA_CHUNKS; i++) {
|
||||
allChunks[i] = read12Bits(seed, i);
|
||||
}
|
||||
|
||||
// Find the first non-zero data chunk (leading zero suppression)
|
||||
int firstNonZero = -1;
|
||||
for (int i = 0; i < MAX_DATA_CHUNKS; i++) {
|
||||
if (allChunks[i] != 0) {
|
||||
firstNonZero = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
int numDataChunks = (firstNonZero == -1) ? 0 : (MAX_DATA_CHUNKS - firstNonZero);
|
||||
int checksum = 0;
|
||||
for (int c : allChunks) {
|
||||
checksum ^= c;
|
||||
}
|
||||
|
||||
byte[][] phrase = new byte[numDataChunks + 1][];
|
||||
phrase[0] = WORDS[checksum].getBytes(StandardCharsets.UTF_8);
|
||||
for (int i = 0; i < numDataChunks; i++) {
|
||||
phrase[i + 1] = WORDS[allChunks[firstNonZero + i]].getBytes(StandardCharsets.UTF_8);
|
||||
}
|
||||
return phrase;
|
||||
}
|
||||
|
||||
public static byte[] decode(byte[][] phrase) {
|
||||
if (phrase.length < 1) {
|
||||
throw new IllegalArgumentException("Phrase too short");
|
||||
}
|
||||
|
||||
int[] chunks = new int[phrase.length];
|
||||
for (int i = 0; i < phrase.length; i++) {
|
||||
String word = new String(phrase[i], StandardCharsets.UTF_8);
|
||||
Integer idx = WORD_MAP.get(word);
|
||||
if (idx == null) {
|
||||
throw new IllegalArgumentException("Unknown word in mnemonic at index " + i);
|
||||
}
|
||||
chunks[i] = idx;
|
||||
}
|
||||
|
||||
int expectedChecksum = chunks[0];
|
||||
int actualChecksum = 0;
|
||||
for (int i = 1; i < chunks.length; i++) {
|
||||
actualChecksum ^= chunks[i];
|
||||
}
|
||||
|
||||
if (expectedChecksum != actualChecksum) {
|
||||
throw new RuntimeException("Mnemonic checksum failed");
|
||||
}
|
||||
|
||||
int numDataChunks = chunks.length - 1;
|
||||
byte[] result = new byte[32];
|
||||
for (int i = 0; i < numDataChunks; i++) {
|
||||
int chunkIdx = MAX_DATA_CHUNKS - numDataChunks + i;
|
||||
write12Bits(result, chunkIdx, chunks[i + 1]);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
private static int read12Bits(byte[] seed, int chunkIdx) {
|
||||
int bitStart = chunkIdx * WIDTH;
|
||||
int val = 0;
|
||||
for (int i = 0; i < WIDTH; i++) {
|
||||
int pos = bitStart + i;
|
||||
if (pos >= 8) { // Skip 8-bit leading zero padding
|
||||
int seedBitPos = pos - 8;
|
||||
int byteIdx = seedBitPos / 8;
|
||||
int bitInByte = 7 - (seedBitPos % 8); // Big-endian bit order
|
||||
if (((seed[byteIdx] & 0xFF) >> bitInByte & 1) == 1) {
|
||||
val |= (1 << (11 - i));
|
||||
}
|
||||
}
|
||||
}
|
||||
return val;
|
||||
}
|
||||
|
||||
private static void write12Bits(byte[] seed, int chunkIdx, int val) {
|
||||
int bitStart = chunkIdx * WIDTH;
|
||||
for (int i = 0; i < WIDTH; i++) {
|
||||
int pos = bitStart + i;
|
||||
if (pos >= 8) {
|
||||
int seedBitPos = pos - 8;
|
||||
int byteIdx = seedBitPos / 8;
|
||||
int bitInByte = 7 - (seedBitPos % 8);
|
||||
if (((val >> (11 - i)) & 1) == 1) {
|
||||
seed[byteIdx] |= (byte) (1 << bitInByte);
|
||||
} else {
|
||||
seed[byteIdx] &= (byte) ~(1 << bitInByte);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,367 @@
|
||||
/*
|
||||
* 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.util.Arrays;
|
||||
|
||||
// QR encoder for Byte mode.
|
||||
// Ported from qr.erl.
|
||||
|
||||
public final class QR {
|
||||
|
||||
private QR() {}
|
||||
|
||||
public enum ECC {
|
||||
M(0);
|
||||
final int bits;
|
||||
ECC(int bits) { this.bits = bits; }
|
||||
}
|
||||
|
||||
private static final int PRIME_MODULUS = 285;
|
||||
private static final int FORMAT_INFO_POLY = 1335;
|
||||
private static final int FORMAT_INFO_MASK = 21522;
|
||||
private static final int VERSION_INFO_POLY = 7973;
|
||||
|
||||
private static final int[][] ALIGNMENT_COORDINATES = {
|
||||
{}, // V0
|
||||
{}, // V1
|
||||
{6, 18}, {6, 22}, {6, 26}, {6, 30}, {6, 34}, {6, 22, 38}, {6, 24, 42}, {6, 26, 46},
|
||||
{6, 28, 50}, {6, 30, 54}, {6, 32, 58}, {6, 34, 62}, {6, 26, 46, 66}, {6, 26, 48, 70},
|
||||
{6, 26, 50, 74}, {6, 30, 54, 78}, {6, 30, 56, 82}, {6, 30, 58, 86}, {6, 34, 62, 90},
|
||||
{6, 28, 50, 72, 94}, {6, 26, 50, 74, 98}, {6, 30, 54, 78, 102}, {6, 28, 54, 80, 106},
|
||||
{6, 32, 58, 84, 110}, {6, 30, 58, 86, 114}, {6, 34, 62, 90, 118}, {6, 26, 50, 74, 98, 122},
|
||||
{6, 30, 54, 78, 102, 126}, {6, 26, 52, 78, 104, 130}, {6, 30, 56, 82, 108, 134},
|
||||
{6, 34, 60, 86, 112, 138}, {6, 30, 58, 86, 114, 142}, {6, 34, 62, 90, 118, 146},
|
||||
{6, 30, 54, 78, 102, 126, 150}, {6, 24, 50, 76, 102, 128, 154}, {6, 28, 54, 80, 106, 132, 158},
|
||||
{6, 32, 58, 84, 110, 136, 162}, {6, 26, 54, 82, 110, 138, 166}, {6, 30, 58, 86, 114, 142, 170}
|
||||
};
|
||||
|
||||
private record VersionInfo(ECC ecc, int version, int byteCapacity, int[][] blocks, int remainder) {}
|
||||
|
||||
private static final VersionInfo[] VERSION_TABLE = {
|
||||
new VersionInfo(ECC.M, 1, 14, new int[][]{{1, 26, 16}}, 0),
|
||||
new VersionInfo(ECC.M, 2, 26, new int[][]{{1, 44, 28}}, 7),
|
||||
new VersionInfo(ECC.M, 3, 42, new int[][]{{1, 70, 44}}, 7),
|
||||
new VersionInfo(ECC.M, 4, 62, new int[][]{{2, 50, 32}}, 7),
|
||||
new VersionInfo(ECC.M, 5, 84, new int[][]{{2, 67, 43}}, 7),
|
||||
new VersionInfo(ECC.M, 6, 106, new int[][]{{4, 43, 27}}, 7),
|
||||
new VersionInfo(ECC.M, 7, 122, new int[][]{{4, 49, 31}}, 0),
|
||||
new VersionInfo(ECC.M, 8, 152, new int[][]{{2, 60, 38}, {2, 61, 39}}, 0),
|
||||
new VersionInfo(ECC.M, 9, 180, new int[][]{{3, 58, 36}, {2, 59, 37}}, 0),
|
||||
new VersionInfo(ECC.M, 10, 213, new int[][]{{4, 69, 43}, {1, 70, 44}}, 0),
|
||||
new VersionInfo(ECC.M, 11, 251, new int[][]{{1, 80, 50}, {4, 81, 51}}, 0),
|
||||
new VersionInfo(ECC.M, 12, 287, new int[][]{{6, 58, 36}, {2, 59, 37}}, 0),
|
||||
new VersionInfo(ECC.M, 13, 331, new int[][]{{8, 59, 37}, {1, 60, 38}}, 0),
|
||||
new VersionInfo(ECC.M, 14, 362, new int[][]{{4, 64, 40}, {5, 65, 41}}, 3),
|
||||
new VersionInfo(ECC.M, 15, 412, new int[][]{{5, 65, 41}, {5, 66, 42}}, 3),
|
||||
new VersionInfo(ECC.M, 40, 3391, new int[][]{{1, 3391, 2953}}, 0)
|
||||
};
|
||||
|
||||
public static boolean[][] encode(String text) {
|
||||
return encode(text.getBytes(java.nio.charset.StandardCharsets.UTF_8), ECC.M);
|
||||
}
|
||||
|
||||
public static boolean[][] encode(byte[] data, ECC ecc) {
|
||||
VersionInfo vi = chooseVersion(data.length, ecc);
|
||||
if (vi == null) throw new IllegalArgumentException("Data too large for supported versions");
|
||||
|
||||
BitBuffer buffer = new BitBuffer();
|
||||
buffer.add(4, 4); // Byte mode
|
||||
int cciBits = vi.version <= 9 ? 8 : 16;
|
||||
buffer.add(data.length, cciBits);
|
||||
for (byte b : data) buffer.add(b & 0xFF, 8);
|
||||
buffer.add(0, 4); // Terminator
|
||||
|
||||
int totalDataBytesCount = 0;
|
||||
for (int[] b : vi.blocks) totalDataBytesCount += b[0] * b[2];
|
||||
|
||||
if (buffer.bitCount() % 8 != 0) buffer.add(0, 8 - (buffer.bitCount() % 8));
|
||||
|
||||
boolean toggle = true;
|
||||
while (buffer.bitCount() / 8 < totalDataBytesCount) {
|
||||
buffer.add(toggle ? 0xEC : 0x11, 8);
|
||||
toggle = !toggle;
|
||||
}
|
||||
|
||||
byte[] codewords = generateCodewords(vi, buffer.toByteArray());
|
||||
|
||||
int dim = 17 + vi.version * 4;
|
||||
boolean[][] matrix = new boolean[dim][dim];
|
||||
boolean[][] reserved = new boolean[dim][dim];
|
||||
|
||||
placeFixedPatterns(vi, matrix, reserved);
|
||||
placeData(vi, matrix, reserved, codewords, vi.remainder);
|
||||
|
||||
applyMask(matrix, reserved, 0);
|
||||
placeFormatInfo(vi, matrix, 0);
|
||||
if (vi.version >= 7) placeVersionInfo(vi, matrix);
|
||||
|
||||
return matrix;
|
||||
}
|
||||
|
||||
private static VersionInfo chooseVersion(int length, ECC ecc) {
|
||||
for (VersionInfo vi : VERSION_TABLE) {
|
||||
if (vi.ecc == ecc && vi.byteCapacity >= length) return vi;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static byte[] generateCodewords(VersionInfo vi, byte[] data) {
|
||||
Gf256 field = new Gf256(PRIME_MODULUS);
|
||||
int totalBlocks = 0;
|
||||
for (int[] b : vi.blocks) totalBlocks += b[0];
|
||||
|
||||
byte[][] dataBlocks = new byte[totalBlocks][];
|
||||
byte[][] eccBlocks = new byte[totalBlocks][];
|
||||
|
||||
int blockIdx = 0;
|
||||
int dataOffset = 0;
|
||||
for (int[] b : vi.blocks) {
|
||||
int count = b[0];
|
||||
int dataBytes = b[2];
|
||||
int totalBytes = b[1];
|
||||
int eccBytes = totalBytes - dataBytes;
|
||||
int[] gen = generator(field, eccBytes);
|
||||
|
||||
for (int i = 0; i < count; i++) {
|
||||
byte[] block = Arrays.copyOfRange(data, dataOffset, dataOffset + dataBytes);
|
||||
dataBlocks[blockIdx] = block;
|
||||
eccBlocks[blockIdx] = encodeRS(field, block, gen, eccBytes);
|
||||
dataOffset += dataBytes;
|
||||
blockIdx++;
|
||||
}
|
||||
}
|
||||
|
||||
int totalCodewords = 0;
|
||||
for (int[] b : vi.blocks) totalCodewords += b[0] * b[1];
|
||||
byte[] result = new byte[totalCodewords];
|
||||
int resOffset = 0;
|
||||
|
||||
int maxData = 0;
|
||||
for (byte[] b : dataBlocks) maxData = Math.max(maxData, b.length);
|
||||
for (int i = 0; i < maxData; i++) {
|
||||
for (byte[] b : dataBlocks) {
|
||||
if (i < b.length) result[resOffset++] = b[i];
|
||||
}
|
||||
}
|
||||
int eccLen = eccBlocks[0].length;
|
||||
for (int i = 0; i < eccLen; i++) {
|
||||
for (byte[] b : eccBlocks) {
|
||||
result[resOffset++] = b[i];
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static int[] generator(Gf256 f, int degree) {
|
||||
int[] g = {1};
|
||||
for (int i = 0; i < degree; i++) {
|
||||
g = f.polynomialProduct(g, new int[]{1, f.exponent(i)});
|
||||
}
|
||||
return g;
|
||||
}
|
||||
|
||||
private static byte[] encodeRS(Gf256 f, byte[] data, int[] gen, int eccBytes) {
|
||||
int[] poly = new int[data.length + eccBytes];
|
||||
for (int i = 0; i < data.length; i++) poly[i] = data[i] & 0xFF;
|
||||
int[] rem = f.divide(poly, gen);
|
||||
byte[] res = new byte[eccBytes];
|
||||
int offset = eccBytes - rem.length;
|
||||
for (int i = 0; i < rem.length; i++) res[offset + i] = (byte) rem[i];
|
||||
return res;
|
||||
}
|
||||
|
||||
private static void placeFixedPatterns(VersionInfo vi, boolean[][] matrix, boolean[][] reserved) {
|
||||
int dim = matrix.length;
|
||||
placeFinder(matrix, reserved, 0, 0);
|
||||
placeFinder(matrix, reserved, dim - 7, 0);
|
||||
placeFinder(matrix, reserved, 0, dim - 7);
|
||||
|
||||
// Separators around finders
|
||||
for (int i = 0; i < 8; i++) {
|
||||
reserved[7][i] = reserved[i][7] = true;
|
||||
reserved[7][dim - 1 - i] = reserved[i][dim - 8] = true;
|
||||
reserved[dim - 8][i] = reserved[dim - 1 - i][7] = true;
|
||||
}
|
||||
|
||||
// Timing patterns
|
||||
for (int i = 8; i < dim - 8; i++) {
|
||||
matrix[6][i] = matrix[i][6] = (i % 2 == 0);
|
||||
reserved[6][i] = reserved[i][6] = true;
|
||||
}
|
||||
|
||||
// Alignment patterns
|
||||
int[] coords = ALIGNMENT_COORDINATES[vi.version];
|
||||
for (int y : coords) {
|
||||
for (int x : coords) {
|
||||
if (isFinderArea(x, y, dim)) continue;
|
||||
placeAlignment(matrix, reserved, x, y);
|
||||
}
|
||||
}
|
||||
|
||||
// Dark module
|
||||
matrix[4 * vi.version + 9][8] = true;
|
||||
reserved[4 * vi.version + 9][8] = true;
|
||||
|
||||
// Reserved areas for format and version info
|
||||
for (int i = 0; i < 9; i++) reserved[i][8] = reserved[8][i] = true;
|
||||
for (int i = 0; i < 8; i++) reserved[dim - 1 - i][8] = reserved[8][dim - 1 - i] = true;
|
||||
|
||||
if (vi.version >= 7) {
|
||||
for (int i = 0; i < 6; i++) {
|
||||
for (int j = 0; j < 3; j++) {
|
||||
reserved[dim - 11 + j][i] = reserved[i][dim - 11 + j] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isFinderArea(int x, int y, int dim) {
|
||||
return (x < 9 && y < 9) || (x > dim - 10 && y < 9) || (x < 9 && y > dim - 10);
|
||||
}
|
||||
|
||||
private static void placeFinder(boolean[][] m, boolean[][] r, int x, int y) {
|
||||
for (int j = 0; j < 7; j++) {
|
||||
for (int i = 0; i < 7; i++) {
|
||||
int px = x + i, py = y + j;
|
||||
r[py][px] = true;
|
||||
boolean black = (i == 0 || i == 6 || j == 0 || j == 6 || (i >= 2 && i <= 4 && j >= 2 && j <= 4));
|
||||
m[py][px] = black;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void placeAlignment(boolean[][] m, boolean[][] r, int x, int y) {
|
||||
for (int j = -2; j <= 2; j++) {
|
||||
for (int i = -2; i <= 2; i++) {
|
||||
r[y + j][x + i] = true;
|
||||
m[y + j][x + i] = (Math.abs(i) == 2 || Math.abs(j) == 2 || (i == 0 && j == 0));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void placeData(VersionInfo vi, boolean[][] m, boolean[][] r, byte[] data, int remainderBits) {
|
||||
int dim = m.length;
|
||||
int bitIdx = 0;
|
||||
int totalBits = data.length * 8 + remainderBits;
|
||||
int x = dim - 1, y = dim - 1, dir = -1;
|
||||
|
||||
while (x >= 0) {
|
||||
if (x == 6) x--;
|
||||
for (int i = 0; i < 2; i++) {
|
||||
int cx = x - i;
|
||||
if (!r[y][cx]) {
|
||||
if (bitIdx < totalBits) {
|
||||
int byteIdx = bitIdx / 8;
|
||||
int shift = 7 - (bitIdx % 8);
|
||||
boolean bit = byteIdx < data.length && ((data[byteIdx] >> shift) & 1) != 0;
|
||||
m[y][cx] = bit;
|
||||
bitIdx++;
|
||||
}
|
||||
}
|
||||
}
|
||||
y += dir;
|
||||
if (y < 0 || y >= dim) {
|
||||
y -= dir; x -= 2; dir = -dir;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void applyMask(boolean[][] m, boolean[][] r, int pattern) {
|
||||
for (int y = 0; y < m.length; y++) {
|
||||
for (int x = 0; x < m.length; x++) {
|
||||
if (!r[y][x] && isMasked(x, y, pattern)) m[y][x] = !m[y][x];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isMasked(int x, int y, int pattern) {
|
||||
return switch (pattern) {
|
||||
case 0 -> (x + y) % 2 == 0;
|
||||
case 1 -> y % 2 == 0;
|
||||
case 2 -> x % 3 == 0;
|
||||
case 3 -> (x + y) % 3 == 0;
|
||||
case 4 -> (y / 2 + x / 3) % 2 == 0;
|
||||
case 5 -> ((x * y) % 2) + ((x * y) % 3) == 0;
|
||||
case 6 -> (((x * y) % 2) + ((x * y) % 3)) % 2 == 0;
|
||||
case 7 -> (((x + y) % 2) + ((x * y) % 3)) % 2 == 0;
|
||||
default -> false;
|
||||
};
|
||||
}
|
||||
|
||||
private static final int[][] FORMAT_INFO_COORDS_TL = {
|
||||
{8, 0}, {8, 1}, {8, 2}, {8, 3}, {8, 4}, {8, 5}, {8, 7}, {8, 8}, {7, 8}, {5, 8}, {4, 8}, {3, 8}, {2, 8}, {1, 8}, {0, 8}
|
||||
};
|
||||
|
||||
private static void placeFormatInfo(VersionInfo vi, boolean[][] m, int mask) {
|
||||
int info = (vi.ecc.bits << 3) | mask;
|
||||
int bch = bch(info, FORMAT_INFO_POLY, 10);
|
||||
int full = ((info << 10) | bch) ^ FORMAT_INFO_MASK;
|
||||
int dim = m.length;
|
||||
|
||||
for (int i = 0; i < 15; i++) {
|
||||
boolean bit = ((full >> i) & 1) != 0;
|
||||
// Top-left strip
|
||||
m[FORMAT_INFO_COORDS_TL[i][0]][FORMAT_INFO_COORDS_TL[i][1]] = bit;
|
||||
|
||||
// Bottom-left / Top-right strips
|
||||
if (i < 8) m[8][dim - 1 - i] = bit;
|
||||
else m[dim - 15 + i][8] = bit;
|
||||
}
|
||||
}
|
||||
|
||||
private static void placeVersionInfo(VersionInfo vi, boolean[][] m) {
|
||||
int bch = bch(vi.version, VERSION_INFO_POLY, 12);
|
||||
int full = (vi.version << 12) | bch;
|
||||
int dim = m.length;
|
||||
for (int i = 0; i < 18; i++) {
|
||||
boolean bit = ((full >> i) & 1) != 0;
|
||||
m[dim - 11 + i % 3][i / 3] = bit;
|
||||
m[i / 3][dim - 11 + i % 3] = bit;
|
||||
}
|
||||
}
|
||||
|
||||
private static int bch(int data, int poly, int degree) {
|
||||
int d = data << degree;
|
||||
int msb = 31 - Integer.numberOfLeadingZeros(poly);
|
||||
for (int i = 31 - Integer.numberOfLeadingZeros(d); i >= degree; i--) {
|
||||
if (((d >> i) & 1) != 0) d ^= (poly << (i - msb));
|
||||
}
|
||||
return d;
|
||||
}
|
||||
|
||||
private static class BitBuffer {
|
||||
private byte[] data = new byte[32];
|
||||
private int bitIdx = 0;
|
||||
public void add(int value, int bits) {
|
||||
ensureCapacity((bitIdx + bits + 7) / 8);
|
||||
for (int i = bits - 1; i >= 0; i--) {
|
||||
if (((value >> i) & 1) != 0) data[bitIdx / 8] |= (1 << (7 - (bitIdx % 8)));
|
||||
bitIdx++;
|
||||
}
|
||||
}
|
||||
public int bitCount() { return bitIdx; }
|
||||
public byte[] toByteArray() { return Arrays.copyOf(data, (bitIdx + 7) / 8); }
|
||||
private void ensureCapacity(int bytes) {
|
||||
if (bytes > data.length) data = Arrays.copyOf(data, Math.max(data.length * 2, bytes));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,232 @@
|
||||
/*
|
||||
* 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.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
public final class RLP {
|
||||
|
||||
// RLP only has two types: byte arrays and lists of lists of byte arrays
|
||||
public static class RLPException extends RuntimeException {
|
||||
public RLPException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Data Models - lists and items
|
||||
|
||||
public abstract static class RLP_Data {
|
||||
public boolean isItem() { return this instanceof RLP_Item; }
|
||||
public boolean isList() { return this instanceof RLP_List; }
|
||||
|
||||
public RLP_Item asItem() {
|
||||
if (isItem()) return (RLP_Item) this;
|
||||
throw new RLPException("Expected RLP Item, found List");
|
||||
}
|
||||
|
||||
public RLP_List asList() {
|
||||
if (isList()) return (RLP_List) this;
|
||||
throw new RLPException("Expected RLP List, found Item");
|
||||
}
|
||||
}
|
||||
|
||||
public static final class RLP_Item extends RLP_Data {
|
||||
public final byte[] bytes;
|
||||
|
||||
public RLP_Item(byte[] bytes) {
|
||||
this.bytes = (bytes != null) ? bytes : new byte[0];
|
||||
}
|
||||
|
||||
public byte[] getBytes() { return bytes; }
|
||||
}
|
||||
|
||||
public static final class RLP_List extends RLP_Data {
|
||||
public final List<RLP_Data> items;
|
||||
|
||||
public RLP_List(List<RLP_Data> items) {
|
||||
this.items = (items != null) ? items : new ArrayList<>();
|
||||
}
|
||||
|
||||
public List<RLP_Data> getItems() { return items; }
|
||||
}
|
||||
|
||||
public record DecodeResult(RLP_Data data, int consumed) {}
|
||||
|
||||
|
||||
// API
|
||||
|
||||
private RLP() {}
|
||||
|
||||
public static byte[] encode(RLP_Data data) {
|
||||
if (data instanceof RLP_Item item) {
|
||||
return encodeItem(item.bytes);
|
||||
} else if (data instanceof RLP_List list) {
|
||||
return encodeList(list.items);
|
||||
}
|
||||
throw new RLPException("Unsupported RLP data type");
|
||||
}
|
||||
|
||||
public static RLP_Data decode(byte[] buffer) {
|
||||
if (buffer == null || buffer.length == 0) {
|
||||
return new RLP_Item(new byte[0]);
|
||||
}
|
||||
DecodeResult result = decodeFrom(buffer, 0);
|
||||
if (result.consumed != buffer.length) {
|
||||
throw new RLPException("Buffer contains " + (buffer.length - result.consumed) + " trailing bytes");
|
||||
}
|
||||
return result.data;
|
||||
}
|
||||
|
||||
// Decodes a single RLP object from a buffer starting at the given offset.
|
||||
// Useful for stream decoding.
|
||||
public static DecodeResult decodeFrom(byte[] buffer, int offset) {
|
||||
if (buffer == null || offset >= buffer.length) {
|
||||
throw new RLPException("Buffer underflow at offset " + offset);
|
||||
}
|
||||
|
||||
int prefix = buffer[offset] & 0xFF;
|
||||
|
||||
if (prefix <= 0x7F) {
|
||||
return new DecodeResult(new RLP_Item(new byte[] { (byte) prefix }), 1);
|
||||
}
|
||||
|
||||
if (prefix <= 0xB7) {
|
||||
int payloadLen = prefix - 0x80;
|
||||
checkBounds(buffer, offset + 1, payloadLen);
|
||||
byte[] payload = Arrays.copyOfRange(buffer, offset + 1, offset + 1 + payloadLen);
|
||||
return new DecodeResult(new RLP_Item(payload), 1 + payloadLen);
|
||||
}
|
||||
|
||||
if (prefix <= 0xBF) {
|
||||
int lenLen = prefix - 0xB7;
|
||||
checkBounds(buffer, offset + 1, lenLen);
|
||||
int payloadLen = bigEndianToInt(buffer, offset + 1, offset + 1 + lenLen);
|
||||
checkBounds(buffer, offset + 1 + lenLen, payloadLen);
|
||||
byte[] payload = Arrays.copyOfRange(buffer, offset + 1 + lenLen, offset + 1 + lenLen + payloadLen);
|
||||
return new DecodeResult(new RLP_Item(payload), 1 + lenLen + payloadLen);
|
||||
}
|
||||
|
||||
if (prefix <= 0xF7) {
|
||||
int payloadLen = prefix - 0xC0;
|
||||
checkBounds(buffer, offset + 1, payloadLen);
|
||||
RLP_List list = parseListSequence(buffer, offset + 1, offset + 1 + payloadLen);
|
||||
return new DecodeResult(list, 1 + payloadLen);
|
||||
}
|
||||
|
||||
int lenLen = prefix - 0xF7;
|
||||
checkBounds(buffer, offset + 1, lenLen);
|
||||
int payloadLen = bigEndianToInt(buffer, offset + 1, offset + 1 + lenLen);
|
||||
checkBounds(buffer, offset + 1 + lenLen, payloadLen);
|
||||
RLP_List list = parseListSequence(buffer, offset + 1 + lenLen, offset + 1 + lenLen + payloadLen);
|
||||
return new DecodeResult(list, 1 + lenLen + payloadLen);
|
||||
}
|
||||
|
||||
private static byte[] encodeItem(byte[] bytes) {
|
||||
if (bytes.length == 1 && (bytes[0] & 0xFF) <= 0x7F) {
|
||||
return bytes;
|
||||
}
|
||||
return prefixData(bytes, 0x80, 0xB7);
|
||||
}
|
||||
|
||||
private static byte[] encodeList(List<RLP_Data> items) {
|
||||
if (items.isEmpty()) {
|
||||
return new byte[] { (byte) 0xC0 };
|
||||
}
|
||||
|
||||
byte[][] encodedChildren = new byte[items.size()][];
|
||||
int totalPayloadLen = 0;
|
||||
for (int i = 0; i < items.size(); i++) {
|
||||
encodedChildren[i] = encode(items.get(i));
|
||||
totalPayloadLen += encodedChildren[i].length;
|
||||
}
|
||||
|
||||
byte[] prefix = prefixLength(totalPayloadLen, 0xC0, 0xF7);
|
||||
byte[] result = new byte[prefix.length + totalPayloadLen];
|
||||
System.arraycopy(prefix, 0, result, 0, prefix.length);
|
||||
|
||||
int ptr = prefix.length;
|
||||
for (byte[] child : encodedChildren) {
|
||||
System.arraycopy(child, 0, result, ptr, child.length);
|
||||
ptr += child.length;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static byte[] prefixData(byte[] payload, int shortOffset, int longOffset) {
|
||||
byte[] prefix = prefixLength(payload.length, shortOffset, longOffset);
|
||||
byte[] result = new byte[prefix.length + payload.length];
|
||||
System.arraycopy(prefix, 0, result, 0, prefix.length);
|
||||
System.arraycopy(payload, 0, result, prefix.length, payload.length);
|
||||
return result;
|
||||
}
|
||||
|
||||
private static byte[] prefixLength(int len, int shortOffset, int longOffset) {
|
||||
if (len <= 55) {
|
||||
return new byte[] { (byte) (shortOffset + len) };
|
||||
}
|
||||
byte[] lenB = intToBigEndian(len);
|
||||
byte[] p = new byte[1 + lenB.length];
|
||||
p[0] = (byte) (longOffset + lenB.length);
|
||||
System.arraycopy(lenB, 0, p, 1, lenB.length);
|
||||
return p;
|
||||
}
|
||||
|
||||
private static byte[] intToBigEndian(int val) {
|
||||
if (val == 0) return new byte[0];
|
||||
int size = (Integer.numberOfLeadingZeros(val) == 32) ? 1 : (32 - Integer.numberOfLeadingZeros(val) + 7) / 8;
|
||||
byte[] b = new byte[size];
|
||||
for (int i = size - 1; i >= 0; i--) {
|
||||
b[i] = (byte) (val & 0xFF);
|
||||
val >>>= 8;
|
||||
}
|
||||
return b;
|
||||
}
|
||||
|
||||
private static RLP_List parseListSequence(byte[] b, int cursor, int limit) {
|
||||
List<RLP_Data> elements = new ArrayList<>();
|
||||
while (cursor < limit) {
|
||||
DecodeResult res = decodeFrom(b, cursor);
|
||||
elements.add(res.data);
|
||||
cursor += res.consumed;
|
||||
}
|
||||
if (cursor != limit) {
|
||||
throw new RLPException("List payload overflow or invalid child encoding");
|
||||
}
|
||||
return new RLP_List(elements);
|
||||
}
|
||||
|
||||
private static int bigEndianToInt(byte[] b, int start, int end) {
|
||||
int res = 0;
|
||||
for (int i = start; i < end; i++) {
|
||||
res = (res << 8) | (b[i] & 0xFF);
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
private static void checkBounds(byte[] buffer, int start, int length) {
|
||||
if (length < 0 || (start + length) > buffer.length) {
|
||||
throw new RLPException("Incomplete RLP data: requested " + length + " bytes from offset " + start);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,344 @@
|
||||
/*
|
||||
* 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.math.BigDecimal;
|
||||
import java.math.BigInteger;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
// ZJ: Tiny JSON parser, ported from zj.erl.
|
||||
//
|
||||
// NOTE:
|
||||
// Does not *quite* live up to ZJ in every respect, but is sufficient for GRIDS.
|
||||
// Uses standard Java exceptions for errors.
|
||||
|
||||
public final class ZJ {
|
||||
|
||||
public static class ParseException extends RuntimeException {
|
||||
public final int position;
|
||||
public ParseException(String message, int position) {
|
||||
super(message + " at position " + position);
|
||||
this.position = position;
|
||||
}
|
||||
}
|
||||
|
||||
private ZJ() {}
|
||||
|
||||
public static Object decode(String json) {
|
||||
if (json == null || json.isEmpty()) return null;
|
||||
return new Parser(json).parse();
|
||||
}
|
||||
|
||||
public static String encode(Object value) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
encodeValue(value, sb);
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
private static void encodeValue(Object value, StringBuilder sb) {
|
||||
if (value == null) {
|
||||
sb.append("null");
|
||||
} else if (value instanceof String) {
|
||||
sb.append('"');
|
||||
escape((String) value, sb);
|
||||
sb.append('"');
|
||||
} else if (value instanceof Boolean) {
|
||||
sb.append(value);
|
||||
} else if (value instanceof Number) {
|
||||
sb.append(value);
|
||||
} else if (value instanceof Map) {
|
||||
encodeMap((Map<?, ?>) value, sb);
|
||||
} else if (value instanceof List) {
|
||||
encodeList((List<?>) value, sb);
|
||||
} else if (value instanceof Object[]) {
|
||||
encodeArray((Object[]) value, sb);
|
||||
} else {
|
||||
throw new IllegalArgumentException("Unsupported JSON value type: " + value.getClass().getName());
|
||||
}
|
||||
}
|
||||
|
||||
private static void encodeMap(Map<?, ?> map, StringBuilder sb) {
|
||||
sb.append('{');
|
||||
boolean first = true;
|
||||
for (Map.Entry<?, ?> entry : map.entrySet()) {
|
||||
if (!first) sb.append(',');
|
||||
first = false;
|
||||
sb.append('"');
|
||||
escape(entry.getKey().toString(), sb);
|
||||
sb.append("\":");
|
||||
encodeValue(entry.getValue(), sb);
|
||||
}
|
||||
sb.append('}');
|
||||
}
|
||||
|
||||
private static void encodeList(List<?> list, StringBuilder sb) {
|
||||
sb.append('[');
|
||||
boolean first = true;
|
||||
for (Object item : list) {
|
||||
if (!first) sb.append(',');
|
||||
first = false;
|
||||
encodeValue(item, sb);
|
||||
}
|
||||
sb.append(']');
|
||||
}
|
||||
|
||||
private static void encodeArray(Object[] array, StringBuilder sb) {
|
||||
sb.append('[');
|
||||
boolean first = true;
|
||||
for (Object item : array) {
|
||||
if (!first) sb.append(',');
|
||||
first = false;
|
||||
encodeValue(item, sb);
|
||||
}
|
||||
sb.append(']');
|
||||
}
|
||||
|
||||
private static void escape(String s, StringBuilder sb) {
|
||||
for (int i = 0; i < s.length(); i++) {
|
||||
char c = s.charAt(i);
|
||||
switch (c) {
|
||||
case '"' -> sb.append("\\\"");
|
||||
case '\\' -> sb.append("\\\\");
|
||||
case '\b' -> sb.append("\\b");
|
||||
case '\f' -> sb.append("\\f");
|
||||
case '\n' -> sb.append("\\n");
|
||||
case '\r' -> sb.append("\\r");
|
||||
case '\t' -> sb.append("\\t");
|
||||
default -> {
|
||||
if (c < 32 || c > 126) {
|
||||
sb.append(String.format("\\u%04x", (int) c));
|
||||
} else {
|
||||
sb.append(c);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static class Parser {
|
||||
private final String json;
|
||||
private int pos = 0;
|
||||
|
||||
Parser(String json) {
|
||||
this.json = json;
|
||||
}
|
||||
|
||||
Object parse() {
|
||||
seek();
|
||||
if (pos >= json.length()) return null;
|
||||
Object result = value();
|
||||
seek();
|
||||
if (pos != json.length()) {
|
||||
throw new ParseException("Unexpected trailing data", pos);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private Object value() {
|
||||
char c = json.charAt(pos);
|
||||
return switch (c) {
|
||||
case '{' -> object();
|
||||
case '[' -> array();
|
||||
case '"' -> string();
|
||||
case 't' -> bool(true);
|
||||
case 'f' -> bool(false);
|
||||
case 'n' -> nil();
|
||||
default -> {
|
||||
if (c == '-' || (c >= '0' && c <= '9')) {
|
||||
yield number();
|
||||
}
|
||||
throw new ParseException("Unexpected character: " + c, pos);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private Map<String, Object> object() {
|
||||
pos++; // skip '{'
|
||||
Map<String, Object> map = new LinkedHashMap<>();
|
||||
seek();
|
||||
if (pos < json.length() && json.charAt(pos) == '}') {
|
||||
pos++;
|
||||
return map;
|
||||
}
|
||||
while (true) {
|
||||
seek();
|
||||
String key = string();
|
||||
seek();
|
||||
if (pos >= json.length() || json.charAt(pos) != ':') {
|
||||
throw new ParseException("Expected ':'", pos);
|
||||
}
|
||||
pos++;
|
||||
seek();
|
||||
Object val = value();
|
||||
map.put(key, val);
|
||||
seek();
|
||||
if (pos < json.length() && json.charAt(pos) == '}') {
|
||||
pos++;
|
||||
break;
|
||||
}
|
||||
if (pos >= json.length() || json.charAt(pos) != ',') {
|
||||
throw new ParseException("Expected ',' or '}'", pos);
|
||||
}
|
||||
pos++;
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
private List<Object> array() {
|
||||
pos++; // skip '['
|
||||
List<Object> list = new ArrayList<>();
|
||||
seek();
|
||||
if (pos < json.length() && json.charAt(pos) == ']') {
|
||||
pos++;
|
||||
return list;
|
||||
}
|
||||
while (true) {
|
||||
seek();
|
||||
list.add(value());
|
||||
seek();
|
||||
if (pos < json.length() && json.charAt(pos) == ']') {
|
||||
pos++;
|
||||
break;
|
||||
}
|
||||
if (pos >= json.length() || json.charAt(pos) != ',') {
|
||||
throw new ParseException("Expected ',' or ']'", pos);
|
||||
}
|
||||
pos++;
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
private String string() {
|
||||
if (json.charAt(pos) != '"') throw new ParseException("Expected '\"'", pos);
|
||||
pos++;
|
||||
StringBuilder sb = new StringBuilder();
|
||||
while (pos < json.length()) {
|
||||
char c = json.charAt(pos++);
|
||||
if (c == '"') return sb.toString();
|
||||
if (c < 0x20) throw new ParseException("Unescaped control character", pos - 1);
|
||||
if (c == '\\') {
|
||||
if (pos >= json.length()) throw new ParseException("Unterminated escape", pos);
|
||||
char esc = json.charAt(pos++);
|
||||
switch (esc) {
|
||||
case '"' -> sb.append('"');
|
||||
case '\\' -> sb.append('\\');
|
||||
case '/' -> sb.append('/');
|
||||
case 'b' -> sb.append('\b');
|
||||
case 'f' -> sb.append('\f');
|
||||
case 'n' -> sb.append('\n');
|
||||
case 'r' -> sb.append('\r');
|
||||
case 't' -> sb.append('\t');
|
||||
case 'u' -> {
|
||||
if (pos + 4 > json.length()) throw new ParseException("Invalid unicode escape", pos);
|
||||
String hex = json.substring(pos, pos + 4);
|
||||
try {
|
||||
sb.append((char) Integer.parseInt(hex, 16));
|
||||
} catch (NumberFormatException e) {
|
||||
throw new ParseException("Invalid hex in unicode escape", pos);
|
||||
}
|
||||
pos += 4;
|
||||
}
|
||||
default -> throw new ParseException("Unknown escape: " + esc, pos - 1);
|
||||
}
|
||||
} else {
|
||||
sb.append(c);
|
||||
}
|
||||
}
|
||||
throw new ParseException("Unterminated string", pos);
|
||||
}
|
||||
|
||||
private Boolean bool(boolean expected) {
|
||||
String s = expected ? "true" : "false";
|
||||
if (json.startsWith(s, pos)) {
|
||||
pos += s.length();
|
||||
return expected;
|
||||
}
|
||||
throw new ParseException("Expected " + s, pos);
|
||||
}
|
||||
|
||||
private Object nil() {
|
||||
if (json.startsWith("null", pos)) {
|
||||
pos += 4;
|
||||
return null;
|
||||
}
|
||||
throw new ParseException("Expected null", pos);
|
||||
}
|
||||
|
||||
private Number number() {
|
||||
int start = pos;
|
||||
if (pos < json.length() && json.charAt(pos) == '-') pos++;
|
||||
|
||||
if (pos >= json.length()) throw new ParseException("Unexpected end of number", pos);
|
||||
|
||||
char c = json.charAt(pos);
|
||||
if (c == '0') {
|
||||
pos++;
|
||||
} else if (c >= '1' && c <= '9') {
|
||||
pos++;
|
||||
while (pos < json.length() && isDigit(json.charAt(pos))) pos++;
|
||||
} else {
|
||||
throw new ParseException("Expected digit", pos);
|
||||
}
|
||||
|
||||
boolean isDecimal = false;
|
||||
if (pos < json.length() && json.charAt(pos) == '.') {
|
||||
isDecimal = true;
|
||||
pos++;
|
||||
if (pos >= json.length() || !isDigit(json.charAt(pos))) {
|
||||
throw new ParseException("Expected digit after '.'", pos);
|
||||
}
|
||||
while (pos < json.length() && isDigit(json.charAt(pos))) pos++;
|
||||
}
|
||||
|
||||
if (pos < json.length() && (json.charAt(pos) == 'e' || json.charAt(pos) == 'E')) {
|
||||
isDecimal = true;
|
||||
pos++;
|
||||
if (pos < json.length() && (json.charAt(pos) == '+' || json.charAt(pos) == '-')) pos++;
|
||||
if (pos >= json.length() || !isDigit(json.charAt(pos))) {
|
||||
throw new ParseException("Expected digit in exponent", pos);
|
||||
}
|
||||
while (pos < json.length() && isDigit(json.charAt(pos))) pos++;
|
||||
}
|
||||
|
||||
String s = json.substring(start, pos);
|
||||
if (isDecimal) return new BigDecimal(s);
|
||||
try {
|
||||
return Long.parseLong(s);
|
||||
} catch (NumberFormatException e) {
|
||||
return new BigInteger(s);
|
||||
}
|
||||
}
|
||||
|
||||
private void seek() {
|
||||
while (pos < json.length() && Character.isWhitespace(json.charAt(pos))) pos++;
|
||||
}
|
||||
|
||||
// This is a nitpick but it turns out that Character.isDigit() accepts a bunch of possible
|
||||
// unicode digits as well as ASCII '0' through '9'. That's normally good, but not for
|
||||
// parsing JSON! (Shoutout to P.H. -- "only ever permit ASCII" wins this one time.)
|
||||
private static boolean isDigit(char c) {
|
||||
return c >= '0' && c <= '9';
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,337 @@
|
||||
/*
|
||||
* 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.formatting;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.util.HashMap;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
|
||||
|
||||
// GajuFormat provides formatting and parsing for Gaju and Puck units,
|
||||
// maintaining 1-for-1 parity with the Erlang implementation (hz_format.erl).
|
||||
// One Gaju = 10^18 Pucks.
|
||||
//
|
||||
// NOTE: Craig 2026-08-21
|
||||
// This module will give you inane warnings in an IDE if you use it as a submodule.
|
||||
// Don't worry about them. They are just noise.
|
||||
|
||||
public final class GajuFormat {
|
||||
public enum Type { US, JP, METRIC, LEGACY }
|
||||
public enum Unit { GAJU, PUCK }
|
||||
public record FormatSpec(Type type, Unit unit, char separator, int span) {}
|
||||
|
||||
public static FormatSpec standardSpec(Type type, Unit unit) {
|
||||
return switch (type) {
|
||||
case US -> new FormatSpec(type, unit, ',', 3);
|
||||
case JP -> new FormatSpec(type, unit, ' ', 4);
|
||||
case METRIC, LEGACY -> new FormatSpec(type, unit, ' ', 3);
|
||||
};
|
||||
}
|
||||
|
||||
private GajuFormat() {}
|
||||
|
||||
private static final String GAJU_MARK = "木";
|
||||
private static final String PUCK_MARK = "本";
|
||||
private static final int FRACTION_LEN = 18;
|
||||
private static final BigInteger ONE_GAJU = BigInteger.TEN.pow(FRACTION_LEN);
|
||||
|
||||
private static final String[] JP_RANKS = {"", "万", "億", "兆", "京", "垓", "秭", "穣", "溝", "澗", "正", "載", "極"};
|
||||
private static final String[] METRIC_RANKS = {"", "k ", "m ", "g ", "t ", "p ", "e ", "z ", "y ", "r ", "Q "};
|
||||
private static final String[] LEGACY_RANKS = {"", "k ", "m ", "b ", "t ", "q ", "e ", "z ", "y ", "r ", "Q "};
|
||||
|
||||
public static String amount(FormatSpec spec, byte[] puckBytes) {
|
||||
return amount(spec, new BigInteger(puckBytes));
|
||||
}
|
||||
|
||||
public static String amount(FormatSpec spec, BigInteger pucks) {
|
||||
boolean isNegative = pucks.signum() < 0;
|
||||
BigInteger absPucks = pucks.abs();
|
||||
|
||||
return switch (spec.type()) {
|
||||
case US -> formatWestern(spec, absPucks, isNegative);
|
||||
case JP -> formatMyriad(spec, absPucks, JP_RANKS, isNegative);
|
||||
case METRIC -> formatBestern(spec, absPucks, METRIC_RANKS, isNegative, "G", "P");
|
||||
case LEGACY -> formatBestern(spec, absPucks, LEGACY_RANKS, isNegative, "G", "P");
|
||||
};
|
||||
}
|
||||
|
||||
public static String approxAmount(FormatSpec spec, BigInteger pucks, int precision) {
|
||||
boolean isNegative = pucks.signum() < 0;
|
||||
BigInteger absPucks = pucks.abs();
|
||||
|
||||
if (spec.unit() == Unit.PUCK) {
|
||||
return amount(spec, pucks);
|
||||
}
|
||||
|
||||
BigInteger[] divRem = absPucks.divideAndRemainder(ONE_GAJU);
|
||||
String gajuStr = chunkString(divRem[0].toString(), spec.separator(), spec.span(), false);
|
||||
String sign = isNegative ? "-" : "";
|
||||
String head = GAJU_MARK + sign + gajuStr;
|
||||
|
||||
String puckFull = String.format(Locale.US, "%018d", divRem[1]);
|
||||
int prec = Math.min(precision, FRACTION_LEN);
|
||||
String significant = puckFull.substring(0, prec);
|
||||
String rest = puckFull.substring(prec);
|
||||
|
||||
boolean hasMore = false;
|
||||
for (int i = 0; i < rest.length(); i++) {
|
||||
if (rest.charAt(i) != '0') {
|
||||
hasMore = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
String resultTail = cleanTrailingZeros(significant);
|
||||
if (resultTail.isEmpty()) {
|
||||
return hasMore ? head + "...." : head;
|
||||
}
|
||||
|
||||
return head + "." + chunkString(resultTail, spec.separator(), spec.span(), true) + (hasMore ? "..." : "");
|
||||
}
|
||||
|
||||
private static String formatWestern(FormatSpec spec, BigInteger absPucks, boolean isNegative) {
|
||||
String sign = isNegative ? "-" : "";
|
||||
if (spec.unit() == Unit.PUCK) {
|
||||
return PUCK_MARK + sign + chunkString(absPucks.toString(), spec.separator(), spec.span(), false);
|
||||
}
|
||||
|
||||
BigInteger[] divRem = absPucks.divideAndRemainder(ONE_GAJU);
|
||||
String gajuStr = chunkString(divRem[0].toString(), spec.separator(), spec.span(), false);
|
||||
String puckStr = String.format(Locale.US, "%018d", divRem[1]);
|
||||
puckStr = cleanTrailingZeros(puckStr);
|
||||
|
||||
if (puckStr.isEmpty()) {
|
||||
return GAJU_MARK + sign + gajuStr;
|
||||
}
|
||||
return GAJU_MARK + sign + gajuStr + "." + chunkString(puckStr, spec.separator(), spec.span(), true);
|
||||
}
|
||||
|
||||
private static String formatMyriad(FormatSpec spec, BigInteger absPucks, String[] ranks, boolean isNegative) {
|
||||
String sign = isNegative ? "-" : "";
|
||||
if (spec.unit() == Unit.PUCK) {
|
||||
return sign + processRanks(absPucks, ranks, 4, PUCK_MARK, false);
|
||||
}
|
||||
|
||||
BigInteger[] divRem = absPucks.divideAndRemainder(ONE_GAJU);
|
||||
String gajuFormatted = processRanks(divRem[0], ranks, 4, GAJU_MARK, false);
|
||||
|
||||
if (divRem[1].equals(BigInteger.ZERO)) return sign + gajuFormatted + " ";
|
||||
|
||||
return sign + gajuFormatted + " " + processRanks(divRem[1], ranks, 4, PUCK_MARK, false);
|
||||
}
|
||||
|
||||
private static String formatBestern(FormatSpec spec, BigInteger absPucks, String[] ranks, boolean isNegative, String gSuffix, String pSuffix) {
|
||||
String sign = isNegative ? "-" : "";
|
||||
if (spec.unit() == Unit.PUCK) {
|
||||
return PUCK_MARK + sign + processRanks(absPucks, ranks, 3, pSuffix, true);
|
||||
}
|
||||
|
||||
BigInteger[] divRem = absPucks.divideAndRemainder(ONE_GAJU);
|
||||
String gajuPart = GAJU_MARK + sign + processRanks(divRem[0], ranks, 3, gSuffix, true);
|
||||
|
||||
if (divRem[1].equals(BigInteger.ZERO)) return gajuPart + " ";
|
||||
|
||||
return gajuPart + " " + processRanks(divRem[1], ranks, 3, pSuffix, true);
|
||||
}
|
||||
|
||||
private static String cleanTrailingZeros(String str) {
|
||||
int idx = str.length() - 1;
|
||||
while (idx >= 0) {
|
||||
char c = str.charAt(idx);
|
||||
if (Character.isDigit(c)) {
|
||||
if (c != '0') break;
|
||||
}
|
||||
idx--;
|
||||
}
|
||||
return idx < 0 ? "" : str.substring(0, idx + 1);
|
||||
}
|
||||
|
||||
private static String chunkString(String str, char sep, int span, boolean isFraction) {
|
||||
if (str.isEmpty()) return "";
|
||||
StringBuilder sb = new StringBuilder();
|
||||
int len = str.length();
|
||||
for (int i = 0; i < len; i++) {
|
||||
if (i > 0 && (isFraction ? i % span == 0 : (len - i) % span == 0)) {
|
||||
sb.append(sep);
|
||||
}
|
||||
sb.append(str.charAt(i));
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
private static String processRanks(BigInteger value, String[] ranks, int span, String endingSymbol, boolean useSpaces) {
|
||||
if (value.equals(BigInteger.ZERO)) return "0" + (useSpaces ? " " : "") + endingSymbol;
|
||||
|
||||
StringBuilder result = new StringBuilder();
|
||||
BigInteger divisor = BigInteger.TEN.pow(span);
|
||||
int rankIndex = 0;
|
||||
|
||||
BigInteger current = value;
|
||||
while (current.compareTo(BigInteger.ZERO) > 0) {
|
||||
BigInteger[] dr = current.divideAndRemainder(divisor);
|
||||
long val = dr[1].longValue();
|
||||
if (val > 0) {
|
||||
String rank = ranks[rankIndex];
|
||||
result.insert(0, val + rank);
|
||||
}
|
||||
current = dr[0];
|
||||
rankIndex++;
|
||||
}
|
||||
|
||||
String res = result.toString().trim();
|
||||
return res + (useSpaces ? " " : "") + endingSymbol;
|
||||
}
|
||||
|
||||
private static final Map<Character, BigInteger> MULTIPLIERS = new HashMap<>();
|
||||
static {
|
||||
MULTIPLIERS.put('万', BigInteger.valueOf(10_000L));
|
||||
MULTIPLIERS.put('億', BigInteger.valueOf(100_000_000L));
|
||||
MULTIPLIERS.put('兆', BigInteger.valueOf(1_000_000_000_000L));
|
||||
MULTIPLIERS.put('京', BigInteger.valueOf(10_000_000_000_000_000L));
|
||||
MULTIPLIERS.put('垓', BigInteger.TEN.pow(20));
|
||||
MULTIPLIERS.put('秭', BigInteger.TEN.pow(24));
|
||||
MULTIPLIERS.put('穣', BigInteger.TEN.pow(28));
|
||||
MULTIPLIERS.put('溝', BigInteger.TEN.pow(32));
|
||||
MULTIPLIERS.put('澗', BigInteger.TEN.pow(36));
|
||||
MULTIPLIERS.put('正', BigInteger.TEN.pow(40));
|
||||
MULTIPLIERS.put('載', BigInteger.TEN.pow(44));
|
||||
MULTIPLIERS.put('極', BigInteger.TEN.pow(48));
|
||||
|
||||
MULTIPLIERS.put('k', BigInteger.valueOf(1_000L));
|
||||
MULTIPLIERS.put('m', BigInteger.valueOf(1_000_000L));
|
||||
MULTIPLIERS.put('g', BigInteger.valueOf(1_000_000_000L));
|
||||
MULTIPLIERS.put('b', BigInteger.valueOf(1_000_000_000L));
|
||||
MULTIPLIERS.put('t', BigInteger.valueOf(1_000_000_000_000L));
|
||||
MULTIPLIERS.put('q', BigInteger.valueOf(1_000_000_000_000_000L));
|
||||
MULTIPLIERS.put('p', BigInteger.valueOf(1_000_000_000_000_000L));
|
||||
MULTIPLIERS.put('e', BigInteger.TEN.pow(18));
|
||||
MULTIPLIERS.put('z', BigInteger.TEN.pow(21));
|
||||
MULTIPLIERS.put('y', BigInteger.TEN.pow(24));
|
||||
MULTIPLIERS.put('r', BigInteger.TEN.pow(27));
|
||||
MULTIPLIERS.put('Q', BigInteger.TEN.pow(30));
|
||||
}
|
||||
|
||||
public static byte[] read(String rawInput) {
|
||||
if (rawInput == null || rawInput.isEmpty()) throw new IllegalArgumentException("Empty input");
|
||||
|
||||
String input = normalize(rawInput);
|
||||
boolean isNegative = false;
|
||||
if (input.startsWith("-") || input.startsWith("-") || input.startsWith("−")) {
|
||||
isNegative = true;
|
||||
input = input.substring(1).trim();
|
||||
}
|
||||
|
||||
boolean forceGaju = input.startsWith(GAJU_MARK);
|
||||
boolean forcePuck = input.startsWith(PUCK_MARK);
|
||||
if (forceGaju || forcePuck) {
|
||||
input = input.substring(1).trim();
|
||||
}
|
||||
|
||||
BigInteger gajuTotal = BigInteger.ZERO;
|
||||
BigInteger puckTotal = BigInteger.ZERO;
|
||||
|
||||
if (input.contains(".")) {
|
||||
int dotIdx = input.indexOf('.');
|
||||
String gajuPart = input.substring(0, dotIdx);
|
||||
String puckPart = input.substring(dotIdx + 1);
|
||||
|
||||
BigInteger gVal = parseRawRanked(gajuPart);
|
||||
StringBuilder sb = new StringBuilder(puckPart);
|
||||
while (sb.length() < FRACTION_LEN) sb.append('0');
|
||||
if (sb.length() > FRACTION_LEN) throw new IllegalArgumentException("Precision overflow");
|
||||
BigInteger pVal = new BigInteger(sb.toString());
|
||||
|
||||
puckTotal = gVal.multiply(ONE_GAJU).add(pVal);
|
||||
} else {
|
||||
boolean treatAsGaju = !forcePuck;
|
||||
StringBuilder digits = new StringBuilder();
|
||||
|
||||
for (int i = 0; i < input.length(); i++) {
|
||||
char c = input.charAt(i);
|
||||
if (Character.isDigit(c)) {
|
||||
digits.append(c);
|
||||
} else if (c == 'G' || c == '木') {
|
||||
BigInteger val = (digits.length() > 0) ? new BigInteger(digits.toString()) : BigInteger.ZERO;
|
||||
gajuTotal = gajuTotal.add(val);
|
||||
digits.setLength(0);
|
||||
treatAsGaju = false;
|
||||
} else if (c == 'P' || c == '本') {
|
||||
BigInteger val = (digits.length() > 0) ? new BigInteger(digits.toString()) : BigInteger.ZERO;
|
||||
puckTotal = puckTotal.add(val);
|
||||
digits.setLength(0);
|
||||
break;
|
||||
} else if (MULTIPLIERS.containsKey(c)) {
|
||||
if (digits.length() == 0) continue;
|
||||
BigInteger val = new BigInteger(digits.toString()).multiply(MULTIPLIERS.get(c));
|
||||
if (treatAsGaju) gajuTotal = gajuTotal.add(val);
|
||||
else puckTotal = puckTotal.add(val);
|
||||
digits.setLength(0);
|
||||
}
|
||||
}
|
||||
if (digits.length() > 0) {
|
||||
BigInteger val = new BigInteger(digits.toString());
|
||||
if (treatAsGaju) gajuTotal = gajuTotal.add(val);
|
||||
else puckTotal = puckTotal.add(val);
|
||||
}
|
||||
puckTotal = gajuTotal.multiply(ONE_GAJU).add(puckTotal);
|
||||
}
|
||||
|
||||
if (isNegative) puckTotal = puckTotal.negate();
|
||||
return puckTotal.toByteArray();
|
||||
}
|
||||
|
||||
private static String normalize(String raw) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (int i = 0; i < raw.length(); i++) {
|
||||
char c = raw.charAt(i);
|
||||
if (c >= '0' && c <= '9') {
|
||||
sb.append((char) (c - '0' + '0'));
|
||||
} else if (c == ',' || c == ',' || c == '_' || c == ' ' || c == '\u3000' || c == '\t' || c == '\n' || c == '\r') {
|
||||
continue;
|
||||
} else {
|
||||
sb.append(c);
|
||||
}
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
private static BigInteger parseRawRanked(String input) {
|
||||
BigInteger total = BigInteger.ZERO;
|
||||
StringBuilder currentDigits = new StringBuilder();
|
||||
|
||||
for (int i = 0; i < input.length(); i++) {
|
||||
char c = input.charAt(i);
|
||||
if (Character.isDigit(c)) {
|
||||
currentDigits.append(c);
|
||||
} else if (MULTIPLIERS.containsKey(c)) {
|
||||
if (currentDigits.length() == 0) continue;
|
||||
BigInteger val = new BigInteger(currentDigits.toString()).multiply(MULTIPLIERS.get(c));
|
||||
total = total.add(val);
|
||||
currentDigits.setLength(0);
|
||||
}
|
||||
}
|
||||
if (currentDigits.length() > 0) {
|
||||
total = total.add(new BigInteger(currentDigits.toString()));
|
||||
}
|
||||
return total;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
/*
|
||||
* 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.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
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 byte[] doubleSha256(byte[] data) {
|
||||
try {
|
||||
MessageDigest digest = MessageDigest.getInstance("SHA-256");
|
||||
return digest.digest(digest.digest(data));
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new RuntimeException("SHA-256 not available", e);
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
/*
|
||||
* 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.math.BigInteger;
|
||||
import java.net.URI;
|
||||
import java.net.URLDecoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
// GRIDS URL handling, ported from hz_grids.erl.
|
||||
|
||||
public final class Grids {
|
||||
|
||||
private Grids() {}
|
||||
|
||||
public enum Verb {
|
||||
SPEND,
|
||||
TRANSFER,
|
||||
SIGN
|
||||
}
|
||||
|
||||
public enum Context {
|
||||
CHAIN,
|
||||
NODE,
|
||||
HTTP,
|
||||
HTTPS
|
||||
}
|
||||
|
||||
public static final class ParseResult {
|
||||
public final Verb verb;
|
||||
public final Context context;
|
||||
public final String location;
|
||||
public final String recipient;
|
||||
public final BigInteger amount;
|
||||
public final byte[] payload;
|
||||
public final String url; // For SIGN verb, the reconstructed URL
|
||||
|
||||
private ParseResult(Verb verb, Context context, String location, String recipient, BigInteger amount, byte[] payload, String url) {
|
||||
this.verb = verb;
|
||||
this.context = context;
|
||||
this.location = location;
|
||||
this.recipient = recipient;
|
||||
this.amount = amount;
|
||||
this.payload = payload;
|
||||
this.url = url;
|
||||
}
|
||||
|
||||
public static ParseResult spend(Context context, String location, String recipient, BigInteger amount, byte[] payload) {
|
||||
return new ParseResult(Verb.SPEND, context, location, recipient, amount, payload, null);
|
||||
}
|
||||
|
||||
public static ParseResult transfer(Context context, String location, String recipient, BigInteger amount, byte[] payload) {
|
||||
return new ParseResult(Verb.TRANSFER, context, location, recipient, amount, payload, null);
|
||||
}
|
||||
|
||||
public static ParseResult sign(Context context, String url) {
|
||||
return new ParseResult(Verb.SIGN, context, null, null, null, null, url);
|
||||
}
|
||||
}
|
||||
|
||||
public static ParseResult parse(String gridsUrl) {
|
||||
android.util.Log.i("Grids", "Parsing URL: " + gridsUrl);
|
||||
try {
|
||||
URI uri = new URI(gridsUrl);
|
||||
String scheme = uri.getScheme();
|
||||
String host = uri.getHost();
|
||||
String path = uri.getPath();
|
||||
String query = uri.getQuery();
|
||||
|
||||
android.util.Log.i("Grids", "Components: scheme=" + scheme + ", host=" + host + ", path=" + path);
|
||||
|
||||
if (!"grids".equals(scheme) && !"grid".equals(scheme)) {
|
||||
throw new IllegalArgumentException("Invalid scheme: " + scheme);
|
||||
}
|
||||
|
||||
if (path.startsWith("/1/s/")) {
|
||||
String recipient = path.substring(5);
|
||||
Map<String, String> qargs = parseQuery(query);
|
||||
BigInteger amount = new BigInteger(qargs.getOrDefault("a", "0"));
|
||||
byte[] payload = decodePayload(qargs.getOrDefault("p", ""));
|
||||
return ParseResult.spend(Context.CHAIN, host, recipient, amount, payload);
|
||||
} else if (path.startsWith("/1/t/")) {
|
||||
String recipient = path.substring(5);
|
||||
Map<String, String> qargs = parseQuery(query);
|
||||
BigInteger amount = new BigInteger(qargs.getOrDefault("a", "0"));
|
||||
byte[] payload = decodePayload(qargs.getOrDefault("p", ""));
|
||||
String location = host + (uri.getPort() != -1 ? ":" + uri.getPort() : "");
|
||||
return ParseResult.transfer(Context.NODE, location, recipient, amount, payload);
|
||||
} else if (path.startsWith("/1/d/")) {
|
||||
String subPath = path.substring(4); // Includes the leading slash
|
||||
String httpScheme = "grids".equals(scheme) ? "https" : "http";
|
||||
String hostPart = host + (uri.getPort() != -1 ? ":" + uri.getPort() : "");
|
||||
String httpUrl = httpScheme + "://" + hostPart + subPath + (query != null ? "?" + query : "");
|
||||
return ParseResult.sign("grids".equals(scheme) ? Context.HTTPS : Context.HTTP, httpUrl);
|
||||
} else {
|
||||
throw new IllegalArgumentException("Unknown verb in path: " + path);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Failed to parse GRIDS URL: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
public static final String REQ_TYPE_MESSAGE = "message";
|
||||
public static final String REQ_TYPE_BINARY = "binary";
|
||||
public static final String REQ_TYPE_TX = "tx";
|
||||
|
||||
public static Map<String, Object> makeRequest(String type, Object payload, String publicId, String networkId) {
|
||||
Map<String, Object> req = new LinkedHashMap<>();
|
||||
req.put("grids", 1);
|
||||
req.put("chain", "gajumaru");
|
||||
req.put("network_id", networkId);
|
||||
req.put("type", type);
|
||||
req.put("public_id", publicId);
|
||||
req.put("payload", payload);
|
||||
return req;
|
||||
}
|
||||
|
||||
private static Map<String, String> parseQuery(String query) {
|
||||
Map<String, String> params = new LinkedHashMap<>();
|
||||
if (query == null || query.isEmpty()) return params;
|
||||
String[] pairs = query.split("&");
|
||||
for (String pair : pairs) {
|
||||
int idx = pair.indexOf("=");
|
||||
if (idx > 0) {
|
||||
params.put(URLDecoder.decode(pair.substring(0, idx), StandardCharsets.UTF_8),
|
||||
URLDecoder.decode(pair.substring(idx + 1), StandardCharsets.UTF_8));
|
||||
} else {
|
||||
params.put(URLDecoder.decode(pair, StandardCharsets.UTF_8), "");
|
||||
}
|
||||
}
|
||||
return params;
|
||||
}
|
||||
|
||||
private static byte[] decodePayload(String p) {
|
||||
if (p == null || p.isEmpty()) return new byte[0];
|
||||
// The Erlang side uses uri_string:compose_query which percent-encodes.
|
||||
// URLDecoder.decode should have handled it in parseQuery.
|
||||
return p.getBytes(StandardCharsets.UTF_8);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
/*
|
||||
* 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.io.BufferedReader;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.OutputStream;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URL;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Map;
|
||||
import swiss.qpq.gajumaru.core.encoding.ZJ;
|
||||
|
||||
// Minimal HTTP client for GRIDS and Node interaction.
|
||||
|
||||
public final class Http {
|
||||
|
||||
private Http() {}
|
||||
|
||||
public static Object get(String url) throws Exception {
|
||||
return request(url, "GET", null);
|
||||
}
|
||||
|
||||
public static Object post(String url, Object body) throws Exception {
|
||||
return request(url, "POST", body);
|
||||
}
|
||||
|
||||
private static Object request(String urlStr, String method, Object body) throws Exception {
|
||||
android.util.Log.i("Http", "Request [" + method + "]: " + urlStr);
|
||||
URL url = new URL(urlStr);
|
||||
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
|
||||
conn.setRequestMethod(method);
|
||||
conn.setConnectTimeout(15000);
|
||||
conn.setReadTimeout(15000);
|
||||
conn.setInstanceFollowRedirects(true);
|
||||
|
||||
// Emulate a standard browser agent to avoid being blocked by strict servers
|
||||
conn.setRequestProperty("Accept", "application/json, text/plain, */*");
|
||||
conn.setRequestProperty("User-Agent", "Mozilla/5.0 (Linux; Android 10) GajuMobile/1.0");
|
||||
|
||||
if (body != null) {
|
||||
conn.setRequestProperty("Content-Type", "application/json");
|
||||
conn.setDoOutput(true);
|
||||
try (OutputStream os = conn.getOutputStream()) {
|
||||
byte[] input;
|
||||
if (body instanceof String) {
|
||||
input = ((String) body).getBytes(StandardCharsets.UTF_8);
|
||||
} else {
|
||||
input = ZJ.encode(body).getBytes(StandardCharsets.UTF_8);
|
||||
}
|
||||
os.write(input, 0, input.length);
|
||||
}
|
||||
}
|
||||
|
||||
int code = conn.getResponseCode();
|
||||
android.util.Log.i("Http", "Response Code for " + urlStr + ": " + code);
|
||||
|
||||
StringBuilder response = new StringBuilder();
|
||||
try (java.io.InputStream is = (code >= 200 && code < 300) ? conn.getInputStream() : conn.getErrorStream();
|
||||
BufferedReader br = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8))) {
|
||||
String line;
|
||||
while ((line = br.readLine()) != null) {
|
||||
response.append(line).append("\n");
|
||||
}
|
||||
}
|
||||
|
||||
String responseStr = response.toString().trim();
|
||||
|
||||
if (code < 200 || code >= 300) {
|
||||
String reason = "HTTP " + code;
|
||||
try {
|
||||
Object decoded = ZJ.decode(responseStr);
|
||||
if (decoded instanceof Map) {
|
||||
Object r = ((Map<?, ?>) decoded).get("reason");
|
||||
if (r != null) reason = r.toString();
|
||||
}
|
||||
} catch (Exception ignored) {}
|
||||
throw new Exception(reason + " (URL: " + urlStr + ")");
|
||||
}
|
||||
|
||||
if (responseStr.isEmpty()) return null;
|
||||
|
||||
try {
|
||||
return ZJ.decode(responseStr);
|
||||
} catch (Exception e) {
|
||||
if (responseStr.startsWith("<")) {
|
||||
android.util.Log.e("Http", "Server returned HTML for " + urlStr + ". Body preview: " +
|
||||
(responseStr.length() > 500 ? responseStr.substring(0, 500) : responseStr));
|
||||
throw new Exception("Server returned HTML instead of JSON. Full URL and body logged to Logcat.");
|
||||
}
|
||||
throw new Exception("JSON parse failed for " + urlStr + ": " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
/*
|
||||
* 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.List;
|
||||
import java.util.Map;
|
||||
import java.util.HashMap;
|
||||
|
||||
/**
|
||||
* NodeClient provides a simple interface to query Gajumaru nodes.
|
||||
* Ported from hz.erl
|
||||
*/
|
||||
public final class NodeClient {
|
||||
|
||||
public static final class Endpoint {
|
||||
public final String host;
|
||||
public final int port;
|
||||
public final boolean useTls;
|
||||
|
||||
public Endpoint(String host, int port) {
|
||||
this(host, port, false);
|
||||
}
|
||||
|
||||
public Endpoint(String host, int port, boolean useTls) {
|
||||
this.host = host;
|
||||
this.port = port;
|
||||
this.useTls = useTls;
|
||||
}
|
||||
}
|
||||
|
||||
private final List<Endpoint> endpoints;
|
||||
|
||||
public NodeClient(List<Endpoint> endpoints) {
|
||||
if (endpoints == null || endpoints.isEmpty()) {
|
||||
throw new IllegalArgumentException("At least one endpoint is required");
|
||||
}
|
||||
this.endpoints = endpoints;
|
||||
}
|
||||
|
||||
public Map<String, Object> status() throws Exception {
|
||||
return (Map<String, Object>) request("/v3/status", "GET", null);
|
||||
}
|
||||
|
||||
public long topHeight() throws Exception {
|
||||
Map<String, Object> res = (Map<String, Object>) request("/v3/headers/top", "GET", null);
|
||||
return ((Number) res.get("height")).longValue();
|
||||
}
|
||||
|
||||
public Map<String, Object> account(String accountId) throws Exception {
|
||||
return (Map<String, Object>) request("/v3/accounts/" + accountId, "GET", null);
|
||||
}
|
||||
|
||||
public long nextNonce(String accountId) throws Exception {
|
||||
try {
|
||||
Map<String, Object> res = (Map<String, Object>) request("/v3/accounts/" + accountId + "/next-nonce", "GET", null);
|
||||
return ((Number) res.get("next_nonce")).longValue();
|
||||
} catch (Exception e) {
|
||||
if (e.getMessage() != null && e.getMessage().contains("Account not found")) {
|
||||
return 1;
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
public Map<String, Object> postTx(String signedTx) throws Exception {
|
||||
Map<String, String> body = new HashMap<>();
|
||||
body.put("tx", signedTx);
|
||||
return (Map<String, Object>) request("/v3/transactions", "POST", body);
|
||||
}
|
||||
|
||||
public Map<String, Object> tx(String txHash) throws Exception {
|
||||
return (Map<String, Object>) request("/v3/transactions/" + txHash, "GET", null);
|
||||
}
|
||||
|
||||
public Map<String, Object> txInfo(String txHash) throws Exception {
|
||||
return (Map<String, Object>) request("/v3/transactions/" + txHash + "/info", "GET", null);
|
||||
}
|
||||
|
||||
private Object request(String path, String method, Object body) throws Exception {
|
||||
Exception lastException = null;
|
||||
for (Endpoint endpoint : endpoints) {
|
||||
try {
|
||||
String protocol = endpoint.useTls ? "https://" : "http://";
|
||||
String url = protocol + endpoint.host + ":" + endpoint.port + path;
|
||||
|
||||
if (endpoint.useTls) {
|
||||
try {
|
||||
return "GET".equals(method) ? Http.get(url) : Http.post(url, body);
|
||||
} catch (Exception e) {
|
||||
// Fallback to HTTP
|
||||
String httpUrl = "http://" + endpoint.host + ":" + endpoint.port + path;
|
||||
return "GET".equals(method) ? Http.get(httpUrl) : Http.post(httpUrl, body);
|
||||
}
|
||||
} else {
|
||||
return "GET".equals(method) ? Http.get(url) : Http.post(url, body);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
lastException = e;
|
||||
}
|
||||
}
|
||||
throw new Exception("All endpoints failed. Last error: " + (lastException != null ? lastException.getMessage() : "unknown"), lastException);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
/*
|
||||
* 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.math.BigInteger;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.List;
|
||||
import swiss.qpq.gajumaru.core.crypto.Blake2b;
|
||||
import swiss.qpq.gajumobile.security.GajuNative;
|
||||
import swiss.qpq.gajumaru.core.data.Id;
|
||||
import swiss.qpq.gajumaru.core.data.SignedTx;
|
||||
import swiss.qpq.gajumaru.core.data.SpendTx;
|
||||
import swiss.qpq.gajumaru.core.encoding.ApiEncoder;
|
||||
|
||||
/**
|
||||
* TransactionService provides high-level tools to build and sign transactions
|
||||
* and other chain data.
|
||||
*/
|
||||
public final class TransactionService {
|
||||
|
||||
private TransactionService() {}
|
||||
|
||||
/**
|
||||
* Builds and signs a Spend transaction using a 32-byte seed.
|
||||
*/
|
||||
public static String buildSpendTx(
|
||||
String networkId,
|
||||
String senderId,
|
||||
String recipientId,
|
||||
BigInteger amount,
|
||||
BigInteger gasPrice,
|
||||
BigInteger gas,
|
||||
long ttl,
|
||||
long nonce,
|
||||
String payload,
|
||||
byte[] seed
|
||||
) {
|
||||
// 1. Decode IDs
|
||||
Id sender = new Id(Id.Tag.ACCOUNT, ApiEncoder.decode(senderId).payload());
|
||||
|
||||
ApiEncoder.DecodeResult recipientResult = ApiEncoder.decode(recipientId);
|
||||
Id.Tag recipientTag = recipientResult.type() == ApiEncoder.Type.CONTRACT_PUBKEY ? Id.Tag.CONTRACT : Id.Tag.ACCOUNT;
|
||||
Id recipient = new Id(recipientTag, recipientResult.payload());
|
||||
|
||||
// 2. Build SpendTx
|
||||
byte[] payloadBytes = payload != null ? payload.getBytes(StandardCharsets.UTF_8) : new byte[0];
|
||||
SpendTx tx = new SpendTx(sender, recipient, amount, gasPrice, gas, ttl, nonce, payloadBytes);
|
||||
byte[] serializedTx = tx.serialize();
|
||||
|
||||
// 3. Prepare NetworkHash
|
||||
byte[] networkIdBytes = networkId.getBytes(StandardCharsets.UTF_8);
|
||||
byte[] txHash = Blake2b.hash(serializedTx, 32);
|
||||
byte[] networkHash = new byte[networkIdBytes.length + txHash.length];
|
||||
System.arraycopy(networkIdBytes, 0, networkHash, 0, networkIdBytes.length);
|
||||
System.arraycopy(txHash, 0, networkHash, networkIdBytes.length, txHash.length);
|
||||
|
||||
// 4. Expand Seed and Sign
|
||||
byte[] keypair = GajuNative.cryptoSignSeedKeypair(seed);
|
||||
if (keypair == null) throw new RuntimeException("Native keypair generation failed");
|
||||
|
||||
byte[] secretKey = new byte[64];
|
||||
System.arraycopy(keypair, 32, secretKey, 0, 64);
|
||||
|
||||
byte[] signature = GajuNative.cryptoSignDetached(networkHash, secretKey);
|
||||
|
||||
GajuNative.memzero(keypair);
|
||||
GajuNative.memzero(secretKey);
|
||||
|
||||
if (signature == null) throw new RuntimeException("Native signing failed");
|
||||
|
||||
// 5. Wrap in SignedTx
|
||||
SignedTx signedTx = new SignedTx(List.of(signature), serializedTx);
|
||||
|
||||
return ApiEncoder.encode(ApiEncoder.Type.TRANSACTION, signedTx.serialize());
|
||||
}
|
||||
|
||||
/**
|
||||
* Signs a message using the Gajumaru standard (prefixed and hashed).
|
||||
* @param secretKey 64-byte expanded secret key.
|
||||
*/
|
||||
public static byte[] signMessage(String message, byte[] secretKey) {
|
||||
byte[] msgBytes = message.getBytes(StandardCharsets.UTF_8);
|
||||
byte[] prefix = "Gajumaru Signed Message:\n".getBytes(StandardCharsets.UTF_8);
|
||||
|
||||
byte[] pSize = VarInt.encode(prefix.length);
|
||||
byte[] mSize = VarInt.encode(msgBytes.length);
|
||||
|
||||
byte[] smashed = new byte[pSize.length + prefix.length + mSize.length + msgBytes.length];
|
||||
int pos = 0;
|
||||
System.arraycopy(pSize, 0, smashed, pos, pSize.length); pos += pSize.length;
|
||||
System.arraycopy(prefix, 0, smashed, pos, prefix.length); pos += prefix.length;
|
||||
System.arraycopy(mSize, 0, smashed, pos, mSize.length); pos += mSize.length;
|
||||
System.arraycopy(msgBytes, 0, smashed, pos, msgBytes.length);
|
||||
|
||||
byte[] hashed = Blake2b.hash(smashed, 32);
|
||||
return GajuNative.cryptoSignDetached(hashed, secretKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Signs binary data using the Gajumaru standard (prefixed and hashed).
|
||||
* @param secretKey 64-byte expanded secret key.
|
||||
*/
|
||||
public static byte[] signBinary(byte[] data, byte[] secretKey) {
|
||||
byte[] prefix = "Gajumaru Signed Binary:".getBytes(StandardCharsets.UTF_8);
|
||||
byte[] target = new byte[prefix.length + data.length];
|
||||
System.arraycopy(prefix, 0, target, 0, prefix.length);
|
||||
System.arraycopy(data, 0, target, prefix.length, data.length);
|
||||
|
||||
byte[] hashed = Blake2b.hash(target, 32);
|
||||
return GajuNative.cryptoSignDetached(hashed, secretKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Signs a transaction.
|
||||
* @param secretKey 64-byte expanded secret key.
|
||||
*/
|
||||
public static String signTx(byte[] txData, String networkId, byte[] secretKey) {
|
||||
byte[] nidBytes = networkId.getBytes(StandardCharsets.UTF_8);
|
||||
byte[] hash = Blake2b.hash(txData, 32);
|
||||
|
||||
byte[] networkHash = new byte[nidBytes.length + hash.length];
|
||||
System.arraycopy(nidBytes, 0, networkHash, 0, nidBytes.length);
|
||||
System.arraycopy(hash, 0, networkHash, nidBytes.length, hash.length);
|
||||
|
||||
byte[] signature = GajuNative.cryptoSignDetached(networkHash, secretKey);
|
||||
if (signature == null) throw new RuntimeException("Native signing failed");
|
||||
|
||||
SignedTx signedTx = new SignedTx(List.of(signature), txData);
|
||||
return ApiEncoder.encode(ApiEncoder.Type.TRANSACTION, signedTx.serialize());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* 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.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
|
||||
// Bitcoin-style Variable-length integer encoding.
|
||||
// Port of vencode in hz.erl.
|
||||
|
||||
public final class VarInt {
|
||||
|
||||
private VarInt() {}
|
||||
|
||||
public static byte[] encode(long n) {
|
||||
if (n < 0) {
|
||||
throw new IllegalArgumentException("Negative values not supported: " + n);
|
||||
}
|
||||
if (n < 0xfd) {
|
||||
return new byte[]{(byte) n};
|
||||
} else if (n <= 0xffff) {
|
||||
ByteBuffer bb = ByteBuffer.allocate(3);
|
||||
bb.order(ByteOrder.LITTLE_ENDIAN);
|
||||
bb.put((byte) 0xfd);
|
||||
bb.putShort((short) n);
|
||||
return bb.array();
|
||||
} else if (n <= 0xffffffffL) {
|
||||
ByteBuffer bb = ByteBuffer.allocate(5);
|
||||
bb.order(ByteOrder.LITTLE_ENDIAN);
|
||||
bb.put((byte) 0xfe);
|
||||
bb.putInt((int) n);
|
||||
return bb.array();
|
||||
} else {
|
||||
ByteBuffer bb = ByteBuffer.allocate(9);
|
||||
bb.order(ByteOrder.LITTLE_ENDIAN);
|
||||
bb.put((byte) 0xff);
|
||||
bb.putLong(n);
|
||||
return bb.array();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,570 @@
|
||||
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.util.Map;
|
||||
import java.util.HashMap;
|
||||
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.encoding.ZJ;
|
||||
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.data.SpendTx;
|
||||
import swiss.qpq.gajumaru.core.data.SignedTx;
|
||||
import swiss.qpq.gajumaru.core.encoding.Mnemonic;
|
||||
import swiss.qpq.gajumaru.core.tools.CryptoUtils;
|
||||
import swiss.qpq.gajumaru.core.tools.Grids;
|
||||
|
||||
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 "ed25519_malleability" -> { System.out.print(ed25519_malleability()); }
|
||||
case "api_encode" -> { System.out.print(api_encode(args[1])); }
|
||||
case "id_serialization" -> { System.out.print(id_serialization(args[1])); }
|
||||
case "spend_tx" -> { System.out.print(spend_tx(args[1])); }
|
||||
case "signed_tx" -> { System.out.print(signed_tx(args[1])); }
|
||||
case "mnemonic" -> { System.out.print(mnemonic(args[1])); }
|
||||
case "zj_encode" -> { System.out.print(zj_encode(args[1])); }
|
||||
case "zj_decode" -> { System.out.print(zj_decode(args[1])); }
|
||||
case "grids_parse" -> { System.out.print(grids_parse(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 ed25519_malleability() {
|
||||
// L in little-endian
|
||||
byte[] L_bytes = {
|
||||
(byte) 0xed, (byte) 0xd3, (byte) 0xf5, (byte) 0x5c, (byte) 0x1a, (byte) 0x63, (byte) 0x12, (byte) 0x58,
|
||||
(byte) 0xd6, (byte) 0x9c, (byte) 0xf7, (byte) 0xa2, (byte) 0xde, (byte) 0xf9, (byte) 0xde, (byte) 0x14,
|
||||
(byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
|
||||
(byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x10
|
||||
};
|
||||
byte[] sig = new byte[64];
|
||||
System.arraycopy(L_bytes, 0, sig, 32, 32); // S = L
|
||||
boolean result = Ed25519.verify(new byte[32], new byte[0], sig);
|
||||
return Boolean.toString(result);
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
private static String spend_tx(String workingPath) throws IOException {
|
||||
Path testPath = Path.of(workingPath, "spend_tx.test");
|
||||
Path resPath = Path.of(workingPath, "spend_tx.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("\\|");
|
||||
SpendTx tx = new SpendTx(
|
||||
Id.deserialize(CryptoUtils.hexToBin(p[0])),
|
||||
Id.deserialize(CryptoUtils.hexToBin(p[1])),
|
||||
new BigInteger(p[2]),
|
||||
new BigInteger(p[3]),
|
||||
new BigInteger(p[4]),
|
||||
Long.parseLong(p[5]),
|
||||
Long.parseLong(p[6]),
|
||||
CryptoUtils.hexToBin(p[7])
|
||||
);
|
||||
results.add(CryptoUtils.binToHex(tx.serialize()));
|
||||
}
|
||||
Files.write(resPath, results);
|
||||
return resPath.toString();
|
||||
}
|
||||
|
||||
private static String signed_tx(String workingPath) throws IOException {
|
||||
Path testPath = Path.of(workingPath, "signed_tx.test");
|
||||
Path resPath = Path.of(workingPath, "signed_tx.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("\\|");
|
||||
List<byte[]> sigs = new ArrayList<>();
|
||||
for (String s : p[0].split(",")) {
|
||||
sigs.add(CryptoUtils.hexToBin(s));
|
||||
}
|
||||
SignedTx tx = new SignedTx(sigs, CryptoUtils.hexToBin(p[1]));
|
||||
results.add(CryptoUtils.binToHex(tx.serialize()));
|
||||
}
|
||||
Files.write(resPath, results);
|
||||
return resPath.toString();
|
||||
}
|
||||
|
||||
private static String mnemonic(String workingPath) throws IOException {
|
||||
Path testPath = Path.of(workingPath, "mnemonic.test");
|
||||
Path resPath = Path.of(workingPath, "mnemonic.java.txt");
|
||||
List<String> lines = Files.readAllLines(testPath);
|
||||
List<String> results = new ArrayList<>();
|
||||
for (String hex : lines) {
|
||||
if (hex.trim().isEmpty()) continue;
|
||||
byte[] seed = CryptoUtils.hexToBin(hex);
|
||||
byte[][] phrase = Mnemonic.encode(seed);
|
||||
|
||||
// Join words with space
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (int i = 0; i < phrase.length; i++) {
|
||||
if (i > 0) sb.append(" ");
|
||||
sb.append(new String(phrase[i], java.nio.charset.StandardCharsets.UTF_8));
|
||||
}
|
||||
results.add(sb.toString());
|
||||
|
||||
// Roundtrip test
|
||||
byte[] decoded = Mnemonic.decode(phrase);
|
||||
if (!java.util.Arrays.equals(seed, decoded)) {
|
||||
throw new RuntimeException("Mnemonic roundtrip failed in Java!");
|
||||
}
|
||||
|
||||
CryptoUtils.wipe(seed);
|
||||
for (byte[] word : phrase) CryptoUtils.wipe(word);
|
||||
}
|
||||
Files.write(resPath, results);
|
||||
return resPath.toString();
|
||||
}
|
||||
|
||||
private static String zj_encode(String workingPath) throws IOException {
|
||||
Path testPath = Path.of(workingPath, "zj_encode.test");
|
||||
Path resPath = Path.of(workingPath, "zj_encode.java.txt");
|
||||
List<String> lines = Files.readAllLines(testPath);
|
||||
List<String> results = new ArrayList<>();
|
||||
for (String line : lines) {
|
||||
if (line.trim().isEmpty()) continue;
|
||||
// For testing, we decode then re-encode to check consistency
|
||||
Object val = ZJ.decode(line);
|
||||
results.add(ZJ.encode(val));
|
||||
}
|
||||
Files.write(resPath, results);
|
||||
return resPath.toString();
|
||||
}
|
||||
|
||||
private static String zj_decode(String workingPath) throws IOException {
|
||||
Path testPath = Path.of(workingPath, "zj_decode.test");
|
||||
Path resPath = Path.of(workingPath, "zj_decode.java.txt");
|
||||
List<String> lines = Files.readAllLines(testPath);
|
||||
List<String> results = new ArrayList<>();
|
||||
for (String line : lines) {
|
||||
if (line.trim().isEmpty()) continue;
|
||||
Object val = ZJ.decode(line);
|
||||
// We encode it back to string for comparison or output format
|
||||
results.add(ZJ.encode(val));
|
||||
}
|
||||
Files.write(resPath, results);
|
||||
return resPath.toString();
|
||||
}
|
||||
|
||||
private static String grids_parse(String workingPath) throws IOException {
|
||||
Path testPath = Path.of(workingPath, "grids_parse.test");
|
||||
Path resPath = Path.of(workingPath, "grids_parse.java.txt");
|
||||
List<String> lines = Files.readAllLines(testPath);
|
||||
List<String> results = new ArrayList<>();
|
||||
for (String url : lines) {
|
||||
if (url.trim().isEmpty()) continue;
|
||||
Grids.ParseResult res = Grids.parse(url);
|
||||
Map<String, Object> map = new HashMap<>();
|
||||
map.put("verb", res.verb.name());
|
||||
map.put("context", res.context.name());
|
||||
map.put("location", res.location);
|
||||
map.put("recipient", res.recipient);
|
||||
map.put("amount", res.amount != null ? res.amount.toString() : null);
|
||||
map.put("payload", res.payload != null ? CryptoUtils.binToHex(res.payload) : null);
|
||||
map.put("url", res.url);
|
||||
results.add(ZJ.encode(map));
|
||||
}
|
||||
Files.write(resPath, results);
|
||||
return resPath.toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,676 @@
|
||||
%%% @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,
|
||||
"spend_tx" => fun spend_tx/0,
|
||||
"signed_tx" => fun signed_tx/0,
|
||||
"mnemonic" => fun mnemonic/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).
|
||||
|
||||
|
||||
spend_tx() ->
|
||||
Temp = temp_dir(),
|
||||
TestFile = filename:join(Temp, "spend_tx.test"),
|
||||
Cases = [gen_spend_tx() || _ <- lists:seq(1, 20)],
|
||||
Lines = [serialize_spend_tx(C) || C <- Cases],
|
||||
ok = filelib:ensure_dir(TestFile),
|
||||
ok = file:write_file(TestFile, unicode:characters_to_binary([[L, "\n"] || L <- Lines])),
|
||||
Expected = [bin_to_hex(gmser_chain_objects:serialize(spend_tx, 1, spend_tx_template(), C)) || C <- Cases],
|
||||
Run = "bin/run spend_tx " ++ 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).
|
||||
|
||||
spend_tx_template() ->
|
||||
[{sender_id, id},
|
||||
{recipient_id, id},
|
||||
{amount, int},
|
||||
{gas_price, int},
|
||||
{gas, int},
|
||||
{ttl, int},
|
||||
{nonce, int},
|
||||
{payload, binary}].
|
||||
|
||||
gen_spend_tx() ->
|
||||
[{sender_id, gmser_id:create(account, rand:bytes(32))},
|
||||
{recipient_id, gmser_id:create(account, rand:bytes(32))},
|
||||
{amount, random_pucks(12)},
|
||||
{gas_price, 1000000000},
|
||||
{gas, 20000},
|
||||
{ttl, rand:uniform(1000000)},
|
||||
{nonce, rand:uniform(10000)},
|
||||
{payload, rand:bytes(rand:uniform(100))}].
|
||||
|
||||
serialize_spend_tx(Fields) ->
|
||||
ID = fun(Key) -> bin_to_hex(gmser_id:encode(proplists:get_value(Key, Fields))) end,
|
||||
Int = fun(Key) -> integer_to_list(proplists:get_value(Key, Fields)) end,
|
||||
Bin = fun(Key) -> bin_to_hex(proplists:get_value(Key, Fields)) end,
|
||||
Parts = [ID(sender_id), ID(recipient_id), Int(amount), Int(gas_price), Int(gas), Int(ttl), Int(nonce), Bin(payload)],
|
||||
string:join(Parts, "|").
|
||||
|
||||
|
||||
signed_tx() ->
|
||||
Temp = temp_dir(),
|
||||
TestFile = filename:join(Temp, "signed_tx.test"),
|
||||
Cases = [gen_signed_tx() || _ <- lists:seq(1, 20)],
|
||||
Lines = [serialize_signed_tx(C) || C <- Cases],
|
||||
ok = filelib:ensure_dir(TestFile),
|
||||
ok = file:write_file(TestFile, unicode:characters_to_binary([[L, "\n"] || L <- Lines])),
|
||||
Expected = [bin_to_hex(gmser_chain_objects:serialize(signed_tx, 1, signed_tx_template(), C)) || C <- Cases],
|
||||
Run = "bin/run signed_tx " ++ 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).
|
||||
|
||||
signed_tx_template() ->
|
||||
[{signatures, [binary]},
|
||||
{transaction, binary}].
|
||||
|
||||
gen_signed_tx() ->
|
||||
[{signatures, [rand:bytes(64) || _ <- lists:seq(1, rand:uniform(3))]},
|
||||
{transaction, rand:bytes(rand:uniform(500))}].
|
||||
|
||||
serialize_signed_tx(Fields) ->
|
||||
Sigs = proplists:get_value(signatures, Fields),
|
||||
SigsHex = string:join([bin_to_hex(S) || S <- Sigs], ","),
|
||||
TxHex = bin_to_hex(proplists:get_value(transaction, Fields)),
|
||||
SigsHex ++ "|" ++ TxHex.
|
||||
|
||||
|
||||
mnemonic() ->
|
||||
Temp = temp_dir(),
|
||||
TestFile = filename:join(Temp, "mnemonic.test"),
|
||||
ok = filelib:ensure_dir(TestFile),
|
||||
Cases = [rand:bytes(32) || _ <- lists:seq(1, 20)],
|
||||
ok = file:write_file(TestFile, [[bin_to_hex(S), "\n"] || S <- Cases]),
|
||||
Expected = [unicode:characters_to_list(hz_key_master:encode(S)) || S <- Cases],
|
||||
Run = "bin/run mnemonic " ++ 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