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());