Add ASN.1-to-Java codegen and RLP codec for spend/signed tx subset.
Generate typed records from GajumaruChainObjects.asn, implement Asn1Rlp and BasicEncoders matching Erlang static serialization, and add golden-vector equivalence tests against gmser_chain_objects output.
This commit is contained in:
@@ -0,0 +1,15 @@
|
||||
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
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,213 @@
|
||||
/*
|
||||
* 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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
/*
|
||||
* 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));
|
||||
};
|
||||
}
|
||||
}
|
||||
+221
@@ -0,0 +1,221 @@
|
||||
/*
|
||||
* 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, <<1:256>>),
|
||||
* Recip = gmser_id:create(account, <<2:256>>),
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user