Skip to content
Merged
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
@@ -1,32 +1,26 @@
package com.alipay.antchain.bridge.plugins.ethereum2;

import java.math.BigInteger;
import java.util.Arrays;

import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.ObjectUtil;
import com.alipay.antchain.bridge.commons.core.base.ConsensusState;
import com.alipay.antchain.bridge.commons.core.base.CrossChainMessage;
import com.alipay.antchain.bridge.commons.core.bta.IBlockchainTrustAnchor;
import com.alipay.antchain.bridge.plugins.ethereum2.abi.AuthMsg;
import com.alipay.antchain.bridge.plugins.ethereum2.core.*;
import com.alipay.antchain.bridge.plugins.ethereum2.core.eth.EthLogTopic;
import com.alipay.antchain.bridge.plugins.ethereum2.core.eth.EthReceiptProof;
import com.alipay.antchain.bridge.plugins.lib.HeteroChainDataVerifierService;
import com.alipay.antchain.bridge.plugins.spi.ptc.AbstractHCDVSService;
import com.alipay.antchain.bridge.plugins.spi.ptc.core.VerifyResult;
import org.apache.tuweni.bytes.Bytes;
import org.apache.tuweni.bytes.Bytes32;
import org.hyperledger.besu.datatypes.Address;
import org.web3j.tx.Contract;
import org.web3j.utils.Numeric;
import tech.pegasys.teku.infrastructure.unsigned.UInt64;

