-
-
Notifications
You must be signed in to change notification settings - Fork 1.8k
fix: dynamic array encoding #2174
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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; | ||
|
|
@@ -307,11 +306,25 @@ 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 | ||
|
|
||
| // Add typehash | ||
| encTypes.add("bytes32"); | ||
| encValues.add(typeHash(primaryType)); | ||
|
|
||
| // Calculate header size (static part) | ||
| int headSize = 32; // typehash | ||
| for (StructuredData.Entry field : types.get(primaryType)) { | ||
| if (field.getType().contains("[]")) { | ||
| headSize += 32; // Dynamic array offset takes 32 bytes | ||
| } else { | ||
| headSize += 32; // Static type takes 32 bytes | ||
| } | ||
| } | ||
|
|
||
| // Track total size of dynamic data | ||
| int dynamicDataSize = 0; | ||
|
|
||
| // Add field contents | ||
| for (StructuredData.Entry field : types.get(primaryType)) { | ||
| Object value = data.get(field.getName()); | ||
|
|
@@ -337,86 +350,117 @@ public byte[] encodeData(String primaryType, HashMap<String, Object> data) | |
| } else if (arrayTypePattern.matcher(field.getType()).find()) { | ||
| 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 | ||
| // Add offset position, considering actual size of all previous dynamic data | ||
| encValues.add(BigInteger.valueOf(headSize + dynamicDataSize)); | ||
|
|
||
| // Prepare dynamic data | ||
| ByteArrayOutputStream dynamicBuffer = new ByteArrayOutputStream(); | ||
| // Write array length | ||
| byte[] lengthBytes = | ||
| Numeric.toBytesPadded(BigInteger.valueOf(arrayItems.size()), 32); | ||
| dynamicBuffer.write(lengthBytes, 0, lengthBytes.length); | ||
|
|
||
| // Write array elements | ||
| for (Object arrayItem : arrayItems) { | ||
| BigInteger itemValue = convertToBigInt(arrayItem); | ||
| byte[] itemBytes = Numeric.toBytesPadded(itemValue, 32); | ||
| 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; | ||
| } 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) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this should be handled |
||
| } | ||
| } | ||
| } | ||
|
|
||
| if (!atleastOneConstructorExistsForGivenParametersType) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. please keep the same old check, it gives more context than "decoded" |
||
| 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(); | ||
| } | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,100 @@ | ||
| /* | ||
| * Copyright 2025 Web3 Labs Ltd. | ||
| * | ||
| * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with | ||
| * the License. You may obtain a copy of the License at | ||
| * | ||
| * http://www.apache.org/licenses/LICENSE-2.0 | ||
| * | ||
| * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on | ||
| * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the | ||
| * specific language governing permissions and limitations under the License. | ||
| */ | ||
| package org.web3j.crypto; | ||
|
|
||
| import java.io.ByteArrayOutputStream; | ||
| import java.util.HashMap; | ||
|
|
||
| import org.junit.jupiter.api.Test; | ||
|
|
||
| import org.web3j.utils.Numeric; | ||
|
|
||
| import static org.junit.jupiter.api.Assertions.*; | ||
|
|
||
| public class StructuredDataEncoderTest { | ||
|
|
||
| @Test | ||
| public void testCustomEncodeData() throws Exception { | ||
|
|
||
| // json | ||
|
|
||
| String jsonMessage = | ||
| """ | ||
| { | ||
| "domain" : { | ||
| "chainId" : "84532", | ||
| "name" : "Test", | ||
| "verifyingContract" : "0xe483dea6aa7d3831173379d81e5c08874f1042e7", | ||
| "version" : "1.0.0" | ||
| }, | ||
| "message" : { | ||
| "amounts" : [ 0, 0 ], | ||
| "to" : "0xe483dea6aa7d3831173379d81e5c08874f1042e7", | ||
| "tokenIds" : [ 0, 1 ], | ||
| "validityEndTimestamp" : 1743005854, | ||
| "validityStartTimestamp" : 1742919454, | ||
| "salt" : "82" | ||
| }, | ||
| "primaryType" : "ClaimRequest", | ||
| "types" : { | ||
| "ClaimRequest" : [ { | ||
| "name" : "to", | ||
| "type" : "address" | ||
| }, { | ||
| "name" : "tokenIds", | ||
| "type" : "uint256[]" | ||
| }, { | ||
| "name" : "amounts", | ||
| "type" : "uint256[]" | ||
| }, { | ||
| "name" : "validityStartTimestamp", | ||
| "type" : "uint128" | ||
| }, { | ||
| "name" : "validityEndTimestamp", | ||
| "type" : "uint128" | ||
| }, { | ||
| "name" : "salt", | ||
| "type" : "uint256" | ||
| } ], | ||
| "EIP712Domain" : [ { | ||
| "name" : "name", | ||
| "type" : "string" | ||
| }, { | ||
| "name" : "version", | ||
| "type" : "string" | ||
| }, { | ||
| "name" : "chainId", | ||
| "type" : "uint256" | ||
| }, { | ||
| "name" : "verifyingContract", | ||
| "type" : "address" | ||
| } ] | ||
| } | ||
| } | ||
| """; | ||
|
|
||
| System.out.println("JSON Message: " + jsonMessage); | ||
|
|
||
| // struct instance | ||
| StructuredDataEncoder encoder = new StructuredDataEncoder(jsonMessage); | ||
| encoder.hashStructuredData(); | ||
| ByteArrayOutputStream baos = new ByteArrayOutputStream(); | ||
|
|
||
| byte[] dataHash = | ||
| encoder.encodeData( | ||
| encoder.jsonMessageObject.getPrimaryType(), | ||
| (HashMap<String, Object>) encoder.jsonMessageObject.getMessage()); | ||
| System.out.println("################ hash is " + Numeric.toHexString(dataHash)); | ||
| baos.write(dataHash, 0, dataHash.length); | ||
|
Comment on lines
+86
to
+98
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This has nothing to do with a unit test. Please create a proper test smart contract abi and assert the expected output of the encode result |
||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the check here makes no sense as the same line of code is executed every time