From 8ab6ab9a15cb933e20afd6a808bbfa84600e54cb Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E5=86=AF=E5=98=89=E9=A2=96?= <568296125@qq.com>
Date: Wed, 3 Jun 2026 23:14:57 +0800
Subject: [PATCH 1/2] [fix][ethereum2][offchain] fix period-tail sync committee
verification
---
.../ethereum2/offchain-plugin/pom.xml | 3 +
.../plugins/ethereum2/EthereumBBCService.java | 13 ++--
.../ethereum2/EthereumHcdvsService.java | 68 ++++++++++++++++---
.../plugins/ethereum2/core/AcbEthClient.java | 5 +-
.../core/EthConsensusEndorsements.java | 18 ++++-
.../ethereum2/core/EthConsensusStateData.java | 18 +++--
.../ethereum2/core/EthSubjectIdentity.java | 12 ++++
.../core/eth/beacon/AcbBeaconClient.java | 7 +-
.../ethereum2/EthereumBBCServiceTest.java | 3 +-
.../plugins/ethereum2/EthereumHcdvsTest.java | 20 ++++++
10 files changed, 140 insertions(+), 27 deletions(-)
diff --git a/acb-sdk/pluginset/ethereum2/offchain-plugin/pom.xml b/acb-sdk/pluginset/ethereum2/offchain-plugin/pom.xml
index 13847e02..067b25ef 100644
--- a/acb-sdk/pluginset/ethereum2/offchain-plugin/pom.xml
+++ b/acb-sdk/pluginset/ethereum2/offchain-plugin/pom.xml
@@ -233,6 +233,9 @@
**/*.sol
+
+ lib/ptc/CommitteePtcVerifier.sol
+
diff --git a/acb-sdk/pluginset/ethereum2/offchain-plugin/src/main/java/com/alipay/antchain/bridge/plugins/ethereum2/EthereumBBCService.java b/acb-sdk/pluginset/ethereum2/offchain-plugin/src/main/java/com/alipay/antchain/bridge/plugins/ethereum2/EthereumBBCService.java
index 12df3c6a..12c21472 100644
--- a/acb-sdk/pluginset/ethereum2/offchain-plugin/src/main/java/com/alipay/antchain/bridge/plugins/ethereum2/EthereumBBCService.java
+++ b/acb-sdk/pluginset/ethereum2/offchain-plugin/src/main/java/com/alipay/antchain/bridge/plugins/ethereum2/EthereumBBCService.java
@@ -436,15 +436,14 @@ public ConsensusState readConsensusState(BigInteger slot) {
);
}
- var beaconBlock = this.acbEthClient.getBeaconBlockBySlot(slot.add(BigInteger.ONE));
- if (ObjectUtil.isNull(beaconBlock)) {
- throw new RuntimeException("get a null result for next beacon block by slot: " + slot.add(BigInteger.ONE));
- }
- if (beaconBlock.getBody().getOptionalSyncAggregate().isEmpty()) {
- throw new RuntimeException("has no sync aggregate in beacon block by slot " + slot.add(BigInteger.ONE));
+ if (beaconBlockWithSyncAggregate.getBody().getOptionalSyncAggregate().isEmpty()) {
+ throw new RuntimeException("has no sync aggregate in beacon block by slot " + beaconBlockWithSyncAggregate.getSlot());
}
- var ethConsensusEndorsements = new EthConsensusEndorsements(beaconBlock.getBody().getOptionalSyncAggregate().get());
+ var ethConsensusEndorsements = new EthConsensusEndorsements(
+ beaconBlockWithSyncAggregate.getBody().getOptionalSyncAggregate().get(),
+ beaconBlockWithSyncAggregate.getSlot()
+ );
return new ConsensusState(
slot,
diff --git a/acb-sdk/pluginset/ethereum2/offchain-plugin/src/main/java/com/alipay/antchain/bridge/plugins/ethereum2/EthereumHcdvsService.java b/acb-sdk/pluginset/ethereum2/offchain-plugin/src/main/java/com/alipay/antchain/bridge/plugins/ethereum2/EthereumHcdvsService.java
index 50ebbd11..9412b707 100644
--- a/acb-sdk/pluginset/ethereum2/offchain-plugin/src/main/java/com/alipay/antchain/bridge/plugins/ethereum2/EthereumHcdvsService.java
+++ b/acb-sdk/pluginset/ethereum2/offchain-plugin/src/main/java/com/alipay/antchain/bridge/plugins/ethereum2/EthereumHcdvsService.java
@@ -63,8 +63,13 @@ public VerifyResult verifyAnchorConsensusState(IBlockchainTrustAnchor bta, Conse
getHCDVSLogger().info("successful to verify anchor consensus state ⚓️ (slot: {}, hash: {}) for domain {} now!",
anchorState.getHeight().toString(), anchorState.getHashHex(), bta.getDomain().toString());
if (ethConsensusStateData.getLightClientUpdateWrapper() != null) {
- getHCDVSLogger().info("light client update inside anchor consensus state, update the sync committee");
- ethSubjectIdentity.setCurrentSyncCommittee(ethConsensusStateData.getLightClientUpdateWrapper().getNextSyncCommittee());
+ ethSubjectIdentity.setNextSyncCommittee(ethConsensusStateData.getLightClientUpdateWrapper().getNextSyncCommittee());
+ }
+ if (ethConsensusStateData.isLastSlotForCurrentPeriod(ethSubjectIdentity.getEth2ChainConfig().getSyncPeriodLength())
+ && ObjectUtil.isNotNull(ethSubjectIdentity.getNextSyncCommittee())) {
+ getHCDVSLogger().info("light client update inside last-slot anchor consensus state, switch current sync committee");
+ ethSubjectIdentity.setCurrentSyncCommittee(ethSubjectIdentity.getNextSyncCommittee());
+ ethSubjectIdentity.setNextSyncCommittee(null);
}
anchorState.setConsensusNodeInfo(ethSubjectIdentity.toJson().getBytes());
return VerifyResult.success();
@@ -123,8 +128,55 @@ public VerifyResult verifyConsensusState(ConsensusState stateToVerify, Consensus
new String(stateToVerify.getEndorsements()),
ethSubjectIdentity.getCurrentSyncCommittee().getPubkeys().size()
);
+ var syncPeriodLength = ethSubjectIdentity.getEth2ChainConfig().getSyncPeriodLength();
+ var currPeriod = ethConsensusStateData.getCurrSyncPeriod(syncPeriodLength);
+ var signatureSlot = ethEndorsements.getSignatureSlotOrDefault(currSlot.increment());
+ var signaturePeriod = signatureSlot.dividedBy(syncPeriodLength);
+ var shouldRotateCommittee = ethConsensusStateData.isLastSlotForCurrentPeriod(syncPeriodLength);
+
+ if (shouldRotateCommittee) {
+ if (ethConsensusStateData.getLightClientUpdateWrapper() == null) {
+ getHCDVSLogger().error("❌ has none light client update for the last slot {} for current period {}",
+ ethConsensusStateData.getBeaconBlockHeader().getSlot().toString(),
+ currPeriod
+ );
+ return VerifyResult.fail("none light client update at last slot in period");
+ }
+ try {
+ ethConsensusStateData.validateLightClientUpdate(
+ ethSubjectIdentity.getCurrentSyncCommittee(),
+ ethSubjectIdentity.getEth2ChainConfig()
+ );
+ } catch (InvalidConsensusDataException e) {
+ getHCDVSLogger().error("❌ failed to verify light client update at last slot {}",
+ stateToVerify.getHeight().toString(), e);
+ return VerifyResult.fail("failed to verify light client update: {}", e.getMessage());
+ }
+ ethSubjectIdentity.setNextSyncCommittee(ethConsensusStateData.getLightClientUpdateWrapper().getNextSyncCommittee());
+ }
+
+ var committeeForBlockVerification = ethSubjectIdentity.getCurrentSyncCommittee();
+ if (signaturePeriod.equals(currPeriod)) {
+ committeeForBlockVerification = ethSubjectIdentity.getCurrentSyncCommittee();
+ } else if (signaturePeriod.equals(currPeriod.increment())) {
+ if (ObjectUtil.isNull(ethSubjectIdentity.getNextSyncCommittee())) {
+ getHCDVSLogger().error("❌ missing next sync committee for slot {}, while endorsements come from signature slot {}",
+ stateToVerify.getHeight().toString(), signatureSlot.toString());
+ return VerifyResult.fail("missing next sync committee for endorsements");
+ }
+ committeeForBlockVerification = ethSubjectIdentity.getNextSyncCommittee();
+ } else {
+ getHCDVSLogger().error("❌ unexpected endorsements signature slot {} for state slot {}",
+ signatureSlot.toString(), stateToVerify.getHeight().toString());
+ return VerifyResult.fail("unexpected endorsements signature slot");
+ }
+
try {
- ethConsensusStateData.validate(ethSubjectIdentity.getCurrentSyncCommittee(), ethEndorsements, ethSubjectIdentity.getEth2ChainConfig());
+ ethConsensusStateData.validateBlock(
+ committeeForBlockVerification,
+ ethEndorsements,
+ ethSubjectIdentity.getEth2ChainConfig()
+ );
} catch (InvalidConsensusDataException e) {
getHCDVSLogger().error("❌ failed to verify eth consensus state data (slot: {}, hash: {})",
stateToVerify.getHeight().toString(), stateToVerify.getHashHex(), e);
@@ -133,18 +185,12 @@ public VerifyResult verifyConsensusState(ConsensusState stateToVerify, Consensus
}
if (ethConsensusStateData.isLastSlotForCurrentPeriod(ethSubjectIdentity.getEth2ChainConfig().getSyncPeriodLength())) {
- if (ethConsensusStateData.getLightClientUpdateWrapper() == null) {
- getHCDVSLogger().error("❌ has none light client update for the last slot {} for current period {}",
- ethConsensusStateData.getBeaconBlockHeader().getSlot().toString(),
- ethConsensusStateData.getCurrSyncPeriod(ethSubjectIdentity.getEth2ChainConfig().getSyncPeriodLength())
- );
- return VerifyResult.fail("none light client update at last slot in period");
- }
getHCDVSLogger().info("🗳️ last slot {} for current period {}, update the sync committee",
ethConsensusStateData.getBeaconBlockHeader().getSlot().toString(),
ethConsensusStateData.getCurrSyncPeriod(ethSubjectIdentity.getEth2ChainConfig().getSyncPeriodLength())
);
- ethSubjectIdentity.setCurrentSyncCommittee(ethConsensusStateData.getLightClientUpdateWrapper().getNextSyncCommittee());
+ ethSubjectIdentity.setCurrentSyncCommittee(ethSubjectIdentity.getNextSyncCommittee());
+ ethSubjectIdentity.setNextSyncCommittee(null);
}
stateToVerify.setConsensusNodeInfo(ethSubjectIdentity.toJson().getBytes());
diff --git a/acb-sdk/pluginset/ethereum2/offchain-plugin/src/main/java/com/alipay/antchain/bridge/plugins/ethereum2/core/AcbEthClient.java b/acb-sdk/pluginset/ethereum2/offchain-plugin/src/main/java/com/alipay/antchain/bridge/plugins/ethereum2/core/AcbEthClient.java
index 532adb5a..058e16f6 100644
--- a/acb-sdk/pluginset/ethereum2/offchain-plugin/src/main/java/com/alipay/antchain/bridge/plugins/ethereum2/core/AcbEthClient.java
+++ b/acb-sdk/pluginset/ethereum2/offchain-plugin/src/main/java/com/alipay/antchain/bridge/plugins/ethereum2/core/AcbEthClient.java
@@ -512,7 +512,10 @@ public EthConsensusStateData getEthConsensusStateData(BigInteger slot, String am
ethConsensusStateData.setAmContractHex(amContract);
// last slot for this period
var currPeriod = currentSyncCommitteePeriod(slot);
- var currPeriodEndSlot = currPeriod.multiply(BigInteger.valueOf(config.getEth2ChainConfig().getSyncPeriodLength()));
+ var currPeriodEndSlot = currPeriod
+ .add(BigInteger.ONE)
+ .multiply(BigInteger.valueOf(config.getEth2ChainConfig().getSyncPeriodLength()))
+ .subtract(BigInteger.ONE);
if (currPeriodEndSlot.equals(slot)) {
// fetch the sync committee update
getBbcLogger().info("get light client update for next period: {}", currPeriod.add(BigInteger.ONE));
diff --git a/acb-sdk/pluginset/ethereum2/offchain-plugin/src/main/java/com/alipay/antchain/bridge/plugins/ethereum2/core/EthConsensusEndorsements.java b/acb-sdk/pluginset/ethereum2/offchain-plugin/src/main/java/com/alipay/antchain/bridge/plugins/ethereum2/core/EthConsensusEndorsements.java
index d5e36c25..ba6de3de 100644
--- a/acb-sdk/pluginset/ethereum2/offchain-plugin/src/main/java/com/alipay/antchain/bridge/plugins/ethereum2/core/EthConsensusEndorsements.java
+++ b/acb-sdk/pluginset/ethereum2/offchain-plugin/src/main/java/com/alipay/antchain/bridge/plugins/ethereum2/core/EthConsensusEndorsements.java
@@ -1,7 +1,9 @@
package com.alipay.antchain.bridge.plugins.ethereum2.core;
+import cn.hutool.core.util.ObjectUtil;
import com.alibaba.fastjson.JSONObject;
import lombok.*;
+import tech.pegasys.teku.infrastructure.unsigned.UInt64;
import tech.pegasys.teku.infrastructure.json.JsonUtil;
import tech.pegasys.teku.spec.datastructures.blocks.blockbody.versions.altair.SyncAggregate;
import tech.pegasys.teku.spec.datastructures.blocks.blockbody.versions.altair.SyncAggregateSchema;
@@ -16,7 +18,8 @@ public static EthConsensusEndorsements fromJson(String json, int syncCommitteeSi
try {
JSONObject jsonObject = JSONObject.parseObject(json);
return new EthConsensusEndorsements(
- JsonUtil.parse(jsonObject.getString("sync_aggregate"), SyncAggregateSchema.create(syncCommitteeSize).getJsonTypeDefinition())
+ JsonUtil.parse(jsonObject.getString("sync_aggregate"), SyncAggregateSchema.create(syncCommitteeSize).getJsonTypeDefinition()),
+ jsonObject.containsKey("signature_slot") ? UInt64.valueOf(jsonObject.getString("signature_slot")) : null
);
} catch (Exception e) {
throw new RuntimeException("failed to parse EthConsensusEndorsements from json: ", e);
@@ -25,10 +28,23 @@ public static EthConsensusEndorsements fromJson(String json, int syncCommitteeSi
private SyncAggregate syncAggregate;
+ private UInt64 signatureSlot;
+
+ public EthConsensusEndorsements(SyncAggregate syncAggregate) {
+ this.syncAggregate = syncAggregate;
+ }
+
+ public UInt64 getSignatureSlotOrDefault(UInt64 fallback) {
+ return ObjectUtil.defaultIfNull(signatureSlot, fallback);
+ }
+
@SneakyThrows
public String toJson() {
JSONObject jsonObject = new JSONObject();
jsonObject.put("sync_aggregate", JsonUtil.serialize(syncAggregate, syncAggregate.getSchema().getJsonTypeDefinition()));
+ if (ObjectUtil.isNotNull(signatureSlot)) {
+ jsonObject.put("signature_slot", signatureSlot.toString());
+ }
return jsonObject.toJSONString();
}
}
diff --git a/acb-sdk/pluginset/ethereum2/offchain-plugin/src/main/java/com/alipay/antchain/bridge/plugins/ethereum2/core/EthConsensusStateData.java b/acb-sdk/pluginset/ethereum2/offchain-plugin/src/main/java/com/alipay/antchain/bridge/plugins/ethereum2/core/EthConsensusStateData.java
index 7354a565..e8c65243 100644
--- a/acb-sdk/pluginset/ethereum2/offchain-plugin/src/main/java/com/alipay/antchain/bridge/plugins/ethereum2/core/EthConsensusStateData.java
+++ b/acb-sdk/pluginset/ethereum2/offchain-plugin/src/main/java/com/alipay/antchain/bridge/plugins/ethereum2/core/EthConsensusStateData.java
@@ -96,6 +96,11 @@ public void setAmContractHex(String contractHex) {
}
public void validate(SyncCommittee currSyncCommittee, EthConsensusEndorsements endorsements, Eth2ChainConfig eth2ChainConfig) {
+ validateBlock(currSyncCommittee, endorsements, eth2ChainConfig);
+ validateLightClientUpdate(currSyncCommittee, eth2ChainConfig);
+ }
+
+ public void validateBlock(SyncCommittee currSyncCommittee, EthConsensusEndorsements endorsements, Eth2ChainConfig eth2ChainConfig) {
if (ObjectUtil.isNotNull(this.beaconBlockHeader)) {
var schemaDefinitions = eth2ChainConfig.getCurrentSchemaDefinitions(this.beaconBlockHeader.getSlot().bigIntegerValue());
var bodySchema = schemaDefinitions.getBlindedBeaconBlockBodySchema();
@@ -127,9 +132,10 @@ public void validate(SyncCommittee currSyncCommittee, EthConsensusEndorsements e
}
}
+ var signatureSlot = endorsements.getSignatureSlotOrDefault(this.beaconBlockHeader.getSlot().increment());
var signingRoot = new SigningData(
this.beaconBlockHeader.getRoot(),
- Bytes32.wrap(eth2ChainConfig.getForkBySlot(this.beaconBlockHeader.getSlot().bigIntegerValue()).getDomain())
+ Bytes32.wrap(eth2ChainConfig.getForkBySlot(signatureSlot.bigIntegerValue()).getDomain())
).hashTreeRoot();
if (!BLSSignatureVerifier.SIMPLE.verify(
@@ -140,14 +146,16 @@ public void validate(SyncCommittee currSyncCommittee, EthConsensusEndorsements e
throw new InvalidConsensusDataException("sync committee signature is invalid");
}
}
+ }
+ public void validateLightClientUpdate(SyncCommittee currSyncCommittee, Eth2ChainConfig eth2ChainConfig) {
if (lightClientUpdateWrapper != null) {
- validateLightClientUpdate(lightClientUpdateWrapper, currSyncCommittee, eth2ChainConfig);
+ validateLightClientUpdateInternal(lightClientUpdateWrapper, currSyncCommittee, eth2ChainConfig);
}
}
public boolean isLastSlotForCurrentPeriod(long syncPeriodLength) {
- return this.beaconBlockHeader.getSlot().mod(syncPeriodLength).equals(UInt64.ZERO);
+ return this.beaconBlockHeader.getSlot().mod(syncPeriodLength).equals(UInt64.valueOf(syncPeriodLength - 1L));
}
public UInt64 getCurrSyncPeriod(long syncPeriodLength) {
@@ -177,7 +185,7 @@ public String toJson() {
return jsonObject.toJSONString();
}
- private void validateLightClientUpdate(
+ private void validateLightClientUpdateInternal(
LightClientUpdateWrapper lightClientUpdate,
SyncCommittee currentSyncCommittee,
Eth2ChainConfig eth2ChainConfig
@@ -216,7 +224,7 @@ private void validateLightClientUpdate(
// verify sync committee signature
var signingRoot = new SigningData(
attestedHeader.hashTreeRoot(),
- Bytes32.wrap(eth2ChainConfig.getForkBySlot(this.beaconBlockHeader.getSlot().bigIntegerValue()).getDomain())
+ Bytes32.wrap(eth2ChainConfig.getForkBySlot(lightClientUpdate.getSignatureSlot().bigIntegerValue()).getDomain())
).hashTreeRoot();
if (!BLSSignatureVerifier.SIMPLE.verify(
contributionPubkeys,
diff --git a/acb-sdk/pluginset/ethereum2/offchain-plugin/src/main/java/com/alipay/antchain/bridge/plugins/ethereum2/core/EthSubjectIdentity.java b/acb-sdk/pluginset/ethereum2/offchain-plugin/src/main/java/com/alipay/antchain/bridge/plugins/ethereum2/core/EthSubjectIdentity.java
index 800ef023..934ffbef 100644
--- a/acb-sdk/pluginset/ethereum2/offchain-plugin/src/main/java/com/alipay/antchain/bridge/plugins/ethereum2/core/EthSubjectIdentity.java
+++ b/acb-sdk/pluginset/ethereum2/offchain-plugin/src/main/java/com/alipay/antchain/bridge/plugins/ethereum2/core/EthSubjectIdentity.java
@@ -34,6 +34,10 @@ public static EthSubjectIdentity fromJson(String json) {
if (subjectId.getCurrentSyncCommittee().getPubkeys().size() != subjectId.getEth2ChainConfig().getSyncCommitteeSize()) {
throw new RuntimeException("sync committee size not match with chain config");
}
+ if (subjectId.getNextSyncCommittee() != null
+ && subjectId.getNextSyncCommittee().getPubkeys().size() != subjectId.getEth2ChainConfig().getSyncCommitteeSize()) {
+ throw new RuntimeException("next sync committee size not match with chain config");
+ }
return subjectId;
}
@@ -73,9 +77,17 @@ public void write(JSONSerializer serializer, Object object, Object fieldName, Ty
@JSONField(name = "current_sync_committee", deserializeUsing = SyncCommitteeDeserializer.class, serializeUsing = SyncCommitteeSerializer.class)
private SyncCommittee currentSyncCommittee;
+ @JSONField(name = "next_sync_committee", deserializeUsing = SyncCommitteeDeserializer.class, serializeUsing = SyncCommitteeSerializer.class)
+ private SyncCommittee nextSyncCommittee;
+
@JSONField(name = "eth2_chain_config", deserializeUsing = Eth2ChainConfigDeserializer.class)
private Eth2ChainConfig eth2ChainConfig;
+ public EthSubjectIdentity(SyncCommittee currentSyncCommittee, Eth2ChainConfig eth2ChainConfig) {
+ this.currentSyncCommittee = currentSyncCommittee;
+ this.eth2ChainConfig = eth2ChainConfig;
+ }
+
public String toJson() {
return JSON.toJSONString(this);
}
diff --git a/acb-sdk/pluginset/ethereum2/offchain-plugin/src/main/java/com/alipay/antchain/bridge/plugins/ethereum2/core/eth/beacon/AcbBeaconClient.java b/acb-sdk/pluginset/ethereum2/offchain-plugin/src/main/java/com/alipay/antchain/bridge/plugins/ethereum2/core/eth/beacon/AcbBeaconClient.java
index 531b837b..9363cffa 100644
--- a/acb-sdk/pluginset/ethereum2/offchain-plugin/src/main/java/com/alipay/antchain/bridge/plugins/ethereum2/core/eth/beacon/AcbBeaconClient.java
+++ b/acb-sdk/pluginset/ethereum2/offchain-plugin/src/main/java/com/alipay/antchain/bridge/plugins/ethereum2/core/eth/beacon/AcbBeaconClient.java
@@ -321,7 +321,12 @@ public Map getRemoteSpec() {
switch (resp.statusCode()) {
case 200 -> {
var body = JSON.parseObject(resp.body(), ObjectAndMetaData.class);
- return JSON.parseObject(body.getData(), new TypeReference<>(){});
+ Map remoteSpec = JSON.parseObject(body.getData(), new TypeReference<>(){});
+ remoteSpec.putIfAbsent("GOSSIP_MAX_SIZE", "10485760");
+ remoteSpec.putIfAbsent("MAX_CHUNK_SIZE", "10485760");
+ remoteSpec.putIfAbsent("TTFB_TIMEOUT", "5");
+ remoteSpec.putIfAbsent("RESP_TIMEOUT", "10");
+ return remoteSpec;
}
default -> throw new RuntimeException(StrUtil.format("failed to spec config: {}", resp.body()));
}
diff --git a/acb-sdk/pluginset/ethereum2/offchain-plugin/src/test/java/com/alipay/antchain/bridge/plugins/ethereum2/EthereumBBCServiceTest.java b/acb-sdk/pluginset/ethereum2/offchain-plugin/src/test/java/com/alipay/antchain/bridge/plugins/ethereum2/EthereumBBCServiceTest.java
index 10dc7cef..b91bc7ca 100644
--- a/acb-sdk/pluginset/ethereum2/offchain-plugin/src/test/java/com/alipay/antchain/bridge/plugins/ethereum2/EthereumBBCServiceTest.java
+++ b/acb-sdk/pluginset/ethereum2/offchain-plugin/src/test/java/com/alipay/antchain/bridge/plugins/ethereum2/EthereumBBCServiceTest.java
@@ -739,7 +739,7 @@ public void testReadConsensusState() {
period = currSlot.divide(BigInteger.valueOf(syncPeriodLength));
log.info("found period {}", period);
- currSlot = period.multiply(BigInteger.valueOf(syncPeriodLength));
+ currSlot = period.add(BigInteger.ONE).multiply(BigInteger.valueOf(syncPeriodLength)).subtract(BigInteger.ONE);
cs = ethereumBBCService.readConsensusState(currSlot);
currBlock = ethereumBBCService.getAcbEthClient().getBeaconBlockBySlot(currSlot);
@@ -756,6 +756,7 @@ public void testReadConsensusState() {
Assert.assertNotNull(stateData.getBeaconBlockHeader());
Assert.assertNotNull(stateData.getExecutionPayloadBranches());
Assert.assertEquals(period, stateData.getCurrSyncPeriod(syncPeriodLength).bigIntegerValue());
+ Assert.assertTrue(stateData.isLastSlotForCurrentPeriod(syncPeriodLength));
endorsements = EthConsensusEndorsements.fromJson(new String(cs.getEndorsements()), specConfig.getSyncCommitteeSize());
Assert.assertNotNull(endorsements.getSyncAggregate());
diff --git a/acb-sdk/pluginset/ethereum2/offchain-plugin/src/test/java/com/alipay/antchain/bridge/plugins/ethereum2/EthereumHcdvsTest.java b/acb-sdk/pluginset/ethereum2/offchain-plugin/src/test/java/com/alipay/antchain/bridge/plugins/ethereum2/EthereumHcdvsTest.java
index 086606a9..84f934be 100644
--- a/acb-sdk/pluginset/ethereum2/offchain-plugin/src/test/java/com/alipay/antchain/bridge/plugins/ethereum2/EthereumHcdvsTest.java
+++ b/acb-sdk/pluginset/ethereum2/offchain-plugin/src/test/java/com/alipay/antchain/bridge/plugins/ethereum2/EthereumHcdvsTest.java
@@ -15,6 +15,7 @@
import com.alipay.antchain.bridge.commons.core.bta.BlockchainTrustAnchorFactory;
import com.alipay.antchain.bridge.commons.core.bta.IBlockchainTrustAnchor;
import com.alipay.antchain.bridge.commons.utils.crypto.SignAlgoEnum;
+import com.alipay.antchain.bridge.plugins.ethereum2.core.EthConsensusStateData;
import com.alipay.antchain.bridge.plugins.ethereum2.core.EthSubjectIdentity;
import com.alipay.antchain.bridge.plugins.ethereum2.tools.EthBbcTools;
import lombok.extern.slf4j.Slf4j;
@@ -22,6 +23,7 @@
import org.slf4j.Logger;
import org.web3j.utils.Numeric;
+import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
@@ -147,6 +149,24 @@ public void testHcdvsVerifyCrossChainMessage() {
assertTrue(result.isSuccess());
}
+ @Test
+ public void testPeriodTailBoundary() {
+ var chainConfig = BTA_SUBJECT_IDENTITY.getEth2ChainConfig();
+ var parentStateData = EthConsensusStateData.fromJson(
+ new String(PARENT_CS_WHERE_PERIOD_END.getStateData()),
+ chainConfig.getCurrentSchemaDefinitions(PARENT_CS_WHERE_PERIOD_END.getHeight()),
+ chainConfig.getSpecConfig()
+ );
+ var currStateData = EthConsensusStateData.fromJson(
+ new String(CS_WHERE_PERIOD_END.getStateData()),
+ chainConfig.getCurrentSchemaDefinitions(CS_WHERE_PERIOD_END.getHeight()),
+ chainConfig.getSpecConfig()
+ );
+
+ assertTrue(parentStateData.isLastSlotForCurrentPeriod(chainConfig.getSyncPeriodLength()));
+ assertFalse(currStateData.isLastSlotForCurrentPeriod(chainConfig.getSyncPeriodLength()));
+ }
+
@Test
public void testHcdvsParseMessageFromLedgerData() {
var raw = ETHEREUM_HCDVS_SERVICE.parseMessageFromLedgerData(MSG1.getProvableData().getLedgerData());
From d82f3c418426b255726c8f302b1c8ea1dfc3c8c4 Mon Sep 17 00:00:00 2001
From: 0xstride <73103011+fengjy73@users.noreply.github.com>
Date: Sat, 8 Aug 2026 17:47:56 +0800
Subject: [PATCH 2/2] [fix][ethereum2] handle sync committee boundaries
---
.../plugins/ethereum2/EthereumBBCService.java | 5 +
.../ethereum2/EthereumHcdvsService.java | 177 +++++++++++-------
.../plugins/ethereum2/core/AcbEthClient.java | 72 ++++---
.../ethereum2/core/EthConsensusStateData.java | 11 +-
.../ethereum2/core/EthSubjectIdentity.java | 4 +
.../plugins/ethereum2/EthereumHcdvsTest.java | 125 +++++++++++++
6 files changed, 306 insertions(+), 88 deletions(-)
diff --git a/acb-sdk/pluginset/ethereum2/offchain-plugin/src/main/java/com/alipay/antchain/bridge/plugins/ethereum2/EthereumBBCService.java b/acb-sdk/pluginset/ethereum2/offchain-plugin/src/main/java/com/alipay/antchain/bridge/plugins/ethereum2/EthereumBBCService.java
index 12c21472..461375e3 100644
--- a/acb-sdk/pluginset/ethereum2/offchain-plugin/src/main/java/com/alipay/antchain/bridge/plugins/ethereum2/EthereumBBCService.java
+++ b/acb-sdk/pluginset/ethereum2/offchain-plugin/src/main/java/com/alipay/antchain/bridge/plugins/ethereum2/EthereumBBCService.java
@@ -444,6 +444,11 @@ public ConsensusState readConsensusState(BigInteger slot) {
beaconBlockWithSyncAggregate.getBody().getOptionalSyncAggregate().get(),
beaconBlockWithSyncAggregate.getSlot()
);
+ this.acbEthClient.populateLightClientUpdateIfRequired(
+ ethConsensusData,
+ slot,
+ beaconBlockWithSyncAggregate.getSlot().bigIntegerValue()
+ );
return new ConsensusState(
slot,
diff --git a/acb-sdk/pluginset/ethereum2/offchain-plugin/src/main/java/com/alipay/antchain/bridge/plugins/ethereum2/EthereumHcdvsService.java b/acb-sdk/pluginset/ethereum2/offchain-plugin/src/main/java/com/alipay/antchain/bridge/plugins/ethereum2/EthereumHcdvsService.java
index 9412b707..1472a9c6 100644
--- a/acb-sdk/pluginset/ethereum2/offchain-plugin/src/main/java/com/alipay/antchain/bridge/plugins/ethereum2/EthereumHcdvsService.java
+++ b/acb-sdk/pluginset/ethereum2/offchain-plugin/src/main/java/com/alipay/antchain/bridge/plugins/ethereum2/EthereumHcdvsService.java
@@ -21,6 +21,7 @@
import org.web3j.tx.Contract;
import org.web3j.utils.Numeric;
import tech.pegasys.teku.infrastructure.unsigned.UInt64;
+import tech.pegasys.teku.spec.datastructures.state.SyncCommittee;
@HeteroChainDataVerifierService(pluginId = "plugin-ethereum2", products = "ethereum2")
public class EthereumHcdvsService extends AbstractHCDVSService {
@@ -53,7 +54,7 @@ public VerifyResult verifyAnchorConsensusState(IBlockchainTrustAnchor bta, Conse
ethSubjectIdentity.getCurrentSyncCommittee().getPubkeys().size()
);
try {
- ethConsensusStateData.validate(ethSubjectIdentity.getCurrentSyncCommittee(), ethEndorsements, ethSubjectIdentity.getEth2ChainConfig());
+ verifyAndUpdateSyncCommittee(ethSubjectIdentity, ethConsensusStateData, ethEndorsements);
} catch (InvalidConsensusDataException e) {
getHCDVSLogger().error("failed to verify eth consensus state data (slot: {}, hash: {}) for domain {}",
anchorState.getHeight().toString(), anchorState.getHashHex(), bta.getDomain().toString(), e);
@@ -62,15 +63,6 @@ public VerifyResult verifyAnchorConsensusState(IBlockchainTrustAnchor bta, Conse
getHCDVSLogger().info("successful to verify anchor consensus state ⚓️ (slot: {}, hash: {}) for domain {} now!",
anchorState.getHeight().toString(), anchorState.getHashHex(), bta.getDomain().toString());
- if (ethConsensusStateData.getLightClientUpdateWrapper() != null) {
- ethSubjectIdentity.setNextSyncCommittee(ethConsensusStateData.getLightClientUpdateWrapper().getNextSyncCommittee());
- }
- if (ethConsensusStateData.isLastSlotForCurrentPeriod(ethSubjectIdentity.getEth2ChainConfig().getSyncPeriodLength())
- && ObjectUtil.isNotNull(ethSubjectIdentity.getNextSyncCommittee())) {
- getHCDVSLogger().info("light client update inside last-slot anchor consensus state, switch current sync committee");
- ethSubjectIdentity.setCurrentSyncCommittee(ethSubjectIdentity.getNextSyncCommittee());
- ethSubjectIdentity.setNextSyncCommittee(null);
- }
anchorState.setConsensusNodeInfo(ethSubjectIdentity.toJson().getBytes());
return VerifyResult.success();
}
@@ -129,54 +121,17 @@ public VerifyResult verifyConsensusState(ConsensusState stateToVerify, Consensus
ethSubjectIdentity.getCurrentSyncCommittee().getPubkeys().size()
);
var syncPeriodLength = ethSubjectIdentity.getEth2ChainConfig().getSyncPeriodLength();
- var currPeriod = ethConsensusStateData.getCurrSyncPeriod(syncPeriodLength);
- var signatureSlot = ethEndorsements.getSignatureSlotOrDefault(currSlot.increment());
- var signaturePeriod = signatureSlot.dividedBy(syncPeriodLength);
- var shouldRotateCommittee = ethConsensusStateData.isLastSlotForCurrentPeriod(syncPeriodLength);
-
- if (shouldRotateCommittee) {
- if (ethConsensusStateData.getLightClientUpdateWrapper() == null) {
- getHCDVSLogger().error("❌ has none light client update for the last slot {} for current period {}",
- ethConsensusStateData.getBeaconBlockHeader().getSlot().toString(),
- currPeriod
- );
- return VerifyResult.fail("none light client update at last slot in period");
- }
- try {
- ethConsensusStateData.validateLightClientUpdate(
- ethSubjectIdentity.getCurrentSyncCommittee(),
- ethSubjectIdentity.getEth2ChainConfig()
- );
- } catch (InvalidConsensusDataException e) {
- getHCDVSLogger().error("❌ failed to verify light client update at last slot {}",
- stateToVerify.getHeight().toString(), e);
- return VerifyResult.fail("failed to verify light client update: {}", e.getMessage());
- }
- ethSubjectIdentity.setNextSyncCommittee(ethConsensusStateData.getLightClientUpdateWrapper().getNextSyncCommittee());
- }
-
- var committeeForBlockVerification = ethSubjectIdentity.getCurrentSyncCommittee();
- if (signaturePeriod.equals(currPeriod)) {
- committeeForBlockVerification = ethSubjectIdentity.getCurrentSyncCommittee();
- } else if (signaturePeriod.equals(currPeriod.increment())) {
- if (ObjectUtil.isNull(ethSubjectIdentity.getNextSyncCommittee())) {
- getHCDVSLogger().error("❌ missing next sync committee for slot {}, while endorsements come from signature slot {}",
- stateToVerify.getHeight().toString(), signatureSlot.toString());
- return VerifyResult.fail("missing next sync committee for endorsements");
+ if (ObjectUtil.isNull(ethSubjectIdentity.getCurrentSyncCommitteePeriod())) {
+ var parentPeriod = parentConsensusData.getCurrSyncPeriod(syncPeriodLength).bigIntegerValue();
+ if (parentConsensusData.isLastSlotForCurrentPeriod(syncPeriodLength)
+ && ObjectUtil.isNull(ethSubjectIdentity.getNextSyncCommittee())) {
+ parentPeriod = parentPeriod.add(BigInteger.ONE);
}
- committeeForBlockVerification = ethSubjectIdentity.getNextSyncCommittee();
- } else {
- getHCDVSLogger().error("❌ unexpected endorsements signature slot {} for state slot {}",
- signatureSlot.toString(), stateToVerify.getHeight().toString());
- return VerifyResult.fail("unexpected endorsements signature slot");
+ ethSubjectIdentity.setCurrentSyncCommitteePeriod(parentPeriod);
}
try {
- ethConsensusStateData.validateBlock(
- committeeForBlockVerification,
- ethEndorsements,
- ethSubjectIdentity.getEth2ChainConfig()
- );
+ verifyAndUpdateSyncCommittee(ethSubjectIdentity, ethConsensusStateData, ethEndorsements);
} catch (InvalidConsensusDataException e) {
getHCDVSLogger().error("❌ failed to verify eth consensus state data (slot: {}, hash: {})",
stateToVerify.getHeight().toString(), stateToVerify.getHashHex(), e);
@@ -184,15 +139,6 @@ public VerifyResult verifyConsensusState(ConsensusState stateToVerify, Consensus
}
}
- if (ethConsensusStateData.isLastSlotForCurrentPeriod(ethSubjectIdentity.getEth2ChainConfig().getSyncPeriodLength())) {
- getHCDVSLogger().info("🗳️ last slot {} for current period {}, update the sync committee",
- ethConsensusStateData.getBeaconBlockHeader().getSlot().toString(),
- ethConsensusStateData.getCurrSyncPeriod(ethSubjectIdentity.getEth2ChainConfig().getSyncPeriodLength())
- );
- ethSubjectIdentity.setCurrentSyncCommittee(ethSubjectIdentity.getNextSyncCommittee());
- ethSubjectIdentity.setNextSyncCommittee(null);
- }
-
stateToVerify.setConsensusNodeInfo(ethSubjectIdentity.toJson().getBytes());
getHCDVSLogger().info("🌈 successful to verify consensus state (slot: {}, root: {}) now!",
@@ -200,6 +146,111 @@ public VerifyResult verifyConsensusState(ConsensusState stateToVerify, Consensus
return VerifyResult.success();
}
+ void verifyAndUpdateSyncCommittee(
+ EthSubjectIdentity subjectIdentity,
+ EthConsensusStateData consensusStateData,
+ EthConsensusEndorsements endorsements
+ ) {
+ var syncPeriodLength = subjectIdentity.getEth2ChainConfig().getSyncPeriodLength();
+ var headerPeriod = consensusStateData.getCurrSyncPeriod(syncPeriodLength).bigIntegerValue();
+ var signatureSlot = endorsements.getSignatureSlotOrDefault(
+ consensusStateData.getBeaconBlockHeader().getSlot().increment()
+ );
+
+ advanceCurrentSyncCommitteeToPeriod(subjectIdentity, headerPeriod);
+ validateAndStoreNextSyncCommittee(subjectIdentity, consensusStateData);
+ consensusStateData.validateBlock(
+ getCommitteeForSignaturePeriod(
+ subjectIdentity,
+ signatureSlot.dividedBy(syncPeriodLength).bigIntegerValue()
+ ),
+ endorsements,
+ subjectIdentity.getEth2ChainConfig()
+ );
+ rotateCommitteeAfterPeriodTail(subjectIdentity, consensusStateData);
+ }
+
+ void advanceCurrentSyncCommitteeToPeriod(EthSubjectIdentity subjectIdentity, BigInteger targetPeriod) {
+ if (ObjectUtil.isNull(subjectIdentity.getCurrentSyncCommitteePeriod())) {
+ subjectIdentity.setCurrentSyncCommitteePeriod(targetPeriod);
+ return;
+ }
+ if (subjectIdentity.getCurrentSyncCommitteePeriod().equals(targetPeriod)) {
+ return;
+ }
+ if (!subjectIdentity.getCurrentSyncCommitteePeriod().add(BigInteger.ONE).equals(targetPeriod)) {
+ throw new InvalidConsensusDataException("unexpected sync committee period transition");
+ }
+ if (ObjectUtil.isNull(subjectIdentity.getNextSyncCommittee())) {
+ throw new InvalidConsensusDataException("missing next sync committee for period transition");
+ }
+
+ subjectIdentity.setCurrentSyncCommittee(subjectIdentity.getNextSyncCommittee());
+ subjectIdentity.setNextSyncCommittee(null);
+ subjectIdentity.setCurrentSyncCommitteePeriod(targetPeriod);
+ }
+
+ private void validateAndStoreNextSyncCommittee(
+ EthSubjectIdentity subjectIdentity,
+ EthConsensusStateData consensusStateData
+ ) {
+ if (ObjectUtil.isNull(consensusStateData.getLightClientUpdateWrapper())) {
+ return;
+ }
+
+ consensusStateData.validateLightClientUpdate(
+ subjectIdentity.getCurrentSyncCommittee(),
+ subjectIdentity.getEth2ChainConfig()
+ );
+ var authenticatedNext = consensusStateData.getLightClientUpdateWrapper().getNextSyncCommittee();
+ if (ObjectUtil.isNotNull(subjectIdentity.getNextSyncCommittee())
+ && !subjectIdentity.getNextSyncCommittee().hashTreeRoot().equals(authenticatedNext.hashTreeRoot())) {
+ throw new InvalidConsensusDataException("conflicting next sync committee");
+ }
+ subjectIdentity.setNextSyncCommittee(authenticatedNext);
+ }
+
+ private SyncCommittee getCommitteeForSignaturePeriod(
+ EthSubjectIdentity subjectIdentity,
+ BigInteger signaturePeriod
+ ) {
+ var currentPeriod = subjectIdentity.getCurrentSyncCommitteePeriod();
+ if (signaturePeriod.equals(currentPeriod)) {
+ return subjectIdentity.getCurrentSyncCommittee();
+ }
+ if (signaturePeriod.equals(currentPeriod.add(BigInteger.ONE))) {
+ if (ObjectUtil.isNull(subjectIdentity.getNextSyncCommittee())) {
+ throw new InvalidConsensusDataException("missing next sync committee for endorsements");
+ }
+ return subjectIdentity.getNextSyncCommittee();
+ }
+ throw new InvalidConsensusDataException("unexpected endorsements signature period");
+ }
+
+ private void rotateCommitteeAfterPeriodTail(
+ EthSubjectIdentity subjectIdentity,
+ EthConsensusStateData consensusStateData
+ ) {
+ if (!consensusStateData.isLastSlotForCurrentPeriod(
+ subjectIdentity.getEth2ChainConfig().getSyncPeriodLength()
+ )) {
+ return;
+ }
+ if (ObjectUtil.isNull(subjectIdentity.getNextSyncCommittee())) {
+ throw new InvalidConsensusDataException("missing next sync committee at period tail");
+ }
+
+ getHCDVSLogger().info("🗳️ last slot {} for current period {}, update the sync committee",
+ consensusStateData.getBeaconBlockHeader().getSlot().toString(),
+ subjectIdentity.getCurrentSyncCommitteePeriod()
+ );
+ subjectIdentity.setCurrentSyncCommittee(subjectIdentity.getNextSyncCommittee());
+ subjectIdentity.setNextSyncCommittee(null);
+ subjectIdentity.setCurrentSyncCommitteePeriod(
+ subjectIdentity.getCurrentSyncCommitteePeriod().add(BigInteger.ONE)
+ );
+ }
+
@Override
public VerifyResult verifyCrossChainMessage(CrossChainMessage message, ConsensusState currState) {
if (new BigInteger(currState.getHash()).equals(BigInteger.ZERO)) {
diff --git a/acb-sdk/pluginset/ethereum2/offchain-plugin/src/main/java/com/alipay/antchain/bridge/plugins/ethereum2/core/AcbEthClient.java b/acb-sdk/pluginset/ethereum2/offchain-plugin/src/main/java/com/alipay/antchain/bridge/plugins/ethereum2/core/AcbEthClient.java
index 058e16f6..53528d5c 100644
--- a/acb-sdk/pluginset/ethereum2/offchain-plugin/src/main/java/com/alipay/antchain/bridge/plugins/ethereum2/core/AcbEthClient.java
+++ b/acb-sdk/pluginset/ethereum2/offchain-plugin/src/main/java/com/alipay/antchain/bridge/plugins/ethereum2/core/AcbEthClient.java
@@ -510,33 +510,12 @@ public List readAuthMessagesFromBlock(BigInteger slot, String
public EthConsensusStateData getEthConsensusStateData(BigInteger slot, String amContract) {
var ethConsensusStateData = new EthConsensusStateData();
ethConsensusStateData.setAmContractHex(amContract);
- // last slot for this period
- var currPeriod = currentSyncCommitteePeriod(slot);
- var currPeriodEndSlot = currPeriod
- .add(BigInteger.ONE)
- .multiply(BigInteger.valueOf(config.getEth2ChainConfig().getSyncPeriodLength()))
- .subtract(BigInteger.ONE);
- if (currPeriodEndSlot.equals(slot)) {
- // fetch the sync committee update
- getBbcLogger().info("get light client update for next period: {}", currPeriod.add(BigInteger.ONE));
- var lightClientUpdate = getLightClientUpdate(slot);
- if (ObjectUtil.isNull(lightClientUpdate)) {
- getBbcLogger().error("none update found for period: {}", currPeriod);
- throw new RuntimeException(StrUtil.format("none update found for period: {}", currPeriod.toString()));
- }
- ethConsensusStateData.setLightClientUpdateWrapper(lightClientUpdate);
- }
getBbcLogger().info("has ccmsg on slot {} or already has no cache, will fetch the whole beacon block...", slot);
- var signedBeaconBlock = getBeaconBlockBySlot(slot);
- if (ObjectUtil.isNull(signedBeaconBlock)) {
- return ethConsensusStateData;
- }
- if (signedBeaconBlock.getBeaconBlock().isEmpty()) {
- getBbcLogger().warn("slot {} has no beacon block, could be empty", slot);
+ var beaconBlock = getBeaconBlockBySlot(slot);
+ if (ObjectUtil.isNull(beaconBlock)) {
return ethConsensusStateData;
}
- var beaconBlock = signedBeaconBlock.getBeaconBlock().get();
if (beaconBlock.getBody().getOptionalExecutionPayloadHeader().isEmpty()) {
throw new RuntimeException("no execution payload found in beacon block as slot " + slot);
}
@@ -552,9 +531,56 @@ public EthConsensusStateData getEthConsensusStateData(BigInteger slot, String am
)
);
+ populateLightClientUpdateIfRequired(ethConsensusStateData, slot, null);
+
return ethConsensusStateData;
}
+ public void populateLightClientUpdateIfRequired(
+ EthConsensusStateData ethConsensusStateData,
+ BigInteger stateSlot,
+ BigInteger signatureSlot
+ ) {
+ var syncPeriodLength = BigInteger.valueOf(config.getEth2ChainConfig().getSyncPeriodLength());
+ var statePeriod = stateSlot.divide(syncPeriodLength);
+ var signaturePeriod = ObjectUtil.isNull(signatureSlot) ? statePeriod : signatureSlot.divide(syncPeriodLength);
+ if (signaturePeriod.compareTo(statePeriod.add(BigInteger.ONE)) > 0) {
+ throw new RuntimeException(StrUtil.format(
+ "signature slot {} is more than one sync committee period ahead of state slot {}",
+ signatureSlot, stateSlot
+ ));
+ }
+ if (!requiresLightClientUpdate(stateSlot, signatureSlot, syncPeriodLength.longValue())) {
+ return;
+ }
+ if (ObjectUtil.isNotNull(ethConsensusStateData.getLightClientUpdateWrapper())) {
+ return;
+ }
+
+ getBbcLogger().info("get light client update for next period: {}", statePeriod.add(BigInteger.ONE));
+ var lightClientUpdate = getLightClientUpdate(stateSlot);
+ if (ObjectUtil.isNull(lightClientUpdate)) {
+ getBbcLogger().error("none update found for period: {}", statePeriod);
+ throw new RuntimeException(StrUtil.format("none update found for period: {}", statePeriod.toString()));
+ }
+ ethConsensusStateData.setLightClientUpdateWrapper(lightClientUpdate);
+ }
+
+ public static boolean requiresLightClientUpdate(
+ BigInteger stateSlot,
+ BigInteger signatureSlot,
+ long syncPeriodLength
+ ) {
+ var periodLength = BigInteger.valueOf(syncPeriodLength);
+ var statePeriod = stateSlot.divide(periodLength);
+ var periodEndSlot = statePeriod.add(BigInteger.ONE).multiply(periodLength).subtract(BigInteger.ONE);
+ if (periodEndSlot.equals(stateSlot)) {
+ return true;
+ }
+ return ObjectUtil.isNotNull(signatureSlot)
+ && signatureSlot.divide(periodLength).compareTo(statePeriod) > 0;
+ }
+
public boolean hasTpBtaOnPtcHub(String ptcHubAddress, CrossChainLane tpbtaLane, int tpBtaVersion) {
try {
PtcHub ptcHub = PtcHub.load(ptcHubAddress, this.web3j, this.rawTransactionManager, null);
diff --git a/acb-sdk/pluginset/ethereum2/offchain-plugin/src/main/java/com/alipay/antchain/bridge/plugins/ethereum2/core/EthConsensusStateData.java b/acb-sdk/pluginset/ethereum2/offchain-plugin/src/main/java/com/alipay/antchain/bridge/plugins/ethereum2/core/EthConsensusStateData.java
index e8c65243..65680201 100644
--- a/acb-sdk/pluginset/ethereum2/offchain-plugin/src/main/java/com/alipay/antchain/bridge/plugins/ethereum2/core/EthConsensusStateData.java
+++ b/acb-sdk/pluginset/ethereum2/offchain-plugin/src/main/java/com/alipay/antchain/bridge/plugins/ethereum2/core/EthConsensusStateData.java
@@ -1,5 +1,6 @@
package com.alipay.antchain.bridge.plugins.ethereum2.core;
+import java.math.BigInteger;
import java.util.ArrayList;
import java.util.List;
@@ -135,7 +136,7 @@ public void validateBlock(SyncCommittee currSyncCommittee, EthConsensusEndorseme
var signatureSlot = endorsements.getSignatureSlotOrDefault(this.beaconBlockHeader.getSlot().increment());
var signingRoot = new SigningData(
this.beaconBlockHeader.getRoot(),
- Bytes32.wrap(eth2ChainConfig.getForkBySlot(signatureSlot.bigIntegerValue()).getDomain())
+ Bytes32.wrap(eth2ChainConfig.getForkBySlot(getSyncCommitteeForkSlot(signatureSlot)).getDomain())
).hashTreeRoot();
if (!BLSSignatureVerifier.SIMPLE.verify(
@@ -224,7 +225,7 @@ private void validateLightClientUpdateInternal(
// verify sync committee signature
var signingRoot = new SigningData(
attestedHeader.hashTreeRoot(),
- Bytes32.wrap(eth2ChainConfig.getForkBySlot(lightClientUpdate.getSignatureSlot().bigIntegerValue()).getDomain())
+ Bytes32.wrap(eth2ChainConfig.getForkBySlot(getSyncCommitteeForkSlot(lightClientUpdate.getSignatureSlot())).getDomain())
).hashTreeRoot();
if (!BLSSignatureVerifier.SIMPLE.verify(
contributionPubkeys,
@@ -234,4 +235,10 @@ private void validateLightClientUpdateInternal(
throw new InvalidConsensusDataException("sync committee signature is invalid");
}
}
+
+ static BigInteger getSyncCommitteeForkSlot(UInt64 signatureSlot) {
+ return signatureSlot.bigIntegerValue()
+ .max(BigInteger.ONE)
+ .subtract(BigInteger.ONE);
+ }
}
diff --git a/acb-sdk/pluginset/ethereum2/offchain-plugin/src/main/java/com/alipay/antchain/bridge/plugins/ethereum2/core/EthSubjectIdentity.java b/acb-sdk/pluginset/ethereum2/offchain-plugin/src/main/java/com/alipay/antchain/bridge/plugins/ethereum2/core/EthSubjectIdentity.java
index 934ffbef..e4398ce7 100644
--- a/acb-sdk/pluginset/ethereum2/offchain-plugin/src/main/java/com/alipay/antchain/bridge/plugins/ethereum2/core/EthSubjectIdentity.java
+++ b/acb-sdk/pluginset/ethereum2/offchain-plugin/src/main/java/com/alipay/antchain/bridge/plugins/ethereum2/core/EthSubjectIdentity.java
@@ -2,6 +2,7 @@
import java.io.IOException;
import java.lang.reflect.Type;
+import java.math.BigInteger;
import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson.JSON;
@@ -80,6 +81,9 @@ public void write(JSONSerializer serializer, Object object, Object fieldName, Ty
@JSONField(name = "next_sync_committee", deserializeUsing = SyncCommitteeDeserializer.class, serializeUsing = SyncCommitteeSerializer.class)
private SyncCommittee nextSyncCommittee;
+ @JSONField(name = "current_sync_committee_period")
+ private BigInteger currentSyncCommitteePeriod;
+
@JSONField(name = "eth2_chain_config", deserializeUsing = Eth2ChainConfigDeserializer.class)
private Eth2ChainConfig eth2ChainConfig;
diff --git a/acb-sdk/pluginset/ethereum2/offchain-plugin/src/test/java/com/alipay/antchain/bridge/plugins/ethereum2/EthereumHcdvsTest.java b/acb-sdk/pluginset/ethereum2/offchain-plugin/src/test/java/com/alipay/antchain/bridge/plugins/ethereum2/EthereumHcdvsTest.java
index 84f934be..1f99b4bb 100644
--- a/acb-sdk/pluginset/ethereum2/offchain-plugin/src/test/java/com/alipay/antchain/bridge/plugins/ethereum2/EthereumHcdvsTest.java
+++ b/acb-sdk/pluginset/ethereum2/offchain-plugin/src/test/java/com/alipay/antchain/bridge/plugins/ethereum2/EthereumHcdvsTest.java
@@ -15,17 +15,26 @@
import com.alipay.antchain.bridge.commons.core.bta.BlockchainTrustAnchorFactory;
import com.alipay.antchain.bridge.commons.core.bta.IBlockchainTrustAnchor;
import com.alipay.antchain.bridge.commons.utils.crypto.SignAlgoEnum;
+import com.alipay.antchain.bridge.plugins.ethereum2.core.AcbEthClient;
+import com.alipay.antchain.bridge.plugins.ethereum2.core.EthConsensusEndorsements;
import com.alipay.antchain.bridge.plugins.ethereum2.core.EthConsensusStateData;
import com.alipay.antchain.bridge.plugins.ethereum2.core.EthSubjectIdentity;
import com.alipay.antchain.bridge.plugins.ethereum2.tools.EthBbcTools;
import lombok.extern.slf4j.Slf4j;
import org.junit.Test;
+import org.mockito.InOrder;
import org.slf4j.Logger;
import org.web3j.utils.Numeric;
import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.inOrder;
+import static org.mockito.Mockito.spy;
@Slf4j
public class EthereumHcdvsTest {
@@ -167,6 +176,122 @@ public void testPeriodTailBoundary() {
assertFalse(currStateData.isLastSlotForCurrentPeriod(chainConfig.getSyncPeriodLength()));
}
+ @Test
+ public void testSignatureAtForkActivationUsesPreviousSlotDomain() {
+ var subjectIdentity = EthSubjectIdentity.fromJson(BTA_SUBJECT_IDENTITY.toJson());
+ var chainConfig = subjectIdentity.getEth2ChainConfig();
+ var tailStateData = EthConsensusStateData.fromJson(
+ new String(PARENT_CS_WHERE_PERIOD_END.getStateData()),
+ chainConfig.getCurrentSchemaDefinitions(PARENT_CS_WHERE_PERIOD_END.getHeight()),
+ chainConfig.getSpecConfig()
+ );
+ var endorsements = EthConsensusEndorsements.fromJson(
+ new String(PARENT_CS_WHERE_PERIOD_END.getEndorsements()),
+ subjectIdentity.getCurrentSyncCommittee().getPubkeys().size()
+ );
+ var signatureSlot = endorsements.getSignatureSlotOrDefault(
+ tailStateData.getBeaconBlockHeader().getSlot().increment()
+ );
+ var epochLength = BigInteger.valueOf(chainConfig.getEpochLength());
+ assertEquals(BigInteger.ZERO, signatureSlot.bigIntegerValue().mod(epochLength));
+
+ chainConfig.addFork(
+ "TEST_ACTIVATION",
+ signatureSlot.bigIntegerValue().divide(epochLength),
+ new byte[]{0x66, 0x66, 0x66, 0x66}
+ );
+ assertFalse(Arrays.equals(
+ chainConfig.getForkBySlot(signatureSlot.bigIntegerValue()).getDomain(),
+ chainConfig.getForkBySlot(signatureSlot.bigIntegerValue().subtract(BigInteger.ONE)).getDomain()
+ ));
+
+ tailStateData.validateBlock(
+ subjectIdentity.getCurrentSyncCommittee(),
+ endorsements,
+ chainConfig
+ );
+ }
+
+ @Test
+ public void testMissedPeriodTailCarriesAndPromotesNextCommittee() {
+ var subjectIdentity = EthSubjectIdentity.fromJson(BTA_SUBJECT_IDENTITY.toJson());
+ var chainConfig = subjectIdentity.getEth2ChainConfig();
+ var tailStateData = EthConsensusStateData.fromJson(
+ new String(PARENT_CS_WHERE_PERIOD_END.getStateData()),
+ chainConfig.getCurrentSchemaDefinitions(PARENT_CS_WHERE_PERIOD_END.getHeight()),
+ chainConfig.getSpecConfig()
+ );
+ var postBoundaryStateData = EthConsensusStateData.fromJson(
+ new String(CS_WHERE_PERIOD_END.getStateData()),
+ chainConfig.getCurrentSchemaDefinitions(CS_WHERE_PERIOD_END.getHeight()),
+ chainConfig.getSpecConfig()
+ );
+ var periodLength = BigInteger.valueOf(chainConfig.getSyncPeriodLength());
+ var tailSlot = tailStateData.getBeaconBlockHeader().getSlot().bigIntegerValue();
+ var currentPeriod = tailSlot.divide(periodLength);
+ var authenticatedNext = postBoundaryStateData.getLightClientUpdateWrapper().getNextSyncCommittee();
+
+ assertTrue(AcbEthClient.requiresLightClientUpdate(
+ tailSlot.subtract(BigInteger.ONE),
+ tailSlot.add(BigInteger.ONE),
+ chainConfig.getSyncPeriodLength()
+ ));
+
+ subjectIdentity.setCurrentSyncCommitteePeriod(currentPeriod);
+ subjectIdentity.setNextSyncCommittee(authenticatedNext);
+ ETHEREUM_HCDVS_SERVICE.advanceCurrentSyncCommitteeToPeriod(
+ subjectIdentity,
+ currentPeriod.add(BigInteger.ONE)
+ );
+
+ assertEquals(authenticatedNext.hashTreeRoot(), subjectIdentity.getCurrentSyncCommittee().hashTreeRoot());
+ assertEquals(currentPeriod.add(BigInteger.ONE), subjectIdentity.getCurrentSyncCommitteePeriod());
+ assertNull(subjectIdentity.getNextSyncCommittee());
+ }
+
+ @Test
+ public void testAnchorAtPeriodTailAuthenticatesNextCommitteeBeforeBlock() {
+ var subjectIdentity = EthSubjectIdentity.fromJson(BTA_SUBJECT_IDENTITY.toJson());
+ var chainConfig = subjectIdentity.getEth2ChainConfig();
+ var tailStateData = EthConsensusStateData.fromJson(
+ new String(PARENT_CS_WHERE_PERIOD_END.getStateData()),
+ chainConfig.getCurrentSchemaDefinitions(PARENT_CS_WHERE_PERIOD_END.getHeight()),
+ chainConfig.getSpecConfig()
+ );
+ var postBoundaryStateData = EthConsensusStateData.fromJson(
+ new String(CS_WHERE_PERIOD_END.getStateData()),
+ chainConfig.getCurrentSchemaDefinitions(CS_WHERE_PERIOD_END.getHeight()),
+ chainConfig.getSpecConfig()
+ );
+ var endorsements = EthConsensusEndorsements.fromJson(
+ new String(PARENT_CS_WHERE_PERIOD_END.getEndorsements()),
+ subjectIdentity.getCurrentSyncCommittee().getPubkeys().size()
+ );
+ var currentCommittee = subjectIdentity.getCurrentSyncCommittee();
+ var authenticatedNext = postBoundaryStateData.getLightClientUpdateWrapper().getNextSyncCommittee();
+ var stateDataSpy = spy(tailStateData);
+ stateDataSpy.setLightClientUpdateWrapper(postBoundaryStateData.getLightClientUpdateWrapper());
+ doNothing().when(stateDataSpy).validateLightClientUpdate(any(), any());
+ doNothing().when(stateDataSpy).validateBlock(any(), any(), any());
+
+ ETHEREUM_HCDVS_SERVICE.verifyAndUpdateSyncCommittee(subjectIdentity, stateDataSpy, endorsements);
+
+ InOrder validationOrder = inOrder(stateDataSpy);
+ validationOrder.verify(stateDataSpy).validateLightClientUpdate(
+ currentCommittee,
+ chainConfig
+ );
+ validationOrder.verify(stateDataSpy).validateBlock(authenticatedNext, endorsements, chainConfig);
+
+ var tailPeriod = tailStateData.getCurrSyncPeriod(chainConfig.getSyncPeriodLength()).bigIntegerValue();
+ assertEquals(tailPeriod.add(BigInteger.ONE), subjectIdentity.getCurrentSyncCommitteePeriod());
+ assertEquals(
+ authenticatedNext.hashTreeRoot(),
+ subjectIdentity.getCurrentSyncCommittee().hashTreeRoot()
+ );
+ assertNull(subjectIdentity.getNextSyncCommittee());
+ }
+
@Test
public void testHcdvsParseMessageFromLedgerData() {
var raw = ETHEREUM_HCDVS_SERVICE.parseMessageFromLedgerData(MSG1.getProvableData().getLedgerData());