Skip to content

Commit

Permalink
fix: Handle serialization and deserialization of bigint/bytestring wh…
Browse files Browse the repository at this point in the history
…en length > 64 (#398)

* fix: Handle serialization and deserialization of bigint as ByteString

* chore: Remove encodeAsByteString() and only handle BS to BigInt decoding

* fix: #399 Handle encoding/decoding of BigInt and ByteString when length > 64
  • Loading branch information
satran004 authored Aug 1, 2024
1 parent 7a07ace commit d338b9b
Show file tree
Hide file tree
Showing 10 changed files with 396 additions and 23 deletions.
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

0 comments on commit d338b9b

Please sign in to comment.