May org.json.JSONObject used in org.hyperledger.fabric.contract.execution.JSONTransactionSerializer lead submit payloads not matching?
When I call a submit method from application via gateway, I received an Exception below.
Caused by: org.hyperledger.fabric.client.EndorseException: io.grpc.StatusRuntimeException: ABORTED: failed to collect enough transaction endorsements, see attached details for more info
at org.hyperledger.fabric.client.GatewayClient.endorse(GatewayClient.java:74)
at org.hyperledger.fabric.client.ProposalImpl.endorse(ProposalImpl.java:76)
at org.hyperledger.fabric.client.Proposal.endorse(Proposal.java:64)
at pers.u8f23.fabric.app.api.AbstractAssetContractSubmit.createAsset(AbstractAssetContractSubmit.java:13)
at pers.u8f23.fabric.app.Main.chaincodeOperations(Main.java:84)
at pers.u8f23.fabric.app.Main.main(Main.java:65)
Caused by: io.grpc.StatusRuntimeException: ABORTED: failed to collect enough transaction endorsements, see attached details for more info
at io.grpc.stub.ClientCalls.toStatusRuntimeException(ClientCalls.java:268)
at io.grpc.stub.ClientCalls.getUnchecked(ClientCalls.java:249)
at io.grpc.stub.ClientCalls.blockingUnaryCall(ClientCalls.java:167)
at org.hyperledger.fabric.protos.gateway.GatewayGrpc$GatewayBlockingStub.endorse(GatewayGrpc.java:455)
at org.hyperledger.fabric.client.GatewayClient.endorse(GatewayClient.java:72)
... 5 more
Detail message attached to this Exception is ProposalResponsePayloads do not match.
Returned type is a generic class. Both the generic type and the type argument are defined as type in fabric demo .
public interface AbstractAssetContractSubmit {
Response<Asset> createAsset(Context context, String value);
Response<Void> deleteAsset(Context context, String assetId);
Response<Void> updateAsset(Context context, String assetId, String value);
}
@DataType
public final class Response<T> {
@Property
private final T body;
@Property
private final int code;
@Property
private final String msg;
public Response(@JsonProperty("body") T body, @JsonProperty("code") int code,
@JsonProperty("msg") String msg) {
this.body = body;
this.code = code;
this.msg = msg;
}
public T getBody() {
return this.body;
}
public int getCode() {
return this.code;
}
public String getMsg() {
return this.msg;
}
}
@DataType
public final class Asset {
@Property
private final String id;
@Property
private final String creatorId;
@Property
private final String ownerId;
@Property
private final long createTime;
@Property
private final long lastTransferTime;
@Property
private final long lastUpdateTime;
@Property
private final String assetValue;
public Asset(@JsonProperty("id") String id, @JsonProperty("creatorId") String creatorId,
@JsonProperty("ownerId") String ownerId, @JsonProperty("createTime") long createTime,
@JsonProperty("lastTransferTime") long lastTransferTime,
@JsonProperty("lastUpdateTime") long lastUpdateTime,
@JsonProperty("assetValue") String assetValue) {
this.id = id;
this.creatorId = creatorId;
this.ownerId = ownerId;
this.createTime = createTime;
this.lastTransferTime = lastTransferTime;
this.lastUpdateTime = lastUpdateTime;
this.assetValue = assetValue;
}
public String getId() {
return this.id;
}
public String getCreatorId() {
return this.creatorId;
}
public String getOwnerId() {
return this.ownerId;
}
public long getCreateTime() {
return this.createTime;
}
public long getLastTransferTime() {
return this.lastTransferTime;
}
public long getLastUpdateTime() {
return this.lastUpdateTime;
}
public String getAssetValue() {
return this.assetValue;
}
}
Does this chaincode api support java generic type as method return type? Anyone is welcome to answer this question.
I found below codes from line 72 of org.hyperledger.fabric.contract.execution.JSONTransactionSerializer .
// at this point we can assert that the value is
// representing a complex data type
// so we can get this from
// the type registry, and get the list of propertyNames
// it should have
final DataTypeDefinition dtd = this.typeRegistry.getDataType(ts);
final Set<String> keySet = dtd.getProperties().keySet();
final String[] propNames = keySet.toArray(new String[keySet.size()]);
// Note: whilst the current JSON library does pretty much
// everything is required, this part is hard.
// we want to create a JSON Object based on the value,
// with certain property names.
// Based on the constructors available we need to have a two
// step process, create a JSON Object, then create the object
// we really want based on the propNames
final JSONObject obj = new JSONObject(new JSONObject(value), propNames);
buffer = obj.toString().getBytes(UTF_8);
Those codes seem to be used to serializer complex objects returned by chaincode methods. The given value is processed by org.json.JSONObject, which using java.util.HashMap to obtain key-value pairs and is not sure to make pairs ordered by fixed sequence. I suspect this is a bug.
What logs do you see from the Gateway peer? The logs there should give you a breakdown of what the mismatch was between responses.
I have had a quick look through the implementation and I suspect that you are correct about the built-in JSON serializer not ensuring consistent field ordering in JSON objects. Both the JSON org.json serializer and the fabric-chaincode-shim TypeSchema use unordered maps to store fields.
As a workaround, I would suggest using an alternative serialization solution, such as Jackson with its @JsonPropertyOrder annotation. You could either do the serialization of transaction function return values explicitly in your smart contract code, and declare the transaction function as returning a String, or provide your own serializer implementation that does the same.
@8f235831 just reviewing your problem report again and I notice that you have Response<Asset> createAsset(Context context, String value). I guess this is the transaction function that is failing for your. I also guess that the value parameter here will be used as the assetValue property on the created asset, which is then returned. How are the other asset properties (such as the unique asset id, and the asset createTime field) generated?
Transaction functions must be deterministic. They are typically invoked multiple times on peers belonging to different organisations to gain sufficient endorsements to meet the effective endorsement policy. Every peer on which the transaction function is invoked must generate exactly the same asset properties, otherwise endorsement will fail due to mismatched response messages.
While there might be an issue with JSON field ordering, I am concerned that there is a fundamental problem with the scenario you are using, and that is the likely cause of the error you are seeing. Typically the properties of the asset (including id and createTime) would be defined at the client end and then sent to the smart contract createAsset transaction function. This ensures that all peers executing the transaction function use exactly the same asset field values. In this case there would be no need to return the asset from the createAsset function since the client end created the asset.
Thanks for your reply.
Those properties are assigned with determined values. createTime are assigned with a timestamp from transaction context. Asset id are generated from transaction time and other determained values . There are no random values or functions used in my chaincode.
Now my chaincode functions return String instead of raw Java objects, and I noticed that the problem mentioned at the beginning of the issue has not appeared again.
long createTime = context.getStub().getTxTimestamp().toEpochMilli();
public final class UidUtils {
private UidUtils() {
}
public static String generateUid(Instant instant, String clientId, String assetValue) {
ByteBuffer buffer = ByteBuffer.allocate(Long.BYTES + Integer.BYTES + Short.BYTES * 2);
buffer.putLong(instant.getEpochSecond());
buffer.putInt(instant.getNano());
buffer.putShort(crc16(clientId));
buffer.putShort(crc16(assetValue));
return Base64.getEncoder().encodeToString(buffer.array());
}
private static short crc16(String str) {
byte[] bytes = str.getBytes(StandardCharsets.UTF_8);
int crc = 0xFFFF;
for (byte b : bytes) {
crc = (crc & 0xFF00) | (crc & 0x00FF) ^ (b & 0xFF);
for (int j = 0; j < 8; j++) {
if ((crc & 0x0001) > 0) {
crc = crc >> 1;
crc = crc ^ 0xA001;
} else {
crc = crc >> 1;
}
}
}
return (short) crc;
}
}
Thank you very much for the details and confirmation. In this case, it certainly sounds like you were hitting an issue with determinism in the built-in JSON serializer.