Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: Handle serialization and deserialization of bigint/bytestring when length > 64 #398

Merged
merged 4 commits into from
Aug 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.bloxbean.cardano.client.common.cbor.custom;

import co.nstant.in.cbor.model.ByteString;

import java.util.List;

/**
* This class is used to represent a ByteString which is chunked into multiple byte arrays. Each chunk length is less than or equal to 64 bytes
* This is used in PlutusData serialization/deserialization when the byte array is greater than 64 bytes and need to be chunked.
* For more details, how chunking is done in Cardano, refer the below link
* https://github.com/IntersectMBO/plutus/blob/441b76d9e9745dfedb2afc29920498bdf632f162/plutus-core/plutus-core/src/PlutusCore/Data.hs#L243
*/
public class ChunkedByteString extends ByteString {
private List<byte[]> chunks;

public ChunkedByteString(List<byte[]> chunks) {
super(new byte[0]);
this.chunks = chunks;
setChunked(true);
}

public List<byte[]> getChunks() {
return chunks;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package com.bloxbean.cardano.client.common.cbor.custom;

import co.nstant.in.cbor.CborEncoder;
import co.nstant.in.cbor.CborException;
import co.nstant.in.cbor.encoder.ByteStringEncoder;
import co.nstant.in.cbor.model.ByteString;
import co.nstant.in.cbor.model.MajorType;
import co.nstant.in.cbor.model.SimpleValue;

import java.io.OutputStream;
import java.util.List;

/**
* A custom ByteStringEncoder impl to handle serialization of {@link ChunkedByteString} in PlutusData.
*/
public class CustomByteStringEncoder extends ByteStringEncoder {
public CustomByteStringEncoder(CborEncoder encoder, OutputStream outputStream) {
super(encoder, outputStream);
}

@Override
public void encode(ByteString byteString) throws CborException {
if (byteString instanceof ChunkedByteString) {
List<byte[]> chunks = ((ChunkedByteString) byteString).getChunks();
this.encodeTypeChunked(MajorType.BYTE_STRING);
for (byte[] chunk : chunks) {
this.encode(new ByteString(chunk));
}
this.encoder.encode(SimpleValue.BREAK);
} else {
super.encode(byteString);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,24 @@

import co.nstant.in.cbor.CborEncoder;
import co.nstant.in.cbor.CborException;
import co.nstant.in.cbor.model.DataItem;
import co.nstant.in.cbor.model.Map;
import co.nstant.in.cbor.model.SimpleValue;
import co.nstant.in.cbor.model.Tag;
import co.nstant.in.cbor.model.*;

import java.io.OutputStream;

import static co.nstant.in.cbor.model.MajorType.BYTE_STRING;
import static co.nstant.in.cbor.model.MajorType.MAP;

/**
* A custom CborEncoder impl to handle serialization of {@link SortedMap} when canonical cbor is set in CborEncoder level.
* This class exists to handle the scenario where a Map's key is already sorted by the caller, so
* encoder doesn't need to sort it again even if canonical = true.
*
* It is also used to handle chunked ByteStrings in PlutusData serialization/deserialization.
*/
public class CustomCborEncoder extends CborEncoder {

private CustomMapEncoder customMapEncoder;
private CustomByteStringEncoder chunkByteStringEncoder;

/**
* Initialize a new encoder which writes the binary encoded data to an
Expand All @@ -29,6 +30,7 @@ public class CustomCborEncoder extends CborEncoder {
public CustomCborEncoder(OutputStream outputStream) {
super(outputStream);
this.customMapEncoder = new CustomMapEncoder(this, outputStream);
this.chunkByteStringEncoder = new CustomByteStringEncoder(this, outputStream);
}

/**
Expand All @@ -40,18 +42,25 @@ public CustomCborEncoder(OutputStream outputStream) {
* an problem with the {@link OutputStream}.
*/
public void encode(DataItem dataItem) throws CborException {
//If Map type, handle it here. Otherwise, delegates to default implementation in CborEncoder
if (dataItem == null) {
dataItem = SimpleValue.NULL;
}

//If Map type or ByteString, handle it here. Otherwise, delegates to default implementation in CborEncoder
if (dataItem.getMajorType().equals(MAP)) {
if (dataItem == null) {
dataItem = SimpleValue.NULL;
if (dataItem.hasTag()) {
Tag tagDi = dataItem.getTag();
encode(tagDi);
}

customMapEncoder.encode((Map) dataItem);
} else if (dataItem.getMajorType().equals(BYTE_STRING)) {
if (dataItem.hasTag()) {
Tag tagDi = dataItem.getTag();
encode(tagDi);
}

customMapEncoder.encode((Map) dataItem);
chunkByteStringEncoder.encode((ByteString) dataItem);
} else {
super.encode(dataItem);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
package com.bloxbean.cardano.client.plutus.spec;

import co.nstant.in.cbor.model.DataItem;
import co.nstant.in.cbor.model.NegativeInteger;
import co.nstant.in.cbor.model.*;
import co.nstant.in.cbor.model.Number;
import co.nstant.in.cbor.model.UnsignedInteger;
import com.bloxbean.cardano.client.exception.CborDeserializationException;
import com.bloxbean.cardano.client.common.cbor.custom.ChunkedByteString;
import com.bloxbean.cardano.client.exception.CborSerializationException;
import com.bloxbean.cardano.client.plutus.spec.serializers.BigIntDataJsonDeserializer;
import com.bloxbean.cardano.client.plutus.spec.serializers.BigIntDataJsonSerializer;
Expand All @@ -14,6 +12,8 @@

import java.math.BigInteger;

import static com.bloxbean.cardano.client.plutus.util.Bytes.getChunks;

@Getter
@AllArgsConstructor
@NoArgsConstructor
Expand All @@ -24,13 +24,33 @@
public class BigIntPlutusData implements PlutusData {
private BigInteger value;

public static BigIntPlutusData deserialize(Number numberDI) throws CborDeserializationException {
public static BigIntPlutusData deserialize(Number numberDI) {
if (numberDI == null)
return null;

return new BigIntPlutusData(numberDI.getValue());
}

public static BigIntPlutusData deserialize(ByteString byteString) {
if (byteString == null)
return null;

var tag = byteString.getTag();
if (tag != null) {
switch ((int) tag.getValue()) {
case BIG_UINT_TAG:
return BigIntPlutusData.of(new BigInteger(1, byteString.getBytes()));
case BIG_NINT_TAG:
return BigIntPlutusData.of(MINUS_ONE.subtract(new BigInteger(1, byteString.getBytes())));
default:
throw new IllegalArgumentException("Invalid tag for BigIntPlutusData");
}
} else {
throw new IllegalArgumentException("Missing tag for BigIntPlutusData");
}
}


public static BigIntPlutusData of(int i) {
return new BigIntPlutusData(BigInteger.valueOf(i));
}
Expand All @@ -47,13 +67,33 @@ public static BigIntPlutusData of(BigInteger b) {
public DataItem serialize() throws CborSerializationException {
DataItem di = null;
if (value != null) {
if (value.compareTo(BigInteger.ZERO) == 0 || value.compareTo(BigInteger.ZERO) == 1) {
di = new UnsignedInteger(value);
if (value.bitLength() <= BYTES_LIMIT) {
if (value.signum() >= 0) {
di = new UnsignedInteger(value);
} else {
di = new NegativeInteger(value);
}
} else {
di = new NegativeInteger(value);
byte[] bytes = value.toByteArray();
if (value.signum() < 0) {
bytes = negateBytes(bytes);
di = new ChunkedByteString(getChunks(bytes, BYTES_LIMIT));
di.setTag(BIG_NINT_TAG);
} else {
di = new ChunkedByteString(getChunks(bytes, BYTES_LIMIT));
di.setTag(BIG_UINT_TAG);
}
}
}

return di;
}

private byte[] negateBytes(byte[] bytes) {
for (int i = 0; i < bytes.length; i++) {
bytes[i] = (byte) ~bytes[i];
}
return bytes;
}

}
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
package com.bloxbean.cardano.client.plutus.spec;

import co.nstant.in.cbor.model.ByteString;
import co.nstant.in.cbor.model.DataItem;
import co.nstant.in.cbor.model.UnicodeString;
import co.nstant.in.cbor.model.*;
import com.bloxbean.cardano.client.common.cbor.custom.ChunkedByteString;
import com.bloxbean.cardano.client.exception.CborDeserializationException;
import com.bloxbean.cardano.client.exception.CborSerializationException;
import com.bloxbean.cardano.client.plutus.spec.serializers.BytesDataJsonDeserializer;
Expand All @@ -13,6 +12,8 @@

import java.nio.charset.StandardCharsets;

import static com.bloxbean.cardano.client.plutus.util.Bytes.getChunks;

@Getter
@AllArgsConstructor
@NoArgsConstructor
Expand All @@ -38,11 +39,16 @@ public static BytesPlutusData of(String str) {
return new BytesPlutusData(str.getBytes(StandardCharsets.UTF_8));
}

//https://github.com/IntersectMBO/plutus/blob/441b76d9e9745dfedb2afc29920498bdf632f162/plutus-core/plutus-core/src/PlutusCore/Data.hs#L243
@Override
public DataItem serialize() throws CborSerializationException {
DataItem di = null;
if (value != null) {
di = new ByteString(value);
if (value.length <= BYTES_LIMIT) {
di = new ByteString(value);
} else { //More than 64, chunk it
di = new ChunkedByteString(getChunks(value, BYTES_LIMIT));
}
}

return di;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,15 @@
import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.NonNull;

import java.math.BigInteger;

public interface PlutusData {
int BIG_UINT_TAG = 2;
int BIG_NINT_TAG = 3;
BigInteger MINUS_ONE = BigInteger.valueOf(-1);

int BYTES_LIMIT = 64;


// plutus_data = ; New
// constr<plutus_data>
Expand Down Expand Up @@ -41,7 +49,13 @@ static PlutusData deserialize(DataItem dataItem) throws CborDeserializationExcep
if (dataItem instanceof Number) {
return BigIntPlutusData.deserialize((Number) dataItem);
} else if (dataItem instanceof ByteString) {
return BytesPlutusData.deserialize((ByteString) dataItem);
var tag = dataItem.getTag();
if (tag != null &&
(tag.getValue() == BIG_UINT_TAG || tag.getValue() == BIG_NINT_TAG)){
return BigIntPlutusData.deserialize((ByteString) dataItem);
} else {
return BytesPlutusData.deserialize((ByteString) dataItem);
}
} else if (dataItem instanceof UnicodeString) {
return BytesPlutusData.deserialize(((UnicodeString) dataItem));
} else if (dataItem instanceof Array) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package com.bloxbean.cardano.client.plutus.util;

import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;

class Bytes {
public class Bytes {
public static byte[] concat(byte[]... arrays) {
int totalLength = 0;
for (byte[] array : arrays) {
Expand All @@ -16,4 +18,19 @@ public static byte[] concat(byte[]... arrays) {

return byteBuffer.array();
}

public static List<byte[]> getChunks(byte[] b, int maxChunkSize) {
List<byte[]> result = new ArrayList<>();

ByteBuffer buffer = ByteBuffer.wrap(b);

while (buffer.remaining() > 0) {
int chunkSize = Math.min(buffer.remaining(), maxChunkSize);
byte[] chunk = new byte[chunkSize];
buffer.get(chunk);
result.add(chunk);
}

return result;
}
}
Loading
Loading