From aa3dd91fc35d750b4786494102034e1ee6e5dd33 Mon Sep 17 00:00:00 2001 From: 0xstride <73103011+fengjy73@users.noreply.github.com> Date: Thu, 30 Jul 2026 17:01:06 +0800 Subject: [PATCH 1/3] Fix platform UCP reporting compatibility --- .../receiver/handler/AsyncReceiveHandler.java | 11 +- .../service/report/UcpReportJsonBuilder.java | 107 +++++++++++++++--- .../report/UcpReportJsonBuilderTest.java | 66 ++++++++++- .../commons/utils/codec/EvmCoderUtil.java | 24 +++- .../bridge/commons/EvmCoderUtilTest.java | 49 ++++++++ 5 files changed, 235 insertions(+), 22 deletions(-) create mode 100644 acb-sdk/antchain-bridge-commons/src/test/java/com/alipay/antchain/bridge/commons/EvmCoderUtilTest.java diff --git a/acb-relayer/r-core/src/main/java/com/alipay/antchain/bridge/relayer/core/service/receiver/handler/AsyncReceiveHandler.java b/acb-relayer/r-core/src/main/java/com/alipay/antchain/bridge/relayer/core/service/receiver/handler/AsyncReceiveHandler.java index b48d393f..e4ac3511 100644 --- a/acb-relayer/r-core/src/main/java/com/alipay/antchain/bridge/relayer/core/service/receiver/handler/AsyncReceiveHandler.java +++ b/acb-relayer/r-core/src/main/java/com/alipay/antchain/bridge/relayer/core/service/receiver/handler/AsyncReceiveHandler.java @@ -46,11 +46,6 @@ public class AsyncReceiveHandler { private PlatformReportClient platformReportClient; public void receiveUniformCrosschainPackets(List ucpContexts) { - - ucpContexts.stream() - .filter(context -> !context.isFromNetwork()) - .forEach(platformReportClient::reportUcp); - int rowsNum = crossChainMessageRepository.putUniformCrosschainPackets(ucpContexts); if (ucpContexts.size() != rowsNum) { throw new RuntimeException( @@ -61,6 +56,12 @@ public void receiveUniformCrosschainPackets(List ); } log.info("put PENDING UCPs [ {} ] to pool success", ucpContexts.stream().map(UniformCrosschainPacketContext::getUcpId).reduce((s, s2) -> s + ", " + s2).orElse("")); + + // External reporting must never precede the Relayer's own durable UCP record. + // PlatformReportClient contains all network/build failures, so the core receive path stays valid. + ucpContexts.stream() + .filter(context -> !context.isFromNetwork()) + .forEach(platformReportClient::reportUcp); } public void receiveAuthMessages(List authMsgWrappers) { diff --git a/acb-relayer/r-core/src/main/java/com/alipay/antchain/bridge/relayer/core/service/report/UcpReportJsonBuilder.java b/acb-relayer/r-core/src/main/java/com/alipay/antchain/bridge/relayer/core/service/report/UcpReportJsonBuilder.java index bf9e7e72..76b64bfe 100644 --- a/acb-relayer/r-core/src/main/java/com/alipay/antchain/bridge/relayer/core/service/report/UcpReportJsonBuilder.java +++ b/acb-relayer/r-core/src/main/java/com/alipay/antchain/bridge/relayer/core/service/report/UcpReportJsonBuilder.java @@ -5,6 +5,7 @@ import java.nio.charset.CodingErrorAction; import java.nio.charset.StandardCharsets; import java.time.Instant; +import java.util.Base64; import java.util.List; import cn.hutool.core.util.HexUtil; @@ -33,47 +34,54 @@ @Component public class UcpReportJsonBuilder { + private static final int MAX_MONITOR_MESSAGE_BYTES = 4 * 1024 * 1024; + public JSONObject build(UniformCrosschainPacketContext context) { JSONObject body = new JSONObject(true); body.put("ucpId", context.getUcpId()); - body.put("rawUcp", hex(context.getUcp().encode())); + byte[] rawUcp = context.getUcp().encode(); + body.put("rawUcpBase64", Base64.getEncoder().encodeToString(rawUcp)); + // Retained for older receivers while rawUcpBase64 is the current API contract. + body.put("rawUcp", hex(rawUcp)); JSONObject source = new JSONObject(true); source.put("product", context.getProduct()); source.put("blockchainId", context.getBlockchainId()); source.put("domain", context.getSrcDomain()); body.put("source", source); - body.put("ucp", buildUcp(context.getUcp())); + JSONObject ucp = buildUcp(context.getUcp(), isMonitorProduct(context.getProduct())); + body.put("ucp", ucp); + addCompatibilityMessageFields(body, ucp); return body; } - private JSONObject buildUcp(UniformCrosschainPacket ucp) { + private JSONObject buildUcp(UniformCrosschainPacket ucp, boolean monitorProduct) { JSONObject json = new JSONObject(true); json.put("version", ucp.getVersion()); json.put("srcDomain", ucp.getSrcDomain().getDomain()); - json.put("srcMessage", buildCrossChainMessage(ucp.getSrcMessage())); + json.put("srcMessage", buildCrossChainMessage(ucp.getSrcMessage(), monitorProduct)); json.put("ptcId", buildObjectIdentity(ucp.getPtcId())); json.put("tpProof", buildThirdPartyProof(ucp.getTpProof())); return json; } - private JSONObject buildCrossChainMessage(CrossChainMessage message) { + private JSONObject buildCrossChainMessage(CrossChainMessage message, boolean monitorProduct) { JSONObject json = new JSONObject(true); json.put("type", message.getType().name()); - json.put("message", buildMessageBody(message)); + json.put("message", buildMessageBody(message, monitorProduct)); json.put("provableData", buildProvableData(message.getProvableData())); return json; } - private Object buildMessageBody(CrossChainMessage message) { + private Object buildMessageBody(CrossChainMessage message, boolean monitorProduct) { if (message.getType() != CrossChainMessage.CrossChainMessageType.AUTH_MSG) { return parseOpaqueData(message.getMessage()); } - JSONObject authMessage = tryBuildAuthMessage(message.getMessage()); + JSONObject authMessage = tryBuildAuthMessage(message.getMessage(), monitorProduct); return ObjectUtil.isNull(authMessage) ? parseOpaqueData(message.getMessage()) : authMessage; } - private JSONObject tryBuildAuthMessage(byte[] rawMessage) { + private JSONObject tryBuildAuthMessage(byte[] rawMessage, boolean monitorProduct) { try { IAuthMessage authMessage = AuthMessageFactory.createAuthMessage(rawMessage); JSONObject json = new JSONObject(true); @@ -83,7 +91,7 @@ private JSONObject tryBuildAuthMessage(byte[] rawMessage) { if (authMessage instanceof AuthMessageV2) { json.put("trustLevel", ((AuthMessageV2) authMessage).getTrustLevel().name()); } - json.put("payload", buildAuthPayload(authMessage)); + json.put("payload", buildAuthPayload(authMessage, monitorProduct)); JSONObject result = new JSONObject(true); result.put("authMessage", json); @@ -93,7 +101,7 @@ private JSONObject tryBuildAuthMessage(byte[] rawMessage) { } } - private Object buildAuthPayload(IAuthMessage authMessage) { + private Object buildAuthPayload(IAuthMessage authMessage, boolean monitorProduct) { if (authMessage.getUpperProtocol() != 0) { return parseOpaqueData(authMessage.getPayload()); } @@ -114,7 +122,7 @@ private Object buildAuthPayload(IAuthMessage authMessage) { sdpJson.put("timeoutMeasure", sdpMessage.getTimeoutMeasure().name()); sdpJson.put("timeout", sdpMessage.getTimeout().toString()); } - sdpJson.put("payload", buildSdpPayload(sdpMessage.getPayload())); + sdpJson.put("payload", buildSdpPayload(sdpMessage.getPayload(), monitorProduct)); JSONObject result = new JSONObject(true); result.put("sdpMessage", sdpJson); @@ -124,7 +132,10 @@ private Object buildAuthPayload(IAuthMessage authMessage) { } } - private Object buildSdpPayload(byte[] payload) { + private Object buildSdpPayload(byte[] payload, boolean monitorProduct) { + if (!monitorProduct || !isStructurallyValidMonitorMessage(payload)) { + return parseOpaqueData(payload); + } try { IMonitorMessage monitorMessage = MonitorMessageFactory.createMonitorMessage(payload); if (monitorMessage.getMonitorType() < 1 || monitorMessage.getMonitorType() > 4) { @@ -144,6 +155,74 @@ private Object buildSdpPayload(byte[] payload) { } } + private boolean isMonitorProduct(String product) { + return "dioxide2".equalsIgnoreCase(product) || "ethereum3".equalsIgnoreCase(product); + } + + private boolean isStructurallyValidMonitorMessage(byte[] payload) { + if (ObjectUtil.isNull(payload) + || payload.length < 68 + || payload.length > MAX_MONITOR_MESSAGE_BYTES) { + return false; + } + int monitorType = readInt(payload, payload.length - 4); + if (monitorType < 1 || monitorType > 4) { + return false; + } + int offset = payload.length - 4; + offset = previousVarBytesOffset(payload, offset); + if (offset < 0) { + return false; + } + offset = previousVarBytesOffset(payload, offset); + return offset == 0; + } + + private int previousVarBytesOffset(byte[] payload, int offset) { + if (offset < 32 || offset > payload.length) { + return -1; + } + int length = readInt(payload, offset - 4); + if (length < 0 || length > MAX_MONITOR_MESSAGE_BYTES) { + return -1; + } + long paddedLength = ((long) length + 31L) / 32L * 32L; + long previousOffset = (long) offset - 32L - paddedLength; + return previousOffset < 0L ? -1 : (int) previousOffset; + } + + private int readInt(byte[] value, int offset) { + return ((value[offset] & 0xff) << 24) + | ((value[offset + 1] & 0xff) << 16) + | ((value[offset + 2] & 0xff) << 8) + | (value[offset + 3] & 0xff); + } + + private void addCompatibilityMessageFields(JSONObject body, JSONObject ucp) { + JSONObject srcMessage = ucp.getJSONObject("srcMessage"); + if (ObjectUtil.isNull(srcMessage)) { + return; + } + Object messageValue = srcMessage.get("message"); + if (!(messageValue instanceof JSONObject)) { + return; + } + JSONObject message = (JSONObject) messageValue; + JSONObject authMessage = message.getJSONObject("authMessage"); + if (ObjectUtil.isNull(authMessage)) { + return; + } + body.put("am", authMessage); + Object authPayloadValue = authMessage.get("payload"); + if (authPayloadValue instanceof JSONObject) { + JSONObject authPayload = (JSONObject) authPayloadValue; + JSONObject sdpMessage = authPayload.getJSONObject("sdpMessage"); + if (ObjectUtil.isNotNull(sdpMessage)) { + body.put("sdp", sdpMessage); + } + } + } + private JSONObject buildProvableData(CrossChainMessage.ProvableLedgerData data) { if (ObjectUtil.isNull(data)) { return null; @@ -177,7 +256,7 @@ private JSONObject buildThirdPartyProof(ThirdPartyProof proof) { json.put("tpbtaVersion", proof.getTpbtaVersion()); JSONObject resp = new JSONObject(true); if (ObjectUtil.isNotNull(proof.getResp())) { - JSONObject authMessage = tryBuildAuthMessage(proof.getResp().getBody()); + JSONObject authMessage = tryBuildAuthMessage(proof.getResp().getBody(), false); resp.put( "body", ObjectUtil.isNull(authMessage) ? parseOpaqueData(proof.getResp().getBody()) : authMessage diff --git a/acb-relayer/r-core/src/test/java/com/alipay/antchain/bridge/relayer/core/service/report/UcpReportJsonBuilderTest.java b/acb-relayer/r-core/src/test/java/com/alipay/antchain/bridge/relayer/core/service/report/UcpReportJsonBuilderTest.java index 6a086310..9f868fec 100644 --- a/acb-relayer/r-core/src/test/java/com/alipay/antchain/bridge/relayer/core/service/report/UcpReportJsonBuilderTest.java +++ b/acb-relayer/r-core/src/test/java/com/alipay/antchain/bridge/relayer/core/service/report/UcpReportJsonBuilderTest.java @@ -2,6 +2,7 @@ import java.math.BigInteger; import java.nio.charset.StandardCharsets; +import java.util.Base64; import java.util.Collections; import cn.hutool.core.util.HexUtil; @@ -61,7 +62,7 @@ public void testBuildMonitoredUcp() { ); UniformCrosschainPacketContext context = new UniformCrosschainPacketContext(); context.setUcpId("ucp-test"); - context.setProduct("test-product"); + context.setProduct("dioxide2"); context.setBlockchainId("test-chain"); context.setUcp(new UniformCrosschainPacket(new CrossChainDomain("source.example"), crossChainMessage, null)); @@ -74,6 +75,12 @@ public void testBuildMonitoredUcp() { Assert.assertEquals("ucp-test", body.getString("ucpId")); Assert.assertEquals(HexUtil.encodeHexStr(context.getUcp().encode()), body.getString("rawUcp")); + Assert.assertEquals( + Base64.getEncoder().encodeToString(context.getUcp().encode()), + body.getString("rawUcpBase64") + ); + Assert.assertEquals(1, body.getJSONObject("am").getIntValue("version")); + Assert.assertEquals("target.example", body.getJSONObject("sdp").getString("targetDomain")); Assert.assertEquals("source.example", ucp.getString("srcDomain")); Assert.assertEquals("AUTH_MSG", srcMessage.getString("type")); Assert.assertEquals("target.example", sdp.getString("targetDomain")); @@ -85,6 +92,63 @@ public void testBuildMonitoredUcp() { Assert.assertFalse(body.toJSONString().contains("\"rawHex\"")); } + @Test + public void testMalformedMonitorLengthFallsBackWithoutAllocation() { + byte[] malformedPayload = new byte[68]; + malformedPayload[60] = 0x7f; + malformedPayload[61] = (byte) 0xff; + malformedPayload[62] = (byte) 0xff; + malformedPayload[63] = (byte) 0xff; + malformedPayload[67] = 1; + + SDPMessageV1 sdpMessage = new SDPMessageV1(); + sdpMessage.setTargetDomain(new CrossChainDomain("target.example")); + sdpMessage.setTargetIdentity(new CrossChainIdentity(new byte[32])); + sdpMessage.setSequence(-1); + sdpMessage.setPayload(malformedPayload); + + AuthMessageV1 authMessage = new AuthMessageV1(); + authMessage.setIdentity(new CrossChainIdentity(new byte[32])); + authMessage.setUpperProtocol(0); + authMessage.setPayload(sdpMessage.encode()); + + UniformCrosschainPacketContext context = buildContext(createAuthMessage(authMessage)); + context.setProduct("dioxide2"); + Object payload = new UcpReportJsonBuilder().build(context) + .getJSONObject("sdp") + .get("payload"); + + Assert.assertEquals(HexUtil.encodeHexStr(malformedPayload), payload); + } + + @Test + public void testUnmonitoredProductTreatsMonitorEnvelopeAsOpaque() { + MonitorMessageV1 monitorMessage = new MonitorMessageV1(); + monitorMessage.setMonitorType(2); + monitorMessage.setMonitorMsg("rule-hit"); + monitorMessage.setPayload("business-data".getBytes(StandardCharsets.UTF_8)); + + SDPMessageV1 sdpMessage = new SDPMessageV1(); + sdpMessage.setTargetDomain(new CrossChainDomain("target.example")); + sdpMessage.setTargetIdentity(new CrossChainIdentity(new byte[32])); + sdpMessage.setSequence(-1); + sdpMessage.setPayload(monitorMessage.encode()); + + AuthMessageV1 authMessage = new AuthMessageV1(); + authMessage.setIdentity(new CrossChainIdentity(new byte[32])); + authMessage.setUpperProtocol(0); + authMessage.setPayload(sdpMessage.encode()); + + UniformCrosschainPacketContext context = buildContext(createAuthMessage(authMessage)); + context.setProduct("dioxide"); + Object payload = new UcpReportJsonBuilder().build(context) + .getJSONObject("sdp") + .get("payload"); + + Assert.assertTrue(payload instanceof String); + Assert.assertFalse(String.valueOf(payload).contains("monitorMessage")); + } + @Test public void testBuildJsonLedgerDataAndBinaryFallback() { CrossChainMessage crossChainMessage = CrossChainMessage.createCrossChainMessage( diff --git a/acb-sdk/antchain-bridge-commons/src/main/java/com/alipay/antchain/bridge/commons/utils/codec/EvmCoderUtil.java b/acb-sdk/antchain-bridge-commons/src/main/java/com/alipay/antchain/bridge/commons/utils/codec/EvmCoderUtil.java index 78a815ae..02c0f1d7 100644 --- a/acb-sdk/antchain-bridge-commons/src/main/java/com/alipay/antchain/bridge/commons/utils/codec/EvmCoderUtil.java +++ b/acb-sdk/antchain-bridge-commons/src/main/java/com/alipay/antchain/bridge/commons/utils/codec/EvmCoderUtil.java @@ -23,14 +23,34 @@ public class EvmCoderUtil { public static CoderResult parseVarBytes(byte[] rawMessage, int offset) { + if (rawMessage == null) { + throw new IllegalArgumentException("raw message is null"); + } + if (offset < 32 || offset > rawMessage.length) { + throw new IllegalArgumentException("invalid EVM var-bytes offset: " + offset); + } + offset -= 4; byte[] rawLen = new byte[4]; System.arraycopy(rawMessage, offset, rawLen, 0, 4); + int rawLength = ByteUtil.bytesToInt(rawLen, ByteOrder.BIG_ENDIAN); + if (rawLength < 0) { + throw new IllegalArgumentException("negative EVM var-bytes length"); + } + + long evmWordCount = ((long) rawLength + 31L) / 32L; + long encodedLength = 32L + evmWordCount * 32L; + long originalOffset = (long) offset + 4L; + if (encodedLength > originalOffset) { + throw new IllegalArgumentException( + "EVM var-bytes length " + rawLength + " exceeds available input" + ); + } offset -= 28; - byte[] raw = new byte[ByteUtil.bytesToInt(rawLen, ByteOrder.BIG_ENDIAN)]; + byte[] raw = new byte[rawLength]; - int evmWordCnt = calcEvmWordNum(raw.length); + int evmWordCnt = (int) evmWordCount; int index = 0; while (evmWordCnt-- > 0) { offset -= 32; diff --git a/acb-sdk/antchain-bridge-commons/src/test/java/com/alipay/antchain/bridge/commons/EvmCoderUtilTest.java b/acb-sdk/antchain-bridge-commons/src/test/java/com/alipay/antchain/bridge/commons/EvmCoderUtilTest.java new file mode 100644 index 00000000..1efbc431 --- /dev/null +++ b/acb-sdk/antchain-bridge-commons/src/test/java/com/alipay/antchain/bridge/commons/EvmCoderUtilTest.java @@ -0,0 +1,49 @@ +package com.alipay.antchain.bridge.commons; + +import java.nio.ByteOrder; + +import cn.hutool.core.util.ByteUtil; +import com.alipay.antchain.bridge.commons.utils.codec.CoderResult; +import com.alipay.antchain.bridge.commons.utils.codec.EvmCoderUtil; +import org.junit.Assert; +import org.junit.Test; + +public class EvmCoderUtilTest { + + @Test + public void testRoundTripVarBytes() { + byte[] input = new byte[33]; + input[0] = 1; + input[32] = 2; + byte[] encoded = new byte[96]; + int offset = EvmCoderUtil.sinkVarBytes(input, encoded, encoded.length); + + CoderResult decoded = EvmCoderUtil.parseVarBytes(encoded, encoded.length); + Assert.assertEquals(offset, decoded.getOffset()); + Assert.assertArrayEquals(input, decoded.getResult()); + } + + @Test + public void testRejectsLengthLargerThanInputBeforeAllocation() { + byte[] malformed = new byte[32]; + System.arraycopy( + ByteUtil.intToBytes(Integer.MAX_VALUE, ByteOrder.BIG_ENDIAN), + 0, + malformed, + 28, + 4 + ); + + try { + EvmCoderUtil.parseVarBytes(malformed, malformed.length); + Assert.fail("expected malformed length to be rejected"); + } catch (IllegalArgumentException expected) { + Assert.assertTrue(expected.getMessage().contains("exceeds available input")); + } + } + + @Test(expected = IllegalArgumentException.class) + public void testRejectsInvalidOffset() { + EvmCoderUtil.parseVarBytes(new byte[32], 31); + } +} From 106cb6933b3742f48200318f91b9e2bdda251e12 Mon Sep 17 00:00:00 2001 From: 0xstride <73103011+fengjy73@users.noreply.github.com> Date: Thu, 30 Jul 2026 17:27:02 +0800 Subject: [PATCH 2/3] Avoid disabled UCP report payload construction --- .../service/report/PlatformReportClient.java | 3 +++ .../report/PlatformReportClientTest.java | 24 +++++++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/acb-relayer/r-core/src/main/java/com/alipay/antchain/bridge/relayer/core/service/report/PlatformReportClient.java b/acb-relayer/r-core/src/main/java/com/alipay/antchain/bridge/relayer/core/service/report/PlatformReportClient.java index 82f2388b..87716e1c 100644 --- a/acb-relayer/r-core/src/main/java/com/alipay/antchain/bridge/relayer/core/service/report/PlatformReportClient.java +++ b/acb-relayer/r-core/src/main/java/com/alipay/antchain/bridge/relayer/core/service/report/PlatformReportClient.java @@ -77,6 +77,9 @@ public void init() { } public void reportUcp(UniformCrosschainPacketContext context) { + if (!enabled || StrUtil.isEmpty(apiKey)) { + return; + } post("/api/cc-relayer/ucps", context.getUcpId(), ucpReportJsonBuilder.build(context)); } diff --git a/acb-relayer/r-core/src/test/java/com/alipay/antchain/bridge/relayer/core/service/report/PlatformReportClientTest.java b/acb-relayer/r-core/src/test/java/com/alipay/antchain/bridge/relayer/core/service/report/PlatformReportClientTest.java index 6a7771a0..354c4817 100644 --- a/acb-relayer/r-core/src/test/java/com/alipay/antchain/bridge/relayer/core/service/report/PlatformReportClientTest.java +++ b/acb-relayer/r-core/src/test/java/com/alipay/antchain/bridge/relayer/core/service/report/PlatformReportClientTest.java @@ -9,6 +9,7 @@ import com.alibaba.fastjson.JSONObject; import com.alipay.antchain.bridge.commons.core.base.SendResponseResult; import com.alipay.antchain.bridge.relayer.commons.model.SDPMsgWrapper; +import com.alipay.antchain.bridge.relayer.commons.model.UniformCrosschainPacketContext; import org.junit.Assert; import org.junit.Test; import org.springframework.http.HttpEntity; @@ -19,6 +20,18 @@ public class PlatformReportClientTest { + @Test + public void testDisabledReportingDoesNotBuildUcpPayload() throws Exception { + PlatformReportClient client = new PlatformReportClient(); + CountingUcpReportJsonBuilder builder = new CountingUcpReportJsonBuilder(); + setField(client, "enabled", false); + setField(client, "ucpReportJsonBuilder", builder); + + client.reportUcp(new UniformCrosschainPacketContext()); + + Assert.assertEquals(0, builder.buildCount); + } + @Test public void testUcpIdIsFirstBodyFieldForInterfacesTwoToFour() throws Exception { CapturingRestTemplate restTemplate = new CapturingRestTemplate(); @@ -84,6 +97,17 @@ private CapturedRequest(String path, String body) { } } + private static class CountingUcpReportJsonBuilder extends UcpReportJsonBuilder { + + private int buildCount; + + @Override + public JSONObject build(UniformCrosschainPacketContext context) { + buildCount++; + return new JSONObject(true); + } + } + private static class CapturingRestTemplate extends RestTemplate { private final List requests = new ArrayList<>(); From 40bfc665e8211540f9544610dc3884d2847ca408 Mon Sep 17 00:00:00 2001 From: 0xstride <73103011+fengjy73@users.noreply.github.com> Date: Sun, 2 Aug 2026 23:08:59 +0800 Subject: [PATCH 3/3] Normalize reported source transaction hashes --- .../service/report/UcpReportJsonBuilder.java | 20 +++++++++- .../report/UcpReportJsonBuilderTest.java | 38 +++++++++++++++++++ 2 files changed, 57 insertions(+), 1 deletion(-) diff --git a/acb-relayer/r-core/src/main/java/com/alipay/antchain/bridge/relayer/core/service/report/UcpReportJsonBuilder.java b/acb-relayer/r-core/src/main/java/com/alipay/antchain/bridge/relayer/core/service/report/UcpReportJsonBuilder.java index 76b64bfe..77547783 100644 --- a/acb-relayer/r-core/src/main/java/com/alipay/antchain/bridge/relayer/core/service/report/UcpReportJsonBuilder.java +++ b/acb-relayer/r-core/src/main/java/com/alipay/antchain/bridge/relayer/core/service/report/UcpReportJsonBuilder.java @@ -234,10 +234,28 @@ private JSONObject buildProvableData(CrossChainMessage.ProvableLedgerData data) json.put("timestampUtc", Instant.ofEpochMilli(data.getTimestamp()).toString()); json.put("ledgerData", parseOpaqueData(data.getLedgerData())); json.put("proof", parseOpaqueData(data.getProof())); - json.put("txHash", hex(data.getTxHash())); + json.put("txHash", encodeTxHashForReport(data.getTxHash())); return json; } + /** + * The plugin API exposes the transaction hash as bytes, but plugins do not agree on what those bytes mean: + * chains such as Dioxide store the chain-native hash text as UTF-8, while chains such as Mychain store the + * decoded binary hash. The platform report contract is UTF-8 HEX, so first recover a chain-native text value + * and then hex-encode its UTF-8 bytes. This keeps Dioxide output stable and gives binary-hash chains the same + * wire representation without changing the UCP or plugin-internal hash representation. + */ + private String encodeTxHashForReport(byte[] value) { + if (ObjectUtil.isNull(value)) { + return null; + } + String txHash = decodeUtf8(value); + if (ObjectUtil.isNull(txHash) || !isPrintable(txHash)) { + txHash = hex(value); + } + return hex(txHash.getBytes(StandardCharsets.UTF_8)); + } + private JSONObject buildObjectIdentity(ObjectIdentity identity) { if (ObjectUtil.isNull(identity)) { return null; diff --git a/acb-relayer/r-core/src/test/java/com/alipay/antchain/bridge/relayer/core/service/report/UcpReportJsonBuilderTest.java b/acb-relayer/r-core/src/test/java/com/alipay/antchain/bridge/relayer/core/service/report/UcpReportJsonBuilderTest.java index 9f868fec..550e0538 100644 --- a/acb-relayer/r-core/src/test/java/com/alipay/antchain/bridge/relayer/core/service/report/UcpReportJsonBuilderTest.java +++ b/acb-relayer/r-core/src/test/java/com/alipay/antchain/bridge/relayer/core/service/report/UcpReportJsonBuilderTest.java @@ -175,6 +175,27 @@ public void testBuildJsonLedgerDataAndBinaryFallback() { Assert.assertFalse(body.toJSONString().contains("\"rawHex\"")); } + @Test + public void testBuildNormalizesSourceTxHashAsUtf8Hex() { + String dioxideTxHash = "z4vcnp2r4wn6nyvx4gz851msx5d5yg78ber6qdvfkhxje5fhz770"; + JSONObject dioxideProvableData = buildProvableData(dioxideTxHash.getBytes(StandardCharsets.UTF_8)); + Assert.assertEquals( + HexUtil.encodeHexStr(dioxideTxHash.getBytes(StandardCharsets.UTF_8)), + dioxideProvableData.getString("txHash") + ); + + String mychainTxHash = "e0b6d9013195594002730aa7cdba82b2cb1aabd469b8554ea90086c9f6422d90"; + JSONObject mychainProvableData = buildProvableData(HexUtil.decodeHex(mychainTxHash)); + Assert.assertEquals( + HexUtil.encodeHexStr(mychainTxHash.getBytes(StandardCharsets.UTF_8)), + mychainProvableData.getString("txHash") + ); + Assert.assertEquals( + mychainTxHash, + new String(HexUtil.decodeHex(mychainProvableData.getString("txHash")), StandardCharsets.UTF_8) + ); + } + @Test public void testBuildAuthV2WithSdpV2AndV3() { assertAuthV2AndSdpVersion(2); @@ -315,6 +336,23 @@ private CrossChainMessage createDeveloperMessage() { ); } + private JSONObject buildProvableData(byte[] txHash) { + CrossChainMessage crossChainMessage = CrossChainMessage.createCrossChainMessage( + CrossChainMessage.CrossChainMessageType.DEVELOPER_DESIGN, + 104L, + 123456793L, + new byte[]{0x01}, + new byte[]{0x02}, + new byte[0], + new byte[0], + txHash + ); + return new UcpReportJsonBuilder().build(buildContext(crossChainMessage)) + .getJSONObject("ucp") + .getJSONObject("srcMessage") + .getJSONObject("provableData"); + } + private UniformCrosschainPacketContext buildContext(CrossChainMessage crossChainMessage) { UniformCrosschainPacketContext context = new UniformCrosschainPacketContext(); context.setUcpId("ucp-test");