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..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 @@ -436,15 +436,19 @@ 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() + ); + 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 50ebbd11..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,10 +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) { - getHCDVSLogger().info("light client update inside anchor consensus state, update the sync committee"); - ethSubjectIdentity.setCurrentSyncCommittee(ethConsensusStateData.getLightClientUpdateWrapper().getNextSyncCommittee()); - } anchorState.setConsensusNodeInfo(ethSubjectIdentity.toJson().getBytes()); return VerifyResult.success(); } @@ -123,8 +120,18 @@ public VerifyResult verifyConsensusState(ConsensusState stateToVerify, Consensus new String(stateToVerify.getEndorsements()), ethSubjectIdentity.getCurrentSyncCommittee().getPubkeys().size() ); + var syncPeriodLength = ethSubjectIdentity.getEth2ChainConfig().getSyncPeriodLength(); + if (ObjectUtil.isNull(ethSubjectIdentity.getCurrentSyncCommitteePeriod())) { + var parentPeriod = parentConsensusData.getCurrSyncPeriod(syncPeriodLength).bigIntegerValue(); + if (parentConsensusData.isLastSlotForCurrentPeriod(syncPeriodLength) + && ObjectUtil.isNull(ethSubjectIdentity.getNextSyncCommittee())) { + parentPeriod = parentPeriod.add(BigInteger.ONE); + } + ethSubjectIdentity.setCurrentSyncCommitteePeriod(parentPeriod); + } + 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: {})", stateToVerify.getHeight().toString(), stateToVerify.getHashHex(), e); @@ -132,21 +139,6 @@ 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()); - } - stateToVerify.setConsensusNodeInfo(ethSubjectIdentity.toJson().getBytes()); getHCDVSLogger().info("🌈 successful to verify consensus state (slot: {}, root: {}) now!", @@ -154,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 532adb5a..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,30 +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.multiply(BigInteger.valueOf(config.getEth2ChainConfig().getSyncPeriodLength())); - 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); } @@ -549,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/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..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; @@ -96,6 +97,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 +133,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(getSyncCommitteeForkSlot(signatureSlot)).getDomain()) ).hashTreeRoot(); if (!BLSSignatureVerifier.SIMPLE.verify( @@ -140,14 +147,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 +186,7 @@ public String toJson() { return jsonObject.toJSONString(); } - private void validateLightClientUpdate( + private void validateLightClientUpdateInternal( LightClientUpdateWrapper lightClientUpdate, SyncCommittee currentSyncCommittee, Eth2ChainConfig eth2ChainConfig @@ -216,7 +225,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(getSyncCommitteeForkSlot(lightClientUpdate.getSignatureSlot())).getDomain()) ).hashTreeRoot(); if (!BLSSignatureVerifier.SIMPLE.verify( contributionPubkeys, @@ -226,4 +235,10 @@ private void validateLightClientUpdate( 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 800ef023..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; @@ -34,6 +35,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 +78,20 @@ 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 = "current_sync_committee_period") + private BigInteger currentSyncCommitteePeriod; + @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..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,15 +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 { @@ -147,6 +158,140 @@ 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 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());