diff --git a/eth-reference-tests/src/referenceTest/java/tech/pegasys/teku/reference/KzgRetriever.java b/eth-reference-tests/src/referenceTest/java/tech/pegasys/teku/reference/KzgRetriever.java index 76769d65a4f..a21514a1cb5 100644 --- a/eth-reference-tests/src/referenceTest/java/tech/pegasys/teku/reference/KzgRetriever.java +++ b/eth-reference-tests/src/referenceTest/java/tech/pegasys/teku/reference/KzgRetriever.java @@ -25,10 +25,11 @@ public class KzgRetriever { private static final Map TRUSTED_SETUP_FILES_BY_NETWORK = Maps.newHashMap(); public static KZG getKzgWithLoadedTrustedSetup(final Spec spec, final String network) { - if (!spec.isMilestoneSupported(SpecMilestone.DENEB)) { - return KZG.NOOP; + if (spec.isMilestoneSupported(SpecMilestone.DENEB) + || spec.isMilestoneSupported(SpecMilestone.ELECTRA)) { + return getKzgWithLoadedTrustedSetup(network); } - return getKzgWithLoadedTrustedSetup(network); + return KZG.NOOP; } public static KZG getKzgWithLoadedTrustedSetup(final String network) { @@ -43,7 +44,7 @@ public static KZG getKzgWithLoadedTrustedSetup(final String network) { () -> new IllegalArgumentException( "No trusted setup configured for " + network))); - final KZG kzg = KZG.getInstance(); + final KZG kzg = KZG.getInstance(false); kzg.loadTrustedSetup(trustedSetupFile); return kzg; } diff --git a/ethereum/networks/src/main/java/tech/pegasys/teku/networks/Eth2NetworkConfiguration.java b/ethereum/networks/src/main/java/tech/pegasys/teku/networks/Eth2NetworkConfiguration.java index c2e4112bb1f..006d377913c 100644 --- a/ethereum/networks/src/main/java/tech/pegasys/teku/networks/Eth2NetworkConfiguration.java +++ b/ethereum/networks/src/main/java/tech/pegasys/teku/networks/Eth2NetworkConfiguration.java @@ -71,6 +71,8 @@ public class Eth2NetworkConfiguration { public static final int DEFAULT_ASYNC_P2P_MAX_QUEUE = DEFAULT_MAX_QUEUE_SIZE; + public static final boolean DEFAULT_RUST_KZG_ENABLED = false; + // at least 5, but happily up to 12 public static final int DEFAULT_VALIDATOR_EXECUTOR_THREADS = Math.max(5, Math.min(Runtime.getRuntime().availableProcessors(), 12)); @@ -114,6 +116,7 @@ public class Eth2NetworkConfiguration { private final boolean forkChoiceLateBlockReorgEnabled; private final boolean forkChoiceUpdatedAlwaysSendPayloadAttributes; private final int pendingAttestationsMaxQueue; + private final boolean rustKzgEnabled; private Eth2NetworkConfiguration( final Spec spec, @@ -141,7 +144,8 @@ private Eth2NetworkConfiguration( final int asyncBeaconChainMaxQueue, final boolean forkChoiceLateBlockReorgEnabled, final boolean forkChoiceUpdatedAlwaysSendPayloadAttributes, - final int pendingAttestationsMaxQueue) { + final int pendingAttestationsMaxQueue, + final boolean rustKzgEnabled) { this.spec = spec; this.constants = constants; this.stateBoostrapConfig = stateBoostrapConfig; @@ -172,6 +176,7 @@ private Eth2NetworkConfiguration( this.forkChoiceUpdatedAlwaysSendPayloadAttributes = forkChoiceUpdatedAlwaysSendPayloadAttributes; this.pendingAttestationsMaxQueue = pendingAttestationsMaxQueue; + this.rustKzgEnabled = rustKzgEnabled; LOG.debug( "P2P async queue - {} threads, max queue size {} ", asyncP2pMaxThreads, asyncP2pMaxQueue); @@ -293,6 +298,10 @@ public boolean isForkChoiceUpdatedAlwaysSendPayloadAttributes() { return forkChoiceUpdatedAlwaysSendPayloadAttributes; } + public boolean isRustKzgEnabled() { + return rustKzgEnabled; + } + @Override public String toString() { return constants; @@ -316,6 +325,7 @@ public boolean equals(final Object o) { && forkChoiceLateBlockReorgEnabled == that.forkChoiceLateBlockReorgEnabled && forkChoiceUpdatedAlwaysSendPayloadAttributes == that.forkChoiceUpdatedAlwaysSendPayloadAttributes + && rustKzgEnabled == that.rustKzgEnabled && Objects.equals(spec, that.spec) && Objects.equals(constants, that.constants) && Objects.equals(stateBoostrapConfig, that.stateBoostrapConfig) @@ -362,7 +372,8 @@ public int hashCode() { asyncBeaconChainMaxQueue, asyncP2pMaxQueue, forkChoiceLateBlockReorgEnabled, - forkChoiceUpdatedAlwaysSendPayloadAttributes); + forkChoiceUpdatedAlwaysSendPayloadAttributes, + rustKzgEnabled); } public static class Builder { @@ -400,6 +411,7 @@ public static class Builder { private boolean forkChoiceUpdatedAlwaysSendPayloadAttributes = DEFAULT_FORK_CHOICE_UPDATED_ALWAYS_SEND_PAYLOAD_ATTRIBUTES; private OptionalInt pendingAttestationsMaxQueue = OptionalInt.empty(); + private boolean rustKzgEnabled = DEFAULT_RUST_KZG_ENABLED; public void spec(final Spec spec) { this.spec = spec; @@ -498,7 +510,8 @@ public Eth2NetworkConfiguration build() { asyncBeaconChainMaxQueue.orElse(DEFAULT_ASYNC_BEACON_CHAIN_MAX_QUEUE), forkChoiceLateBlockReorgEnabled, forkChoiceUpdatedAlwaysSendPayloadAttributes, - pendingAttestationsMaxQueue.orElse(DEFAULT_MAX_QUEUE_PENDING_ATTESTATIONS)); + pendingAttestationsMaxQueue.orElse(DEFAULT_MAX_QUEUE_PENDING_ATTESTATIONS), + rustKzgEnabled); } private void validateCommandLineParameters() { @@ -737,6 +750,11 @@ public Builder epochsStoreBlobs(final String epochsStoreBlobs) { return this; } + public Builder rustKzgEnabled(final boolean rustKzgEnabled) { + this.rustKzgEnabled = rustKzgEnabled; + return this; + } + public Builder applyNetworkDefaults(final String networkName) { Eth2Network.fromStringLenient(networkName) .ifPresentOrElse( diff --git a/ethereum/spec/src/main/java/tech/pegasys/teku/spec/executionlayer/ExecutionLayerChannelStub.java b/ethereum/spec/src/main/java/tech/pegasys/teku/spec/executionlayer/ExecutionLayerChannelStub.java index afa7a669b57..7e9af041716 100644 --- a/ethereum/spec/src/main/java/tech/pegasys/teku/spec/executionlayer/ExecutionLayerChannelStub.java +++ b/ethereum/spec/src/main/java/tech/pegasys/teku/spec/executionlayer/ExecutionLayerChannelStub.java @@ -124,7 +124,7 @@ public ExecutionLayerChannelStub( final KZG kzg; if (spec.isMilestoneSupported(SpecMilestone.DENEB)) { // trusted setup loading will be handled by the BeaconChainController - kzg = KZG.getInstance(); + kzg = KZG.getInstance(false); } else { kzg = KZG.NOOP; } diff --git a/ethereum/spec/src/property-test/java/tech/pegasys/teku/spec/logic/versions/deneb/helpers/KzgResolver.java b/ethereum/spec/src/property-test/java/tech/pegasys/teku/spec/logic/versions/deneb/helpers/KzgResolver.java index 305b7d37f01..c4150c8a0f1 100644 --- a/ethereum/spec/src/property-test/java/tech/pegasys/teku/spec/logic/versions/deneb/helpers/KzgResolver.java +++ b/ethereum/spec/src/property-test/java/tech/pegasys/teku/spec/logic/versions/deneb/helpers/KzgResolver.java @@ -51,7 +51,7 @@ private KZG getKzgWithTrustedSetup() { private static class KzgAutoLoadFree implements Store.CloseOnReset { - private final KZG kzg = KZG.getInstance(); + private final KZG kzg = KZG.getInstance(false); private KzgAutoLoadFree() { TrustedSetupLoader.loadTrustedSetupForTests(kzg); diff --git a/gradle/versions.gradle b/gradle/versions.gradle index 05db206741f..f5e690f1bd0 100644 --- a/gradle/versions.gradle +++ b/gradle/versions.gradle @@ -35,6 +35,7 @@ dependencyManagement { dependency 'io.libp2p:jvm-libp2p:1.2.2-RELEASE' dependency 'tech.pegasys:jblst:0.3.12' dependency 'io.consensys.protocols:jc-kzg-4844:2.1.1' + dependency 'io.github.crate-crypto:java-eth-kzg:0.5.2' dependency 'org.hdrhistogram:HdrHistogram:2.2.2' diff --git a/infrastructure/kzg/build.gradle b/infrastructure/kzg/build.gradle index 2da0808d1b1..66a8b08f7fc 100644 --- a/infrastructure/kzg/build.gradle +++ b/infrastructure/kzg/build.gradle @@ -5,6 +5,7 @@ dependencies { implementation 'io.consensys.tuweni:tuweni-bytes' implementation 'io.consensys.tuweni:tuweni-ssz' implementation 'io.consensys.protocols:jc-kzg-4844' + implementation "io.github.crate-crypto:java-eth-kzg" implementation 'commons-io:commons-io' testFixturesImplementation 'com.google.guava:guava' diff --git a/infrastructure/kzg/src/main/java/tech/pegasys/teku/kzg/CKZG4844.java b/infrastructure/kzg/src/main/java/tech/pegasys/teku/kzg/CKZG4844.java index 16a417918ae..ca973616fea 100644 --- a/infrastructure/kzg/src/main/java/tech/pegasys/teku/kzg/CKZG4844.java +++ b/infrastructure/kzg/src/main/java/tech/pegasys/teku/kzg/CKZG4844.java @@ -13,9 +13,14 @@ package tech.pegasys.teku.kzg; +import static ethereum.ckzg4844.CKZG4844JNI.BYTES_PER_CELL; +import static ethereum.ckzg4844.CKZG4844JNI.BYTES_PER_COMMITMENT; + import ethereum.ckzg4844.CKZG4844JNI; +import ethereum.ckzg4844.CellsAndProofs; import java.util.List; import java.util.Optional; +import java.util.stream.IntStream; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.apache.tuweni.bytes.Bytes; @@ -149,4 +154,58 @@ public KZGProof computeBlobKzgProof(final Bytes blob, final KZGCommitment kzgCom "Failed to compute KZG proof for blob with commitment " + kzgCommitment, ex); } } + + @Override + public List computeCellsAndProofs(final Bytes blob) { + final CellsAndProofs cellsAndProofs = + CKZG4844JNI.computeCellsAndKzgProofs(blob.toArrayUnsafe()); + final List cells = KZGCell.splitBytes(Bytes.wrap(cellsAndProofs.getCells())); + final List proofs = KZGProof.splitBytes(Bytes.wrap(cellsAndProofs.getProofs())); + if (cells.size() != proofs.size()) { + throw new KZGException("Cells and proofs size differ"); + } + return IntStream.range(0, cells.size()) + .mapToObj(i -> new KZGCellAndProof(cells.get(i), proofs.get(i))) + .toList(); + } + + @Override + public boolean verifyCellProofBatch( + final List commitments, + final List cellWithIdList, + final List proofs) { + if (commitments.size() != cellWithIdList.size() || cellWithIdList.size() != proofs.size()) { + throw new KZGException("Cells, proofs and commitments sizes should match"); + } + return CKZG4844JNI.verifyCellKzgProofBatch( + CKZG4844Utils.flattenBytes( + commitments.stream() + .map(kzgCommitment -> (Bytes) kzgCommitment.getBytesCompressed()) + .toList(), + commitments.size() * BYTES_PER_COMMITMENT), + cellWithIdList.stream() + .mapToLong(cellWithIds -> cellWithIds.columnId().id().longValue()) + .toArray(), + CKZG4844Utils.flattenBytes( + cellWithIdList.stream().map(cellWithIds -> cellWithIds.cell().bytes()).toList(), + cellWithIdList.size() * BYTES_PER_CELL), + CKZG4844Utils.flattenProofs(proofs)); + } + + @Override + public List recoverCellsAndProofs(final List cells) { + final long[] cellIds = cells.stream().mapToLong(c -> c.columnId().id().longValue()).toArray(); + final byte[] cellBytes = + CKZG4844Utils.flattenBytes( + cells.stream().map(c -> c.cell().bytes()).toList(), cells.size() * BYTES_PER_CELL); + final CellsAndProofs cellsAndProofs = CKZG4844JNI.recoverCellsAndKzgProofs(cellIds, cellBytes); + final List fullCells = KZGCell.splitBytes(Bytes.wrap(cellsAndProofs.getCells())); + final List fullProofs = KZGProof.splitBytes(Bytes.wrap(cellsAndProofs.getProofs())); + if (fullCells.size() != fullProofs.size()) { + throw new KZGException("Cells and proofs size differ"); + } + return IntStream.range(0, fullCells.size()) + .mapToObj(i -> new KZGCellAndProof(fullCells.get(i), fullProofs.get(i))) + .toList(); + } } diff --git a/infrastructure/kzg/src/main/java/tech/pegasys/teku/kzg/CKZG4844Utils.java b/infrastructure/kzg/src/main/java/tech/pegasys/teku/kzg/CKZG4844Utils.java index 14fe464db20..a347a87ebcb 100644 --- a/infrastructure/kzg/src/main/java/tech/pegasys/teku/kzg/CKZG4844Utils.java +++ b/infrastructure/kzg/src/main/java/tech/pegasys/teku/kzg/CKZG4844Utils.java @@ -27,12 +27,12 @@ import java.util.ArrayList; import java.util.List; import java.util.function.Function; +import java.util.stream.IntStream; import org.apache.tuweni.bytes.Bytes; import tech.pegasys.teku.infrastructure.http.UrlSanitizer; import tech.pegasys.teku.infrastructure.io.resource.ResourceLoader; class CKZG4844Utils { - private static final int MAX_BYTES_TO_FLATTEN = 100_663_296; // ~100.66 MB or 768 blobs public static byte[] flattenBlobs(final List blobs) { @@ -59,6 +59,16 @@ public static byte[] flattenG2Points(final List g2Points) { return flattenBytes(g2Points, BYTES_PER_G2 * g2Points.size()); } + static List bytesChunked(final Bytes bytes, final int chunkSize) { + if (bytes.size() % chunkSize != 0) { + throw new IllegalArgumentException("Invalid bytes size: " + bytes.size()); + } + return IntStream.range(0, bytes.size() / chunkSize) + .map(i -> i * chunkSize) + .mapToObj(startIdx -> bytes.slice(startIdx, chunkSize)) + .toList(); + } + public static TrustedSetup parseTrustedSetupFile(final String trustedSetupFile) throws IOException { final String sanitizedTrustedSetup = UrlSanitizer.sanitizePotentialUrl(trustedSetupFile); @@ -99,7 +109,7 @@ public static TrustedSetup parseTrustedSetupFile(final String trustedSetupFile) } } - private static byte[] flattenBytes(final List toFlatten, final int expectedSize) { + static byte[] flattenBytes(final List toFlatten, final int expectedSize) { return flattenBytes(toFlatten, Bytes::toArrayUnsafe, expectedSize); } diff --git a/infrastructure/kzg/src/main/java/tech/pegasys/teku/kzg/KZG.java b/infrastructure/kzg/src/main/java/tech/pegasys/teku/kzg/KZG.java index ac62d177177..86e86507f7d 100644 --- a/infrastructure/kzg/src/main/java/tech/pegasys/teku/kzg/KZG.java +++ b/infrastructure/kzg/src/main/java/tech/pegasys/teku/kzg/KZG.java @@ -13,6 +13,7 @@ package tech.pegasys.teku.kzg; +import java.math.BigInteger; import java.util.List; import org.apache.tuweni.bytes.Bytes; import org.apache.tuweni.bytes.Bytes48; @@ -22,12 +23,16 @@ * entry-point for all KZG operations in Teku. */ public interface KZG { - + BigInteger BLS_MODULUS = + new BigInteger( + "52435875175126190479447740508185965837690552500527637822603658699938581184513"); int BYTES_PER_G1 = 48; int BYTES_PER_G2 = 96; + int CELLS_PER_EXT_BLOB = 128; + int FIELD_ELEMENTS_PER_BLOB = 4096; - static KZG getInstance() { - return CKZG4844.getInstance(); + static KZG getInstance(final boolean rustKzgEnabled) { + return rustKzgEnabled ? RustWithCKZG.getInstance() : CKZG4844.getInstance(); } KZG NOOP = @@ -65,6 +70,24 @@ public KZGProof computeBlobKzgProof(final Bytes blob, final KZGCommitment kzgCom throws KZGException { return KZGProof.fromBytesCompressed(Bytes48.ZERO); } + + @Override + public List computeCellsAndProofs(Bytes blob) { + throw new RuntimeException("Not implemented"); + } + + @Override + public boolean verifyCellProofBatch( + List commitments, + List cellWithIDs, + List proofs) { + return false; + } + + @Override + public List recoverCellsAndProofs(List cells) { + throw new RuntimeException("Not implemented"); + } }; void loadTrustedSetup(String trustedSetupFile) throws KZGException; @@ -81,4 +104,15 @@ boolean verifyBlobKzgProofBatch( KZGCommitment blobToKzgCommitment(Bytes blob) throws KZGException; KZGProof computeBlobKzgProof(Bytes blob, KZGCommitment kzgCommitment) throws KZGException; + + // Fulu PeerDAS methods + + List computeCellsAndProofs(Bytes blob); + + boolean verifyCellProofBatch( + List commitments, + List cellWithIDs, + List proofs); + + List recoverCellsAndProofs(List cells); } diff --git a/infrastructure/kzg/src/main/java/tech/pegasys/teku/kzg/KZGCell.java b/infrastructure/kzg/src/main/java/tech/pegasys/teku/kzg/KZGCell.java new file mode 100644 index 00000000000..fea47abab49 --- /dev/null +++ b/infrastructure/kzg/src/main/java/tech/pegasys/teku/kzg/KZGCell.java @@ -0,0 +1,28 @@ +/* + * Copyright Consensys Software Inc., 2024 + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package tech.pegasys.teku.kzg; + +import static ethereum.ckzg4844.CKZG4844JNI.BYTES_PER_CELL; + +import java.util.List; +import org.apache.tuweni.bytes.Bytes; + +public record KZGCell(Bytes bytes) { + + static final KZGCell ZERO = new KZGCell(Bytes.wrap(new byte[BYTES_PER_CELL])); + + static List splitBytes(final Bytes bytes) { + return CKZG4844Utils.bytesChunked(bytes, BYTES_PER_CELL).stream().map(KZGCell::new).toList(); + } +} diff --git a/infrastructure/kzg/src/main/java/tech/pegasys/teku/kzg/KZGCellAndProof.java b/infrastructure/kzg/src/main/java/tech/pegasys/teku/kzg/KZGCellAndProof.java new file mode 100644 index 00000000000..6f62a30ffd3 --- /dev/null +++ b/infrastructure/kzg/src/main/java/tech/pegasys/teku/kzg/KZGCellAndProof.java @@ -0,0 +1,16 @@ +/* + * Copyright Consensys Software Inc., 2024 + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package tech.pegasys.teku.kzg; + +public record KZGCellAndProof(KZGCell cell, KZGProof proof) {} diff --git a/infrastructure/kzg/src/main/java/tech/pegasys/teku/kzg/KZGCellID.java b/infrastructure/kzg/src/main/java/tech/pegasys/teku/kzg/KZGCellID.java new file mode 100644 index 00000000000..80269c77d36 --- /dev/null +++ b/infrastructure/kzg/src/main/java/tech/pegasys/teku/kzg/KZGCellID.java @@ -0,0 +1,27 @@ +/* + * Copyright Consensys Software Inc., 2024 + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package tech.pegasys.teku.kzg; + +import tech.pegasys.teku.infrastructure.unsigned.UInt64; + +public record KZGCellID(UInt64 id) { + + public static KZGCellID fromCellColumnIndex(final int idx) { + return new KZGCellID(UInt64.valueOf(idx)); + } + + int getColumnIndex() { + return id.intValue(); + } +} diff --git a/infrastructure/kzg/src/main/java/tech/pegasys/teku/kzg/KZGCellWithColumnId.java b/infrastructure/kzg/src/main/java/tech/pegasys/teku/kzg/KZGCellWithColumnId.java new file mode 100644 index 00000000000..14a7c3d9f65 --- /dev/null +++ b/infrastructure/kzg/src/main/java/tech/pegasys/teku/kzg/KZGCellWithColumnId.java @@ -0,0 +1,21 @@ +/* + * Copyright Consensys Software Inc., 2024 + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package tech.pegasys.teku.kzg; + +public record KZGCellWithColumnId(KZGCell cell, KZGCellID columnId) { + + public static KZGCellWithColumnId fromCellAndColumn(final KZGCell cell, final int columnIndex) { + return new KZGCellWithColumnId(cell, KZGCellID.fromCellColumnIndex(columnIndex)); + } +} diff --git a/infrastructure/kzg/src/main/java/tech/pegasys/teku/kzg/KZGException.java b/infrastructure/kzg/src/main/java/tech/pegasys/teku/kzg/KZGException.java index 3aa2dbbd318..ff4d16b968b 100644 --- a/infrastructure/kzg/src/main/java/tech/pegasys/teku/kzg/KZGException.java +++ b/infrastructure/kzg/src/main/java/tech/pegasys/teku/kzg/KZGException.java @@ -18,4 +18,8 @@ public class KZGException extends RuntimeException { public KZGException(final String message, final Throwable cause) { super(message, cause); } + + public KZGException(final String message) { + super(message); + } } diff --git a/infrastructure/kzg/src/main/java/tech/pegasys/teku/kzg/KZGProof.java b/infrastructure/kzg/src/main/java/tech/pegasys/teku/kzg/KZGProof.java index 6b7402d9d45..e5f75e02824 100644 --- a/infrastructure/kzg/src/main/java/tech/pegasys/teku/kzg/KZGProof.java +++ b/infrastructure/kzg/src/main/java/tech/pegasys/teku/kzg/KZGProof.java @@ -14,8 +14,9 @@ package tech.pegasys.teku.kzg; import static com.google.common.base.Preconditions.checkArgument; +import static ethereum.ckzg4844.CKZG4844JNI.BYTES_PER_PROOF; -import ethereum.ckzg4844.CKZG4844JNI; +import java.util.List; import java.util.Objects; import org.apache.tuweni.bytes.Bytes; import org.apache.tuweni.bytes.Bytes48; @@ -29,12 +30,11 @@ public static KZGProof fromHexString(final String hexString) { public static KZGProof fromSSZBytes(final Bytes bytes) { checkArgument( - bytes.size() == CKZG4844JNI.BYTES_PER_PROOF, - "Expected " + CKZG4844JNI.BYTES_PER_PROOF + " bytes but received %s.", + bytes.size() == BYTES_PER_PROOF, + "Expected " + BYTES_PER_PROOF + " bytes but received %s.", bytes.size()); return SSZ.decode( - bytes, - reader -> new KZGProof(Bytes48.wrap(reader.readFixedBytes(CKZG4844JNI.BYTES_PER_PROOF)))); + bytes, reader -> new KZGProof(Bytes48.wrap(reader.readFixedBytes(BYTES_PER_PROOF)))); } public static KZGProof fromBytesCompressed(final Bytes48 bytes) throws IllegalArgumentException { @@ -45,6 +45,12 @@ public static KZGProof fromArray(final byte[] bytes) { return fromBytesCompressed(Bytes48.wrap(bytes)); } + static List splitBytes(final Bytes bytes) { + return CKZG4844Utils.bytesChunked(bytes, BYTES_PER_PROOF).stream() + .map(b -> new KZGProof(Bytes48.wrap(b))) + .toList(); + } + private final Bytes48 bytesCompressed; public KZGProof(final Bytes48 bytesCompressed) { diff --git a/infrastructure/kzg/src/main/java/tech/pegasys/teku/kzg/RustKZG.java b/infrastructure/kzg/src/main/java/tech/pegasys/teku/kzg/RustKZG.java new file mode 100644 index 00000000000..53489af8b81 --- /dev/null +++ b/infrastructure/kzg/src/main/java/tech/pegasys/teku/kzg/RustKZG.java @@ -0,0 +1,147 @@ +/* + * Copyright Consensys Software Inc., 2022 + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package tech.pegasys.teku.kzg; + +import com.google.common.collect.Streams; +import ethereum.cryptography.CellsAndProofs; +import ethereum.cryptography.LibEthKZG; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Stream; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.apache.tuweni.bytes.Bytes; +import org.apache.tuweni.bytes.Bytes48; + +/** + * Wrapper around LibPeerDASKZG Rust PeerDAS library + * + *

This class should be a singleton + */ +final class RustKZG implements KZG { + + private static final Logger LOG = LogManager.getLogger(); + private static final int NUMBER_OF_THREADS = 1; + + @SuppressWarnings("NonFinalStaticField") + private static RustKZG instance; + + private LibEthKZG library; + private boolean initialized; + + static synchronized RustKZG getInstance() { + if (instance == null) { + instance = new RustKZG(); + } + return instance; + } + + private RustKZG() {} + + @Override + public synchronized void loadTrustedSetup(final String trustedSetupFile) throws KZGException { + if (!initialized) { + try { + this.library = new LibEthKZG(true, NUMBER_OF_THREADS); + this.initialized = true; + LOG.info("Loaded LibPeerDASKZG library"); + } catch (final Exception ex) { + throw new KZGException("Failed to load LibPeerDASKZG Rust library", ex); + } + } + } + + @Override + public synchronized void freeTrustedSetup() throws KZGException { + if (!initialized) { + throw new KZGException("Trusted setup already freed"); + } + try { + library.close(); + this.initialized = false; + } catch (final Exception ex) { + throw new KZGException("Failed to free trusted setup", ex); + } + } + + @Override + public boolean verifyBlobKzgProof( + final Bytes blob, final KZGCommitment kzgCommitment, final KZGProof kzgProof) + throws KZGException { + throw new RuntimeException("LibPeerDASKZG library doesn't support verifyBlobKzgProof"); + } + + @Override + public boolean verifyBlobKzgProofBatch( + final List blobs, + final List kzgCommitments, + final List kzgProofs) + throws KZGException { + throw new RuntimeException("LibPeerDASKZG library doesn't support verifyBlobKzgProofBatch"); + } + + @Override + public KZGCommitment blobToKzgCommitment(final Bytes blob) throws KZGException { + throw new RuntimeException("LibPeerDASKZG library doesn't support blobToKzgCommitment"); + } + + @Override + public KZGProof computeBlobKzgProof(final Bytes blob, final KZGCommitment kzgCommitment) + throws KZGException { + throw new RuntimeException("LibPeerDASKZG library doesn't support computeBlobKzgProof"); + } + + @Override + public List computeCellsAndProofs(final Bytes blob) { + final CellsAndProofs cellsAndProofs = library.computeCellsAndKZGProofs(blob.toArrayUnsafe()); + final Stream kzgCellStream = + Arrays.stream(cellsAndProofs.getCells()).map(Bytes::wrap).map(KZGCell::new); + + final Stream kzgProofStream = + Arrays.stream(cellsAndProofs.getProofs()).map(Bytes48::wrap).map(KZGProof::new); + + return Streams.zip(kzgCellStream, kzgProofStream, KZGCellAndProof::new).toList(); + } + + @Override + public boolean verifyCellProofBatch( + final List commitments, + final List cellWithIdList, + final List proofs) { + return library.verifyCellKZGProofBatch( + commitments.stream().map(KZGCommitment::toArrayUnsafe).toArray(byte[][]::new), + cellWithIdList.stream() + .mapToLong(cellWithIds -> cellWithIds.columnId().id().longValue()) + .toArray(), + cellWithIdList.stream() + .map(cellWithIds -> cellWithIds.cell().bytes().toArrayUnsafe()) + .toArray(byte[][]::new), + proofs.stream().map(KZGProof::toArrayUnsafe).toArray(byte[][]::new)); + } + + @Override + public List recoverCellsAndProofs(final List cells) { + final long[] cellIds = cells.stream().mapToLong(c -> c.columnId().id().longValue()).toArray(); + final byte[][] cellBytes = + cells.stream().map(c -> c.cell().bytes().toArrayUnsafe()).toArray(byte[][]::new); + final CellsAndProofs cellsAndProofs = library.recoverCellsAndKZGProofs(cellIds, cellBytes); + final byte[][] recoveredCells = cellsAndProofs.getCells(); + final Stream kzgCellStream = + Arrays.stream(recoveredCells).map(Bytes::wrap).map(KZGCell::new); + final byte[][] recoveredProofs = cellsAndProofs.getProofs(); + final Stream kzgProofStream = + Arrays.stream(recoveredProofs).map(Bytes48::wrap).map(KZGProof::new); + return Streams.zip(kzgCellStream, kzgProofStream, KZGCellAndProof::new).toList(); + } +} diff --git a/infrastructure/kzg/src/main/java/tech/pegasys/teku/kzg/RustWithCKZG.java b/infrastructure/kzg/src/main/java/tech/pegasys/teku/kzg/RustWithCKZG.java new file mode 100644 index 00000000000..972a5e7693a --- /dev/null +++ b/infrastructure/kzg/src/main/java/tech/pegasys/teku/kzg/RustWithCKZG.java @@ -0,0 +1,122 @@ +/* + * Copyright Consensys Software Inc., 2022 + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package tech.pegasys.teku.kzg; + +import java.util.List; +import org.apache.tuweni.bytes.Bytes; + +/** + * Rust KZG library with fallback to C-KZG4844 on non-implemented methods (EIP-4844 methods are + * currently not implemented in Rust library) + * + *

This class should be a singleton + */ +final class RustWithCKZG implements KZG { + + @SuppressWarnings("NonFinalStaticField") + private static RustWithCKZG instance; + + private final CKZG4844 ckzg4844Delegate; + private final RustKZG rustKzgDelegeate; + + static synchronized RustWithCKZG getInstance() { + if (instance == null) { + instance = new RustWithCKZG(); + } + return instance; + } + + private RustWithCKZG() { + this.ckzg4844Delegate = CKZG4844.getInstance(); + this.rustKzgDelegeate = RustKZG.getInstance(); + } + + @Override + public synchronized void loadTrustedSetup(final String trustedSetupFile) throws KZGException { + ckzg4844Delegate.loadTrustedSetup(trustedSetupFile); + rustKzgDelegeate.loadTrustedSetup(trustedSetupFile); + } + + @Override + public synchronized void freeTrustedSetup() throws KZGException { + KZGException ckzg4844DelegateException = null; + try { + ckzg4844Delegate.freeTrustedSetup(); + } catch (final KZGException ex) { + ckzg4844DelegateException = ex; + } + KZGException rustKzgDelegateException = null; + try { + rustKzgDelegeate.freeTrustedSetup(); + } catch (final KZGException ex) { + rustKzgDelegateException = ex; + } + if (ckzg4844DelegateException != null || rustKzgDelegateException != null) { + if (ckzg4844DelegateException != null && rustKzgDelegateException != null) { + throw new KZGException( + "RustKZG and CKZG4844 libraries failed to free trusted setup", + ckzg4844DelegateException); + } else if (ckzg4844DelegateException != null) { + throw ckzg4844DelegateException; + } else { + throw rustKzgDelegateException; + } + } + } + + @Override + public boolean verifyBlobKzgProof( + final Bytes blob, final KZGCommitment kzgCommitment, final KZGProof kzgProof) + throws KZGException { + return ckzg4844Delegate.verifyBlobKzgProof(blob, kzgCommitment, kzgProof); + } + + @Override + public boolean verifyBlobKzgProofBatch( + final List blobs, + final List kzgCommitments, + final List kzgProofs) + throws KZGException { + return ckzg4844Delegate.verifyBlobKzgProofBatch(blobs, kzgCommitments, kzgProofs); + } + + @Override + public KZGCommitment blobToKzgCommitment(final Bytes blob) throws KZGException { + return ckzg4844Delegate.blobToKzgCommitment(blob); + } + + @Override + public KZGProof computeBlobKzgProof(final Bytes blob, final KZGCommitment kzgCommitment) + throws KZGException { + return ckzg4844Delegate.computeBlobKzgProof(blob, kzgCommitment); + } + + @Override + public List computeCellsAndProofs(final Bytes blob) { + return rustKzgDelegeate.computeCellsAndProofs(blob); + } + + @Override + public boolean verifyCellProofBatch( + final List commitments, + final List cellWithIds, + final List proofs) { + return rustKzgDelegeate.verifyCellProofBatch(commitments, cellWithIds, proofs); + } + + @Override + public List recoverCellsAndProofs(final List cells) { + return rustKzgDelegeate.recoverCellsAndProofs(cells); + } +} diff --git a/infrastructure/kzg/src/test/java/tech/pegasys/teku/kzg/CKZG4844Test.java b/infrastructure/kzg/src/test/java/tech/pegasys/teku/kzg/CKZG4844Test.java index fb42031aef2..d5fb01b5b61 100644 --- a/infrastructure/kzg/src/test/java/tech/pegasys/teku/kzg/CKZG4844Test.java +++ b/infrastructure/kzg/src/test/java/tech/pegasys/teku/kzg/CKZG4844Test.java @@ -13,283 +13,8 @@ package tech.pegasys.teku.kzg; -import static ethereum.ckzg4844.CKZG4844JNI.BLS_MODULUS; -import static ethereum.ckzg4844.CKZG4844JNI.BYTES_PER_BLOB; -import static ethereum.ckzg4844.CKZG4844JNI.FIELD_ELEMENTS_PER_BLOB; -import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; -import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat; -import static org.junit.jupiter.api.Assertions.assertThrows; - -import com.google.common.collect.Streams; -import ethereum.ckzg4844.CKZGException; -import ethereum.ckzg4844.CKZGException.CKZGError; -import java.math.BigInteger; -import java.nio.ByteOrder; -import java.util.List; -import java.util.Random; -import java.util.stream.Collectors; -import java.util.stream.IntStream; -import java.util.stream.Stream; -import org.apache.tuweni.bytes.Bytes; -import org.apache.tuweni.units.bigints.UInt256; -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.ValueSource; -import tech.pegasys.teku.kzg.trusted_setups.TrustedSetupLoader; - -public final class CKZG4844Test { - - private static final int RANDOM_SEED = 5566; - private static final Random RND = new Random(RANDOM_SEED); - - private static final CKZG4844 CKZG = CKZG4844.getInstance(); - - @BeforeEach - public void setUp() { - loadTrustedSetup(); - } - - private static void loadTrustedSetup() { - TrustedSetupLoader.loadTrustedSetupForTests(CKZG); - } - - @AfterAll - public static void cleanUp() throws KZGException { - try { - CKZG.freeTrustedSetup(); - } catch (KZGException ex) { - // NOOP - } - } - - @Test - public void testKzgLoadSameTrustedSetupTwice_shouldNotThrowException() { - loadTrustedSetup(); - } - - @Test - public void testKzgFreeTrustedSetupTwice_shouldThrowException() { - CKZG.freeTrustedSetup(); - assertThrows(KZGException.class, CKZG::freeTrustedSetup); - } - - @Test - public void testUsageWithoutLoadedTrustedSetup_shouldThrowException() { - CKZG.freeTrustedSetup(); - final List exceptions = - List.of( - assertThrows( - KZGException.class, - () -> - CKZG.verifyBlobKzgProofBatch( - List.of(Bytes.fromHexString("0x", BYTES_PER_BLOB)), - List.of(getSampleCommitment()), - List.of(getSampleProof()))), - assertThrows(KZGException.class, () -> CKZG.blobToKzgCommitment(Bytes.EMPTY)), - assertThrows( - KZGException.class, - () -> CKZG.computeBlobKzgProof(Bytes.EMPTY, getSampleCommitment()))); - - assertThat(exceptions) - .allSatisfy( - exception -> assertThat(exception).cause().hasMessage("Trusted Setup is not loaded.")); - } - - @Test - public void testComputingAndVerifyingBatchProofs() { - final int numberOfBlobs = 4; - final List blobs = getSampleBlobs(numberOfBlobs); - final List kzgCommitments = - blobs.stream().map(CKZG::blobToKzgCommitment).collect(Collectors.toList()); - final List kzgProofs = - Streams.zip( - kzgCommitments.stream(), - blobs.stream(), - (kzgCommitment, blob) -> CKZG.computeBlobKzgProof(blob, kzgCommitment)) - .collect(Collectors.toList()); - assertThat(CKZG.verifyBlobKzgProofBatch(blobs, kzgCommitments, kzgProofs)).isTrue(); - - assertThat( - CKZG.verifyBlobKzgProofBatch(getSampleBlobs(numberOfBlobs), kzgCommitments, kzgProofs)) - .isFalse(); - assertThat(CKZG.verifyBlobKzgProofBatch(blobs, getSampleCommitments(numberOfBlobs), kzgProofs)) - .isFalse(); - final List invalidProofs = - getSampleBlobs(numberOfBlobs).stream() - .map((Bytes blob) -> CKZG.computeBlobKzgProof(blob, CKZG.blobToKzgCommitment(blob))) - .collect(Collectors.toList()); - assertThat(CKZG.verifyBlobKzgProofBatch(blobs, kzgCommitments, invalidProofs)).isFalse(); - } - - @Test - public void testVerifyingEmptyBatch() { - assertThat(CKZG.verifyBlobKzgProofBatch(List.of(), List.of(), List.of())).isTrue(); - } - - @Test - public void testComputingAndVerifyingSingleProof() { - final Bytes blob = getSampleBlob(); - final KZGCommitment kzgCommitment = CKZG.blobToKzgCommitment(blob); - final KZGProof kzgProof = CKZG.computeBlobKzgProof(blob, kzgCommitment); - - assertThat(CKZG.verifyBlobKzgProof(blob, kzgCommitment, kzgProof)).isTrue(); - - assertThat(CKZG.verifyBlobKzgProof(getSampleBlob(), kzgCommitment, kzgProof)).isFalse(); - assertThat(CKZG.verifyBlobKzgProof(blob, getSampleCommitment(), kzgProof)).isFalse(); - final Bytes randomBlob = getSampleBlob(); - final KZGProof invalidProof = - CKZG.computeBlobKzgProof(randomBlob, CKZG.blobToKzgCommitment(randomBlob)); - assertThat(CKZG.verifyBlobKzgProof(blob, kzgCommitment, invalidProof)).isFalse(); - } - - @Test - public void testComputingAndVerifyingBatchSingleProof() { - final int numberOfBlobs = 1; - final List blobs = getSampleBlobs(numberOfBlobs); - final List kzgCommitments = - blobs.stream().map(CKZG::blobToKzgCommitment).collect(Collectors.toList()); - final List kzgProofs = - Streams.zip( - kzgCommitments.stream(), - blobs.stream(), - (kzgCommitment, blob) -> CKZG.computeBlobKzgProof(blob, kzgCommitment)) - .collect(Collectors.toList()); - assertThat(kzgProofs.size()).isEqualTo(1); - assertThat(CKZG.verifyBlobKzgProofBatch(blobs, kzgCommitments, kzgProofs)).isTrue(); - - assertThat( - CKZG.verifyBlobKzgProofBatch(getSampleBlobs(numberOfBlobs), kzgCommitments, kzgProofs)) - .isFalse(); - assertThat(CKZG.verifyBlobKzgProofBatch(blobs, getSampleCommitments(numberOfBlobs), kzgProofs)) - .isFalse(); - final List invalidProofs = - getSampleBlobs(numberOfBlobs).stream() - .map((Bytes blob) -> CKZG.computeBlobKzgProof(blob, CKZG.blobToKzgCommitment(blob))) - .collect(Collectors.toList()); - assertThat(CKZG.verifyBlobKzgProofBatch(blobs, kzgCommitments, invalidProofs)).isFalse(); - } - - @Test - public void testVerifyingBatchProofsThrowsIfSizesDoesntMatch() { - final int numberOfBlobs = 4; - final List blobs = getSampleBlobs(numberOfBlobs); - final List kzgCommitments = - blobs.stream().map(CKZG::blobToKzgCommitment).collect(Collectors.toList()); - final List kzgProofs = - Streams.zip( - kzgCommitments.stream(), - blobs.stream(), - (kzgCommitment, blob) -> CKZG.computeBlobKzgProof(blob, kzgCommitment)) - .collect(Collectors.toList()); - final KZGException kzgException1 = - assertThrows( - KZGException.class, - () -> CKZG.verifyBlobKzgProofBatch(blobs, kzgCommitments, List.of(kzgProofs.get(0)))); - final KZGException kzgException2 = - assertThrows( - KZGException.class, - () -> CKZG.verifyBlobKzgProofBatch(blobs, List.of(kzgCommitments.get(0)), kzgProofs)); - final KZGException kzgException3 = - assertThrows( - KZGException.class, - () -> CKZG.verifyBlobKzgProofBatch(List.of(blobs.get(0)), kzgCommitments, kzgProofs)); - - Stream.of(kzgException1, kzgException2, kzgException3) - .forEach( - ex -> - assertThat(ex) - .cause() - .isInstanceOf(CKZGException.class) - .hasMessageMatching( - "Invalid .+ size. Expected \\d+ bytes but got \\d+. \\(C_KZG_BADARGS\\)")); - } - - @ParameterizedTest(name = "blob={0}") - @ValueSource( - strings = { - "0x0d2024ece3e004271319699b8b00cc010628b6bc0be5457f031fb1db0afd3ff8", - "0x", - "0x925668a49d06f4" - }) - public void testComputingProofWithIncorrectLengthBlobDoesNotCauseSegfault(final String blobHex) { - final Bytes blob = Bytes.fromHexString(blobHex); - - final KZGException kzgException = - assertThrows( - KZGException.class, - () -> CKZG.computeBlobKzgProof(blob, CKZG.blobToKzgCommitment(blob))); - - assertThat(kzgException) - .cause() - .satisfies( - cause -> { - // non-canonical blobs - assertThat(cause).isInstanceOf(CKZGException.class); - final CKZGException cryptoException = (CKZGException) cause; - assertThat(cryptoException.getError()).isEqualTo(CKZGError.C_KZG_BADARGS); - assertThat(cryptoException.getErrorMessage()) - .contains("Invalid blob size. Expected 131072 bytes but got"); - }); - } - - @ParameterizedTest(name = "trusted_setup={0}") - @ValueSource( - strings = { - "broken/trusted_setup_g1_length.txt", - "broken/trusted_setup_g2_length.txt", - "broken/trusted_setup_g2_bytesize.txt" - }) - public void incorrectTrustedSetupFilesShouldThrow(final String filename) { - final Throwable cause = - assertThrows( - KZGException.class, - () -> CKZG.loadTrustedSetup(TrustedSetupLoader.getTrustedSetupFile(filename))) - .getCause(); - assertThat(cause.getMessage()).contains("Failed to parse trusted setup file"); - } - - @Test - public void testInvalidLengthG2PointInNewTrustedSetup() { - assertThatThrownBy( - () -> new TrustedSetup(List.of(), List.of(Bytes.fromHexString("")), List.of())) - .isInstanceOf(IllegalArgumentException.class) - .hasMessage("Expected G2 point to be 96 bytes"); - } - - private List getSampleBlobs(final int count) { - return IntStream.range(0, count).mapToObj(__ -> getSampleBlob()).collect(Collectors.toList()); - } - - private Bytes getSampleBlob() { - return IntStream.range(0, FIELD_ELEMENTS_PER_BLOB) - .mapToObj(__ -> randomBLSFieldElement()) - .map(fieldElement -> Bytes.wrap(fieldElement.toArray(ByteOrder.BIG_ENDIAN))) - .reduce(Bytes::wrap) - .orElse(Bytes.EMPTY); - } - - private List getSampleCommitments(final int count) { - return IntStream.range(0, count) - .mapToObj(__ -> getSampleCommitment()) - .collect(Collectors.toList()); - } - - private KZGCommitment getSampleCommitment() { - return CKZG.blobToKzgCommitment(getSampleBlob()); - } - - private KZGProof getSampleProof() { - return CKZG.computeBlobKzgProof(getSampleBlob(), getSampleCommitment()); - } - - private UInt256 randomBLSFieldElement() { - while (true) { - final BigInteger attempt = new BigInteger(BLS_MODULUS.bitLength(), RND); - if (attempt.compareTo(BLS_MODULUS) < 0) { - return UInt256.valueOf(attempt); - } - } +public final class CKZG4844Test extends KZGAbstractTest { + public CKZG4844Test() { + super(CKZG4844.getInstance()); } } diff --git a/infrastructure/kzg/src/test/java/tech/pegasys/teku/kzg/KZGAbstractTest.java b/infrastructure/kzg/src/test/java/tech/pegasys/teku/kzg/KZGAbstractTest.java new file mode 100644 index 00000000000..6bb2cde9432 --- /dev/null +++ b/infrastructure/kzg/src/test/java/tech/pegasys/teku/kzg/KZGAbstractTest.java @@ -0,0 +1,360 @@ +/* + * Copyright Consensys Software Inc., 2022 + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package tech.pegasys.teku.kzg; + +import static ethereum.ckzg4844.CKZG4844JNI.BYTES_PER_BLOB; +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; +import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static tech.pegasys.teku.kzg.KZG.BLS_MODULUS; +import static tech.pegasys.teku.kzg.KZG.CELLS_PER_EXT_BLOB; +import static tech.pegasys.teku.kzg.KZG.FIELD_ELEMENTS_PER_BLOB; + +import com.google.common.collect.Streams; +import ethereum.ckzg4844.CKZGException; +import ethereum.ckzg4844.CKZGException.CKZGError; +import java.math.BigInteger; +import java.nio.ByteOrder; +import java.util.List; +import java.util.Random; +import java.util.stream.Collectors; +import java.util.stream.IntStream; +import java.util.stream.Stream; +import org.apache.tuweni.bytes.Bytes; +import org.apache.tuweni.units.bigints.UInt256; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import tech.pegasys.teku.kzg.trusted_setups.TrustedSetupLoader; + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +public abstract class KZGAbstractTest { + + private static final int RANDOM_SEED = 5566; + private static final Random RND = new Random(RANDOM_SEED); + + protected final KZG kzg; + + protected KZGAbstractTest(final KZG instance) { + kzg = instance; + } + + @BeforeEach + public void setUp() { + loadTrustedSetup(); + } + + private void loadTrustedSetup() { + TrustedSetupLoader.loadTrustedSetupForTests(kzg); + } + + @AfterAll + public void cleanUp() throws KZGException { + try { + kzg.freeTrustedSetup(); + } catch (KZGException ex) { + // NOOP + } + } + + @Test + public void testKzgLoadSameTrustedSetupTwice_shouldNotThrowException() { + loadTrustedSetup(); + } + + @Test + public void testKzgFreeTrustedSetupTwice_shouldThrowException() { + kzg.freeTrustedSetup(); + assertThrows(KZGException.class, kzg::freeTrustedSetup); + } + + @Test + public void testUsageWithoutLoadedTrustedSetup_shouldThrowException() { + kzg.freeTrustedSetup(); + final List exceptions = + List.of( + assertThrows( + KZGException.class, + () -> + kzg.verifyBlobKzgProofBatch( + List.of(Bytes.fromHexString("0x", BYTES_PER_BLOB)), + List.of(getSampleCommitment()), + List.of(getSampleProof()))), + assertThrows(KZGException.class, () -> kzg.blobToKzgCommitment(Bytes.EMPTY)), + assertThrows( + KZGException.class, + () -> kzg.computeBlobKzgProof(Bytes.EMPTY, getSampleCommitment()))); + + assertThat(exceptions) + .allSatisfy( + exception -> assertThat(exception).cause().hasMessage("Trusted Setup is not loaded.")); + } + + @Test + public void testComputingAndVerifyingBatchProofs() { + final int numberOfBlobs = 4; + final List blobs = getSampleBlobs(numberOfBlobs); + final List kzgCommitments = + blobs.stream().map(kzg::blobToKzgCommitment).collect(Collectors.toList()); + final List kzgProofs = + Streams.zip( + kzgCommitments.stream(), + blobs.stream(), + (kzgCommitment, blob) -> kzg.computeBlobKzgProof(blob, kzgCommitment)) + .collect(Collectors.toList()); + assertThat(kzg.verifyBlobKzgProofBatch(blobs, kzgCommitments, kzgProofs)).isTrue(); + + assertThat( + kzg.verifyBlobKzgProofBatch(getSampleBlobs(numberOfBlobs), kzgCommitments, kzgProofs)) + .isFalse(); + assertThat(kzg.verifyBlobKzgProofBatch(blobs, getSampleCommitments(numberOfBlobs), kzgProofs)) + .isFalse(); + final List invalidProofs = + getSampleBlobs(numberOfBlobs).stream() + .map((Bytes blob) -> kzg.computeBlobKzgProof(blob, kzg.blobToKzgCommitment(blob))) + .collect(Collectors.toList()); + assertThat(kzg.verifyBlobKzgProofBatch(blobs, kzgCommitments, invalidProofs)).isFalse(); + } + + @Test + public void testVerifyingEmptyBatch() { + assertThat(kzg.verifyBlobKzgProofBatch(List.of(), List.of(), List.of())).isTrue(); + } + + @Test + public void testComputingAndVerifyingSingleProof() { + final Bytes blob = getSampleBlob(); + final KZGCommitment kzgCommitment = kzg.blobToKzgCommitment(blob); + final KZGProof kzgProof = kzg.computeBlobKzgProof(blob, kzgCommitment); + + assertThat(kzg.verifyBlobKzgProof(blob, kzgCommitment, kzgProof)).isTrue(); + + assertThat(kzg.verifyBlobKzgProof(getSampleBlob(), kzgCommitment, kzgProof)).isFalse(); + assertThat(kzg.verifyBlobKzgProof(blob, getSampleCommitment(), kzgProof)).isFalse(); + final Bytes randomBlob = getSampleBlob(); + final KZGProof invalidProof = + kzg.computeBlobKzgProof(randomBlob, kzg.blobToKzgCommitment(randomBlob)); + assertThat(kzg.verifyBlobKzgProof(blob, kzgCommitment, invalidProof)).isFalse(); + } + + @Test + public void testComputingAndVerifyingBatchSingleProof() { + final int numberOfBlobs = 1; + final List blobs = getSampleBlobs(numberOfBlobs); + final List kzgCommitments = + blobs.stream().map(kzg::blobToKzgCommitment).collect(Collectors.toList()); + final List kzgProofs = + Streams.zip( + kzgCommitments.stream(), + blobs.stream(), + (kzgCommitment, blob) -> kzg.computeBlobKzgProof(blob, kzgCommitment)) + .collect(Collectors.toList()); + assertThat(kzgProofs.size()).isEqualTo(1); + assertThat(kzg.verifyBlobKzgProofBatch(blobs, kzgCommitments, kzgProofs)).isTrue(); + + assertThat( + kzg.verifyBlobKzgProofBatch(getSampleBlobs(numberOfBlobs), kzgCommitments, kzgProofs)) + .isFalse(); + assertThat(kzg.verifyBlobKzgProofBatch(blobs, getSampleCommitments(numberOfBlobs), kzgProofs)) + .isFalse(); + final List invalidProofs = + getSampleBlobs(numberOfBlobs).stream() + .map((Bytes blob) -> kzg.computeBlobKzgProof(blob, kzg.blobToKzgCommitment(blob))) + .collect(Collectors.toList()); + assertThat(kzg.verifyBlobKzgProofBatch(blobs, kzgCommitments, invalidProofs)).isFalse(); + } + + @Test + public void testVerifyingBatchProofsThrowsIfSizesDoesntMatch() { + final int numberOfBlobs = 4; + final List blobs = getSampleBlobs(numberOfBlobs); + final List kzgCommitments = + blobs.stream().map(kzg::blobToKzgCommitment).collect(Collectors.toList()); + final List kzgProofs = + Streams.zip( + kzgCommitments.stream(), + blobs.stream(), + (kzgCommitment, blob) -> kzg.computeBlobKzgProof(blob, kzgCommitment)) + .collect(Collectors.toList()); + final KZGException kzgException1 = + assertThrows( + KZGException.class, + () -> kzg.verifyBlobKzgProofBatch(blobs, kzgCommitments, List.of(kzgProofs.get(0)))); + final KZGException kzgException2 = + assertThrows( + KZGException.class, + () -> kzg.verifyBlobKzgProofBatch(blobs, List.of(kzgCommitments.get(0)), kzgProofs)); + final KZGException kzgException3 = + assertThrows( + KZGException.class, + () -> kzg.verifyBlobKzgProofBatch(List.of(blobs.get(0)), kzgCommitments, kzgProofs)); + + Stream.of(kzgException1, kzgException2, kzgException3) + .forEach( + ex -> + assertThat(ex) + .cause() + .isInstanceOf(CKZGException.class) + .hasMessageMatching( + "Invalid .+ size. Expected \\d+ bytes but got \\d+. \\(C_KZG_BADARGS\\)")); + } + + @ParameterizedTest(name = "blob={0}") + @ValueSource( + strings = { + "0x0d2024ece3e004271319699b8b00cc010628b6bc0be5457f031fb1db0afd3ff8", + "0x", + "0x925668a49d06f4" + }) + public void testComputingProofWithIncorrectLengthBlobDoesNotCauseSegfault(final String blobHex) { + final Bytes blob = Bytes.fromHexString(blobHex); + + final KZGException kzgException = + assertThrows( + KZGException.class, () -> kzg.computeBlobKzgProof(blob, kzg.blobToKzgCommitment(blob))); + + assertThat(kzgException) + .cause() + .satisfies( + cause -> { + // non-canonical blobs + assertThat(cause).isInstanceOf(CKZGException.class); + final CKZGException cryptoException = (CKZGException) cause; + assertThat(cryptoException.getError()).isEqualTo(CKZGError.C_KZG_BADARGS); + assertThat(cryptoException.getErrorMessage()) + .contains("Invalid blob size. Expected 131072 bytes but got"); + }); + } + + @ParameterizedTest(name = "trusted_setup={0}") + @ValueSource( + strings = { + "broken/trusted_setup_g1_length.txt", + "broken/trusted_setup_g2_length.txt", + "broken/trusted_setup_g2_bytesize.txt" + }) + public void incorrectTrustedSetupFilesShouldThrow(final String filename) { + final Throwable cause = + assertThrows( + KZGException.class, + () -> kzg.loadTrustedSetup(TrustedSetupLoader.getTrustedSetupFile(filename))) + .getCause(); + assertThat(cause.getMessage()).contains("Failed to parse trusted setup file"); + } + + @Disabled("das kzg version crashes") + @Test + public void monomialTrustedSetupFilesShouldThrow() { + final KZGException kzgException = + assertThrows( + KZGException.class, + () -> + kzg.loadTrustedSetup( + TrustedSetupLoader.getTrustedSetupFile("trusted_setup_monomial.txt"))); + assertThat(kzgException.getMessage()).contains("Failed to load trusted setup"); + assertThat(kzgException.getCause().getMessage()) + .contains("There was an error while loading the Trusted Setup. (C_KZG_BADARGS)"); + } + + @Test + public void testInvalidLengthG2PointInNewTrustedSetup() { + assertThatThrownBy( + () -> new TrustedSetup(List.of(), List.of(Bytes.fromHexString("")), List.of())) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Expected G2 point to be 96 bytes"); + } + + static final int CELLS_PER_ORIG_BLOB = CELLS_PER_EXT_BLOB / 2; + + @Test + public void testComputeRecoverCellsAndProofs() { + Bytes blob = getSampleBlob(); + List cellAndProofs = kzg.computeCellsAndProofs(blob); + assertThat(cellAndProofs).hasSize(CELLS_PER_EXT_BLOB); + + List cellsToRecover = + IntStream.range(CELLS_PER_ORIG_BLOB, CELLS_PER_EXT_BLOB) + .mapToObj( + i -> + new KZGCellWithColumnId( + cellAndProofs.get(i).cell(), KZGCellID.fromCellColumnIndex(i))) + .toList(); + + List recoveredCells = kzg.recoverCellsAndProofs(cellsToRecover); + assertThat(recoveredCells).isEqualTo(cellAndProofs); + } + + private List getSampleBlobs(final int count) { + return IntStream.range(0, count).mapToObj(__ -> getSampleBlob()).collect(Collectors.toList()); + } + + @Test + public void testComputeAndVerifyCellProof() { + Bytes blob = getSampleBlob(); + List cellAndProofs = kzg.computeCellsAndProofs(blob); + KZGCommitment kzgCommitment = kzg.blobToKzgCommitment(blob); + + for (int i = 0; i < cellAndProofs.size(); i++) { + assertThat( + kzg.verifyCellProofBatch( + List.of(kzgCommitment), + List.of(KZGCellWithColumnId.fromCellAndColumn(cellAndProofs.get(i).cell(), i)), + List.of(cellAndProofs.get(i).proof()))) + .isTrue(); + var invalidProof = cellAndProofs.get((i + 1) % cellAndProofs.size()).proof(); + assertThat( + kzg.verifyCellProofBatch( + List.of(kzgCommitment), + List.of(KZGCellWithColumnId.fromCellAndColumn(cellAndProofs.get(i).cell(), i)), + List.of(invalidProof))) + .isFalse(); + } + } + + Bytes getSampleBlob() { + return IntStream.range(0, FIELD_ELEMENTS_PER_BLOB) + .mapToObj(__ -> randomBLSFieldElement()) + .map(fieldElement -> Bytes.wrap(fieldElement.toArray(ByteOrder.BIG_ENDIAN))) + .reduce(Bytes::wrap) + .orElse(Bytes.EMPTY); + } + + private List getSampleCommitments(final int count) { + return IntStream.range(0, count) + .mapToObj(__ -> getSampleCommitment()) + .collect(Collectors.toList()); + } + + private KZGCommitment getSampleCommitment() { + return kzg.blobToKzgCommitment(getSampleBlob()); + } + + private KZGProof getSampleProof() { + return kzg.computeBlobKzgProof(getSampleBlob(), getSampleCommitment()); + } + + private UInt256 randomBLSFieldElement() { + while (true) { + final BigInteger attempt = new BigInteger(BLS_MODULUS.bitLength(), RND); + if (attempt.compareTo(BLS_MODULUS) < 0) { + return UInt256.valueOf(attempt); + } + } + } +} diff --git a/infrastructure/kzg/src/test/java/tech/pegasys/teku/kzg/RustKZGTest.java b/infrastructure/kzg/src/test/java/tech/pegasys/teku/kzg/RustKZGTest.java new file mode 100644 index 00000000000..137aa6a8729 --- /dev/null +++ b/infrastructure/kzg/src/test/java/tech/pegasys/teku/kzg/RustKZGTest.java @@ -0,0 +1,105 @@ +/* + * Copyright Consensys Software Inc., 2022 + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package tech.pegasys.teku.kzg; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import org.apache.tuweni.bytes.Bytes; +import org.junit.jupiter.api.Test; + +public final class RustKZGTest extends KZGAbstractTest { + + public RustKZGTest() { + super(RustKZG.getInstance()); + } + + @Test + @Override + @SuppressWarnings("JavaCase") + public void testUsageWithoutLoadedTrustedSetup_shouldThrowException() { + kzg.freeTrustedSetup(); + final Bytes blob = getSampleBlob(); + assertThatThrownBy(() -> kzg.computeCellsAndProofs(blob)) + .isOfAnyClassIn(IllegalStateException.class) + .hasMessageStartingWith("KZG context context has been destroyed"); + } + + @Test + @Override + public void testComputingAndVerifyingBatchProofs() { + assertThatThrownBy(super::testComputingAndVerifyingBatchProofs) + .isOfAnyClassIn(RuntimeException.class) + .hasMessageStartingWith("LibPeerDASKZG library doesn't support"); + } + + @Test + @Override + public void testVerifyingEmptyBatch() { + assertThatThrownBy(super::testVerifyingEmptyBatch) + .isOfAnyClassIn(RuntimeException.class) + .hasMessageStartingWith("LibPeerDASKZG library doesn't support"); + } + + @Test + @Override + public void testComputingAndVerifyingSingleProof() { + assertThatThrownBy(super::testComputingAndVerifyingSingleProof) + .isOfAnyClassIn(RuntimeException.class) + .hasMessageStartingWith("LibPeerDASKZG library doesn't support"); + } + + @Test + @Override + public void testComputingAndVerifyingBatchSingleProof() { + assertThatThrownBy(super::testComputingAndVerifyingBatchSingleProof) + .isOfAnyClassIn(RuntimeException.class) + .hasMessageStartingWith("LibPeerDASKZG library doesn't support"); + } + + @Test + @Override + public void testVerifyingBatchProofsThrowsIfSizesDoesntMatch() { + assertThatThrownBy(super::testVerifyingBatchProofsThrowsIfSizesDoesntMatch) + .isOfAnyClassIn(RuntimeException.class) + .hasMessageStartingWith("LibPeerDASKZG library doesn't support"); + } + + @Override + public void testComputingProofWithIncorrectLengthBlobDoesNotCauseSegfault(final String blobHex) { + // skip, not supported + } + + @Override + public void incorrectTrustedSetupFilesShouldThrow(final String filename) { + // skip, not supported + } + + @Override + public void monomialTrustedSetupFilesShouldThrow() { + // skip, not supported + } + + @Override + public void testInvalidLengthG2PointInNewTrustedSetup() { + // skip, not supported + } + + @Test + @Override + public void testComputeAndVerifyCellProof() { + assertThatThrownBy(super::testComputeAndVerifyCellProof) + .isOfAnyClassIn(RuntimeException.class) + .hasMessageStartingWith("LibPeerDASKZG library doesn't support"); + } +} diff --git a/infrastructure/kzg/src/test/java/tech/pegasys/teku/kzg/RustWithCKZGTest.java b/infrastructure/kzg/src/test/java/tech/pegasys/teku/kzg/RustWithCKZGTest.java new file mode 100644 index 00000000000..e1bb1b89e58 --- /dev/null +++ b/infrastructure/kzg/src/test/java/tech/pegasys/teku/kzg/RustWithCKZGTest.java @@ -0,0 +1,20 @@ +/* + * Copyright Consensys Software Inc., 2024 + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package tech.pegasys.teku.kzg; + +public final class RustWithCKZGTest extends KZGAbstractTest { + public RustWithCKZGTest() { + super(RustWithCKZG.getInstance()); + } +} diff --git a/infrastructure/kzg/src/testFixtures/java/tech/pegasys/teku/kzg/KZGAbstractBenchmark.java b/infrastructure/kzg/src/testFixtures/java/tech/pegasys/teku/kzg/KZGAbstractBenchmark.java new file mode 100644 index 00000000000..2a3f4cec5a1 --- /dev/null +++ b/infrastructure/kzg/src/testFixtures/java/tech/pegasys/teku/kzg/KZGAbstractBenchmark.java @@ -0,0 +1,58 @@ +/* + * Copyright Consensys Software Inc., 2024 + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package tech.pegasys.teku.kzg; + +import java.util.Collections; +import java.util.List; +import tech.pegasys.teku.kzg.trusted_setups.TrustedSetupLoader; + +public class KZGAbstractBenchmark { + private final KZG kzg = KZG.getInstance(false); + + public KZGAbstractBenchmark() { + TrustedSetupLoader.loadTrustedSetupForTests(kzg); + } + + protected KZG getKzg() { + return kzg; + } + + protected void printStats(final List validationTimes) { + int sum = 0; + final int size = validationTimes.size(); + + // Sum of elements + for (int time : validationTimes) { + sum += time; + } + + // Mean + final double mean = (double) sum / size; + System.out.printf("Mean, ms: %.2f%n", mean); + + // Standard Deviation + double sumOfSquares = 0.0; + for (int time : validationTimes) { + sumOfSquares += Math.pow(time - mean, 2); + } + final double standardDeviation = Math.sqrt(sumOfSquares / size); + System.out.printf("Std, ms: %.2f%n", standardDeviation); + + // Min and Max + final int min = Collections.min(validationTimes); + final int max = Collections.max(validationTimes); + System.out.println("Min, ms: " + min); + System.out.println("Max, ms: " + max); + } +} diff --git a/services/beaconchain/src/main/java/tech/pegasys/teku/services/beaconchain/BeaconChainController.java b/services/beaconchain/src/main/java/tech/pegasys/teku/services/beaconchain/BeaconChainController.java index 07d4db164bd..9d6157fc87b 100644 --- a/services/beaconchain/src/main/java/tech/pegasys/teku/services/beaconchain/BeaconChainController.java +++ b/services/beaconchain/src/main/java/tech/pegasys/teku/services/beaconchain/BeaconChainController.java @@ -560,7 +560,7 @@ protected void initExecutionLayer() { protected void initKzg() { if (spec.isMilestoneSupported(SpecMilestone.DENEB)) { - kzg = KZG.getInstance(); + kzg = KZG.getInstance(beaconConfig.eth2NetworkConfig().isRustKzgEnabled()); final String trustedSetupFile = beaconConfig .eth2NetworkConfig() diff --git a/teku/src/main/java/tech/pegasys/teku/cli/options/Eth2NetworkOptions.java b/teku/src/main/java/tech/pegasys/teku/cli/options/Eth2NetworkOptions.java index 4c9ace22913..9b3a1387145 100644 --- a/teku/src/main/java/tech/pegasys/teku/cli/options/Eth2NetworkOptions.java +++ b/teku/src/main/java/tech/pegasys/teku/cli/options/Eth2NetworkOptions.java @@ -94,6 +94,17 @@ public class Eth2NetworkOptions { arity = "1") private String trustedSetup = null; // Depends on network configuration + @Option( + names = {"--Xrust-kzg-enabled"}, + paramLabel = "", + description = + "Use Rust KZG library LibPeerDASKZG with fallback to CKZG4844 for EIP-4844 methods", + arity = "0..1", + fallbackValue = "true", + showDefaultValue = Visibility.ALWAYS, + hidden = true) + private boolean rustKzgEnabled = Eth2NetworkConfiguration.DEFAULT_RUST_KZG_ENABLED; + @Option( names = {"--Xfork-choice-late-block-reorg-enabled"}, paramLabel = "", @@ -378,7 +389,8 @@ private void configureEth2Network(final Eth2NetworkConfiguration.Builder builder .asyncBeaconChainMaxThreads(asyncBeaconChainMaxThreads) .forkChoiceLateBlockReorgEnabled(forkChoiceLateBlockReorgEnabled) .epochsStoreBlobs(epochsStoreBlobs) - .forkChoiceUpdatedAlwaysSendPayloadAttributes(forkChoiceUpdatedAlwaysSendPayloadAttributes); + .forkChoiceUpdatedAlwaysSendPayloadAttributes(forkChoiceUpdatedAlwaysSendPayloadAttributes) + .rustKzgEnabled(rustKzgEnabled); asyncP2pMaxQueue.ifPresent(builder::asyncP2pMaxQueue); pendingAttestationsMaxQueue.ifPresent(builder::pendingAttestationsMaxQueue); asyncBeaconChainMaxQueue.ifPresent(builder::asyncBeaconChainMaxQueue); diff --git a/teku/src/test/java/tech/pegasys/teku/cli/options/Eth2NetworkOptionsTest.java b/teku/src/test/java/tech/pegasys/teku/cli/options/Eth2NetworkOptionsTest.java index be75608a2fb..e6e9529ff8a 100644 --- a/teku/src/test/java/tech/pegasys/teku/cli/options/Eth2NetworkOptionsTest.java +++ b/teku/src/test/java/tech/pegasys/teku/cli/options/Eth2NetworkOptionsTest.java @@ -237,4 +237,16 @@ public void shouldShowGoerliDeprecationWarning() { .isInstanceOf(AssertionError.class) // thrown because we had an error .hasMessageContaining("Goerli support has been removed"); } + + @Test + public void rustKzgFlagShouldBeDisabledByDefault() { + final TekuConfiguration config = getTekuConfigurationFromArguments(); + assertThat(config.eth2NetworkConfiguration().isRustKzgEnabled()).isFalse(); + } + + @Test + public void rustKzgFlagCanBeUsedToToggleRustKzgOn() { + final TekuConfiguration config = getTekuConfigurationFromArguments("--Xrust-kzg-enabled"); + assertThat(config.eth2NetworkConfiguration().isRustKzgEnabled()).isTrue(); + } }