@HeteroChainDataVerifierService(pluginId = "plugin-ethereum2", products = "ethereum2")
public class EthereumHcdvsService extends AbstractHCDVSService {

private static final EthLogTopic SEND_AUTH_MESSAGE_LOG_TOPIC = EthLogTopic.fromHexString("0x79b7516b1b7a6a39fb4b7b22e8667cd3744e5c27425292f8a9f49d1042c0c651");

@Override
public VerifyResult verifyAnchorConsensusState(IBlockchainTrustAnchor bta, ConsensusState anchorState) {
getHCDVSLogger().info("verify anchor consensus state ⚓️ (slot: {}, hash: {}) for domain {} now!",
Expand Down Expand Up @@ -181,38 +175,18 @@ public VerifyResult verifyCrossChainMessage(CrossChainMessage message, Consensus
return VerifyResult.fail("receipt root not equal");
}

var ethAuthMessageLog = EthAuthMessageLog.decodeFromJson(new String(message.getProvableData().getLedgerData()));
var receiptInProof = ethReceiptProof.getEthTransactionReceipt();
if (receiptInProof.getLogs().size() <= ethAuthMessageLog.getLogIndex()) {
getHCDVSLogger().error("❌ log index {} out of range, receipt has only {} logs", ethAuthMessageLog.getLogIndex(), receiptInProof.getLogs().size());
return VerifyResult.fail("log index out of range");
}

var msgLogInProof = ethReceiptProof.getEthTransactionReceipt().getLogs().get(ethAuthMessageLog.getLogIndex());
var msgLogInLedgerData = ethAuthMessageLog.getSendAuthMessageLog();

if (!SEND_AUTH_MESSAGE_LOG_TOPIC.equals(msgLogInProof.getTopics().getFirst())) {
getHCDVSLogger().error("❌ log topic in proof {} not match", msgLogInProof.getTopics().getFirst().toHexString());
return VerifyResult.fail("log topic not match");
}
if (!Arrays.equals(SEND_AUTH_MESSAGE_LOG_TOPIC.toArray(), Numeric.hexStringToByteArray(msgLogInLedgerData.getTopics().getFirst()))) {
getHCDVSLogger().error("❌ log topic in ledger data {} not match", msgLogInLedgerData.getTopics().getFirst());
return VerifyResult.fail("log topic not match");
}
if (!msgLogInProof.getLogger().equals(ethConsensusStateData.getAmContract())) {
getHCDVSLogger().error("❌ logger address in proof {} is not am contract {}",
msgLogInProof.getLogger().toHexString(), ethConsensusStateData.getAmContract().toHexString());
return VerifyResult.fail("logger not am contract");
}
if (!Arrays.equals(ethConsensusStateData.getAmContract().toArray(), Numeric.hexStringToByteArray(msgLogInLedgerData.getAddress()))) {
getHCDVSLogger().error("❌ logger address {} in ledger data is not am contract {}",
msgLogInLedgerData.getAddress(), ethConsensusStateData.getAmContract().toHexString());
return VerifyResult.fail("logger not am contract");
}
if (!Arrays.equals(msgLogInProof.getData().toArray(), Numeric.hexStringToByteArray(msgLogInLedgerData.getData()))) {
getHCDVSLogger().error("❌ log data in proof {} is not equal to ledger data {}",
msgLogInProof.getData().toHexString(), msgLogInLedgerData.getData());
return VerifyResult.fail("log data not match");
try {
var ledgerLog = EthAuthMessageLog.decodeFromJson(new String(message.getProvableData().getLedgerData()));
if (ledgerLog == null || ledgerLog.getSendAuthMessageLog() == null
|| !BigInteger.valueOf(ethReceiptProof.getReceiptIndex()).equals(
ledgerLog.getSendAuthMessageLog().getTransactionIndex())) {
return VerifyResult.fail("receipt transaction index does not match ledger event");
}
ledgerLog.verifyReceiptLog(ethReceiptProof.getEthTransactionReceipt().getLogs(),
ethConsensusStateData.getAmContract(), message.getMessage());
} catch (RuntimeException e) {
// Malformed/ambiguous ledger data must fail verification, never fall back to RPC or success.
return VerifyResult.fail("invalid receipt event: {}", e.getMessage());
}

getHCDVSLogger().info("🌈 crosschain message (slot: {}, txhash: {}) pass the verification",
Expand All @@ -223,10 +197,6 @@ public VerifyResult verifyCrossChainMessage(CrossChainMessage message, Consensus

@Override
public byte[] parseMessageFromLedgerData(byte[] ledgerData) {
var eventValues = Contract.staticExtractEventParameters(
AuthMsg.SENDAUTHMESSAGE_EVENT,
EthAuthMessageLog.decodeFromJson(new String(ledgerData)).getSendAuthMessageLog()
);
return (byte[]) eventValues.getNonIndexedValues().getFirst().getValue();
return EthAuthMessageLog.decodeFromJson(new String(ledgerData)).decodeMessage();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -1031,19 +1031,15 @@ private List<CrossChainMessage> readMessagesFromEntireBlock(BeaconBlock beaconBl

List<TransactionReceipt> finalAllReceiptsInBlock = allReceiptsInBlock;
messageList.addAll(AuthMsg.getSendAuthMessageEvents(receipt).stream()
.filter(x -> StrUtil.equals(x.log.getAddress(), amContractAddressHex))
.filter(x -> StrUtil.equalsIgnoreCase(x.log.getAddress(), amContractAddressHex))
.map(
response -> CrossChainMessage.createCrossChainMessage(
CrossChainMessage.CrossChainMessageType.AUTH_MSG,
beaconBlock.getSlot().bigIntegerValue(),
block.getTimestamp().longValue() * 1000,
beaconBlock.getRoot().toArray(),
response.pkg,
EthAuthMessageLog.builder()
.logIndex(response.log.getLogIndex().intValue())
.sendAuthMessageLog(response.log)
.build()
.encodeToJson().getBytes(),
EthAuthMessageLog.fromReceipt(receipt, response.log).encodeToJson().getBytes(),
getReceiptProof(finalAllReceiptsInBlock, response.log.getTransactionIndex().intValue()).encodeToJson().getBytes(),
Numeric.hexStringToByteArray(receipt.getTransactionHash())
)
Expand Down Expand Up @@ -1082,8 +1078,8 @@ private List<CrossChainMessage> readMessagesByFilter(BeaconBlock beaconBlock, Bi
getBbcLogger().warn("log from node has wrong contract address: {}, expected: {}", logObject.getAddress(), amContractAddressHex);
continue;
}
if (logObject.getTopics().size() != 1 || !StrUtil.equalsIgnoreCase(logObject.getTopics().getFirst(), SEND_AUTH_MESSAGE_LOG_TOPIC)) {
getBbcLogger().warn("log from node has wrong topic: {}, expected: {}", logObject.getTopics().getFirst(), SEND_AUTH_MESSAGE_LOG_TOPIC);
if (logObject.getTopics() == null || logObject.getTopics().size() != 1 || !StrUtil.equalsIgnoreCase(logObject.getTopics().getFirst(), SEND_AUTH_MESSAGE_LOG_TOPIC)) {
getBbcLogger().warn("log from node has wrong topics: {}, expected: {}", logObject.getTopics(), SEND_AUTH_MESSAGE_LOG_TOPIC);
continue;
}

Expand All @@ -1097,24 +1093,18 @@ private List<CrossChainMessage> readMessagesByFilter(BeaconBlock beaconBlock, Bi
var blockTimestamp = block.getTimestamp().longValue() * 1000;
var receiptProof = getReceiptProof(allReceiptsInBlock, logObject.getTransactionIndex().intValue());

messageList.addAll(
AuthMsg.getSendAuthMessageEvents(transactionReceipt).stream().map(
response -> CrossChainMessage.createCrossChainMessage(
CrossChainMessage.CrossChainMessageType.AUTH_MSG,
beaconBlock.getSlot().bigIntegerValue(),
blockTimestamp,
beaconBlock.getRoot().toArray(),
response.pkg,
EthAuthMessageLog.builder()
.logIndex(logObject.getLogIndex().intValue())
.sendAuthMessageLog(logObject)
.build()
.encodeToJson().getBytes(),
receiptProof.encodeToJson().getBytes(),
Numeric.hexStringToByteArray(logObject.getTransactionHash())
)
).toList()
);
// One filter result denotes ONE event, not every AM event in the same transaction.
var ledgerLog = EthAuthMessageLog.fromReceipt(transactionReceipt, logObject);
messageList.add(CrossChainMessage.createCrossChainMessage(
CrossChainMessage.CrossChainMessageType.AUTH_MSG,
beaconBlock.getSlot().bigIntegerValue(),
blockTimestamp,
beaconBlock.getRoot().toArray(),
ledgerLog.decodeMessage(),
ledgerLog.encodeToJson().getBytes(),
receiptProof.encodeToJson().getBytes(),
Numeric.hexStringToByteArray(transactionReceipt.getTransactionHash())
));
}

if (!messageList.isEmpty()) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,18 @@
package com.alipay.antchain.bridge.plugins.ethereum2.core;

import java.util.Arrays;
import java.util.List;
import java.util.Objects;

import com.alibaba.fastjson.JSON;
import com.alipay.antchain.bridge.plugins.ethereum2.abi.AuthMsg;
import com.alipay.antchain.bridge.plugins.ethereum2.core.eth.EthLog;
import lombok.*;
import org.hyperledger.besu.datatypes.Address;
import org.web3j.protocol.core.methods.response.Log;
import org.web3j.protocol.core.methods.response.TransactionReceipt;
import org.web3j.tx.Contract;
import org.web3j.utils.Numeric;

@Builder
@NoArgsConstructor
Expand All @@ -11,14 +21,133 @@
@Setter
public class EthAuthMessageLog {

private static final String TOPIC = "0x79b7516b1b7a6a39fb4b7b22e8667cd3744e5c27425292f8a9f49d1042c0c651";

public static EthAuthMessageLog decodeFromJson(String json) {
return JSON.parseObject(json, EthAuthMessageLog.class);
}

// Receipt-local for new messages. Old collectors populated this with the block-global index.
private Integer logIndex;

// Explicit marker: validators must not fall back to content lookup when this is present.
private Integer receiptLogIndex;

// Preserve the original RPC metadata, including its block-global logIndex.
private Log sendAuthMessageLog;

public static EthAuthMessageLog fromReceipt(TransactionReceipt receipt, Log selected) {
require(receipt != null && receipt.getLogs() != null && selected != null, "missing receipt/log");
require(!selected.isRemoved(), "removed log");
require(selected.getLogIndex() != null && selected.getLogIndex().signum() >= 0
&& selected.getTransactionIndex() != null && selected.getTransactionIndex().signum() >= 0,
"missing or negative RPC index");
require(sameHex(receipt.getTransactionHash(), selected.getTransactionHash())
&& Objects.equals(receipt.getTransactionIndex(), selected.getTransactionIndex()),
"log transaction does not match receipt");
int found = -1;
for (int i = 0; i < receipt.getLogs().size(); i++) {
Log candidate = receipt.getLogs().get(i);
if (Objects.equals(candidate.getLogIndex(), selected.getLogIndex())
&& sameHex(candidate.getTransactionHash(), selected.getTransactionHash())
&& sameHex(candidate.getBlockHash(), selected.getBlockHash())
&& sameRpcContent(candidate, selected)) {
require(found == -1, "ambiguous RPC log");
found = i;
}
}
require(found >= 0, "selected log missing from receipt");
return EthAuthMessageLog.builder().logIndex(found).receiptLogIndex(found)
.sendAuthMessageLog(receipt.getLogs().get(found)).build();
}

public byte[] decodeMessage() {
require(sendAuthMessageLog != null && sendAuthMessageLog.getTopics() != null
&& sendAuthMessageLog.getTopics().size() == 1
&& TOPIC.equalsIgnoreCase(sendAuthMessageLog.getTopics().getFirst()), "invalid AM event topic");
var event = Contract.staticExtractEventParameters(AuthMsg.SENDAUTHMESSAGE_EVENT, sendAuthMessageLog);
require(event != null && event.getNonIndexedValues().size() == 1, "invalid AM event data");
return (byte[]) event.getNonIndexedValues().getFirst().getValue();
}

/**
* Call only after validating the receipt proof against the trusted consensus receipts root.
* Legacy compatibility searches authenticated receipt content, never a node/RPC response.
*/
public EthLog verifyReceiptLog(List<? extends EthLog> logs, Address amContract, byte[] message) {
require(logs != null && sendAuthMessageLog != null && logIndex != null && logIndex >= 0,
"missing or negative log index");
require(!sendAuthMessageLog.isRemoved(), "removed log");
require(Arrays.equals(amContract.toArray(), hex(sendAuthMessageLog.getAddress())), "logger not AM contract");
require(Arrays.equals(message, decodeMessage()), "message does not match ledger event");

if (receiptLogIndex != null) {
require(receiptLogIndex.equals(logIndex) && receiptLogIndex >= 0 && receiptLogIndex < logs.size(),
"receipt log index out of range or inconsistent");
EthLog selected = logs.get(receiptLogIndex);
require(matchesProof(selected), "indexed receipt log does not match ledger event");
return selected;
}

// Old producers used inconsistent index semantics. Only a UNIQUE complete event match is safe.
EthLog match = null;
int matchedIndex = -1;
for (int i = 0; i < logs.size(); i++) {
EthLog candidate = logs.get(i);
if (matchesProof(candidate)) {
require(match == null, "ambiguous legacy receipt log");
match = candidate;
matchedIndex = i;
}
}
require(match != null, "ledger event missing from proven receipt");
require(logIndex == matchedIndex || java.math.BigInteger.valueOf(logIndex).equals(sendAuthMessageLog.getLogIndex()),
"inconsistent legacy log index");
return match;
}

private boolean matchesProof(EthLog proofLog) {
if (!Arrays.equals(proofLog.getLogger().toArray(), hex(sendAuthMessageLog.getAddress()))
|| !Arrays.equals(proofLog.getData().toArray(), hex(sendAuthMessageLog.getData()))
|| proofLog.getTopics().size() != sendAuthMessageLog.getTopics().size()) {
return false;
}
for (int i = 0; i < proofLog.getTopics().size(); i++) {
if (!Arrays.equals(proofLog.getTopics().get(i).toArray(), hex(sendAuthMessageLog.getTopics().get(i)))) {
return false;
}
}
return true;
}

private static boolean sameRpcContent(Log a, Log b) {
if (!sameHex(a.getAddress(), b.getAddress()) || !sameHex(a.getData(), b.getData())
|| a.getTopics() == null || b.getTopics() == null || a.getTopics().size() != b.getTopics().size()) {
return false;
}
for (int i = 0; i < a.getTopics().size(); i++) {
if (!sameHex(a.getTopics().get(i), b.getTopics().get(i))) {
return false;
}
}
return true;
}

private static boolean sameHex(String a, String b) {
return a != null && b != null && a.equalsIgnoreCase(b);
}

private static byte[] hex(String value) {
require(value != null && value.matches("(?i)0x(?:[0-9a-f]{2})*"), "invalid hex in ledger event");
return Numeric.hexStringToByteArray(value);
}

private static void require(boolean valid, String error) {
if (!valid) {
throw new IllegalArgumentException(error);
}
}

public String encodeToJson() {
return JSON.toJSONString(this);
}
Expand Down
Loading