diff --git a/gajumaru-core/src/Testinator.java b/gajumaru-core/src/Testinator.java index 5305976..bacf7a5 100644 --- a/gajumaru-core/src/Testinator.java +++ b/gajumaru-core/src/Testinator.java @@ -20,6 +20,9 @@ public class Testinator { 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()); @@ -53,4 +56,14 @@ public class Testinator { 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(); + } } diff --git a/gajumaru-core/src/main/java/swiss/qpq/gajumaru/core/encoding/RLP.java b/gajumaru-core/src/main/java/swiss/qpq/gajumaru/core/encoding/RLP.java new file mode 100644 index 0000000..5488dc1 --- /dev/null +++ b/gajumaru-core/src/main/java/swiss/qpq/gajumaru/core/encoding/RLP.java @@ -0,0 +1,191 @@ +/* + * 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.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 items; + public RLP_List(List 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 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 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; + } +} diff --git a/gajumaru-core/test/src/gmt.erl b/gajumaru-core/test/src/gmt.erl index 2846371..e706c6d 100644 --- a/gajumaru-core/test/src/gmt.erl +++ b/gajumaru-core/test/src/gmt.erl @@ -13,7 +13,8 @@ mods() -> #{"base64" => fun base64/0, - "base58" => fun base58/0}. + "base58" => fun base58/0, + "rlp" => fun rlp/0}. -spec start(ArgV) -> ok @@ -99,8 +100,8 @@ base58() -> ok = filelib:ensure_dir(TestFile), ok = file:write_file(TestFile, rand:bytes(rand:uniform(5000))), {ok, Bytes} = file:read_file(TestFile), - Base64 = base58:binary_to_base58(Bytes), - ok = file:write_file(ConvFile, Base64), + 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), @@ -112,3 +113,23 @@ base58() -> 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.