Compare commits

..
14 Commits
Author SHA1 Message Date
zxq9 7493b473a7 I hate computers now and the letter "a" has stopped working without mashing it 2026-08-21 17:22:30 +09:00
zxq9 3d3e90a6c6 Lots of stuff...
- Finally completed the chain_objects enumeration and general encoding rule
- Implemented SpendTx and SignedTx... and I *think* they work now (at least my cheesy canned testing says they do)
- Finally decided which way I want to deal with trailing spaces in the advanced variants of GajuFormat
- Found a roughly system agnostic way to deal with secret key memory and added an api for using that (might need more, but that will be clear soon)
2026-08-21 14:37:48 +09:00
zxq9 a4ca21593b Add SUPERCOP to references 2026-08-21 10:35:17 +09:00
zxq9 b751b46edc Add references 2026-08-21 10:31:57 +09:00
zxq9 b95ba8a88f Adding TODO note to Ed25519 2026-08-21 10:18:50 +09:00
zxq9 1de3608c9b WIP 2026-08-21 08:42:43 +09:00
zxq9 4019af58e9 Serialization and Ed25519 (#2)
finally...

Reviewed-on: #2
Co-authored-by: Craig Everett <zxq9@zxq9.com>
Co-committed-by: Craig Everett <zxq9@zxq9.com>
2026-08-20 21:19:16 +09:00
zxq9 ea90d9d3ab WIP 2026-08-12 18:24:12 +09:00
zxq9 78b5fb1512 Lawyer stuff 2026-08-12 18:03:58 +09:00
zxq9 f99e081160 WIP 2026-08-12 18:02:28 +09:00
zxq9 11cdfc4569 WIP 2026-08-12 17:59:35 +09:00
zxq9 8cce885721 WIP 2026-08-12 17:56:21 +09:00
zxq9 8cff34abb4 WIP 2026-08-12 17:24:13 +09:00
zxq9 366c6157af WIP: still need to test against hz_format 2026-07-28 15:58:06 +09:00
38 changed files with 4344 additions and 1575 deletions
+3
View File
@@ -6,9 +6,12 @@ Thumbs.db
.netrwhist
.nvimlog
.idea/
.artifacts/
*.iml
.gradle/
local.properties
Captures/
.externalNativeBuild/
temp
erl_crash.dump
*.class
+26
View File
@@ -2,6 +2,32 @@
Java libraries for the Gajumaru
## Purpose
gm-java provides the basic functionality required to interact with the [Gajumaru](https://gajumaru./io)
blockchain system. This includes serialization, function wrappers for Gajumaru node endpoint calls,
handling of GRIDS URLs, formatting of monetary values, any cryptographic functions not covered in
standard Java libraries, and some network functionality necessary to interact smoothly with
Gajumaru chains (Groot and AC networks).
This codebase may *not* include references to third-party libraries, may *not* permit un-zeroed
byte values to be left on the heap until the JVM eventually decides to garbage collect them,
and may *not* build up a big, gnarly, inheritance-heavy OOPsy object hierarchy.
Classes should generally be structured as `final` classes of functions that operate over data
that can be expressed as Java primitives, most commonly `byte[]` and some form of `int` or
occasionally `BigInteger`.
`BigInteger` is not permitted in cryptographic operations, as it leaves potentially sensitive
artifacts scattered all throughout the heap and can occasionally make the garbage collector
go bananas when performing heavy math operations. `BigInteger` is excellent, however, for things
like representing currency values in Pucks.
All forms of floating-point arithmetic are forbidden in all currency calculations. The Gajumaru
does not have floats, and they do not exist at all in the Sophia language.
## Terms
Copyright (c) 2026 QPQ AG <info@qpq.swiss>. All rights reserved.
+1 -10
View File
@@ -8,15 +8,6 @@ set -e
abs_dir="$(cd -P $(dirname ${BASH_SOURCE}) && pwd)"
project_dir="$(dirname $abs_dir)"
# Prefer Homebrew OpenJDK when /usr/bin/java is a macOS stub.
if [[ -x /opt/homebrew/opt/openjdk/bin/javac ]]; then
export PATH="/opt/homebrew/opt/openjdk/bin:$PATH"
elif [[ -x /opt/homebrew/opt/openjdk@25/bin/javac ]]; then
export PATH="/opt/homebrew/opt/openjdk@25/bin:$PATH"
elif [[ -x /opt/homebrew/opt/openjdk@21/bin/javac ]]; then
export PATH="/opt/homebrew/opt/openjdk@21/bin:$PATH"
fi
# Clean
rm -rf "$project_dir/build/classes/*"
rm -f "$project_dir/Testinator.class"
@@ -27,6 +18,6 @@ find "$project_dir/src/main/java" -name "*.java" | xargs javac \
-d "$project_dir/build/classes"
# Build the Testinator thingy
javac -cp "build/classes" -d build src/Testinator.java
javac -cp "build/classes" -d build test/Testinator.java
echo "Bytecode in $project_dir/build/classes/"
View File
-470
View File
@@ -1,470 +0,0 @@
#!/usr/bin/env python3
"""
Generate Java record declarations from GajumaruChainObjects.asn
(or any module produced by gmser_schema_export).
This is intentionally a thin, schema-shaped codegen: it does NOT emit a
BER/DER codec. The abstract types are for typed construction; RLP remains
the wire format (see swiss.qpq.gajumaru.core.encoding.RLP).
Usage (from gajumaru-core/):
bin/asn1_to_java \\
--asn ../../gajumaru/asn1_generated/GajumaruChainObjects.asn \\
--types Id,SignedTxV1,SpendTxV1
bin/asn1_to_java --asn ... --all
"""
from __future__ import annotations
import argparse
import re
import sys
from dataclasses import dataclass, field
from pathlib import Path
from typing import List, Optional, Tuple, Union
# ---------------------------------------------------------------------------
# ASN.1 model (minimal)
# ---------------------------------------------------------------------------
AsnType = Union[
"PrimitiveType",
"NamedType",
"SequenceType",
"SequenceOfType",
]
@dataclass
class PrimitiveType:
kind: str # integer, octet_string, boolean, fixed_integer
constraint: Optional[str] = None # e.g. "0..255" or "12"
@dataclass
class NamedType:
name: str # Id, BigInt, SpendTxV1, ...
@dataclass
class SequenceField:
name: str
type: AsnType
@dataclass
class SequenceType:
fields: List[SequenceField]
@dataclass
class SequenceOfType:
elem: AsnType
@dataclass
class TypeDef:
name: str
type: AsnType
comment: str = ""
# ---------------------------------------------------------------------------
# Parser
# ---------------------------------------------------------------------------
COMMENT_RE = re.compile(r"--[^\n]*")
TYPE_DEF_RE = re.compile(
r"([A-Z][A-Za-z0-9]*)\s*::=\s*",
)
def strip_comments(text: str) -> str:
return COMMENT_RE.sub("", text)
def parse_module(text: str) -> dict[str, TypeDef]:
text = strip_comments(text)
# Drop module wrapper noise; keep type assignments only.
begin = text.find("BEGIN")
end = text.rfind("END")
if begin >= 0 and end > begin:
text = text[begin + 5 : end]
defs: dict[str, TypeDef] = {}
pos = 0
while True:
m = TYPE_DEF_RE.search(text, pos)
if not m:
break
name = m.group(1)
start = m.end()
# Find next top-level type def or EOF
nxt = TYPE_DEF_RE.search(text, start)
body = text[start : nxt.start() if nxt else len(text)].strip()
# Trim trailing junk
body = body.rstrip().rstrip(";").strip()
try:
asn_type = parse_type(body)
except Exception as e:
raise SystemExit(f"Failed to parse type {name}: {e}\nBody:\n{body[:200]}") from e
defs[name] = TypeDef(name=name, type=asn_type)
pos = nxt.start() if nxt else len(text)
return defs
def parse_type(s: str) -> AsnType:
s = s.strip()
if s.startswith("SEQUENCE OF"):
rest = s[len("SEQUENCE OF") :].strip()
return SequenceOfType(parse_type(rest))
if s.startswith("SEQUENCE"):
rest = s[len("SEQUENCE") :].strip()
if not rest.startswith("{"):
raise ValueError(f"expected SEQUENCE {{...}}, got: {s[:60]}")
inner = extract_braces(rest)
return SequenceType(parse_fields(inner))
if s.startswith("INTEGER"):
rest = s[len("INTEGER") :].strip()
if rest.startswith("(") and rest.endswith(")"):
c = rest[1:-1].strip()
if ".." in c or c == "MAX" or c.endswith("MAX"):
return PrimitiveType("integer", c)
# single value constraint: fixed integer
if re.fullmatch(r"\d+", c):
return PrimitiveType("fixed_integer", c)
return PrimitiveType("integer", c)
return PrimitiveType("integer")
if s.startswith("OCTET STRING"):
rest = s[len("OCTET STRING") :].strip()
c = None
if rest.startswith("(") and ")" in rest:
c = rest[rest.find("(") + 1 : rest.rfind(")")]
return PrimitiveType("octet_string", c)
if s == "BOOLEAN":
return PrimitiveType("boolean")
if re.fullmatch(r"[A-Z][A-Za-z0-9]*", s):
return NamedType(s)
raise ValueError(f"unsupported type expression: {s[:80]!r}")
def extract_braces(s: str) -> str:
"""s starts with '{'; return content inside matching braces."""
assert s[0] == "{"
depth = 0
for i, ch in enumerate(s):
if ch == "{":
depth += 1
elif ch == "}":
depth -= 1
if depth == 0:
return s[1:i]
raise ValueError("unbalanced braces")
def parse_fields(body: str) -> List[SequenceField]:
"""Parse 'name Type, name Type' with nested braces."""
fields: List[SequenceField] = []
parts = split_top_level(body, ",")
for part in parts:
part = part.strip()
if not part:
continue
# field name is first token (lowercase start in our export)
m = re.match(r"([A-Za-z][A-Za-z0-9]*)\s+(.+)$", part, re.DOTALL)
if not m:
raise ValueError(f"bad field: {part[:60]!r}")
fname, ftype = m.group(1), m.group(2).strip()
fields.append(SequenceField(fname, parse_type(ftype)))
return fields
def split_top_level(s: str, sep: str) -> List[str]:
parts: List[str] = []
depth = 0
start = 0
for i, ch in enumerate(s):
if ch == "{":
depth += 1
elif ch == "}":
depth -= 1
elif ch == sep and depth == 0:
parts.append(s[start:i])
start = i + 1
parts.append(s[start:])
return parts
# ---------------------------------------------------------------------------
# Java emission
# ---------------------------------------------------------------------------
HEADER = """\
/*
* Generated by bin/asn1_to_java from GajumaruChainObjects.asn.
* Do not edit by hand — regenerate from the ASN.1 schema.
*
* These types are abstract syntax (headers). Wire encoding is RLP
* via swiss.qpq.gajumaru.core.encoding.RLP / Asn1Rlp, not BER/DER.
*/
"""
def java_type_name(name: str) -> str:
return name
def field_to_java(
f: SequenceField,
owner: str,
nested: List[Tuple[str, SequenceType]],
) -> Tuple[str, str]:
"""Return (javaType, fieldName). May append nested type defs."""
jtype = asn_to_java(f.type, owner, f.name, nested)
return jtype, f.name
def asn_to_java(
t: AsnType,
owner: str,
field_name: str,
nested: List[Tuple[str, SequenceType]],
) -> str:
if isinstance(t, NamedType):
# Built-in aliases
if t.name == "BigInt":
return "java.math.BigInteger"
if t.name in ("Uint8", "Uint16"):
return "int"
if t.name == "Uint32":
return "long"
if t.name in ("Uint64", "Uint128"):
return "java.math.BigInteger"
return t.name
if isinstance(t, PrimitiveType):
if t.kind == "boolean":
return "boolean"
if t.kind == "octet_string":
return "byte[]"
if t.kind == "fixed_integer":
return "int"
if t.kind == "integer":
# constrained small ranges map to int/long when obvious
if t.constraint in ("0..255", "0..65535"):
return "int"
if t.constraint == "0..4294967295":
return "long"
return "java.math.BigInteger"
raise ValueError(t)
if isinstance(t, SequenceOfType):
elem = asn_to_java(t.elem, owner, field_name + "Elem", nested)
return f"java.util.List<{box(elem)}>"
if isinstance(t, SequenceType):
nested_name = f"{owner}_{capitalize(field_name)}"
nested.append((nested_name, t))
# also need to resolve nested field types recursively for emission
return nested_name
raise ValueError(f"unknown type {t}")
def box(jtype: str) -> str:
return {
"int": "Integer",
"long": "Long",
"boolean": "Boolean",
"byte[]": "byte[]", # List<byte[]> is awkward but ok for now
}.get(jtype, jtype)
def capitalize(s: str) -> str:
return s[:1].upper() + s[1:] if s else s
def emit_sequence_record(
name: str,
seq: SequenceType,
package: str,
all_nested: List[Tuple[str, SequenceType]],
) -> str:
nested: List[Tuple[str, SequenceType]] = []
components = []
constants = []
for f in seq.fields:
jtype, jname = field_to_java(f, name, nested)
components.append(f" {jtype} {jname}")
if isinstance(f.type, PrimitiveType) and f.type.kind == "fixed_integer":
if f.name == "tag":
constants.append(f" public static final int TAG = {f.type.constraint};")
elif f.name == "vsn":
constants.append(f" public static final int VSN = {f.type.constraint};")
all_nested.extend(nested)
body = ",\n".join(components)
const_block = ("\n" + "\n".join(constants) + "\n") if constants else ""
return (
f"package {package};\n\n"
f"{HEADER}"
f"public record {name}(\n{body}\n) {{\n"
f"{const_block}"
f"}}\n"
)
def emit_alias(name: str, t: AsnType, package: str) -> Optional[str]:
"""Emit a tiny holder for INTEGER aliases we don't map away."""
# We map BigInt/Uint* at use sites; still emit Id as a record.
if name in ("BigInt", "Uint8", "Uint16", "Uint32", "Uint64", "Uint128"):
return None
if isinstance(t, SequenceType):
return None # handled elsewhere
if isinstance(t, PrimitiveType) and t.kind == "integer":
# skip pure aliases
return None
return None
def collect_dependencies(name: str, defs: dict[str, TypeDef], acc: set[str]) -> None:
if name in acc or name not in defs:
return
acc.add(name)
t = defs[name].type
walk_deps(t, defs, acc)
def walk_deps(t: AsnType, defs: dict[str, TypeDef], acc: set[str]) -> None:
if isinstance(t, NamedType):
if t.name in defs:
collect_dependencies(t.name, defs, acc)
elif isinstance(t, SequenceOfType):
walk_deps(t.elem, defs, acc)
elif isinstance(t, SequenceType):
for f in t.fields:
walk_deps(f.type, defs, acc)
def generate(
defs: dict[str, TypeDef],
wanted: List[str],
package: str,
out_dir: Path,
) -> List[Path]:
# Always include named deps of wanted types
selected: set[str] = set()
for w in wanted:
if w not in defs:
raise SystemExit(f"Unknown type {w}. Available: {', '.join(sorted(defs))}")
collect_dependencies(w, defs, selected)
out_dir.mkdir(parents=True, exist_ok=True)
written: List[Path] = []
# Emit in dependency-friendly order: common first
order = sorted(selected, key=lambda n: (0 if n == "Id" else 1, n))
for name in order:
tdef = defs[name]
t = tdef.type
if isinstance(t, SequenceType):
nested_acc: List[Tuple[str, SequenceType]] = []
src = emit_sequence_record(name, t, package, nested_acc)
path = out_dir / f"{name}.java"
path.write_text(src)
written.append(path)
# nested anonymous sequences as separate top-level records
for nname, nseq in nested_acc:
more: List[Tuple[str, SequenceType]] = []
nsrc = emit_sequence_record(nname, nseq, package, more)
npath = out_dir / f"{nname}.java"
npath.write_text(nsrc)
written.append(npath)
# flatten one level of nesting iteratively
queue = list(more)
while queue:
qn, qs = queue.pop(0)
more2: List[Tuple[str, SequenceType]] = []
qsrc = emit_sequence_record(qn, qs, package, more2)
qpath = out_dir / f"{qn}.java"
qpath.write_text(qsrc)
written.append(qpath)
queue.extend(more2)
else:
alias = emit_alias(name, t, package)
if alias:
path = out_dir / f"{name}.java"
path.write_text(alias)
written.append(path)
return written
def main(argv: List[str]) -> int:
ap = argparse.ArgumentParser(description=__doc__)
ap.add_argument(
"--asn",
type=Path,
required=True,
help="Path to GajumaruChainObjects.asn",
)
ap.add_argument(
"--package",
default="swiss.qpq.gajumaru.core.asn1",
help="Java package for generated sources",
)
ap.add_argument(
"--out",
type=Path,
default=None,
help="Output directory for .java files "
"(default: src/main/java/<package path> under gajumaru-core)",
)
ap.add_argument(
"--types",
default="Id,SignedTxV1,SpendTxV1",
help="Comma-separated type names to generate (plus dependencies)",
)
ap.add_argument(
"--all",
action="store_true",
help="Generate all SEQUENCE types in the module",
)
args = ap.parse_args(argv)
asn_path = args.asn.resolve()
if not asn_path.is_file():
print(f"ASN.1 file not found: {asn_path}", file=sys.stderr)
return 1
text = asn_path.read_text()
defs = parse_module(text)
if not defs:
print("No type definitions parsed", file=sys.stderr)
return 1
if args.all:
wanted = [n for n, d in defs.items() if isinstance(d.type, SequenceType)]
else:
wanted = [t.strip() for t in args.types.split(",") if t.strip()]
if args.out is None:
script_dir = Path(__file__).resolve().parent
project_dir = script_dir.parent
pkg_path = args.package.replace(".", "/")
out_dir = project_dir / "src" / "main" / "java" / pkg_path
else:
out_dir = args.out
written = generate(defs, wanted, args.package, out_dir)
print(f"Parsed {len(defs)} ASN.1 types from {asn_path}")
print(f"Wrote {len(written)} Java file(s) to {out_dir}:")
for p in written:
print(f" {p.name}")
return 0
if __name__ == "__main__":
sys.exit(main(sys.argv[1:]))
-39
View File
@@ -1,39 +0,0 @@
#! /usr/bin/env bash
# Compile main + Asn1Rlp equivalence tests and run them.
# Prefers Homebrew OpenJDK when /usr/bin/java is a macOS stub.
set -euo pipefail
abs_dir="$(cd -P "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
project_dir="$(dirname "$abs_dir")"
if [[ -x /opt/homebrew/opt/openjdk/bin/javac ]]; then
export PATH="/opt/homebrew/opt/openjdk/bin:$PATH"
elif [[ -x /opt/homebrew/opt/openjdk@25/bin/javac ]]; then
export PATH="/opt/homebrew/opt/openjdk@25/bin:$PATH"
elif [[ -x /opt/homebrew/opt/openjdk@21/bin/javac ]]; then
export PATH="/opt/homebrew/opt/openjdk@21/bin:$PATH"
fi
if ! command -v javac >/dev/null || ! javac -version >/dev/null 2>&1; then
echo "No working JDK found (javac)." >&2
exit 1
fi
classes="$project_dir/build/classes"
test_classes="$project_dir/build/test-classes"
mkdir -p "$classes" "$test_classes"
echo "Using $(javac -version 2>&1)"
# Portable source collection (bash 3.2 has no mapfile)
main_sources=$(find "$project_dir/src/main/java" -name '*.java' | sort)
# shellcheck disable=SC2086
javac --release 21 -d "$classes" $main_sources
test_sources=$(find "$project_dir/src/test/java" -name '*Asn1Rlp*.java' | sort)
# shellcheck disable=SC2086
javac --release 21 -cp "$classes" -d "$test_classes" $test_sources
echo "Running Asn1RlpEquivalenceTest..."
java -cp "$classes:$test_classes" swiss.qpq.gajumaru.core.serialization.Asn1RlpEquivalenceTest
-69
View File
@@ -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,15 +0,0 @@
package swiss.qpq.gajumaru.core.asn1;
/*
* Generated by bin/asn1_to_java from GajumaruChainObjects.asn.
* Do not edit by hand — regenerate from the ASN.1 schema.
*
* These types are abstract syntax (headers). Wire encoding is RLP
* via swiss.qpq.gajumaru.core.encoding.RLP / Asn1Rlp, not BER/DER.
*/
public record Id(
int type,
byte[] value
) {
}
@@ -1,20 +0,0 @@
package swiss.qpq.gajumaru.core.asn1;
/*
* Generated by bin/asn1_to_java from GajumaruChainObjects.asn.
* Do not edit by hand — regenerate from the ASN.1 schema.
*
* These types are abstract syntax (headers). Wire encoding is RLP
* via swiss.qpq.gajumaru.core.encoding.RLP / Asn1Rlp, not BER/DER.
*/
public record SignedTxV1(
int tag,
int vsn,
java.util.List<byte[]> signatures,
byte[] transaction
) {
public static final int TAG = 11;
public static final int VSN = 1;
}
@@ -1,26 +0,0 @@
package swiss.qpq.gajumaru.core.asn1;
/*
* Generated by bin/asn1_to_java from GajumaruChainObjects.asn.
* Do not edit by hand — regenerate from the ASN.1 schema.
*
* These types are abstract syntax (headers). Wire encoding is RLP
* via swiss.qpq.gajumaru.core.encoding.RLP / Asn1Rlp, not BER/DER.
*/
public record SpendTxV1(
int tag,
int vsn,
Id senderId,
Id recipientId,
java.math.BigInteger amount,
java.math.BigInteger gasPrice,
java.math.BigInteger gas,
java.math.BigInteger ttl,
java.math.BigInteger nonce,
byte[] payload
) {
public static final int TAG = 12;
public static final int VSN = 1;
}
@@ -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,213 +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.
*
* SPDX-License-Identifier: AGPL-3.0-or-later OR LicenseRef-QPQ-Commercial
*/
package swiss.qpq.gajumaru.core.serialization;
import swiss.qpq.gajumaru.core.asn1.Id;
import swiss.qpq.gajumaru.core.asn1.SignedTxV1;
import swiss.qpq.gajumaru.core.asn1.SpendTxV1;
import swiss.qpq.gajumaru.core.encoding.RLP;
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;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.List;
/**
* Thin RLP production / consumption for ASN.1-shaped chain objects.
*
* <p>Mirrors {@code gmser_asn1_rlp} / {@code gmserialization}: walk typed
* values (generated from the ASN.1 schema) and emit legacy RLP lists of the
* form {@code [tag, vsn | fields…]}. Field names never appear on the wire.
*
* <p>Note on {@link Id}: the ASN.1 model is a SEQUENCE of type+value, but the
* RLP wire form is the 33-byte {@code gmser_id} encoding (a single RLP string).
*
* <p>Scope for now: {@link SpendTxV1}, {@link SignedTxV1}. More types can be
* added as generated records appear.
*/
public final class Asn1Rlp {
private Asn1Rlp() {}
// ------------------------------------------------------------------
// Encode
// ------------------------------------------------------------------
public static byte[] encode(SpendTxV1 tx) {
return RLP.encode(toRlp(tx));
}
public static byte[] encode(SignedTxV1 tx) {
return RLP.encode(toRlp(tx));
}
public static RLP_Data toRlp(SpendTxV1 tx) {
require(tx, "SpendTxV1");
if (tx.tag() != SpendTxV1.TAG || tx.vsn() != SpendTxV1.VSN) {
throw new IllegalArgumentException(
"SpendTxV1 tag/vsn mismatch: got " + tx.tag() + "/" + tx.vsn()
+ ", expected " + SpendTxV1.TAG + "/" + SpendTxV1.VSN);
}
List<RLP_Data> items = new ArrayList<>(10);
items.add(item(BasicEncoders.encodeUint(tx.tag())));
items.add(item(BasicEncoders.encodeUint(tx.vsn())));
items.add(item(BasicEncoders.encodeId(tx.senderId())));
items.add(item(BasicEncoders.encodeId(tx.recipientId())));
items.add(item(BasicEncoders.encodeUint(tx.amount())));
items.add(item(BasicEncoders.encodeUint(tx.gasPrice())));
items.add(item(BasicEncoders.encodeUint(tx.gas())));
items.add(item(BasicEncoders.encodeUint(tx.ttl())));
items.add(item(BasicEncoders.encodeUint(tx.nonce())));
items.add(item(nullToEmpty(tx.payload())));
return new RLP_List(items);
}
public static RLP_Data toRlp(SignedTxV1 tx) {
require(tx, "SignedTxV1");
if (tx.tag() != SignedTxV1.TAG || tx.vsn() != SignedTxV1.VSN) {
throw new IllegalArgumentException(
"SignedTxV1 tag/vsn mismatch: got " + tx.tag() + "/" + tx.vsn()
+ ", expected " + SignedTxV1.TAG + "/" + SignedTxV1.VSN);
}
List<RLP_Data> sigItems = new ArrayList<>();
if (tx.signatures() != null) {
for (byte[] sig : tx.signatures()) {
sigItems.add(item(nullToEmpty(sig)));
}
}
List<RLP_Data> items = new ArrayList<>(4);
items.add(item(BasicEncoders.encodeUint(tx.tag())));
items.add(item(BasicEncoders.encodeUint(tx.vsn())));
items.add(new RLP_List(sigItems));
items.add(item(nullToEmpty(tx.transaction())));
return new RLP_List(items);
}
// ------------------------------------------------------------------
// Decode
// ------------------------------------------------------------------
public static SpendTxV1 decodeSpendTxV1(byte[] wire) {
return fromRlpSpend(RLP.decode(wire));
}
public static SignedTxV1 decodeSignedTxV1(byte[] wire) {
return fromRlpSigned(RLP.decode(wire));
}
public static SpendTxV1 fromRlpSpend(RLP_Data data) {
List<RLP_Data> items = expectList(data, 10, "SpendTxV1");
int tag = decodeIntField(items.get(0), "tag");
int vsn = decodeIntField(items.get(1), "vsn");
if (tag != SpendTxV1.TAG || vsn != SpendTxV1.VSN) {
throw new IllegalArgumentException(
"not a SpendTxV1: tag=" + tag + " vsn=" + vsn);
}
Id sender = BasicEncoders.decodeId(expectItem(items.get(2), "senderId"));
Id recipient = BasicEncoders.decodeId(expectItem(items.get(3), "recipientId"));
BigInteger amount = BasicEncoders.decodeUint(expectItem(items.get(4), "amount"));
BigInteger gasPrice = BasicEncoders.decodeUint(expectItem(items.get(5), "gasPrice"));
BigInteger gas = BasicEncoders.decodeUint(expectItem(items.get(6), "gas"));
BigInteger ttl = BasicEncoders.decodeUint(expectItem(items.get(7), "ttl"));
BigInteger nonce = BasicEncoders.decodeUint(expectItem(items.get(8), "nonce"));
byte[] payload = expectItem(items.get(9), "payload");
return new SpendTxV1(
tag, vsn, sender, recipient,
amount, gasPrice, gas, ttl, nonce, payload);
}
public static SignedTxV1 fromRlpSigned(RLP_Data data) {
List<RLP_Data> items = expectList(data, 4, "SignedTxV1");
int tag = decodeIntField(items.get(0), "tag");
int vsn = decodeIntField(items.get(1), "vsn");
if (tag != SignedTxV1.TAG || vsn != SignedTxV1.VSN) {
throw new IllegalArgumentException(
"not a SignedTxV1: tag=" + tag + " vsn=" + vsn);
}
List<byte[]> signatures = new ArrayList<>();
for (RLP_Data el : expectList(items.get(2), -1, "signatures")) {
signatures.add(expectItem(el, "signature"));
}
byte[] transaction = expectItem(items.get(3), "transaction");
return new SignedTxV1(tag, vsn, signatures, transaction);
}
/**
* Peek tag/vsn from a top-level RLP list without fully decoding.
*
* @return {@code int[]{tag, vsn}}
*/
public static int[] peekTagVsn(byte[] wire) {
RLP_Data data = RLP.decode(wire);
List<RLP_Data> items = expectList(data, -1, "chain object");
if (items.size() < 2) {
throw new IllegalArgumentException("RLP list too short for tag/vsn");
}
return new int[] {
decodeIntField(items.get(0), "tag"),
decodeIntField(items.get(1), "vsn")
};
}
// ------------------------------------------------------------------
// Helpers
// ------------------------------------------------------------------
private static RLP_Item item(byte[] bytes) {
return new RLP_Item(bytes);
}
private static byte[] nullToEmpty(byte[] b) {
return b != null ? b : new byte[0];
}
private static void require(Object o, String name) {
if (o == null) {
throw new IllegalArgumentException(name + " is null");
}
}
private static List<RLP_Data> expectList(RLP_Data data, int expectedSize, String what) {
if (!(data instanceof RLP_List list)) {
throw new IllegalArgumentException(what + ": expected RLP list");
}
List<RLP_Data> items = list.items;
if (expectedSize >= 0 && items.size() != expectedSize) {
throw new IllegalArgumentException(
what + ": expected " + expectedSize + " fields, got " + items.size());
}
return items;
}
private static byte[] expectItem(RLP_Data data, String field) {
if (!(data instanceof RLP_Item item)) {
throw new IllegalArgumentException(field + ": expected RLP item");
}
return item.bytes != null ? item.bytes : new byte[0];
}
private static int decodeIntField(RLP_Data data, String field) {
BigInteger n = BasicEncoders.decodeUint(expectItem(data, field));
if (n.bitLength() > 31) {
throw new IllegalArgumentException(field + " does not fit in int: " + n);
}
return n.intValue();
}
}
@@ -1,130 +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.
*
* SPDX-License-Identifier: AGPL-3.0-or-later OR LicenseRef-QPQ-Commercial
*/
package swiss.qpq.gajumaru.core.serialization;
import swiss.qpq.gajumaru.core.asn1.Id;
import java.math.BigInteger;
import java.util.Arrays;
/**
* Primitive field encoders matching {@code gmserialization:encode_field/2}
* and {@code gmser_id:encode/1}.
*
* <p>These produce bare payloads (not RLP-framed). {@link Asn1Rlp} wraps them
* in RLP items / lists.
*/
public final class BasicEncoders {
private BasicEncoders() {}
/**
* Minimal big-endian unsigned encoding, matching
* {@code binary:encode_unsigned/1}. Zero is {@code <<0>>}.
*/
public static byte[] encodeUint(BigInteger n) {
if (n == null || n.signum() < 0) {
throw new IllegalArgumentException("expected non-negative integer");
}
if (n.signum() == 0) {
return new byte[] { 0 };
}
byte[] raw = n.toByteArray(); // may include a leading 0 sign byte
if (raw[0] == 0) {
return Arrays.copyOfRange(raw, 1, raw.length);
}
return raw;
}
public static byte[] encodeUint(long n) {
if (n < 0) {
throw new IllegalArgumentException("expected non-negative integer");
}
return encodeUint(BigInteger.valueOf(n));
}
public static byte[] encodeUint(int n) {
return encodeUint((long) n);
}
/**
* Inverse of {@link #encodeUint(BigInteger)}.
*/
public static BigInteger decodeUint(byte[] bytes) {
if (bytes == null || bytes.length == 0) {
throw new IllegalArgumentException("empty integer encoding");
}
// Reject non-minimal encodings with a leading zero (except for 0 itself).
if (bytes.length > 1 && bytes[0] == 0) {
throw new IllegalArgumentException("non-minimal integer encoding");
}
return new BigInteger(1, bytes);
}
/**
* 33-byte id wire form: {@code <<Type:8, Value:32/binary>>}, matching
* {@code gmser_id:encode/1} for simple tags (and extended account when
* {@code type >= 0x80}).
*/
public static byte[] encodeId(Id id) {
if (id == null) {
throw new IllegalArgumentException("id is null");
}
byte[] value = id.value();
if (value == null || value.length != 32) {
throw new IllegalArgumentException(
"id value must be 32 bytes, got "
+ (value == null ? "null" : value.length));
}
int type = id.type();
if (type < 0 || type > 255) {
throw new IllegalArgumentException("id type out of byte range: " + type);
}
byte[] out = new byte[33];
out[0] = (byte) type;
System.arraycopy(value, 0, out, 1, 32);
return out;
}
public static Id decodeId(byte[] bytes) {
if (bytes == null || bytes.length != 33) {
throw new IllegalArgumentException(
"id encoding must be 33 bytes, got "
+ (bytes == null ? "null" : bytes.length));
}
int type = bytes[0] & 0xFF;
byte[] value = Arrays.copyOfRange(bytes, 1, 33);
return new Id(type, value);
}
public static byte[] encodeBool(boolean v) {
return new byte[] { (byte) (v ? 1 : 0) };
}
public static boolean decodeBool(byte[] bytes) {
if (bytes == null || bytes.length != 1) {
throw new IllegalArgumentException("bool encoding must be 1 byte");
}
return switch (bytes[0] & 0xFF) {
case 0 -> false;
case 1 -> true;
default -> throw new IllegalArgumentException(
"illegal bool encoding: " + (bytes[0] & 0xFF));
};
}
}
@@ -1,221 +0,0 @@
/*
* Copyright (c) 2026 QPQ AG <info@qpq.swiss>. All rights reserved.
* Project: Gajumaru Core Java Libraries <gajumaru.io>
*
* SPDX-License-Identifier: AGPL-3.0-or-later OR LicenseRef-QPQ-Commercial
*/
package swiss.qpq.gajumaru.core.serialization;
import swiss.qpq.gajumaru.core.asn1.Id;
import swiss.qpq.gajumaru.core.asn1.SignedTxV1;
import swiss.qpq.gajumaru.core.asn1.SpendTxV1;
import java.math.BigInteger;
import java.util.Arrays;
import java.util.HexFormat;
import java.util.List;
/**
* Golden-vector equivalence tests against {@code gmser_chain_objects:serialize/4}
* (Erlang). Run via {@code bin/test-asn1-rlp}.
*
* <p>Vectors generated with:
* <pre>
* Sender = gmser_id:create(account, &lt;&lt;1:256&gt;&gt;),
* Recip = gmser_id:create(account, &lt;&lt;2:256&gt;&gt;),
* gmser_chain_objects:serialize(spend_tx, 1, Template, Fields)
* </pre>
*/
public final class Asn1RlpEquivalenceTest {
private static final HexFormat HEX = HexFormat.of().withUpperCase();
// <<1:256>> and <<2:256>> as 32-byte big-endian
private static final byte[] PUB1 = hex(
"0000000000000000000000000000000000000000000000000000000000000001");
private static final byte[] PUB2 = hex(
"0000000000000000000000000000000000000000000000000000000000000002");
/** account tag = 1 */
private static final Id SENDER = new Id(1, PUB1);
private static final Id RECIPIENT = new Id(1, PUB2);
private static final String SPEND_HEX =
"F8500C01A1010000000000000000000000000000000000000000000000000000000000000001"
+ "A1010000000000000000000000000000000000000000000000000000000000000002"
+ "6401824E200001826869";
private static final String SPEND0_HEX =
"F84C0C01A1010000000000000000000000000000000000000000000000000000000000000001"
+ "A1010000000000000000000000000000000000000000000000000000000000000002"
+ "000000000080";
private static final String SIGNED_HEX =
"F8610B01CA84736967418473696742B852"
+ "F8500C01A1010000000000000000000000000000000000000000000000000000000000000001"
+ "A1010000000000000000000000000000000000000000000000000000000000000002"
+ "6401824E200001826869";
private static final String SENDER_ID_HEX =
"010000000000000000000000000000000000000000000000000000000000000001";
private int failures = 0;
public static void main(String[] args) {
Asn1RlpEquivalenceTest t = new Asn1RlpEquivalenceTest();
t.testEncodeId();
t.testSpendMatchesErlang();
t.testSpendZeroEmptyMatchesErlang();
t.testSpendRoundTrip();
t.testSignedMatchesErlang();
t.testSignedRoundTrip();
t.testPeekTagVsn();
if (t.failures > 0) {
System.err.println(t.failures + " failure(s)");
System.exit(1);
}
System.out.println("All Asn1Rlp equivalence tests passed.");
}
void testEncodeId() {
byte[] enc = BasicEncoders.encodeId(SENDER);
assertHex("encodeId(account, <<1:256>>)", SENDER_ID_HEX, enc);
Id back = BasicEncoders.decodeId(enc);
assertEq("id type", 1, back.type());
assertBytes("id value", PUB1, back.value());
}
void testSpendMatchesErlang() {
SpendTxV1 tx = sampleSpend();
byte[] wire = Asn1Rlp.encode(tx);
assertHex("SpendTxV1 encode vs Erlang", SPEND_HEX, wire);
}
void testSpendZeroEmptyMatchesErlang() {
SpendTxV1 tx = new SpendTxV1(
SpendTxV1.TAG, SpendTxV1.VSN,
SENDER, RECIPIENT,
BigInteger.ZERO, BigInteger.ZERO, BigInteger.ZERO,
BigInteger.ZERO, BigInteger.ZERO,
new byte[0]);
byte[] wire = Asn1Rlp.encode(tx);
assertHex("SpendTxV1 zero/empty vs Erlang", SPEND0_HEX, wire);
}
void testSpendRoundTrip() {
SpendTxV1 tx = sampleSpend();
SpendTxV1 back = Asn1Rlp.decodeSpendTxV1(Asn1Rlp.encode(tx));
assertEq("roundtrip tag", tx.tag(), back.tag());
assertEq("roundtrip vsn", tx.vsn(), back.vsn());
assertEq("roundtrip sender type", tx.senderId().type(), back.senderId().type());
assertBytes("roundtrip sender val", tx.senderId().value(), back.senderId().value());
assertEq("roundtrip amount", tx.amount(), back.amount());
assertEq("roundtrip gasPrice", tx.gasPrice(), back.gasPrice());
assertEq("roundtrip gas", tx.gas(), back.gas());
assertEq("roundtrip ttl", tx.ttl(), back.ttl());
assertEq("roundtrip nonce", tx.nonce(), back.nonce());
assertBytes("roundtrip payload", tx.payload(), back.payload());
}
void testSignedMatchesErlang() {
byte[] inner = Asn1Rlp.encode(sampleSpend());
SignedTxV1 signed = new SignedTxV1(
SignedTxV1.TAG, SignedTxV1.VSN,
List.of(bytes("sigA"), bytes("sigB")),
inner);
byte[] wire = Asn1Rlp.encode(signed);
assertHex("SignedTxV1 encode vs Erlang", SIGNED_HEX, wire);
}
void testSignedRoundTrip() {
byte[] inner = Asn1Rlp.encode(sampleSpend());
SignedTxV1 signed = new SignedTxV1(
SignedTxV1.TAG, SignedTxV1.VSN,
List.of(bytes("sigA"), bytes("sigB")),
inner);
SignedTxV1 back = Asn1Rlp.decodeSignedTxV1(Asn1Rlp.encode(signed));
assertEq("signed tag", signed.tag(), back.tag());
assertEq("signed sigs size", signed.signatures().size(), back.signatures().size());
assertBytes("signed sig0", signed.signatures().get(0), back.signatures().get(0));
assertBytes("signed sig1", signed.signatures().get(1), back.signatures().get(1));
assertBytes("signed tx", signed.transaction(), back.transaction());
// nested spend still decodes
SpendTxV1 spend = Asn1Rlp.decodeSpendTxV1(back.transaction());
assertEq("nested amount", sampleSpend().amount(), spend.amount());
}
void testPeekTagVsn() {
int[] tv = Asn1Rlp.peekTagVsn(hex(SPEND_HEX));
assertEq("peek tag", SpendTxV1.TAG, tv[0]);
assertEq("peek vsn", SpendTxV1.VSN, tv[1]);
int[] tv2 = Asn1Rlp.peekTagVsn(hex(SIGNED_HEX));
assertEq("peek signed tag", SignedTxV1.TAG, tv2[0]);
assertEq("peek signed vsn", SignedTxV1.VSN, tv2[1]);
}
private static SpendTxV1 sampleSpend() {
return new SpendTxV1(
SpendTxV1.TAG, SpendTxV1.VSN,
SENDER, RECIPIENT,
BigInteger.valueOf(100),
BigInteger.valueOf(1),
BigInteger.valueOf(20_000),
BigInteger.ZERO,
BigInteger.ONE,
bytes("hi"));
}
// --- tiny assert helpers ---
private void assertHex(String label, String expectedHex, byte[] actual) {
String got = HEX.formatHex(actual);
if (!expectedHex.equalsIgnoreCase(got)) {
fail(label + "\n expected: " + expectedHex + "\n actual: " + got);
} else {
ok(label);
}
}
private void assertBytes(String label, byte[] expected, byte[] actual) {
if (!Arrays.equals(expected, actual)) {
fail(label + "\n expected: " + HEX.formatHex(expected)
+ "\n actual: " + HEX.formatHex(actual));
} else {
ok(label);
}
}
private void assertEq(String label, Object expected, Object actual) {
if (expected == null ? actual != null : !expected.equals(actual)) {
fail(label + ": expected " + expected + ", got " + actual);
} else {
ok(label);
}
}
private void assertEq(String label, int expected, int actual) {
if (expected != actual) {
fail(label + ": expected " + expected + ", got " + actual);
} else {
ok(label);
}
}
private void ok(String label) {
System.out.println(" ok " + label);
}
private void fail(String msg) {
failures++;
System.err.println("FAIL " + msg);
}
private static byte[] hex(String h) {
return HexFormat.of().parseHex(h);
}
private static byte[] bytes(String s) {
return s.getBytes(java.nio.charset.StandardCharsets.UTF_8);
}
}
-15
View File
@@ -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`.
-135
View File
@@ -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,872 @@
/*
* 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).
// 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};
public static final class Ge {
public final long[] X = new long[10], Y = new long[10], Z = new long[10], T = new long[10];
public void wipe() {
CryptoUtils.wipe(X);
CryptoUtils.wipe(Y);
CryptoUtils.wipe(Z);
CryptoUtils.wipe(T);
}
}
public static final class Scratch {
public final long[]
a = new long[10], b = new long[10], c = new long[10], d = new long[10],
e = new long[10], f = new long[10], g = new long[10], h = new long[10],
tmp = new long[10], t19 = new long[19];
public final long[][] stack = new long[10][10];
public final Ge geTmp = new Ge();
public void wipe() {
CryptoUtils.wipe(a);
CryptoUtils.wipe(b);
CryptoUtils.wipe(c);
CryptoUtils.wipe(d);
CryptoUtils.wipe(e);
CryptoUtils.wipe(f);
CryptoUtils.wipe(g);
CryptoUtils.wipe(h);
CryptoUtils.wipe(tmp);
CryptoUtils.wipe(t19);
for (long[] s : stack) CryptoUtils.wipe(s);
geTmp.wipe();
}
}
private Ed25519() {
}
public static byte[] publicKey(byte[] seed) {
byte[] hash = sha512(seed);
byte[] s = Arrays.copyOfRange(hash, 0, 32);
s[0] &= 248;
s[31] &= 127;
s[31] |= 64;
Scratch sc = new Scratch();
Ge A_point = scalarMulBase(s, sc);
byte[] A = compress(A_point, sc);
A_point.wipe();
sc.wipe();
CryptoUtils.wipe(hash);
CryptoUtils.wipe(s);
return A;
}
public static byte[] sign(byte[] seed, byte[] message) {
byte[] hash = sha512(seed);
byte[] s = Arrays.copyOfRange(hash, 0, 32);
s[0] &= 248;
s[31] &= 127;
s[31] |= 64;
byte[] prefix = Arrays.copyOfRange(hash, 32, 64);
byte[] rHash = sha512(prefix, message);
reduceScalar(rHash);
byte[] r = Arrays.copyOfRange(rHash, 0, 32);
Scratch sc = new Scratch();
Ge R_point = scalarMulBase(r, sc);
byte[] R = compress(R_point, sc);
byte[] A = publicKey(seed);
byte[] kHash = sha512(R, A, message);
reduceScalar(kHash);
byte[] k = Arrays.copyOfRange(kHash, 0, 32);
byte[] S = new byte[32];
scalarMulAdd(S, k, s, r);
byte[] sig = new byte[64];
System.arraycopy(R, 0, sig, 0, 32);
System.arraycopy(S, 0, sig, 32, 32);
R_point.wipe();
sc.wipe();
CryptoUtils.wipe(hash);
CryptoUtils.wipe(s);
CryptoUtils.wipe(prefix);
CryptoUtils.wipe(rHash);
CryptoUtils.wipe(r);
CryptoUtils.wipe(kHash);
CryptoUtils.wipe(k);
CryptoUtils.wipe(A);
return sig;
}
public static boolean verify(byte[] publicKey, byte[] message, byte[] signature) {
if (signature.length != 64) return false;
byte[] R_bytes = Arrays.copyOfRange(signature, 0, 32);
byte[] S_bytes = Arrays.copyOfRange(signature, 32, 64);
if ((S_bytes[31] & 0xe0) != 0) return false;
Scratch sc = new Scratch();
Ge A = decompress(publicKey, sc);
if (A == null) return false;
Ge R = decompress(R_bytes, sc);
if (R == null) return false;
byte[] kHash = sha512(R_bytes, publicKey, message);
reduceScalar(kHash);
byte[] k = Arrays.copyOfRange(kHash, 0, 32);
Ge sB = scalarMulBase(S_bytes, sc);
Ge kA = scalarMul(A, k, sc);
Ge RHS = new Ge();
ge_add(RHS, R, kA, sc);
byte[] LHS_bytes = compress(sB, sc);
byte[] RHS_bytes = compress(RHS, sc);
boolean ok = Arrays.equals(LHS_bytes, RHS_bytes);
A.wipe();
R.wipe();
sB.wipe();
kA.wipe();
RHS.wipe();
sc.wipe();
CryptoUtils.wipe(kHash);
CryptoUtils.wipe(k);
return ok;
}
public static Ge scalarMulBase(byte[] scalar, Scratch s) {
Ge res = new Ge();
fe_0(res.X);
fe_1(res.Y);
fe_1(res.Z);
fe_0(res.T);
Ge q = new Ge();
fe_copy(q.X, BX);
fe_copy(q.Y, BY);
fe_1(q.Z);
fe_mul(q.T, BX, BY, s.t19);
for (int i = 0; i < 256; i++) {
int bit = ((scalar[i / 8] & 0xFF) >>> (i % 8)) & 1;
ge_add(s.geTmp, res, q, s);
ge_cmov(res, s.geTmp, bit);
ge_double(q, q, s);
}
q.wipe();
return res;
}
public static void ge_double_scalarmul_vartime(Ge r, byte[] s, Ge a, byte[] k, Scratch sc) {
Ge sB = scalarMulBase(s, sc);
Ge kA = scalarMul(a, k, sc);
fe_neg(kA.X, kA.X);
fe_neg(kA.T, kA.T);
ge_add(r, sB, kA, sc);
sB.wipe();
kA.wipe();
}
public static Ge scalarMul(Ge p, byte[] scalar, Scratch s) {
Ge res = new Ge();
fe_0(res.X);
fe_1(res.Y);
fe_1(res.Z);
fe_0(res.T);
Ge q = new Ge();
fe_copy(q.X, p.X);
fe_copy(q.Y, p.Y);
fe_copy(q.Z, p.Z);
fe_copy(q.T, p.T);
for (int i = 0; i < 256; i++) {
int bit = ((scalar[i / 8] & 0xFF) >>> (i % 8)) & 1;
ge_add(s.geTmp, res, q, s);
ge_cmov(res, s.geTmp, bit);
ge_double(q, q, s);
}
q.wipe();
return res;
}
public static void ge_add(Ge r, Ge p1, Ge p2, Scratch s) {
long[] yMinusX1 = s.stack[0], yPlusX1 = s.stack[1], yMinusX2 = s.stack[2], yPlusX2 = s.stack[3];
long[] A = s.stack[4], B = s.stack[5], C = s.stack[6], D = s.stack[7];
long[] E = s.stack[8], F = s.stack[9], G = s.a, H = s.b;
fe_sub(yMinusX1, p1.Y, p1.X);
fe_add(yPlusX1, p1.Y, p1.X);
fe_sub(yMinusX2, p2.Y, p2.X);
fe_add(yPlusX2, p2.Y, p2.X);
fe_mul(A, yMinusX1, yMinusX2, s.t19);
fe_mul(B, yPlusX1, yPlusX2, s.t19);
fe_mul(C, p1.T, p2.T, s.t19);
fe_mul(C, C, D2, s.t19);
fe_mul(D, p1.Z, p2.Z, s.t19);
fe_add(D, D, D);
fe_sub(E, B, A);
fe_sub(F, D, C);
fe_add(G, D, C);
fe_add(H, B, A);
fe_mul(r.X, E, F, s.t19);
fe_mul(r.Y, G, H, s.t19);
fe_mul(r.Z, F, G, s.t19);
fe_mul(r.T, E, H, s.t19);
}
public static void ge_double(Ge r, Ge p, Scratch s) {
long[] A = s.stack[0], B = s.stack[1], C = s.stack[2], D = s.stack[3];
long[] E = s.stack[4], F = s.stack[5], G = s.stack[6], H = s.stack[7];
fe_sq(A, p.X, s.t19);
fe_sq(B, p.Y, s.t19);
fe_sq(C, p.Z, s.t19);
fe_add(C, C, C);
fe_neg(D, A);
fe_add(E, p.X, p.Y);
fe_sq(E, E, s.t19);
fe_sub(E, E, A);
fe_sub(E, E, B);
fe_add(G, D, B);
fe_sub(F, G, C);
fe_sub(H, D, B);
fe_mul(r.X, E, F, s.t19);
fe_mul(r.Y, G, H, s.t19);
fe_mul(r.Z, F, G, s.t19);
fe_mul(r.T, E, H, s.t19);
}
private static void ge_cmov(Ge r, Ge p, int b) {
fe_cmov(r.X, p.X, b);
fe_cmov(r.Y, p.Y, b);
fe_cmov(r.Z, p.Z, b);
fe_cmov(r.T, p.T, b);
}
private static void fe_cmov(long[] r, long[] p, int b) {
long mask = -(long) b;
for (int i = 0; i < 10; i++) {
long x = mask & (r[i] ^ p[i]);
r[i] ^= x;
}
}
public static Ge decompress(byte[] b, Scratch s) {
if (b.length != 32) return null;
Ge p = new Ge();
fe_frombytes(p.Y, b);
fe_1(p.Z);
// u = y^2 - 1
fe_sq(s.a, p.Y, s.t19);
fe_sub(s.a, s.a, p.Z);
// v = dy^2 + 1
fe_sq(s.b, p.Y, s.t19);
fe_mul(s.b, s.b, D, s.t19);
fe_add(s.b, s.b, p.Z);
// check root of u/v
fe_invert(s.c, s.b, s);
fe_mul(s.c, s.a, s.c, s.t19); // x2 = u/v
fe_pow22523(s.a, s.c, s); // a = x2^((p-5)/8)
fe_mul(s.a, s.c, s.a, s.t19); // x = x2 * a = x2^((p+3)/8)
fe_sq(s.b, s.a, s.t19);
fe_sub(s.b, s.b, s.c); // x^2 - x2
if (fe_isnonzero(s.b)) {
fe_mul(s.a, s.a, I, s.t19);
fe_sq(s.b, s.a, s.t19);
fe_sub(s.b, s.b, s.c);
if (fe_isnonzero(s.b)) return null;
}
if (fe_isnegative(s.a) != ((b[31] >> 7) & 1)) fe_neg(p.X, s.a);
else fe_copy(p.X, s.a);
fe_mul(p.T, p.X, p.Y, s.t19);
return p;
}
public static byte[] compress(Ge p, Scratch s) {
fe_invert(s.a, p.Z, s);
fe_mul(s.b, p.X, s.a, s.t19);
fe_mul(s.c, p.Y, s.a, s.t19);
byte[] out = fe_contract(s.c);
if (fe_isnegative(s.b) == 1) out[31] |= (byte) 0x80;
return out;
}
private static void fe_0(long[] h) {
for (int i = 0; i < 10; i++) h[i] = 0;
}
private static void fe_1(long[] h) {
h[0] = 1;
for (int i = 1; i < 10; i++) h[i] = 0;
}
private static void fe_copy(long[] h, long[] f) {
System.arraycopy(f, 0, h, 0, 10);
}
private static void fe_add(long[] h, long[] f, long[] g) {
for (int i = 0; i < 10; i++) h[i] = f[i] + g[i];
}
private static void fe_sub(long[] h, long[] f, long[] g) {
for (int i = 0; i < 10; i++) h[i] = f[i] - g[i];
}
private static void fe_neg(long[] h, long[] f) {
for (int i = 0; i < 10; i++) h[i] = -f[i];
}
public static void fe_mul(long[] out, long[] f, long[] g, long[] t) {
t[0] = f[0] * g[0];
t[1] = f[0] * g[1] + f[1] * g[0];
t[2] = f[0] * g[2] + f[2] * g[0] + 2 * f[1] * g[1];
t[3] = f[0] * g[3] + f[3] * g[0] + f[1] * g[2] + f[2] * g[1];
t[4] = f[0] * g[4] + f[4] * g[0] + f[2] * g[2] + 2 * f[1] * g[3] + 2 * f[3] * g[1];
t[5] = f[0] * g[5] + f[5] * g[0] + f[1] * g[4] + f[4] * g[1] + f[2] * g[3] + f[3] * g[2];
t[6] = f[0] * g[6] + f[6] * g[0] + f[2] * g[4] + f[4] * g[2] + 2 * f[1] * g[5] + 2 * f[5] * g[1] + 2 * f[3] * g[3];
t[7] = f[0] * g[7] + f[7] * g[0] + f[1] * g[6] + f[6] * g[1] + f[2] * g[5] + f[5] * g[2] + f[3] * g[4] + f[4] * g[3];
t[8] = f[0] * g[8] + f[8] * g[0] + f[2] * g[6] + f[6] * g[2] + f[4] * g[4] + 2 * f[1] * g[7] + 2 * f[7] * g[1] + 2 * f[3] * g[5] + 2 * f[5] * g[3];
t[9] = f[0] * g[9] + f[9] * g[0] + f[1] * g[8] + f[8] * g[1] + f[2] * g[7] + f[7] * g[2] + f[3] * g[6] + f[6] * g[3] + f[4] * g[5] + f[5] * g[4];
t[10] = 2 * f[1] * g[9] + 2 * f[9] * g[1] + f[2] * g[8] + f[8] * g[2] + 2 * f[3] * g[7] + 2 * f[7] * g[3] + f[4] * g[6] + f[6] * g[4] + 2 * f[5] * g[5];
t[11] = f[2] * g[9] + f[9] * g[2] + f[3] * g[8] + f[8] * g[3] + f[4] * g[7] + f[7] * g[4] + f[5] * g[6] + f[6] * g[5];
t[12] = 2 * f[3] * g[9] + 2 * f[9] * g[3] + f[4] * g[8] + f[8] * g[4] + 2 * f[5] * g[7] + 2 * f[7] * g[5] + f[6] * g[6];
t[13] = f[4] * g[9] + f[9] * g[4] + f[5] * g[8] + f[8] * g[5] + f[6] * g[7] + f[7] * g[6];
t[14] = 2 * f[5] * g[9] + 2 * f[9] * g[5] + f[6] * g[8] + f[8] * g[6] + 2 * f[7] * g[7];
t[15] = f[6] * g[9] + f[9] * g[6] + f[7] * g[8] + f[8] * g[7];
t[16] = 2 * f[7] * g[9] + 2 * f[9] * g[7] + f[8] * g[8];
t[17] = f[8] * g[9] + f[9] * g[8];
t[18] = 2 * f[9] * g[9];
fe_reduce(out, t);
}
public static void fe_sq(long[] out, long[] f, long[] t) {
t[0] = f[0] * f[0];
t[1] = 2 * f[0] * f[1];
t[2] = 2 * f[0] * f[2] + 2 * f[1] * f[1];
t[3] = 2 * f[0] * f[3] + 2 * f[1] * f[2];
t[4] = 2 * f[0] * f[4] + 4 * f[1] * f[3] + f[2] * f[2];
t[5] = 2 * f[0] * f[5] + 2 * f[1] * f[4] + 2 * f[2] * f[3];
t[6] = 2 * f[0] * f[6] + 4 * f[1] * f[5] + 2 * f[2] * f[4] + 2 * f[3] * f[3];
t[7] = 2 * f[0] * f[7] + 2 * f[1] * f[6] + 2 * f[2] * f[5] + 2 * f[3] * f[4];
t[8] = 2 * f[0] * f[8] + 4 * f[1] * f[7] + 2 * f[2] * f[6] + 4 * f[3] * f[5] + f[4] * f[4];
t[9] = 2 * f[0] * f[9] + 2 * f[1] * f[8] + 2 * f[2] * f[7] + 2 * f[3] * f[6] + 2 * f[4] * f[5];
t[10] = 4 * f[1] * f[9] + 2 * f[2] * f[8] + 4 * f[3] * f[7] + 2 * f[4] * f[6] + 2 * f[5] * f[5];
t[11] = 2 * f[2] * f[9] + 2 * f[3] * f[8] + 2 * f[4] * f[7] + 2 * f[5] * f[6];
t[12] = 4 * f[3] * f[9] + 2 * f[4] * f[8] + 4 * f[5] * f[7] + f[6] * f[6];
t[13] = 2 * f[4] * f[9] + 2 * f[5] * f[8] + 2 * f[6] * f[7];
t[14] = 4 * f[5] * f[9] + 2 * f[6] * f[8] + 2 * f[7] * f[7];
t[15] = 2 * f[6] * f[9] + 2 * f[7] * f[8];
t[16] = 4 * f[7] * f[9] + f[8] * f[8];
t[17] = 2 * f[8] * f[9];
t[18] = 2 * f[9] * f[9];
fe_reduce(out, t);
}
public static void fe_reduce(long[] h, long[] t) {
t[0] += t[10] * 19;
t[1] += t[11] * 19;
t[2] += t[12] * 19;
t[3] += t[13] * 19;
t[4] += t[14] * 19;
t[5] += t[15] * 19;
t[6] += t[16] * 19;
t[7] += t[17] * 19;
t[8] += t[18] * 19;
for (int p = 0; p < 2; p++) {
for (int i = 0; i < 9; i++) {
long c = t[i] >> (i % 2 == 0 ? 26 : 25);
t[i] &= (i % 2 == 0 ? 0x3FFFFFFL : 0x1FFFFFFL);
t[i + 1] += c;
}
long c = t[9] >> 25;
t[9] &= 0x1FFFFFFL;
t[0] += c * 19;
}
for (int i = 0; i < 10; i++) h[i] = t[i];
}
public static void fe_frombytes(long[] h, byte[] s) {
for (int i = 0; i < 10; i++) h[i] = 0;
int bitIdx = 0;
for (int i = 0; i < 10; i++) {
int len = (i % 2 == 0 ? 26 : 25);
for (int b = 0; b < len; b++) {
if (bitIdx < 255) {
if ((((s[bitIdx / 8] & 0xFF) >> (bitIdx % 8)) & 1) == 1) h[i] |= (1L << b);
}
bitIdx++;
}
}
}
public static byte[] fe_contract(long[] h) {
long[] val = Arrays.copyOf(h, 10);
for (int p = 0; p < 2; p++) {
for (int i = 0; i < 9; i++) {
long c = val[i] >> (i % 2 == 0 ? 26 : 25);
val[i] &= (i % 2 == 0 ? 0x3FFFFFFL : 0x1FFFFFFL);
val[i + 1] += c;
}
long c = val[9] >> 25;
val[9] &= 0x1FFFFFFL;
val[0] += c * 19;
}
long mask = 1;
for (int i = 9; i >= 0; i--) {
long target = (i % 2 == 0 ? 0x3FFFFFFL : 0x1FFFFFFL);
if (i == 0) target -= 19;
if (val[i] < target) {
mask = 0;
break;
}
if (val[i] > target) break;
}
val[0] += mask * 19;
for (int i = 0; i < 9; i++) {
long c = val[i] >> (i % 2 == 0 ? 26 : 25);
val[i] &= (i % 2 == 0 ? 0x3FFFFFFL : 0x1FFFFFFL);
val[i + 1] += c;
}
val[9] -= mask * (1L << 25);
for (int i = 0; i < 9; i++) {
long c = val[i] >> (i % 2 == 0 ? 26 : 25);
val[i] &= (i % 2 == 0 ? 0x3FFFFFFL : 0x1FFFFFFL);
val[i + 1] += c;
}
val[0] += (val[9] >> 25) * 19;
val[9] &= 0x1FFFFFFL;
byte[] out = new byte[32];
int bitIdx = 0;
for (int i = 0; i < 10; i++) {
int len = (i % 2 == 0 ? 26 : 25);
for (int bit = 0; bit < len; bit++) {
if (bitIdx < 255) {
if (((val[i] >> bit) & 1) == 1) out[bitIdx / 8] |= (byte) (1 << (bitIdx % 8));
bitIdx++;
}
}
}
return out;
}
private static void fe_invert(long[] out, long[] z, Scratch s) {
long[] t0 = s.stack[0], t1 = s.stack[1], z2 = s.stack[2], z9 = s.stack[3], z11 = s.stack[4];
long[] z2_5_0 = s.stack[5], z2_10_0 = s.stack[6], z2_20_0 = s.stack[7], z2_50_0 = s.stack[8], z2_100_0 = s.stack[9];
fe_sq(z2, z, s.t19);
fe_sq(t1, z2, s.t19);
fe_sq(t0, t1, s.t19);
fe_mul(z9, t0, z, s.t19);
fe_mul(z11, z9, z2, s.t19);
fe_sq(t0, z11, s.t19);
fe_mul(z2_5_0, t0, z9, s.t19);
fe_sq(t0, z2_5_0, s.t19);
for (int i = 1; i < 5; i++) fe_sq(t0, t0, s.t19);
fe_mul(z2_10_0, t0, z2_5_0, s.t19);
fe_sq(t0, z2_10_0, s.t19);
for (int i = 1; i < 10; i++) fe_sq(t0, t0, s.t19);
fe_mul(z2_20_0, t0, z2_10_0, s.t19);
fe_sq(t0, z2_20_0, s.t19);
for (int i = 1; i < 20; i++) fe_sq(t0, t0, s.t19);
fe_mul(t0, t0, z2_20_0, s.t19);
fe_sq(t0, t0, s.t19);
for (int i = 1; i < 10; i++) fe_sq(t0, t0, s.t19);
fe_mul(z2_50_0, t0, z2_10_0, s.t19);
fe_sq(t0, z2_50_0, s.t19);
for (int i = 1; i < 50; i++) fe_sq(t0, t0, s.t19);
fe_mul(z2_100_0, t0, z2_50_0, s.t19);
fe_sq(t1, z2_100_0, s.t19);
for (int i = 1; i < 100; i++) fe_sq(t1, t1, s.t19);
fe_mul(t1, t1, z2_100_0, s.t19);
fe_sq(t0, t1, s.t19);
for (int i = 1; i < 50; i++) fe_sq(t0, t0, s.t19);
fe_mul(t0, t0, z2_50_0, s.t19);
fe_sq(t1, t0, s.t19);
for (int i = 1; i < 5; i++) fe_sq(t1, t1, s.t19);
fe_mul(out, t1, z11, s.t19);
}
private static void fe_pow22523(long[] out, long[] in, Scratch s) {
long[] t0 = s.stack[0], t1 = s.stack[1], z2 = s.stack[2], z9 = s.stack[3], z11 = s.stack[4];
long[] z2_5_0 = s.stack[5], z2_10_0 = s.stack[6], z2_20_0 = s.stack[7], z2_50_0 = s.stack[8], z2_100_0 = s.stack[9];
fe_sq(z2, in, s.t19);
fe_sq(t1, z2, s.t19);
fe_sq(t0, t1, s.t19);
fe_mul(z9, t0, in, s.t19);
fe_mul(z11, z9, z2, s.t19);
fe_sq(t0, z11, s.t19);
fe_mul(z2_5_0, t0, z9, s.t19);
fe_sq(t0, z2_5_0, s.t19);
for (int i = 1; i < 5; i++) fe_sq(t0, t0, s.t19);
fe_mul(z2_10_0, t0, z2_5_0, s.t19);
fe_sq(t0, z2_10_0, s.t19);
for (int i = 1; i < 10; i++) fe_sq(t0, t0, s.t19);
fe_mul(z2_20_0, t0, z2_10_0, s.t19);
fe_sq(t0, z2_20_0, s.t19);
for (int i = 1; i < 20; i++) fe_sq(t0, t0, s.t19);
fe_mul(t0, t0, z2_20_0, s.t19);
fe_sq(t0, t0, s.t19);
for (int i = 1; i < 10; i++) fe_sq(t0, t0, s.t19);
fe_mul(z2_50_0, t0, z2_10_0, s.t19);
fe_sq(t0, z2_50_0, s.t19);
for (int i = 1; i < 50; i++) fe_sq(t0, t0, s.t19);
fe_mul(z2_100_0, t0, z2_50_0, s.t19);
fe_sq(t1, z2_100_0, s.t19);
for (int i = 1; i < 100; i++) fe_sq(t1, t1, s.t19);
fe_mul(t1, t1, z2_100_0, s.t19);
fe_sq(t0, t1, s.t19);
for (int i = 1; i < 50; i++) fe_sq(t0, t0, s.t19);
fe_mul(t0, t0, z2_50_0, s.t19);
fe_sq(t0, t0, s.t19);
fe_sq(t0, t0, s.t19);
fe_mul(out, t0, in, s.t19);
}
private static boolean fe_isnonzero(long[] h) {
byte[] s = fe_contract(h);
for (byte b : s) if (b != 0) return true;
return false;
}
private static int fe_isnegative(long[] h) {
byte[] s = fe_contract(h);
return s[0] & 1;
}
public static void reduceScalar(byte[] s) {
long s0 = 2097151 & load3(s, 0);
long s1 = 2097151 & (load4(s, 2) >> 5);
long s2 = 2097151 & (load3(s, 5) >> 2);
long s3 = 2097151 & (load4(s, 7) >> 7);
long s4 = 2097151 & (load4(s, 10) >> 4);
long s5 = 2097151 & (load3(s, 13) >> 1);
long s6 = 2097151 & (load4(s, 15) >> 6);
long s7 = 2097151 & (load3(s, 18) >> 3);
long s8 = 2097151 & load3(s, 21);
long s9 = 2097151 & (load4(s, 23) >> 5);
long s10 = 2097151 & (load3(s, 26) >> 2);
long s11 = 2097151 & (load4(s, 28) >> 7);
long s12 = 2097151 & (load4(s, 31) >> 4);
long s13 = 2097151 & (load3(s, 34) >> 1);
long s14 = 2097151 & (load4(s, 36) >> 6);
long s15 = 2097151 & (load3(s, 39) >> 3);
long s16 = 2097151 & load3(s, 42);
long s17 = 2097151 & (load4(s, 44) >> 5);
long s18 = 2097151 & (load3(s, 47) >> 2);
long s19 = 2097151 & (load4(s, 49) >> 7);
long s20 = 2097151 & (load4(s, 52) >> 4);
long s21 = 2097151 & (load3(s, 55) >> 1);
long s22 = 2097151 & (load4(s, 57) >> 6);
long s23 = (load4(s, 60) >> 3);
s11 += s23 * 666643;
s12 += s23 * 470296;
s13 += s23 * 654183;
s14 -= s23 * 997805;
s15 += s23 * 136657;
s16 -= s23 * 683901;
s10 += s22 * 666643;
s11 += s22 * 470296;
s12 += s22 * 654183;
s13 -= s22 * 997805;
s14 += s22 * 136657;
s15 -= s22 * 683901;
s9 += s21 * 666643;
s10 += s21 * 470296;
s11 += s21 * 654183;
s12 -= s21 * 997805;
s13 += s21 * 136657;
s14 -= s21 * 683901;
s8 += s20 * 666643;
s9 += s20 * 470296;
s10 += s20 * 654183;
s11 -= s20 * 997805;
s12 += s20 * 136657;
s13 -= s20 * 683901;
s7 += s19 * 666643;
s8 += s19 * 470296;
s9 += s19 * 654183;
s10 -= s19 * 997805;
s11 += s19 * 136657;
s12 -= s19 * 683901;
s6 += s18 * 666643;
s7 += s18 * 470296;
s8 += s18 * 654183;
s9 -= s18 * 997805;
s10 += s18 * 136657;
s11 -= s18 * 683901;
long c6 = (s6 + (1 << 20)) >> 21;
s7 += c6;
s6 -= c6 << 21;
long c8 = (s8 + (1 << 20)) >> 21;
s9 += c8;
s8 -= c8 << 21;
long c10 = (s10 + (1 << 20)) >> 21;
s11 += c10;
s10 -= c10 << 21;
long c12 = (s12 + (1 << 20)) >> 21;
s13 += c12;
s12 -= c12 << 21;
long c14 = (s14 + (1 << 20)) >> 21;
s15 += c14;
s14 -= c14 << 21;
long c16 = (s16 + (1 << 20)) >> 21;
s17 += c16;
s16 -= c16 << 21;
long c7 = (s7 + (1 << 20)) >> 21;
s8 += c7;
s7 -= c7 << 21;
long c9 = (s9 + (1 << 20)) >> 21;
s10 += c9;
s9 -= c9 << 21;
long c11 = (s11 + (1 << 20)) >> 21;
s12 += c11;
s11 -= c11 << 21;
long c13 = (s13 + (1 << 20)) >> 21;
s14 += c13;
s13 -= c13 << 21;
long c15 = (s15 + (1 << 20)) >> 21;
s16 += c15;
s15 -= c15 << 21;
s5 += s17 * 666643;
s6 += s17 * 470296;
s7 += s17 * 654183;
s8 -= s17 * 997805;
s9 += s17 * 136657;
s10 -= s17 * 683901;
s4 += s16 * 666643;
s5 += s16 * 470296;
s6 += s16 * 654183;
s7 -= s16 * 997805;
s8 += s16 * 136657;
s9 -= s16 * 683901;
s3 += s15 * 666643;
s4 += s15 * 470296;
s5 += s15 * 654183;
s6 -= s15 * 997805;
s7 += s15 * 136657;
s8 -= s15 * 683901;
s2 += s14 * 666643;
s3 += s14 * 470296;
s4 += s14 * 654183;
s5 -= s14 * 997805;
s6 += s14 * 136657;
s7 -= s14 * 683901;
s1 += s13 * 666643;
s2 += s13 * 470296;
s3 += s13 * 654183;
s4 -= s13 * 997805;
s5 += s13 * 136657;
s6 -= s13 * 683901;
s0 += s12 * 666643;
s1 += s12 * 470296;
s2 += s12 * 654183;
s3 -= s12 * 997805;
s4 += s12 * 136657;
s5 -= s12 * 683901;
long cc0 = (s0 + (1 << 20)) >> 21;
s1 += cc0;
s0 -= cc0 << 21;
long cc2 = (s2 + (1 << 20)) >> 21;
s3 += cc2;
s2 -= cc2 << 21;
long cc4 = (s4 + (1 << 20)) >> 21;
s5 += cc4;
s4 -= cc4 << 21;
long cc6 = (s6 + (1 << 20)) >> 21;
s7 += cc6;
s6 -= cc6 << 21;
long cc8 = (s8 + (1 << 20)) >> 21;
s9 += cc8;
s8 -= cc8 << 21;
long cc10 = (s10 + (1 << 20)) >> 21;
s11 += cc10;
s10 -= cc10 << 21;
long cc1 = (s1 + (1 << 20)) >> 21;
s2 += cc1;
s1 -= cc1 << 21;
long cc3 = (s3 + (1 << 20)) >> 21;
s4 += cc3;
s3 -= cc3 << 21;
long cc5 = (s5 + (1 << 20)) >> 21;
s6 += cc5;
s5 -= cc5 << 21;
long cc7 = (s7 + (1 << 20)) >> 21;
s8 += cc7;
s7 -= cc7 << 21;
long cc9 = (s9 + (1 << 20)) >> 21;
s10 += cc9;
s9 -= cc9 << 21;
long cc11 = (s11 + (1 << 20)) >> 21;
long s12_2 = cc11;
s11 -= cc11 << 21;
s0 += s12_2 * 666643;
s1 += s12_2 * 470296;
s2 += s12_2 * 654183;
s3 -= s12_2 * 997805;
s4 += s12_2 * 136657;
s5 -= s12_2 * 683901;
long ccc0 = s0 >> 21;
s1 += ccc0;
s0 -= ccc0 << 21;
long ccc1 = s1 >> 21;
s2 += ccc1;
s1 -= ccc1 << 21;
long ccc2 = s2 >> 21;
s3 += ccc2;
s2 -= ccc2 << 21;
long ccc3 = s3 >> 21;
s4 += ccc3;
s3 -= ccc3 << 21;
long ccc4 = s4 >> 21;
s5 += ccc4;
s4 -= ccc4 << 21;
long ccc5 = s5 >> 21;
s6 += ccc5;
s5 -= ccc5 << 21;
long ccc6 = s6 >> 21;
s7 += ccc6;
s6 -= ccc6 << 21;
long ccc7 = s7 >> 21;
s8 += ccc7;
s7 -= ccc7 << 21;
long ccc8 = s8 >> 21;
s9 += ccc8;
s8 -= ccc8 << 21;
long ccc9 = s9 >> 21;
s10 += ccc9;
s9 -= ccc9 << 21;
long ccc10 = s10 >> 21;
s11 += ccc10;
s10 -= ccc10 << 21;
long ccc11 = s11 >> 21;
long s12_3 = ccc11;
s11 -= ccc11 << 21;
s0 += s12_3 * 666643;
s1 += s12_3 * 470296;
s2 += s12_3 * 654183;
s3 -= s12_3 * 997805;
s4 += s12_3 * 136657;
s5 -= s12_3 * 683901;
long sccc0 = s0 >> 21;
s1 += sccc0;
s0 -= sccc0 << 21;
long sccc1 = s1 >> 21;
s2 += sccc1;
s1 -= sccc1 << 21;
long sccc2 = s2 >> 21;
s3 += sccc2;
s2 -= sccc2 << 21;
long sccc3 = s3 >> 21;
s4 += sccc3;
s3 -= sccc3 << 21;
long sccc4 = s4 >> 21;
s5 += sccc4;
s4 -= sccc4 << 21;
long sccc5 = s5 >> 21;
s6 += sccc5;
s5 -= sccc5 << 21;
Arrays.fill(s, (byte) 0);
long[] resLimbs = {s0, s1, s2, s3, s4, s5, s6, s7, s8, s9, s10, s11};
int bitIdx = 0;
for (int limbIdx = 0; limbIdx < 12; limbIdx++) {
for (int i = 0; i < 21; i++) {
if (bitIdx < 256) {
if (((resLimbs[limbIdx] >> i) & 1) == 1)
s[bitIdx / 8] |= (byte) (1 << (bitIdx % 8));
bitIdx++;
}
}
}
}
private static long load3(byte[] in, int off) {
return (in[off] & 0xffL) | ((in[off + 1] & 0xffL) << 8) | ((in[off + 2] & 0xffL) << 16);
}
private static long load4(byte[] in, int off) {
return (in[off] & 0xffL) | ((in[off + 1] & 0xffL) << 8) | ((in[off + 2] & 0xffL) << 16) | ((in[off + 3] & 0xffL) << 24);
}
private static void scalarMulAdd(byte[] S, byte[] k, byte[] s, byte[] r) {
long[] res = new long[64];
for (int i = 0; i < 32; i++) {
for (int j = 0; j < 32; j++) res[i + j] += (k[i] & 0xFFL) * (s[j] & 0xFFL);
}
for (int i = 0; i < 32; i++) res[i] += (r[i] & 0xFFL);
byte[] buffer = new byte[64];
long carry = 0;
for (int i = 0; i < 63; i++) {
long val = res[i] + carry;
buffer[i] = (byte) val;
carry = val >>> 8;
}
buffer[63] = (byte) (res[63] + carry);
reduceScalar(buffer);
System.arraycopy(buffer, 0, S, 0, 32);
}
private static byte[] sha512(byte[]... parts) {
try {
MessageDigest md = MessageDigest.getInstance("SHA-512");
for (byte[] p : parts) md.update(p);
return md.digest();
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
}
}
@@ -0,0 +1,172 @@
/*
* Copyright (c) 2026 QPQ AG <info@qpq.swiss>. All rights reserved.
* Project: Gajumaru Core Java Libraries <gajumaru.io>
*
* This program is dual-licensed:
* 1) Under the GNU Affero General Public License as published by the Free
* Software Foundation, either version 3 of the License, or (at your option)
* any later version (AGPL-3.0-or-later).
*
* 2) Under a commercial/proprietary license available directly from QPQ AG.
* If you wish to use this software outside the strict constraints of the
* AGPLv3 (e.g. within a closed-source or proprietary product), you must
* purchase a commercial license from QPQ AG.
*
* Authors:
* - Craig Everett <craigeverett@qpq.swiss>
*
* SPDX-License-Identifier: AGPL-3.0-or-later OR LicenseRef-QPQ-Commercial
*/
package swiss.qpq.gajumaru.core.crypto;
import java.util.Arrays;
import swiss.qpq.gajumaru.core.tools.CryptoUtils;
// Pure Java implementation of Keccak-256 (Keccak[c=512] sponge).
// Based on sha3.erl.
public final class Keccak256 {
private static final int LANE_SIZE = 64;
private static final int STATE_SIZE = 25;
private static final int CAPACITY = 512;
private static final int BITRATE = 1600 - CAPACITY; // 1088 bits = 136 bytes
private static final int BITRATE_BYTES = BITRATE / 8;
private static final long[] ROUND_CONSTANTS = {
0x0000000000000001L, 0x0000000000008082L, 0x800000000000808AL, 0x8000000080008000L,
0x000000000000808BL, 0x0000000080000001L, 0x8000000080008081L, 0x8000000000008009L,
0x000000000000008AL, 0x0000000000000088L, 0x0000000080008009L, 0x000000008000000AL,
0x000000008000808BL, 0x800000000000008BL, 0x8000000000008089L, 0x8000000000008003L,
0x8000000000008002L, 0x8000000000000080L, 0x000000000000800AL, 0x800000008000000AL,
0x8000000080008081L, 0x8000000000008080L, 0x0000000080000001L, 0x8000000080008008L
};
private static final int[] ROTATION_OFFSETS = {
0, 1, 62, 28, 27,
36, 44, 6, 55, 20,
3, 10, 43, 25, 39,
41, 45, 15, 21, 8,
18, 2, 61, 56, 14
};
private Keccak256() {}
public static byte[] hash(byte[] data) {
long[] state = new long[STATE_SIZE];
// Padding: Keccak[c=512] uses 0x01 ... 0x80 (or just 0x01 for non-NIST Keccak)
// Erlang code uses: keccak(Capacity, Message, <<>>, OutputBitLength)
// Pad logic in Erlang: <<Msg/bitstring, Delimiter/bitstring, 1:1, 0:PadZeros, 1:1>>
// With Delimiter = <<>>, it becomes <<1:1, 0:PadZeros, 1:1>> (standard Keccak padding)
byte[] padded = pad(data, BITRATE_BYTES);
for (int i = 0; i < padded.length; i += BITRATE_BYTES) {
absorb(state, padded, i);
}
byte[] result = squeeze(state, 32);
// Memory hygiene
Arrays.fill(state, 0L);
CryptoUtils.wipe(padded);
return result;
}
private static byte[] pad(byte[] data, int rateBytes) {
int mLen = data.length;
int padLen = rateBytes - (mLen % rateBytes);
byte[] padded = new byte[mLen + padLen];
System.arraycopy(data, 0, padded, 0, mLen);
if (padLen == 1) {
padded[mLen] = (byte) 0x81;
} else {
padded[mLen] = (byte) 0x01;
padded[padded.length - 1] |= (byte) 0x80;
}
return padded;
}
private static void absorb(long[] state, byte[] data, int offset) {
for (int i = 0; i < BITRATE_BYTES / 8; i++) {
state[i] ^= readLongLittleEndian(data, offset + i * 8);
}
keccakF(state);
}
private static byte[] squeeze(long[] state, int len) {
byte[] result = new byte[len];
int count = 0;
while (count < len) {
for (int i = 0; i < BITRATE_BYTES / 8 && count < len; i++) {
writeLongLittleEndian(state[i], result, count);
count += 8;
}
if (count < len) {
keccakF(state);
}
}
return result;
}
private static void keccakF(long[] a) {
for (int round = 0; round < 24; round++) {
// Theta
long[] c = new long[5];
for (int x = 0; x < 5; x++) {
c[x] = a[x] ^ a[x + 5] ^ a[x + 10] ^ a[x + 15] ^ a[x + 20];
}
for (int x = 0; x < 5; x++) {
long d = c[(x + 4) % 5] ^ Long.rotateLeft(c[(x + 1) % 5], 1);
for (int y = 0; y < 5; y++) {
a[x + y * 5] ^= d;
}
}
// Rho and Pi
long[] nextA = new long[STATE_SIZE];
for (int x = 0; x < 5; x++) {
for (int y = 0; y < 5; y++) {
int index = x + y * 5;
nextA[y + ((2 * x + 3 * y) % 5) * 5] = Long.rotateLeft(a[index], ROTATION_OFFSETS[index]);
}
}
// Chi
for (int y = 0; y < 5; y++) {
long[] row = new long[5];
for (int x = 0; x < 5; x++) {
row[x] = nextA[x + y * 5];
}
for (int x = 0; x < 5; x++) {
nextA[x + y * 5] = row[x] ^ ((~row[(x + 1) % 5]) & row[(x + 2) % 5]);
}
}
// Iota
nextA[0] ^= ROUND_CONSTANTS[round];
System.arraycopy(nextA, 0, a, 0, STATE_SIZE);
}
}
private static long readLongLittleEndian(byte[] b, int offset) {
long res = 0;
for (int i = 0; i < 8; i++) {
res |= ((long) (b[offset + i] & 0xFF)) << (i * 8);
}
return res;
}
private static void writeLongLittleEndian(long v, byte[] b, int offset) {
for (int i = 0; i < 8; i++) {
b[offset + i] = (byte) ((v >>> (i * 8)) & 0xFF);
}
}
}
@@ -0,0 +1,78 @@
/*
* 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 javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.spec.GCMParameterSpec;
// Vault provides secure AES/GCM encryption and decryption with memory hygiene.
// It uses standard JCE providers but ensures that application-level buffers can be wiped.
//
// Purpose:
// This provides a way to use opaque handles to javax.crypto.SecretKey objects in discrete memory
// instead of exposing your plaintext secret keys to other parts of the system that might leave
// dead references on the heap somewhere that are vulnerable until GC finally hits.
//
// The important thing to remember is that there is still a burden on the caller to take advantage
// of whatever the current platform's best key management facilities are and stick to them.
public final class Vault {
private static final String AES_GCM = "AES/GCM/NoPadding";
private static final int GCM_TAG_LENGTH = 128; // Bits
private Vault() {}
/**
* Encrypts plaintext using AES/GCM.
*
* @param key The SecretKey to use.
* @param iv The initialization vector (should be 12 bytes).
* @param plaintext The data to encrypt.
* @return The ciphertext including the authentication tag.
* @throws Exception if encryption fails.
*/
public static byte[] encrypt(SecretKey key, byte[] iv, byte[] plaintext) throws Exception {
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);
}
/**
* Decrypts ciphertext using AES/GCM.
*
* @param key The SecretKey to use.
* @param iv The initialization vector used during encryption.
* @param ciphertext The data to decrypt (including the authentication tag).
* @return The plaintext data. The caller is RESPONSIBLE for wiping this buffer
* using CryptoUtils.wipe() once it is no longer needed.
* @throws Exception if decryption or authentication fails.
*/
public static byte[] decrypt(SecretKey key, byte[] iv, byte[] ciphertext) throws Exception {
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);
}
}
@@ -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) {}
}
@@ -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,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
private 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,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,329 @@
/*
* 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) {}
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 >= '' && c <= '') {
sb.append((char) (c - '' + '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;
}
}
@@ -13,3 +13,5 @@ doc/erlang.png
rel/example_project
.concrete/DEV_MODE
.rebar
.idea
*.iml
+53
View File
@@ -0,0 +1,53 @@
# GM Java <-> Erlang Tests
The core Java libraries are all written as transform functions over data.
There is little point, therefore, in obsessing over "unit tests" and "regression testing" of
Java code in Java when we have cannonical code in Erlang.
The purpose of this utility is to instead test the Java libraries agains the Erlang libraries
directly.
## How to run
In the current directory simply run `zx runlocal` and the report map will be printed to the screen
and random test data will be in the `temp/` directory.
To run a specific test suite run `zx runlocal [module names]`.
To list module names, run `zx runlocal list`.
## Where things are
The test cases are all generated in gmt.erl, generally based on randomized but valid data that
are fed to reference Erlang implementations. Java code is built using plain old javac, and is
handled by the `gajumaru-core/bin/compile` and `gajumaru-core/bin/run` scripts.
The input to and output from each test is recorded to disk in the `temp/` dir.
The inputs are then read in by equivalent Java functions and the output is then compared.
For a test case to pass the outputs must be identical.
## The Erlang execution context
`zx` manages loading the execution context, so dependencies like gmserialization, ec_utils,
base58, etc. (all of which are targets for which is increasingly turning into a Java port)
are brought in dynamically and managed by `zx`. The easiest way to test a specific function
in Java against the Erlang implementation is to add a call to it in gmt.erl and a matching
call in `../src/Testinator.java`.
The dependencies that are brought in can be listed with `zx list deps`.
The package names follow the pattern `[realm]-[package_name]-[version]`
The location of the sources varies on different systems.
- Linux: `$HOME/zomp/lib/[realm]/[package_name]/[version]/src`
- MacOS: `$HOME/.zx/zomp/lib/[realm]/[package_name]/[version]/src`
- Windows: `%%LOCALAPPDATA%%/zomp/lib/[realm]/[package_name]/[version]/src`
So for example `otpr-ec_utils-1.0.0` has its sources at `~/zomp/lib/otpr/ec_utils/1.0.0/src/`
on a reasonable system.
+496
View File
@@ -0,0 +1,496 @@
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Base64;
import java.util.ArrayList;
import java.util.List;
import java.util.Arrays;
import java.math.BigInteger;
import swiss.qpq.gajumaru.core.encoding.Base58;
import swiss.qpq.gajumaru.core.encoding.RLP;
import swiss.qpq.gajumaru.core.encoding.ApiEncoder;
import swiss.qpq.gajumaru.core.formatting.GajuFormat;
import swiss.qpq.gajumaru.core.crypto.Keccak256;
import swiss.qpq.gajumaru.core.crypto.Blake2b;
import swiss.qpq.gajumaru.core.crypto.Ed25519;
import swiss.qpq.gajumaru.core.data.Id;
import swiss.qpq.gajumaru.core.data.SpendTx;
import swiss.qpq.gajumaru.core.data.SignedTx;
import swiss.qpq.gajumaru.core.encoding.Mnemonic;
import swiss.qpq.gajumaru.core.tools.CryptoUtils;
public class Testinator {
public static void main(String[] args) {
if (args.length < 2) {
System.out.println("Error: Provide the test suite name and the required arguments.");
System.exit(1);
}
try {
switch (args[0]) {
case "base64" -> { System.out.print(base64(args[1])); }
case "base58" -> { System.out.print(base58(args[1])); }
case "base58_check" -> { System.out.print(base58_check(args[1])); }
case "rlp" -> { System.out.print(rlp(args[1])); }
case "rlp_stream" -> { System.out.print(rlp_stream(args[1])); }
case "rlp_fail" -> { System.out.print(rlp_fail(args[1])); }
case "gaju_format" -> { System.out.print(gaju_format(args[1])); }
case "keccak256" -> { System.out.print(keccak256(args[1])); }
case "blake2b" -> { System.out.print(blake2b(args[1])); }
case "ed25519" -> { System.out.print(ed25519(args[1])); }
case "ed25519_verify" -> { System.out.print(ed25519_verify(args[1])); }
case "api_encode" -> { System.out.print(api_encode(args[1])); }
case "id_serialization" -> { System.out.print(id_serialization(args[1])); }
case "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 "fe_parity" -> { System.out.print(fe_parity(args[1])); }
case "reduce_parity" -> { System.out.print(reduce_parity(args[1])); }
case "smb_parity" -> { System.out.print(smb_parity(args[1])); }
case "femul_parity" -> { System.out.print(femul_parity(args[1])); }
case "frombytes_parity" -> { System.out.print(frombytes_parity(args[1])); }
case "ge_parity" -> { System.out.print(ge_parity(args[1], args[2])); }
case "keccak" -> {
byte[] in = CryptoUtils.hexToBin(args[1]);
System.out.println(CryptoUtils.binToHex(Keccak256.hash(in)));
}
case "blake2b_direct" -> {
byte[] in = CryptoUtils.hexToBin(args[1]);
System.out.println(CryptoUtils.binToHex(Blake2b.hash(in)));
}
case "ak_encode" -> {
byte[] in = CryptoUtils.hexToBin(args[1]);
System.out.println(ApiEncoder.encode(ApiEncoder.Type.ACCOUNT_PUBKEY, in));
}
case "ed25519_pub" -> {
byte[] seed = CryptoUtils.hexToBin(args[1]);
System.out.println(CryptoUtils.binToHex(Ed25519.publicKey(seed)));
}
case "id_serialize" -> {
int tag = Integer.parseInt(args[1]);
byte[] val = CryptoUtils.hexToBin(args[2]);
Id id = new Id(Id.Tag.fromValue(tag), val);
System.out.println(CryptoUtils.binToHex(id.serialize()));
}
default -> {
System.out.println("Error: Unknown test suite or command: " + args[0]);
System.exit(1);
}
}
} catch (Exception e) {
System.err.println("Error: " + e.getMessage());
e.printStackTrace();
System.exit(1);
}
}
private static String ge_parity(String aHex, String bHex) throws IOException {
byte[] a = CryptoUtils.hexToBin(aHex);
byte[] b = CryptoUtils.hexToBin(bHex);
Ed25519.Scratch sc = new Ed25519.Scratch();
Ed25519.Ge p1 = Ed25519.scalarMulBase(a, sc);
Ed25519.Ge p2 = Ed25519.scalarMulBase(b, sc);
Ed25519.Ge p3 = new Ed25519.Ge();
Ed25519.ge_add(p3, p1, p2, sc);
byte[] res = Ed25519.compress(p3, sc);
p1.wipe(); p2.wipe(); p3.wipe(); sc.wipe();
return CryptoUtils.binToHex(res) + "|||";
}
private static String frombytes_parity(String workingPath) throws IOException {
Path testPath = Path.of(workingPath, "frombytes.test");
Path resPath = Path.of(workingPath, "frombytes.java.txt");
List<String> lines = Files.readAllLines(testPath);
List<String> results = new ArrayList<>();
for (String hex : lines) {
if (hex.trim().isEmpty()) continue;
byte[] in = CryptoUtils.hexToBin(hex);
long[] h = new long[10];
Ed25519.fe_frombytes(h, in);
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 10; i++) {
if (i > 0) sb.append(",");
sb.append(h[i]);
}
results.add(sb.toString());
}
Files.write(resPath, results);
return resPath.toString();
}
private static String femul_parity(String workingPath) throws IOException {
Path testPath = Path.of(workingPath, "femul.test");
Path resPath = Path.of(workingPath, "femul.java.txt");
List<String> lines = Files.readAllLines(testPath);
List<String> results = new ArrayList<>();
for (String line : lines) {
if (line.trim().isEmpty()) continue;
String[] parts = line.split("\\|");
byte[] a = CryptoUtils.hexToBin(parts[0]);
byte[] b = CryptoUtils.hexToBin(parts[1]);
long[] ha = new long[10], hb = new long[10];
Ed25519.fe_frombytes(ha, a);
Ed25519.fe_frombytes(hb, b);
long[] hr = new long[10];
Ed25519.fe_mul(hr, ha, hb, new long[19]);
byte[] out = Ed25519.fe_contract(hr);
results.add(CryptoUtils.binToHex(out));
}
Files.write(resPath, results);
return resPath.toString();
}
private static String smb_parity(String workingPath) throws IOException {
Path testPath = Path.of(workingPath, "smb.test");
Path resPath = Path.of(workingPath, "smb.java.txt");
List<String> lines = Files.readAllLines(testPath);
List<String> results = new ArrayList<>();
for (String hex : lines) {
if (hex.trim().isEmpty()) continue;
byte[] in = CryptoUtils.hexToBin(hex);
Ed25519.Scratch sc = new Ed25519.Scratch();
Ed25519.Ge p = Ed25519.scalarMulBase(in, sc);
byte[] out = Ed25519.compress(p, sc);
p.wipe(); sc.wipe();
results.add(CryptoUtils.binToHex(out));
}
Files.write(resPath, results);
return resPath.toString();
}
private static String reduce_parity(String workingPath) throws IOException {
Path testPath = Path.of(workingPath, "reduce.test");
Path resPath = Path.of(workingPath, "reduce.java.txt");
List<String> lines = Files.readAllLines(testPath);
List<String> results = new ArrayList<>();
for (String hex : lines) {
if (hex.trim().isEmpty()) continue;
byte[] in = CryptoUtils.hexToBin(hex);
byte[] s = Arrays.copyOf(in, 64);
Ed25519.reduceScalar(s);
byte[] out = Arrays.copyOf(s, 32);
results.add(CryptoUtils.binToHex(out));
}
Files.write(resPath, results);
return resPath.toString();
}
private static String fe_parity(String workingPath) throws IOException {
Path testPath = Path.of(workingPath, "fe.test");
Path resPath = Path.of(workingPath, "fe.java.txt");
List<String> lines = Files.readAllLines(testPath);
List<String> results = new ArrayList<>();
for (String hex : lines) {
if (hex.trim().isEmpty()) continue;
byte[] in = CryptoUtils.hexToBin(hex);
long[] h = new long[10];
Ed25519.fe_frombytes(h, in);
byte[] out = Ed25519.fe_contract(h);
results.add(CryptoUtils.binToHex(out));
}
Files.write(resPath, results);
return resPath.toString();
}
private static String base64(String workingPath) throws IOException {
Path testPath = Path.of(workingPath, "base64.test");
Path encPath = Path.of(workingPath, "base64.java.txt");
Path decPath = Path.of(workingPath, "base64.java.back");
byte[] rawB = Files.readAllBytes(testPath);
byte[] encB = Base64.getEncoder().encode(rawB);
Files.write(encPath, encB);
byte[] readE = Files.readAllBytes(encPath);
byte[] decB = Base64.getDecoder().decode(readE);
Files.write(decPath, decB);
return encPath.toString() + " " + decPath.toString();
}
private static String base58(String workingPath) throws IOException {
Path testPath = Path.of(workingPath, "base58.test");
Path encPath = Path.of(workingPath, "base58.java.txt");
Path decPath = Path.of(workingPath, "base58.java.back");
byte[] rawB = Files.readAllBytes(testPath);
String encS = Base58.encode(rawB);
Files.write(encPath, encS.getBytes());
byte[] readE = Files.readAllBytes(encPath);
String readS = new String(readE);
byte[] decB = Base58.decode(readS);
Files.write(decPath, decB);
return encPath.toString() + " " + decPath.toString();
}
private static String base58_check(String workingPath) throws IOException {
Path testPath = Path.of(workingPath, "base58_check.test");
Path encPath = Path.of(workingPath, "base58_check.java.txt");
Path decPath = Path.of(workingPath, "base58_check.java.back");
byte[] rawB = Files.readAllBytes(testPath);
String encS = Base58.checkEncode(rawB);
Files.write(encPath, encS.getBytes());
byte[] readE = Files.readAllBytes(encPath);
String readS = new String(readE);
byte[] decB = Base58.checkDecode(readS);
Files.write(decPath, decB);
return encPath.toString() + " " + decPath.toString();
}
private static String rlp(String workingPath) throws IOException {
Path testPath = Path.of(workingPath, "rlp.test");
Path resPath = Path.of(workingPath, "rlp.java.back");
byte[] encB = Files.readAllBytes(testPath);
RLP.RLP_Data decRLP = RLP.decode(encB);
RLP.RLP_List root = decRLP.asList();
RLP.RLP_List sub = root.getItems().get(1).asList();
byte[] anchor = sub.getItems().get(1).asItem().getBytes();
byte[] reEnc = RLP.encode(decRLP);
Files.write(resPath, reEnc);
return new String(anchor) + "|" + resPath.toString();
}
private static String rlp_stream(String workingPath) throws IOException {
Path testPath = Path.of(workingPath, "rlp_stream.test");
byte[] buffer = Files.readAllBytes(testPath);
List<String> hashes = new ArrayList<>();
int offset = 0;
while (offset < buffer.length) {
RLP.DecodeResult res = RLP.decodeFrom(buffer, offset);
hashes.add(Integer.toString(res.data().hashCode())); // Just to verify we got objects
offset += res.consumed();
}
return Integer.toString(hashes.size());
}
private static String rlp_fail(String workingPath) throws IOException {
Path testPath = Path.of(workingPath, "rlp_fail.test");
byte[] buffer = Files.readAllBytes(testPath);
try {
RLP.decode(buffer);
return "SUCCESS";
} catch (RLP.RLPException e) {
return "RLP_EXCEPTION: " + e.getMessage();
} catch (Exception e) {
return "OTHER_EXCEPTION: " + e.getClass().getSimpleName();
}
}
private static String gaju_format(String workingPath) throws IOException {
Path testPath = Path.of(workingPath, "gaju_format.test");
Path resPath = Path.of(workingPath, "gaju_format.java.txt");
List<String> lines = Files.readAllLines(testPath);
List<String> results = new ArrayList<>();
for (String line : lines) {
if (line.trim().isEmpty()) continue;
String[] p = line.split("\\|");
String op = p[0];
switch (op) {
case "amount" -> {
GajuFormat.Type style = GajuFormat.Type.valueOf(p[1]);
GajuFormat.Unit unit = GajuFormat.Unit.valueOf(p[2]);
char sep = p[3].charAt(0);
int span = Integer.parseInt(p[4]);
BigInteger val = new BigInteger(p[5]);
results.add(GajuFormat.amount(new GajuFormat.FormatSpec(style, unit, sep, span), val));
}
case "approx" -> {
GajuFormat.Type style = GajuFormat.Type.valueOf(p[1]);
GajuFormat.Unit unit = GajuFormat.Unit.valueOf(p[2]);
char sep = p[3].charAt(0);
int span = Integer.parseInt(p[4]);
BigInteger val = new BigInteger(p[5]);
int prec = Integer.parseInt(p[6]);
results.add(GajuFormat.approxAmount(new GajuFormat.FormatSpec(style, unit, sep, span), val, prec));
}
case "read" -> {
byte[] b = GajuFormat.read(p[1]);
results.add(new BigInteger(b).toString());
}
}
}
Files.write(resPath, results);
return resPath.toString();
}
private static String keccak256(String workingPath) throws IOException {
Path testPath = Path.of(workingPath, "keccak256.test");
Path resPath = Path.of(workingPath, "keccak256.java.txt");
byte[] input = Files.readAllBytes(testPath);
byte[] hash = Keccak256.hash(input);
Files.write(resPath, CryptoUtils.binToHex(hash).getBytes());
return resPath.toString();
}
private static String blake2b(String workingPath) throws IOException {
Path testPath = Path.of(workingPath, "blake2b.test");
Path resPath = Path.of(workingPath, "blake2b.java.txt");
byte[] input = Files.readAllBytes(testPath);
byte[] hash = Blake2b.hash(input);
Files.write(resPath, CryptoUtils.binToHex(hash).getBytes());
return resPath.toString();
}
private static String ed25519(String workingPath) throws IOException {
Path testPath = Path.of(workingPath, "ed25519.test");
Path resPath = Path.of(workingPath, "ed25519.java.txt");
List<String> lines = Files.readAllLines(testPath);
List<String> results = new ArrayList<>();
for (String line : lines) {
if (line.trim().isEmpty()) continue;
String[] parts = line.split("\\|");
byte[] seed = CryptoUtils.hexToBin(parts[0]);
byte[] msg = CryptoUtils.hexToBin(parts[1]);
byte[] pub = Ed25519.publicKey(seed);
byte[] sig = Ed25519.sign(seed, msg);
boolean verify = Ed25519.verify(pub, msg, sig);
results.add(CryptoUtils.binToHex(pub) + "|" + CryptoUtils.binToHex(sig) + "|" + verify);
CryptoUtils.wipe(seed);
CryptoUtils.wipe(msg);
CryptoUtils.wipe(pub);
CryptoUtils.wipe(sig);
}
Files.write(resPath, results);
return resPath.toString();
}
private static String ed25519_verify(String workingPath) throws IOException {
Path seedPath = Path.of(workingPath, "ed25519_seed.test");
Path msgPath = Path.of(workingPath, "ed25519_msg.test");
byte[] seed = CryptoUtils.hexToBin(Files.readString(seedPath).trim());
byte[] message = Files.readAllBytes(msgPath);
byte[] pub = Ed25519.publicKey(seed);
byte[] sig = Ed25519.sign(seed, message);
boolean verify = Ed25519.verify(pub, message, sig);
return CryptoUtils.binToHex(pub) + "|" + CryptoUtils.binToHex(sig) + "|" + verify;
}
private static String api_encode(String workingPath) throws IOException {
Path testPath = Path.of(workingPath, "api_encode.test");
Path resPath = Path.of(workingPath, "api_encode.java.txt");
List<String> lines = Files.readAllLines(testPath);
List<String> results = new ArrayList<>();
for (String line : lines) {
if (line.trim().isEmpty()) continue;
String[] p = line.split("\\|");
ApiEncoder.Type type = ApiEncoder.Type.valueOf(p[0]);
byte[] payload = CryptoUtils.hexToBin(p[1]);
results.add(ApiEncoder.encode(type, payload));
if (type == ApiEncoder.Type.ACCOUNT_SECKEY) {
CryptoUtils.wipe(payload);
}
}
Files.write(resPath, results);
return resPath.toString();
}
private static String id_serialization(String workingPath) throws IOException {
Path testPath = Path.of(workingPath, "id_serialization.test");
Path resPath = Path.of(workingPath, "id_serialization.java.txt");
List<String> lines = Files.readAllLines(testPath);
List<String> results = new ArrayList<>();
for (String line : lines) {
if (line.trim().isEmpty()) continue;
String[] p = line.split("\\|");
Id.Tag tag = Id.Tag.fromValue(Integer.parseInt(p[0]));
byte[] val = CryptoUtils.hexToBin(p[1]);
Id id = new Id(tag, val);
results.add(CryptoUtils.binToHex(id.serialize()));
}
Files.write(resPath, results);
return resPath.toString();
}
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();
}
}
+676
View File
@@ -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}},