From 20e462ec03c8b45b36bad0c11edd57723a10d2f4 Mon Sep 17 00:00:00 2001 From: Craig Everett Date: Sun, 23 Aug 2026 18:16:17 +0900 Subject: [PATCH] WIP --- .../qpq/gajumaru/core/crypto/Ed25519.java | 14 +- .../qpq/gajumaru/core/encoding/Gf256.java | 20 ++ .../swiss/qpq/gajumaru/core/encoding/QR.java | 20 ++ .../swiss/qpq/gajumaru/core/encoding/ZJ.java | 20 ++ .../swiss/qpq/gajumaru/core/tools/Grids.java | 20 ++ .../qpq/gajumaru/core/tools/NodeClient.java | 182 ++++++++++++++++++ .../core/tools/TransactionService.java | 81 ++++++++ 7 files changed, 354 insertions(+), 3 deletions(-) create mode 100644 src/main/java/swiss/qpq/gajumaru/core/tools/NodeClient.java create mode 100644 src/main/java/swiss/qpq/gajumaru/core/tools/TransactionService.java diff --git a/src/main/java/swiss/qpq/gajumaru/core/crypto/Ed25519.java b/src/main/java/swiss/qpq/gajumaru/core/crypto/Ed25519.java index 7fc1919..87359a8 100644 --- a/src/main/java/swiss/qpq/gajumaru/core/crypto/Ed25519.java +++ b/src/main/java/swiss/qpq/gajumaru/core/crypto/Ed25519.java @@ -45,10 +45,18 @@ import swiss.qpq.gajumaru.core.tools.CryptoUtils; // interacting over a JNI boundary with off-heap memory (via java.nio.ByteBuffer) // and aggressive memory sanitation is necessary. // +// WARNING: +// Do NOT compile this code with compiler optimizations turned on. It will remove the memory +// protection functionality entirely as it is "no impact" code from the perspective of +// performance optimization. Configure your build system to deliberately turn off optimizing +// profilers for this module. You will need to do the same for any C code you might interface +// with over JNI as well. Constant time execution, memory wiping, etc. are all viewed as +// no-impact busywork or pessimization from the perspective of a performance profiler. +// // References: -// I don't even know where to start with references, but the tink-java library and pretty much -// everything (and everyone!) referenced on the lib25519 page deserves a mention. -// Of special mention, of course, is SUPERCOP. +// 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 diff --git a/src/main/java/swiss/qpq/gajumaru/core/encoding/Gf256.java b/src/main/java/swiss/qpq/gajumaru/core/encoding/Gf256.java index bee9dd3..b9d653f 100644 --- a/src/main/java/swiss/qpq/gajumaru/core/encoding/Gf256.java +++ b/src/main/java/swiss/qpq/gajumaru/core/encoding/Gf256.java @@ -1,3 +1,23 @@ +/* + * Copyright (c) 2026 QPQ AG . All rights reserved. + * Project: Gajumaru Core Java Libraries + * + * 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 + * + * SPDX-License-Identifier: AGPL-3.0-or-later OR LicenseRef-QPQ-Commercial + */ + package swiss.qpq.gajumaru.core.encoding; import java.util.Arrays; diff --git a/src/main/java/swiss/qpq/gajumaru/core/encoding/QR.java b/src/main/java/swiss/qpq/gajumaru/core/encoding/QR.java index c65b001..9bfe5fc 100644 --- a/src/main/java/swiss/qpq/gajumaru/core/encoding/QR.java +++ b/src/main/java/swiss/qpq/gajumaru/core/encoding/QR.java @@ -1,3 +1,23 @@ +/* + * Copyright (c) 2026 QPQ AG . All rights reserved. + * Project: Gajumaru Core Java Libraries + * + * 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 + * + * SPDX-License-Identifier: AGPL-3.0-or-later OR LicenseRef-QPQ-Commercial + */ + package swiss.qpq.gajumaru.core.encoding; import java.util.Arrays; diff --git a/src/main/java/swiss/qpq/gajumaru/core/encoding/ZJ.java b/src/main/java/swiss/qpq/gajumaru/core/encoding/ZJ.java index d63b15f..fd2ed2a 100644 --- a/src/main/java/swiss/qpq/gajumaru/core/encoding/ZJ.java +++ b/src/main/java/swiss/qpq/gajumaru/core/encoding/ZJ.java @@ -1,3 +1,23 @@ +/* + * Copyright (c) 2026 QPQ AG . All rights reserved. + * Project: Gajumaru Core Java Libraries + * + * 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 + * + * SPDX-License-Identifier: AGPL-3.0-or-later OR LicenseRef-QPQ-Commercial + */ + package swiss.qpq.gajumaru.core.encoding; import java.math.BigDecimal; diff --git a/src/main/java/swiss/qpq/gajumaru/core/tools/Grids.java b/src/main/java/swiss/qpq/gajumaru/core/tools/Grids.java index a5984a0..862ae1a 100644 --- a/src/main/java/swiss/qpq/gajumaru/core/tools/Grids.java +++ b/src/main/java/swiss/qpq/gajumaru/core/tools/Grids.java @@ -1,3 +1,23 @@ +/* + * Copyright (c) 2026 QPQ AG . All rights reserved. + * Project: Gajumaru Core Java Libraries + * + * 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 + * + * SPDX-License-Identifier: AGPL-3.0-or-later OR LicenseRef-QPQ-Commercial + */ + package swiss.qpq.gajumaru.core.tools; import java.math.BigInteger; diff --git a/src/main/java/swiss/qpq/gajumaru/core/tools/NodeClient.java b/src/main/java/swiss/qpq/gajumaru/core/tools/NodeClient.java new file mode 100644 index 0000000..144f00d --- /dev/null +++ b/src/main/java/swiss/qpq/gajumaru/core/tools/NodeClient.java @@ -0,0 +1,182 @@ +/* + * Copyright (c) 2026 QPQ AG . All rights reserved. + * Project: Gajumaru Core Java Libraries + * + * 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 + * + * SPDX-License-Identifier: AGPL-3.0-or-later OR LicenseRef-QPQ-Commercial + */ + +package swiss.qpq.gajumaru.core.tools; + +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.Map; +import java.util.HashMap; +import swiss.qpq.gajumaru.core.encoding.ZJ; + + +// NodeClient provides a simple interface to query Gajumaru nodes. +// Ported from hz.erl +// +// Conceptually very similar to Hakuzaru's main interface, but (at least currently) +// more limited (no contract deployment, call, etc.). The most obvious difference is that +// instead of gm-java being a stateful service the way hz can be, this is a pure library +// and the caller is expected to be the one that is keeping track of a list of possible +// endpoints. +// +// This design will evolve in the future as the concept of network and inter-network +// crawling evolved (and obviously as ACs are introduced). In this iteration, though, +// my goal is to keep things as simple as possible, and that is why this is probably +// somewhat unidiomatic for Java (I don't *really* know, but this works). + +public final class NodeClient { + + public static final class Endpoint { + public final String host; + public final int port; + public final boolean useTls; + + public Endpoint(String host, int port) { + this(host, port, false); + } + + public Endpoint(String host, int port, boolean useTls) { + this.host = host; + this.port = port; + this.useTls = useTls; + } + } + + private final List endpoints; + private int timeout = 5000; + + public NodeClient(List endpoints) { + if (endpoints == null || endpoints.isEmpty()) { + throw new IllegalArgumentException("At least one endpoint is required"); + } + this.endpoints = endpoints; + } + + public void setTimeout(int timeout) { + this.timeout = timeout; + } + + public Map status() throws Exception { + return (Map) request("/v3/status", "GET", null); + } + + public long topHeight() throws Exception { + Map res = (Map) request("/v3/headers/top", "GET", null); + return ((Number) res.get("height")).longValue(); + } + + public Map account(String accountId) throws Exception { + return (Map) request("/v3/accounts/" + accountId, "GET", null); + } + + public long nextNonce(String accountId) throws Exception { + try { + Map res = (Map) request("/v3/accounts/" + accountId + "/next-nonce", "GET", null); + return ((Number) res.get("next_nonce")).longValue(); + } catch (Exception e) { + if (e.getMessage() != null && e.getMessage().contains("Account not found")) { + return 1; + } + throw e; + } + } + + public Map postTx(String signedTx) throws Exception { + Map body = new HashMap<>(); + body.put("tx", signedTx); + return (Map) request("/v3/transactions", "POST", body); + } + + public Map tx(String txHash) throws Exception { + return (Map) request("/v3/transactions/" + txHash, "GET", null); + } + + public Map txInfo(String txHash) throws Exception { + return (Map) request("/v3/transactions/" + txHash + "/info", "GET", null); + } + + private Object request(String path, String method, Object body) throws Exception { + Exception lastException = null; + for (Endpoint endpoint : endpoints) { + try { + if (endpoint.useTls) { + try { + return performRequest(endpoint, path, method, body, true); + } catch (Exception e) { + // Fallback to HTTP + return performRequest(endpoint, path, method, body, false); + } + } else { + return performRequest(endpoint, path, method, body, false); + } + } catch (Exception e) { + lastException = e; + } + } + throw new Exception("All endpoints failed. Last error: " + (lastException != null ? lastException.getMessage() : "unknown"), lastException); + } + + private Object performRequest(Endpoint endpoint, String path, String method, Object body, boolean useTls) throws Exception { + String protocol = useTls ? "https://" : "http://"; + String urlStr = protocol + endpoint.host + ":" + endpoint.port + path; + URL url = new URL(urlStr); + HttpURLConnection conn = (HttpURLConnection) url.openConnection(); + conn.setRequestMethod(method); + conn.setConnectTimeout(timeout); + conn.setReadTimeout(timeout); + conn.setRequestProperty("Accept", "application/json"); + + if (body != null) { + conn.setRequestProperty("Content-Type", "application/json"); + conn.setDoOutput(true); + try (OutputStream os = conn.getOutputStream()) { + byte[] input = ZJ.encode(body).getBytes(StandardCharsets.UTF_8); + os.write(input, 0, input.length); + } + } + + int code = conn.getResponseCode(); + StringBuilder response = new StringBuilder(); + try (BufferedReader br = new BufferedReader(new InputStreamReader( + code >= 200 && code < 300 ? conn.getInputStream() : conn.getErrorStream(), + StandardCharsets.UTF_8))) { + String line; + while ((line = br.readLine()) != null) { + response.append(line.trim()); + } + } + + Object decoded = ZJ.decode(response.toString()); + if (code < 200 || code >= 300) { + String reason = "HTTP " + code; + if (decoded instanceof Map) { + Object r = ((Map) decoded).get("reason"); + if (r != null) reason = r.toString(); + } + throw new Exception(reason); + } + return decoded; + } +} diff --git a/src/main/java/swiss/qpq/gajumaru/core/tools/TransactionService.java b/src/main/java/swiss/qpq/gajumaru/core/tools/TransactionService.java new file mode 100644 index 0000000..642415c --- /dev/null +++ b/src/main/java/swiss/qpq/gajumaru/core/tools/TransactionService.java @@ -0,0 +1,81 @@ +/* + * Copyright (c) 2026 QPQ AG . All rights reserved. + * Project: Gajumaru Core Java Libraries + * + * 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 + * + * SPDX-License-Identifier: AGPL-3.0-or-later OR LicenseRef-QPQ-Commercial + */ + +package swiss.qpq.gajumaru.core.tools; + +import java.math.BigInteger; +import java.nio.charset.StandardCharsets; +import java.util.List; +import swiss.qpq.gajumaru.core.crypto.Blake2b; +import swiss.qpq.gajumaru.core.crypto.Ed25519; +import swiss.qpq.gajumaru.core.data.Id; +import swiss.qpq.gajumaru.core.data.SignedTx; +import swiss.qpq.gajumaru.core.data.SpendTx; +import swiss.qpq.gajumaru.core.encoding.ApiEncoder; + +// This is where the gnarly mess used to generate transactions lives for now. +// They might not stay here, but this is where they are for now. + +public final class TransactionService { + + private TransactionService() {} + + public static String buildSpendTx( + String networkId, + String senderId, + String recipientId, + BigInteger amount, + BigInteger gasPrice, + BigInteger gas, + long ttl, + long nonce, + String payload, + byte[] seckey + ) { + // 1. Decode IDs + Id sender = new Id(Id.Tag.ACCOUNT, ApiEncoder.decode(senderId).payload()); + + // Recipient can be ak_ (account) or ct_ (contract) + ApiEncoder.DecodeResult recipientResult = ApiEncoder.decode(recipientId); + Id.Tag recipientTag = recipientResult.type() == ApiEncoder.Type.CONTRACT_PUBKEY ? Id.Tag.CONTRACT : Id.Tag.ACCOUNT; + Id recipient = new Id(recipientTag, recipientResult.payload()); + + // 2. Build SpendTx + byte[] payloadBytes = payload != null ? payload.getBytes(StandardCharsets.UTF_8) : new byte[0]; + SpendTx tx = new SpendTx(sender, recipient, amount, gasPrice, gas, ttl, nonce, payloadBytes); + byte[] serializedTx = tx.serialize(); + + // 3. Prepare NetworkHash + byte[] networkIdBytes = networkId.getBytes(StandardCharsets.UTF_8); + byte[] txHash = Blake2b.hash(serializedTx, 32); + byte[] networkHash = new byte[networkIdBytes.length + txHash.length]; + System.arraycopy(networkIdBytes, 0, networkHash, 0, networkIdBytes.length); + System.arraycopy(txHash, 0, networkHash, networkIdBytes.length, txHash.length); + + // 4. Sign + byte[] signature = Ed25519.sign(seckey, networkHash); + + // 5. Wrap in SignedTx + SignedTx signedTx = new SignedTx(List.of(signature), serializedTx); + + // 6. Encode for API + return ApiEncoder.encode(ApiEncoder.Type.TRANSACTION, signedTx.serialize()); + } +}