Skip to content

feat: dynamic array encode #2184

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

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
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
159 changes: 99 additions & 60 deletions crypto/src/main/java/org/web3j/crypto/StructuredDataEncoder.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Collections;
Expand Down Expand Up @@ -102,7 +101,7 @@ public Set<String> getDependencies(String primaryType) {
String baseDeclarationTypeName =
arrayTypePattern.matcher(declarationFieldTypeName).find()
? declarationFieldTypeName.substring(
0, declarationFieldTypeName.indexOf('['))
0, declarationFieldTypeName.indexOf('['))
: declarationFieldTypeName;
if (!types.containsKey(baseDeclarationTypeName)) {
// Don't expand on non-user defined types
Expand Down Expand Up @@ -307,16 +306,21 @@ public byte[] encodeData(String primaryType, HashMap<String, Object> data)

List<String> encTypes = new ArrayList<>();
List<Object> encValues = new ArrayList<>();
List<byte[]> dynamicData = new ArrayList<>(); // Store dynamic data

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment


// Add typehash
encTypes.add("bytes32");
encValues.add(typeHash(primaryType));



Comment on lines +315 to +316

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why the extra newlines?

// Add field contents
for (StructuredData.Entry field : types.get(primaryType)) {
Object value = data.get(field.getName());

if (value == null) continue;
if (value == null) {
continue;
}
Comment on lines -319 to +323

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unnecessary change?


if (field.getType().equals("string")) {
encTypes.add("bytes32");
Expand All @@ -335,88 +339,123 @@ public byte[] encodeData(String primaryType, HashMap<String, Object> data)
encTypes.add(field.getType());
encValues.add(Numeric.hexStringToByteArray((String) value));
} else if (arrayTypePattern.matcher(field.getType()).find()) {
// Calculate header size (static part)
int headSize = 32;
Comment on lines +342 to +343

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are not calculating the header size, but hardcoding it - so the comment is misleading.
It's better to convert this into a constant like static final HEADER_SIZE just after the class declaration above the constructor at and use it wherever needed.
If you name the constant descriptively enough, the comment can also be let go of.

// Track total size of dynamic data
int dynamicDataSize = 0;
Comment on lines +344 to +345

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment

String baseTypeName = field.getType().substring(0, field.getType().indexOf('['));
List<Object> arrayItems = getArrayItems(field, value);
ByteArrayOutputStream concatenatedArrayEncodingBuffer = new ByteArrayOutputStream();

for (Object arrayItem : arrayItems) {
byte[] arrayItemEncoding;
if (types.containsKey(baseTypeName)) {
arrayItemEncoding =
sha3(
encodeData(
baseTypeName,
(HashMap<String, Object>)
arrayItem)); // need to hash each user type
// before adding
} else {
arrayItemEncoding =
convertToEncodedItem(
baseTypeName,
arrayItem); // add raw item, packed to 32 bytes

if (baseTypeName.startsWith("uint")
|| baseTypeName.startsWith("int")
|| baseTypeName.equals("address")
|| baseTypeName.equals("bool")) {
// Handle dynamic array
encTypes.add(baseTypeName); // Use base type instead of array type
Comment on lines +353 to +354

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems to be AI-generated.
Nevertheless, if this is correct (I don't have much context), the comment here is useless as it talks about a change, which makes sense in the context of this PR, but won't once merged. Should be removed.
Think - how would the code look like once merged?

// Add offset position, considering actual size of all previous dynamic data
encValues.add(BigInteger.valueOf(headSize + dynamicDataSize));

// Prepare dynamic data
ByteArrayOutputStream dynamicBuffer = new ByteArrayOutputStream();
Comment on lines +358 to +359

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment.

// Write array length
byte[] lengthBytes =
Numeric.toBytesPadded(BigInteger.valueOf(arrayItems.size()), 32);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Extract magic number to a constant

dynamicBuffer.write(lengthBytes, 0, lengthBytes.length);

// Write array elements
for (Object arrayItem : arrayItems) {
BigInteger itemValue = convertToBigInt(arrayItem);
byte[] itemBytes = Numeric.toBytesPadded(itemValue, 32);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here ^

dynamicBuffer.write(itemBytes, 0, itemBytes.length);
}

concatenatedArrayEncodingBuffer.write(
arrayItemEncoding, 0, arrayItemEncoding.length);
byte[] dynamicBytes = dynamicBuffer.toByteArray();
dynamicData.add(dynamicBytes);
// Update total size of dynamic data
dynamicDataSize += dynamicBytes.length;
Comment on lines +374 to +375

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment - possibly because of AI

} else {
// Handle other types of arrays
ByteArrayOutputStream concatenatedArrayEncodingBuffer =
new ByteArrayOutputStream();
for (Object arrayItem : arrayItems) {
byte[] arrayItemEncoding;
if (types.containsKey(baseTypeName)) {
arrayItemEncoding =
sha3(
encodeData(
baseTypeName,
(HashMap<String, Object>) arrayItem));
} else {
arrayItemEncoding = convertToEncodedItem(baseTypeName, arrayItem);
}
concatenatedArrayEncodingBuffer.write(
arrayItemEncoding, 0, arrayItemEncoding.length);
}
byte[] concatenatedArrayEncodings =
concatenatedArrayEncodingBuffer.toByteArray();
byte[] hashedValue = sha3(concatenatedArrayEncodings);
encTypes.add("bytes32");
encValues.add(hashedValue);
}

byte[] concatenatedArrayEncodings = concatenatedArrayEncodingBuffer.toByteArray();
byte[] hashedValue = sha3(concatenatedArrayEncodings);
encTypes.add("bytes32");
encValues.add(hashedValue);
} else if (field.getType().startsWith("uint") || field.getType().startsWith("int")) {
encTypes.add(field.getType());
// convert to BigInteger for ABI constructor compatibility
try {
encValues.add(convertToBigInt(value));
} catch (NumberFormatException | NullPointerException e) {
encValues.add(
value); // value null or failed to convert, fallback to add string as
// before
encValues.add(value);
}
} else {
encTypes.add(field.getType());
encValues.add(value);
}
}

// Write all data
ByteArrayOutputStream baos = new ByteArrayOutputStream();

// Write header (static data and offsets)
for (int i = 0; i < encTypes.size(); i++) {
Class<Type> typeClazz = (Class<Type>) AbiTypes.getType(encTypes.get(i));
String type = encTypes.get(i);
Object value = encValues.get(i);

boolean atleastOneConstructorExistsForGivenParametersType = false;
// Using the Reflection API to get the types of the parameters
Constructor[] constructors = typeClazz.getConstructors();
for (Constructor constructor : constructors) {
// Check which constructor matches
try {
Class[] parameterTypes = constructor.getParameterTypes();
byte[] temp =
Numeric.hexStringToByteArray(
TypeEncoder.encode(
typeClazz
.getDeclaredConstructor(parameterTypes)
.newInstance(encValues.get(i))));
baos.write(temp, 0, temp.length);
atleastOneConstructorExistsForGivenParametersType = true;
break;
} catch (IllegalArgumentException
| NoSuchMethodException
| InstantiationException
| IllegalAccessException
| InvocationTargetException ignored) {
if (type.equals("bytes32")) {
if (value instanceof byte[]) {
baos.write((byte[]) value, 0, ((byte[]) value).length);
} else {
throw new RuntimeException("Expected byte[] for bytes32 type");
}
} else {
Class<Type> typeClazz = (Class<Type>) AbiTypes.getType(type);
Constructor[] constructors = typeClazz.getConstructors();
boolean encoded = false;

for (Constructor constructor : constructors) {
try {
Class[] parameterTypes = constructor.getParameterTypes();
byte[] temp =
Numeric.hexStringToByteArray(
TypeEncoder.encode(
typeClazz
.getDeclaredConstructor(parameterTypes)
.newInstance(value)));
baos.write(temp, 0, temp.length);
encoded = true;
break;
} catch (Exception ignored) {
}
}
}

if (!atleastOneConstructorExistsForGivenParametersType) {
throw new RuntimeException(
String.format(
"Received an invalid argument for which no constructor"
+ " exists for the ABI Class %s",
typeClazz.getSimpleName()));
if (!encoded) {
throw new RuntimeException("Failed to encode parameter");
}
}
}

// Write dynamic data
for (byte[] dynamicBytes : dynamicData) {
baos.write(dynamicBytes, 0, dynamicBytes.length);
}

return baos.toByteArray();
}

Expand Down
Loading
Loading