Skip to content
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
Original file line number Diff line number Diff line change
Expand Up @@ -46,11 +46,6 @@ public class AsyncReceiveHandler {
private PlatformReportClient platformReportClient;

public void receiveUniformCrosschainPackets(List<UniformCrosschainPacketContext> ucpContexts) {

ucpContexts.stream()
.filter(context -> !context.isFromNetwork())
.forEach(platformReportClient::reportUcp);

int rowsNum = crossChainMessageRepository.putUniformCrosschainPackets(ucpContexts);
if (ucpContexts.size() != rowsNum) {
throw new RuntimeException(
Expand All @@ -61,6 +56,12 @@ public void receiveUniformCrosschainPackets(List<UniformCrosschainPacketContext>
);
}
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<AuthMsgWrapper> authMsgWrappers) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand All @@ -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);
Expand All @@ -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());
}
Expand All @@ -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);
Expand All @@ -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) {
Expand All @@ -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;
Expand All @@ -155,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;
Expand All @@ -177,7 +274,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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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();
Expand Down Expand Up @@ -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<CapturedRequest> requests = new ArrayList<>();
Expand Down
Loading