This commit is contained in:
2026-08-23 18:16:17 +09:00
parent 11516b1cca
commit 20e462ec03
7 changed files with 354 additions and 3 deletions
@@ -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
@@ -1,3 +1,23 @@
/*
* 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;
@@ -1,3 +1,23 @@
/*
* 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;
@@ -1,3 +1,23 @@
/*
* Copyright (c) 2026 QPQ AG <info@qpq.swiss>. All rights reserved.
* Project: Gajumaru Core Java Libraries <gajumaru.io>
*
* This program is dual-licensed:
* 1) Under the GNU Affero General Public License as published by the Free
* Software Foundation, either version 3 of the License, or (at your option)
* any later version (AGPL-3.0-or-later).
*
* 2) Under a commercial/proprietary license available directly from QPQ AG.
* If you wish to use this software outside the strict constraints of the
* AGPLv3 (e.g. within a closed-source or proprietary product), you must
* purchase a commercial license from QPQ AG.
*
* Authors:
* - Craig Everett <craigeverett@qpq.swiss>
*
* SPDX-License-Identifier: AGPL-3.0-or-later OR LicenseRef-QPQ-Commercial
*/
package swiss.qpq.gajumaru.core.encoding;
import java.math.BigDecimal;
@@ -1,3 +1,23 @@
/*
* Copyright (c) 2026 QPQ AG <info@qpq.swiss>. All rights reserved.
* Project: Gajumaru Core Java Libraries <gajumaru.io>
*
* This program is dual-licensed:
* 1) Under the GNU Affero General Public License as published by the Free
* Software Foundation, either version 3 of the License, or (at your option)
* any later version (AGPL-3.0-or-later).
*
* 2) Under a commercial/proprietary license available directly from QPQ AG.
* If you wish to use this software outside the strict constraints of the
* AGPLv3 (e.g. within a closed-source or proprietary product), you must
* purchase a commercial license from QPQ AG.
*
* Authors:
* - Craig Everett <craigeverett@qpq.swiss>
*
* SPDX-License-Identifier: AGPL-3.0-or-later OR LicenseRef-QPQ-Commercial
*/
package swiss.qpq.gajumaru.core.tools;
import java.math.BigInteger;
@@ -0,0 +1,182 @@
/*
* Copyright (c) 2026 QPQ AG <info@qpq.swiss>. All rights reserved.
* Project: Gajumaru Core Java Libraries <gajumaru.io>
*
* This program is dual-licensed:
* 1) Under the GNU Affero General Public License as published by the Free
* Software Foundation, either version 3 of the License, or (at your option)
* any later version (AGPL-3.0-or-later).
*
* 2) Under a commercial/proprietary license available directly from QPQ AG.
* If you wish to use this software outside the strict constraints of the
* AGPLv3 (e.g. within a closed-source or proprietary product), you must
* purchase a commercial license from QPQ AG.
*
* Authors:
* - Craig Everett <craigeverett@qpq.swiss>
*
* SPDX-License-Identifier: AGPL-3.0-or-later OR LicenseRef-QPQ-Commercial
*/
package swiss.qpq.gajumaru.core.tools;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.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<Endpoint> endpoints;
private int timeout = 5000;
public NodeClient(List<Endpoint> 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<String, Object> status() throws Exception {
return (Map<String, Object>) request("/v3/status", "GET", null);
}
public long topHeight() throws Exception {
Map<String, Object> res = (Map<String, Object>) request("/v3/headers/top", "GET", null);
return ((Number) res.get("height")).longValue();
}
public Map<String, Object> account(String accountId) throws Exception {
return (Map<String, Object>) request("/v3/accounts/" + accountId, "GET", null);
}
public long nextNonce(String accountId) throws Exception {
try {
Map<String, Object> res = (Map<String, Object>) request("/v3/accounts/" + accountId + "/next-nonce", "GET", null);
return ((Number) res.get("next_nonce")).longValue();
} catch (Exception e) {
if (e.getMessage() != null && e.getMessage().contains("Account not found")) {
return 1;
}
throw e;
}
}
public Map<String, Object> postTx(String signedTx) throws Exception {
Map<String, String> body = new HashMap<>();
body.put("tx", signedTx);
return (Map<String, Object>) request("/v3/transactions", "POST", body);
}
public Map<String, Object> tx(String txHash) throws Exception {
return (Map<String, Object>) request("/v3/transactions/" + txHash, "GET", null);
}
public Map<String, Object> txInfo(String txHash) throws Exception {
return (Map<String, Object>) request("/v3/transactions/" + txHash + "/info", "GET", null);
}
private Object request(String path, String method, Object body) throws Exception {
Exception lastException = null;
for (Endpoint endpoint : endpoints) {
try {
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;
}
}
@@ -0,0 +1,81 @@
/*
* Copyright (c) 2026 QPQ AG <info@qpq.swiss>. All rights reserved.
* Project: Gajumaru Core Java Libraries <gajumaru.io>
*
* This program is dual-licensed:
* 1) Under the GNU Affero General Public License as published by the Free
* Software Foundation, either version 3 of the License, or (at your option)
* any later version (AGPL-3.0-or-later).
*
* 2) Under a commercial/proprietary license available directly from QPQ AG.
* If you wish to use this software outside the strict constraints of the
* AGPLv3 (e.g. within a closed-source or proprietary product), you must
* purchase a commercial license from QPQ AG.
*
* Authors:
* - Craig Everett <craigeverett@qpq.swiss>
*
* SPDX-License-Identifier: AGPL-3.0-or-later OR LicenseRef-QPQ-Commercial
*/
package swiss.qpq.gajumaru.core.tools;
import java.math.BigInteger;
import java.nio.charset.StandardCharsets;
import java.util.List;
import swiss.qpq.gajumaru.core.crypto.Blake2b;
import swiss.qpq.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());
}
}