From 801439d7f6807236e40c3271b70b7ec36aa1b7fb Mon Sep 17 00:00:00 2001 From: garyschulte Date: Fri, 24 Oct 2025 17:35:58 -0700 Subject: [PATCH 1/5] initial precompile registry overlay impl plus graal-native SignatureAlgorithm Signed-off-by: garyschulte --- build.gradle | 3 +- .../poc/block/execution/BlockRunner.java | 29 ++- .../besu/riscv/poc/crypto/SECP256K1Graal.java | 219 ++++++++++++++++++ .../GraalECRECPrecompiledContract.java | 111 +++++++++ 4 files changed, 360 insertions(+), 2 deletions(-) create mode 100644 src/main/java/org/hyperledger/besu/riscv/poc/crypto/SECP256K1Graal.java create mode 100644 src/main/java/org/hyperledger/besu/riscv/poc/evm/precompiles/GraalECRECPrecompiledContract.java diff --git a/build.gradle b/build.gradle index 206a18e..835a80e 100644 --- a/build.gradle +++ b/build.gradle @@ -150,7 +150,8 @@ dependencies { implementation 'org.hyperledger.besu:besu-plugin-api' implementation 'org.hyperledger.besu.internal:besu-metrics-core' implementation 'org.hyperledger.besu.internal:besu-services-kvstore' - implementation 'org.hyperledger.besu.internal:besu-util:25.10-develop-d28fd4f' + implementation 'org.hyperledger.besu.internal:besu-util:25.10-develop-ae58083' + implementation 'org.hyperledger.besu.internal:besu-crypto-algorithms:25.10-develop-ae58083' } graalvmNative { diff --git a/src/main/java/org/hyperledger/besu/riscv/poc/block/execution/BlockRunner.java b/src/main/java/org/hyperledger/besu/riscv/poc/block/execution/BlockRunner.java index a52fae0..a07ac80 100644 --- a/src/main/java/org/hyperledger/besu/riscv/poc/block/execution/BlockRunner.java +++ b/src/main/java/org/hyperledger/besu/riscv/poc/block/execution/BlockRunner.java @@ -17,6 +17,9 @@ import org.hyperledger.besu.config.GenesisConfig; import org.hyperledger.besu.consensus.merge.PostMergeContext; +import org.hyperledger.besu.crypto.SignatureAlgorithm; +import org.hyperledger.besu.crypto.SignatureAlgorithmFactory; +import org.hyperledger.besu.datatypes.Address; import org.hyperledger.besu.datatypes.Hash; import org.hyperledger.besu.ethereum.BlockProcessingResult; import org.hyperledger.besu.ethereum.ProtocolContext; @@ -44,13 +47,18 @@ import org.hyperledger.besu.ethereum.trie.pathbased.bonsai.cache.NoopBonsaiCachedMerkleTrieLoader; import org.hyperledger.besu.ethereum.trie.pathbased.bonsai.storage.BonsaiWorldStateKeyValueStorage; import org.hyperledger.besu.ethereum.worldstate.DataStorageConfiguration; +import org.hyperledger.besu.evm.gascalculator.GasCalculator; import org.hyperledger.besu.evm.internal.EvmConfiguration; +import org.hyperledger.besu.evm.precompile.PrecompileContractRegistry; import org.hyperledger.besu.metrics.noop.NoOpMetricsSystem; import org.hyperledger.besu.plugin.ServiceManager; import org.hyperledger.besu.plugin.services.BesuService; +import org.hyperledger.besu.riscv.poc.crypto.SECP256K1Graal; +import org.hyperledger.besu.riscv.poc.evm.precompiles.GraalECRECPrecompiledContract; import org.hyperledger.besu.services.kvstore.InMemoryKeyValueStorage; import org.hyperledger.besu.services.kvstore.SegmentedInMemoryKeyValueStorage; +import java.nio.charset.Charset; import java.nio.file.Files; import java.nio.file.Path; import java.util.Comparator; @@ -234,6 +242,15 @@ public Optional getService(Class serviceType) { .withServiceManager(serviceManager) .build(); + // TODO: it would be better to implement a lean protocol schedule with its own precompile registry + // but here we just overwrite the precompileRegistry entries to use the graal-specific + // ones. Longer term, this would probably be zkvm specific configs using eCalls. but + // for now, keep-it-simple: + final ProtocolSpec spec = protocolSchedule + .getByBlockHeader(blockchain.getChainHeadHeader()); + final PrecompileContractRegistry registry = spec.getPrecompileContractRegistry(); + decoratePrecompiles(registry, spec.getGasCalculator()); + return new BlockRunner(protocolSchedule, protocolContext, blockchain); } @@ -272,6 +289,10 @@ private static BlockHeader importHeadersAndSetHead( return previous; } + static private void decoratePrecompiles(PrecompileContractRegistry registry, GasCalculator calc) { + registry.put(Address.ECREC, new GraalECRECPrecompiledContract(calc)); + } + private BlockRunner( ProtocolSchedule protocolSchedule, ProtocolContext protocolContext, @@ -402,7 +423,7 @@ private static String loadFileContent( try (var inputStream = BlockRunner.class.getResourceAsStream(path)) { if (inputStream != null) { System.out.println("Loading from classpath: " + path); - return new String(inputStream.readAllBytes()); + return new String(inputStream.readAllBytes(), Charset.defaultCharset()); } } // Fall back to filesystem @@ -415,6 +436,12 @@ public static void main(final String[] args) { System.out.println("Starting BlockRunner ."); CommandLineArgs cmdArgs = parseArguments(args); + + // set graal signature algorithm: + SignatureAlgorithm graalSig = new SECP256K1Graal(); + graalSig.maybeEnableNative(); + SignatureAlgorithmFactory.setInstance(graalSig); + try { ObjectMapper objectMapper = new ObjectMapper(); diff --git a/src/main/java/org/hyperledger/besu/riscv/poc/crypto/SECP256K1Graal.java b/src/main/java/org/hyperledger/besu/riscv/poc/crypto/SECP256K1Graal.java new file mode 100644 index 0000000..37f327c --- /dev/null +++ b/src/main/java/org/hyperledger/besu/riscv/poc/crypto/SECP256K1Graal.java @@ -0,0 +1,219 @@ +/* + * Copyright contributors to Hyperledger Besu. + * + * 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. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package org.hyperledger.besu.riscv.poc.crypto; + +import org.hyperledger.besu.crypto.AbstractSECP256; +import org.hyperledger.besu.crypto.KeyPair; +import org.hyperledger.besu.crypto.SECPPublicKey; +import org.hyperledger.besu.crypto.SECPSignature; +import org.hyperledger.besu.nativelib.secp256k1.LibSecp256k1Graal; + +import java.math.BigInteger; +import java.util.Optional; + +import org.apache.tuweni.bytes.Bytes; +import org.apache.tuweni.bytes.Bytes32; +import org.bouncycastle.crypto.digests.SHA256Digest; +import org.bouncycastle.crypto.signers.DSAKCalculator; +import org.bouncycastle.crypto.signers.HMacDSAKCalculator; +import org.bouncycastle.math.ec.custom.sec.SecP256K1Curve; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * GraalVM-compatible SECP256K1 implementation using LibSecp256k1Graal. + * + *

This implementation uses the GraalVM-compatible native secp256k1 library which is compatible + * with native image compilation, unlike the JNA-based LibSecp256k1. + */ +public class SECP256K1Graal extends AbstractSECP256 { + + private static final Logger LOG = LoggerFactory.getLogger(SECP256K1Graal.class); + + /** The constant CURVE_NAME. */ + public static final String CURVE_NAME = "secp256k1"; + + private boolean useNative; + + /** Instantiates a new SECP256K1Graal. */ + public SECP256K1Graal() { + super(CURVE_NAME, SecP256K1Curve.q); + maybeEnableNative(); + } + + @Override + public void disableNative() { + useNative = false; + } + + @Override + public boolean maybeEnableNative() { + try { + // Check if LibSecp256k1Graal context is available + // Must use isNonNull() for Word types, not comparison to null + if (LibSecp256k1Graal.getContext().isNonNull()) { + LOG.info("Using GraalVM-compatible native secp256k1 implementation"); + } + } catch (UnsatisfiedLinkError | NoClassDefFoundError e) { + LOG.info("GraalVM native secp256k1 not available - {}", e.getMessage()); + useNative = false; + } + return useNative; + } + + @Override + public boolean isNative() { + return useNative; + } + + @Override + public DSAKCalculator getKCalculator() { + return new HMacDSAKCalculator(new SHA256Digest()); + } + + @Override + public SECPSignature sign(final Bytes32 dataHash, final KeyPair keyPair) { + if (useNative) { + return signNative(dataHash, keyPair); + } else { + return super.sign(dataHash, keyPair); + } + } + + @Override + public boolean verify(final Bytes data, final SECPSignature signature, final SECPPublicKey pub) { + if (useNative) { + return verifyNative(data, signature, pub); + } else { + return super.verify(data, signature, pub); + } + } + + @Override + public Optional recoverPublicKeyFromSignature( + final Bytes32 dataHash, final SECPSignature signature) { + if (useNative) { + Optional result = recoverFromSignatureNative(dataHash, signature); + if (result.isEmpty()) { + throw new IllegalArgumentException("Could not recover public key"); + } else { + return result; + } + } else { + return super.recoverPublicKeyFromSignature(dataHash, signature); + } + } + + @Override + public String getCurveName() { + return CURVE_NAME; + } + + @Override + protected BigInteger recoverFromSignature( + final int recId, final BigInteger r, final BigInteger s, final Bytes32 dataHash) { + if (useNative) { + return recoverFromSignatureNative(dataHash, new SECPSignature(r, s, (byte) recId)) + .map(key -> new BigInteger(1, key.getEncoded())) + .orElse(null); + } else { + return super.recoverFromSignature(recId, r, s, dataHash); + } + } + + private SECPSignature signNative(final Bytes32 dataHash, final KeyPair keyPair) { + final byte[] privateKeyBytes = keyPair.getPrivateKey().getEncoded(); + final byte[] messageBytes = dataHash.toArrayUnsafe(); + + // Sign using GraalVM-compatible library + final byte[] signature = + LibSecp256k1Graal.ecdsaSignRecoverable( + LibSecp256k1Graal.getContext(), privateKeyBytes, messageBytes); + + if (signature == null || signature.length != 65) { + throw new RuntimeException( + "Could not natively sign. Private Key is invalid or signature generation failed."); + } + + // signature is 65 bytes: 64 bytes compact (r + s) + 1 byte recId + final Bytes32 r = Bytes32.wrap(signature, 0); + final Bytes32 s = Bytes32.wrap(signature, 32); + final byte recId = signature[64]; + + return SECPSignature.create( + r.toUnsignedBigInteger(), s.toUnsignedBigInteger(), recId, curveOrder); + } + + private boolean verifyNative( + final Bytes data, final SECPSignature signature, final SECPPublicKey pub) { + try { + // Parse public key (need to add 0x04 prefix for uncompressed format) + final Bytes encodedPubKey = Bytes.concatenate(Bytes.of(0x04), pub.getEncodedBytes()); + + final byte[] pubkeyInternal = new byte[64]; + final int parseResult = + LibSecp256k1Graal.ecPubkeyParse( + LibSecp256k1Graal.getContext(), pubkeyInternal, encodedPubKey.toArrayUnsafe()); + + if (parseResult != 1) { + throw new IllegalArgumentException("Could not parse public key"); + } + + // Verify signature (use only r and s, not recovery ID) + final byte[] compactSignature = signature.encodedBytes().slice(0, 64).toArrayUnsafe(); + + return LibSecp256k1Graal.ecdsaVerify( + LibSecp256k1Graal.getContext(), compactSignature, data.toArrayUnsafe(), pubkeyInternal) + != 0; + } catch (final Exception e) { + LOG.error("Native verification failed", e); + return false; + } + } + + private Optional recoverFromSignatureNative( + final Bytes32 dataHash, final SECPSignature signature) { + try { + // Use the compact signature + recId version + final byte[] compactSignature = signature.encodedBytes().slice(0, 64).toArrayUnsafe(); + final int recId = signature.getRecId(); + + final byte[] recoveredPubkey = + LibSecp256k1Graal.ecdsaRecover( + LibSecp256k1Graal.getContext(), compactSignature, dataHash.toArrayUnsafe(), recId); + + if (recoveredPubkey == null) { + return Optional.empty(); + } + + // The recovered pubkey is already in internal 64-byte format + // Serialize to uncompressed format (65 bytes with 0x04 prefix) + final byte[] serialized = + LibSecp256k1Graal.ecPubkeySerialize( + LibSecp256k1Graal.getContext(), recoveredPubkey, false); + + if (serialized == null || serialized.length != 65) { + return Optional.empty(); + } + + // Remove the 0x04 prefix to get the 64-byte public key + return Optional.of(SECPPublicKey.create(Bytes.wrap(serialized).slice(1), ALGORITHM)); + } catch (final Exception e) { + LOG.error("Native recovery failed", e); + return Optional.empty(); + } + } +} diff --git a/src/main/java/org/hyperledger/besu/riscv/poc/evm/precompiles/GraalECRECPrecompiledContract.java b/src/main/java/org/hyperledger/besu/riscv/poc/evm/precompiles/GraalECRECPrecompiledContract.java new file mode 100644 index 0000000..bd9e8a8 --- /dev/null +++ b/src/main/java/org/hyperledger/besu/riscv/poc/evm/precompiles/GraalECRECPrecompiledContract.java @@ -0,0 +1,111 @@ +/* + * Copyright contributors to Hyperledger Besu. + * + * 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. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package org.hyperledger.besu.riscv.poc.evm.precompiles; + +import org.hyperledger.besu.crypto.Hash; +import org.hyperledger.besu.evm.frame.MessageFrame; +import org.hyperledger.besu.evm.gascalculator.GasCalculator; +import org.hyperledger.besu.evm.precompile.ECRECPrecompiledContract; +import org.hyperledger.besu.nativelib.secp256k1.LibSecp256k1Graal; + +import org.apache.tuweni.bytes.Bytes; +import org.apache.tuweni.bytes.Bytes32; +import org.apache.tuweni.bytes.MutableBytes; +import org.apache.tuweni.bytes.MutableBytes32; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * GraalVM native implementation of the ECRECOVER precompiled contract. + * + *

This implementation extends the standard ECRECPrecompiledContract and overrides the compute + * method to use LibSecp256k1Graal for native execution compatible with GraalVM native image + * compilation. + */ +public class GraalECRECPrecompiledContract extends ECRECPrecompiledContract { + + private static final Logger LOG = LoggerFactory.getLogger(GraalECRECPrecompiledContract.class); + private static final int V_BASE = 27; + + /** + * Instantiates a new Graal ECREC precompiled contract. + * + * @param gasCalculator the gas calculator + */ + public GraalECRECPrecompiledContract(final GasCalculator gasCalculator) { + super(gasCalculator); + } + + @Override + public PrecompileContractResult computePrecompile( + final Bytes input, final MessageFrame messageFrame) { + + LOG.info("-----------------------------------"); + LOG.info("USING GraalECRECPrecompiledContract"); + LOG.info("-----------------------------------"); + final int size = input.size(); + final Bytes safeInput = + size >= 128 ? input : Bytes.wrap(input, MutableBytes.create(128 - size)); + + // Validate that bytes 32-62 are zero (v is in byte 63) + if (!safeInput.slice(32, 31).isZero()) { + return PrecompileContractResult.success(Bytes.EMPTY); + } + + try { + final Bytes32 messageHash = Bytes32.wrap(safeInput, 0); + final int recId = safeInput.get(63) - V_BASE; + + // Extract the 64-byte signature (r and s) + final byte[] sigBytes = safeInput.slice(64, 64).toArrayUnsafe(); + + // Call the GraalVM-compatible native recovery function + final byte[] recoveredPubkey = + LibSecp256k1Graal.ecdsaRecover( + LibSecp256k1Graal.getContext(), sigBytes, messageHash.toArrayUnsafe(), recId); + + if (recoveredPubkey == null) { + return PrecompileContractResult.success(Bytes.EMPTY); + } + + // The recovered pubkey is in internal 64-byte format + // Serialize to uncompressed format (65 bytes with 0x04 prefix) + final byte[] serialized = + LibSecp256k1Graal.ecPubkeySerialize( + LibSecp256k1Graal.getContext(), recoveredPubkey, false); + + if (serialized == null || serialized.length != 65) { + return PrecompileContractResult.success(Bytes.EMPTY); + } + + // Hash the 64-byte public key (skip the 0x04 prefix) + final Bytes32 hashed = Hash.keccak256(Bytes.wrap(serialized).slice(1)); + + // Return the last 20 bytes as the address (right-padded to 32 bytes) + final MutableBytes32 result = MutableBytes32.create(); + hashed.slice(12).copyTo(result, 12); + + return PrecompileContractResult.success(result); + + } catch (final IllegalArgumentException e) { + LOG.debug("ECRECOVER failed with illegal argument", e); + return PrecompileContractResult.success(Bytes.EMPTY); + } catch (final Exception e) { + LOG.error("ECRECOVER failed with unexpected error", e); + return PrecompileContractResult.success(Bytes.EMPTY); + } + } +} From 8e9ebcb4dd2d7e647f50c8dac6928bd6bd5a07a0 Mon Sep 17 00:00:00 2001 From: garyschulte Date: Mon, 27 Oct 2025 12:38:41 -0700 Subject: [PATCH 2/5] add altbn128 graal precompiles Signed-off-by: garyschulte --- .../GraalAltBN128AddPrecompiledContract.java | 103 ++++++++++++++++ .../GraalAltBN128MulPrecompiledContract.java | 110 +++++++++++++++++ ...aalAltBN128PairingPrecompiledContract.java | 116 ++++++++++++++++++ 3 files changed, 329 insertions(+) create mode 100644 src/main/java/org/hyperledger/besu/riscv/poc/evm/precompiles/GraalAltBN128AddPrecompiledContract.java create mode 100644 src/main/java/org/hyperledger/besu/riscv/poc/evm/precompiles/GraalAltBN128MulPrecompiledContract.java create mode 100644 src/main/java/org/hyperledger/besu/riscv/poc/evm/precompiles/GraalAltBN128PairingPrecompiledContract.java diff --git a/src/main/java/org/hyperledger/besu/riscv/poc/evm/precompiles/GraalAltBN128AddPrecompiledContract.java b/src/main/java/org/hyperledger/besu/riscv/poc/evm/precompiles/GraalAltBN128AddPrecompiledContract.java new file mode 100644 index 0000000..352cfff --- /dev/null +++ b/src/main/java/org/hyperledger/besu/riscv/poc/evm/precompiles/GraalAltBN128AddPrecompiledContract.java @@ -0,0 +1,103 @@ +/* + * Copyright contributors to Hyperledger Besu. + * + * 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. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package org.hyperledger.besu.riscv.poc.evm.precompiles; + +import static java.nio.charset.StandardCharsets.UTF_8; + +import org.hyperledger.besu.evm.frame.ExceptionalHaltReason; +import org.hyperledger.besu.evm.frame.MessageFrame; +import org.hyperledger.besu.evm.gascalculator.GasCalculator; +import org.hyperledger.besu.evm.precompile.AbstractPrecompiledContract; +import org.hyperledger.besu.nativelib.gnark.LibGnarkEIP196Graal; + +import java.util.Optional; + +import org.apache.tuweni.bytes.Bytes; +import org.graalvm.nativeimage.PinnedObject; +import org.graalvm.nativeimage.c.type.CIntPointer; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * GraalVM-compatible AltBN128 addition precompiled contract (Byzantium version). + * + *

This implementation uses LibGnarkEIP196Graal for native execution compatible with GraalVM + * native image compilation. + */ +public class GraalAltBN128AddPrecompiledContract extends AbstractPrecompiledContract { + + private static final Logger LOG = + LoggerFactory.getLogger(GraalAltBN128AddPrecompiledContract.class); + + private static final int PARAMETER_LENGTH = 128; + private static final long GAS_COST = 500L; // Byzantium cost + + /** + * Instantiates a new Graal AltBN128 Add precompiled contract. + * + * @param gasCalculator the gas calculator + */ + public GraalAltBN128AddPrecompiledContract(final GasCalculator gasCalculator) { + super("AltBN128Add", gasCalculator); + } + + @Override + public long gasRequirement(final Bytes input) { + return GAS_COST; + } + + @Override + public PrecompileContractResult computePrecompile( + final Bytes input, final MessageFrame messageFrame) { + + final byte[] result = new byte[LibGnarkEIP196Graal.EIP196_PREALLOCATE_FOR_RESULT_BYTES]; + final byte[] error = new byte[LibGnarkEIP196Graal.EIP196_PREALLOCATE_FOR_ERROR_BYTES]; + final int[] outputSize = new int[1]; + final int[] errorSize = new int[1]; + + outputSize[0] = LibGnarkEIP196Graal.EIP196_PREALLOCATE_FOR_RESULT_BYTES; + errorSize[0] = LibGnarkEIP196Graal.EIP196_PREALLOCATE_FOR_ERROR_BYTES; + + final int inputSize = Math.min(PARAMETER_LENGTH, input.size()); + final byte[] inputBytes = input.slice(0, inputSize).toArrayUnsafe(); + + try (PinnedObject pinnedInput = PinnedObject.create(inputBytes); + PinnedObject pinnedResult = PinnedObject.create(result); + PinnedObject pinnedError = PinnedObject.create(error); + PinnedObject pinnedOutputSize = PinnedObject.create(outputSize); + PinnedObject pinnedErrorSize = PinnedObject.create(errorSize)) { + + final int errorNo = + LibGnarkEIP196Graal.eip196altbn128G1AddNative( + pinnedInput.addressOfArrayElement(0), + pinnedResult.addressOfArrayElement(0), + pinnedError.addressOfArrayElement(0), + inputSize, + (CIntPointer) pinnedOutputSize.addressOfArrayElement(0), + (CIntPointer) pinnedErrorSize.addressOfArrayElement(0)); + + if (errorNo == 0) { + return PrecompileContractResult.success(Bytes.wrap(result, 0, outputSize[0])); + } else { + final String errorString = new String(error, 0, errorSize[0], UTF_8); + messageFrame.setRevertReason(Bytes.wrap(error, 0, errorSize[0])); + LOG.trace("Error executing AltBN128 Add precompile: '{}'", errorString); + return PrecompileContractResult.halt( + null, Optional.of(ExceptionalHaltReason.PRECOMPILE_ERROR)); + } + } + } +} diff --git a/src/main/java/org/hyperledger/besu/riscv/poc/evm/precompiles/GraalAltBN128MulPrecompiledContract.java b/src/main/java/org/hyperledger/besu/riscv/poc/evm/precompiles/GraalAltBN128MulPrecompiledContract.java new file mode 100644 index 0000000..8b691b7 --- /dev/null +++ b/src/main/java/org/hyperledger/besu/riscv/poc/evm/precompiles/GraalAltBN128MulPrecompiledContract.java @@ -0,0 +1,110 @@ +/* + * Copyright contributors to Hyperledger Besu. + * + * 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. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package org.hyperledger.besu.riscv.poc.evm.precompiles; + +import static java.nio.charset.StandardCharsets.UTF_8; + +import org.hyperledger.besu.evm.frame.ExceptionalHaltReason; +import org.hyperledger.besu.evm.frame.MessageFrame; +import org.hyperledger.besu.evm.gascalculator.GasCalculator; +import org.hyperledger.besu.evm.precompile.AbstractPrecompiledContract; +import org.hyperledger.besu.nativelib.gnark.LibGnarkEIP196Graal; + +import java.util.Optional; + +import org.apache.tuweni.bytes.Bytes; +import org.graalvm.nativeimage.PinnedObject; +import org.graalvm.nativeimage.c.type.CIntPointer; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * GraalVM-compatible AltBN128 scalar multiplication precompiled contract (Byzantium version). + * + *

This implementation uses LibGnarkEIP196Graal for native execution compatible with GraalVM + * native image compilation. + */ +public class GraalAltBN128MulPrecompiledContract extends AbstractPrecompiledContract { + + private static final Logger LOG = + LoggerFactory.getLogger(GraalAltBN128MulPrecompiledContract.class); + + private static final int PARAMETER_LENGTH = 96; + private static final long GAS_COST = 40_000L; // Byzantium cost + private static final Bytes POINT_AT_INFINITY = Bytes.repeat((byte) 0, 64); + + /** + * Instantiates a new Graal AltBN128 Mul precompiled contract. + * + * @param gasCalculator the gas calculator + */ + public GraalAltBN128MulPrecompiledContract(final GasCalculator gasCalculator) { + super("AltBN128Mul", gasCalculator); + } + + @Override + public long gasRequirement(final Bytes input) { + return GAS_COST; + } + + @Override + public PrecompileContractResult computePrecompile( + final Bytes input, final MessageFrame messageFrame) { + + // Early return for point at infinity + if (input.size() >= 64 && input.slice(0, 64).equals(POINT_AT_INFINITY)) { + return new PrecompileContractResult( + POINT_AT_INFINITY, false, MessageFrame.State.COMPLETED_SUCCESS, Optional.empty()); + } + + final byte[] result = new byte[LibGnarkEIP196Graal.EIP196_PREALLOCATE_FOR_RESULT_BYTES]; + final byte[] error = new byte[LibGnarkEIP196Graal.EIP196_PREALLOCATE_FOR_ERROR_BYTES]; + final int[] outputSize = new int[1]; + final int[] errorSize = new int[1]; + + outputSize[0] = LibGnarkEIP196Graal.EIP196_PREALLOCATE_FOR_RESULT_BYTES; + errorSize[0] = LibGnarkEIP196Graal.EIP196_PREALLOCATE_FOR_ERROR_BYTES; + + final int inputSize = Math.min(PARAMETER_LENGTH, input.size()); + final byte[] inputBytes = input.slice(0, inputSize).toArrayUnsafe(); + + try (PinnedObject pinnedInput = PinnedObject.create(inputBytes); + PinnedObject pinnedResult = PinnedObject.create(result); + PinnedObject pinnedError = PinnedObject.create(error); + PinnedObject pinnedOutputSize = PinnedObject.create(outputSize); + PinnedObject pinnedErrorSize = PinnedObject.create(errorSize)) { + + final int errorNo = + LibGnarkEIP196Graal.eip196altbn128G1MulNative( + pinnedInput.addressOfArrayElement(0), + pinnedResult.addressOfArrayElement(0), + pinnedError.addressOfArrayElement(0), + inputSize, + (CIntPointer) pinnedOutputSize.addressOfArrayElement(0), + (CIntPointer) pinnedErrorSize.addressOfArrayElement(0)); + + if (errorNo == 0) { + return PrecompileContractResult.success(Bytes.wrap(result, 0, outputSize[0])); + } else { + final String errorString = new String(error, 0, errorSize[0], UTF_8); + messageFrame.setRevertReason(Bytes.wrap(error, 0, errorSize[0])); + LOG.trace("Error executing AltBN128 Mul precompile: '{}'", errorString); + return PrecompileContractResult.halt( + null, Optional.of(ExceptionalHaltReason.PRECOMPILE_ERROR)); + } + } + } +} diff --git a/src/main/java/org/hyperledger/besu/riscv/poc/evm/precompiles/GraalAltBN128PairingPrecompiledContract.java b/src/main/java/org/hyperledger/besu/riscv/poc/evm/precompiles/GraalAltBN128PairingPrecompiledContract.java new file mode 100644 index 0000000..05279a6 --- /dev/null +++ b/src/main/java/org/hyperledger/besu/riscv/poc/evm/precompiles/GraalAltBN128PairingPrecompiledContract.java @@ -0,0 +1,116 @@ +/* + * Copyright contributors to Hyperledger Besu. + * + * 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. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package org.hyperledger.besu.riscv.poc.evm.precompiles; + +import static java.nio.charset.StandardCharsets.UTF_8; + +import org.hyperledger.besu.evm.frame.ExceptionalHaltReason; +import org.hyperledger.besu.evm.frame.MessageFrame; +import org.hyperledger.besu.evm.gascalculator.GasCalculator; +import org.hyperledger.besu.evm.precompile.AbstractPrecompiledContract; +import org.hyperledger.besu.nativelib.gnark.LibGnarkEIP196Graal; + +import java.util.Optional; + +import org.apache.tuweni.bytes.Bytes; +import org.graalvm.nativeimage.PinnedObject; +import org.graalvm.nativeimage.c.type.CIntPointer; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * GraalVM-compatible AltBN128 pairing check precompiled contract (Byzantium version). + * + *

This implementation uses LibGnarkEIP196Graal for native execution compatible with GraalVM + * native image compilation. + */ +public class GraalAltBN128PairingPrecompiledContract extends AbstractPrecompiledContract { + + private static final Logger LOG = + LoggerFactory.getLogger(GraalAltBN128PairingPrecompiledContract.class); + + private static final int PARAMETER_LENGTH = 192; + private static final long PAIRING_GAS_COST = 80_000L; // Byzantium per-pairing cost + private static final long BASE_GAS_COST = 100_000L; // Byzantium base cost + + /** The constant TRUE (pairing check succeeded). */ + public static final Bytes TRUE = + Bytes.fromHexString("0x0000000000000000000000000000000000000000000000000000000000000001"); + + /** + * Instantiates a new Graal AltBN128 Pairing precompiled contract. + * + * @param gasCalculator the gas calculator + */ + public GraalAltBN128PairingPrecompiledContract(final GasCalculator gasCalculator) { + super("AltBN128Pairing", gasCalculator); + } + + @Override + public long gasRequirement(final Bytes input) { + final int parameters = input.size() / PARAMETER_LENGTH; + return (PAIRING_GAS_COST * parameters) + BASE_GAS_COST; + } + + @Override + public PrecompileContractResult computePrecompile( + final Bytes input, final MessageFrame messageFrame) { + + // Empty input is valid and returns TRUE + if (input.isEmpty()) { + return PrecompileContractResult.success(TRUE); + } + + final byte[] result = new byte[LibGnarkEIP196Graal.EIP196_PREALLOCATE_FOR_RESULT_BYTES]; + final byte[] error = new byte[LibGnarkEIP196Graal.EIP196_PREALLOCATE_FOR_ERROR_BYTES]; + final int[] outputSize = new int[1]; + final int[] errorSize = new int[1]; + + outputSize[0] = LibGnarkEIP196Graal.EIP196_PREALLOCATE_FOR_RESULT_BYTES; + errorSize[0] = LibGnarkEIP196Graal.EIP196_PREALLOCATE_FOR_ERROR_BYTES; + + // Calculate input limit (must be multiple of PARAMETER_LENGTH) + final int inputLimit = (Integer.MAX_VALUE / PARAMETER_LENGTH) * PARAMETER_LENGTH; + final int inputSize = Math.min(inputLimit, input.size()); + final byte[] inputBytes = input.slice(0, inputSize).toArrayUnsafe(); + + try (PinnedObject pinnedInput = PinnedObject.create(inputBytes); + PinnedObject pinnedResult = PinnedObject.create(result); + PinnedObject pinnedError = PinnedObject.create(error); + PinnedObject pinnedOutputSize = PinnedObject.create(outputSize); + PinnedObject pinnedErrorSize = PinnedObject.create(errorSize)) { + + final int errorNo = + LibGnarkEIP196Graal.eip196altbn128PairingNative( + pinnedInput.addressOfArrayElement(0), + pinnedResult.addressOfArrayElement(0), + pinnedError.addressOfArrayElement(0), + inputSize, + (CIntPointer) pinnedOutputSize.addressOfArrayElement(0), + (CIntPointer) pinnedErrorSize.addressOfArrayElement(0)); + + if (errorNo == 0) { + return PrecompileContractResult.success(Bytes.wrap(result, 0, outputSize[0])); + } else { + final String errorString = new String(error, 0, errorSize[0], UTF_8); + messageFrame.setRevertReason(Bytes.wrap(error, 0, errorSize[0])); + LOG.trace("Error executing AltBN128 Pairing precompile: '{}'", errorString); + return PrecompileContractResult.halt( + null, Optional.of(ExceptionalHaltReason.PRECOMPILE_ERROR)); + } + } + } +} From 260cef379c792fa9f4cdee7fbeb700700ed5ded5 Mon Sep 17 00:00:00 2001 From: garyschulte Date: Mon, 27 Oct 2025 14:10:18 -0700 Subject: [PATCH 3/5] trying minimal mainnet protocol spec to reduce instantiation overhead. some precompiles not implemented, test block processing failing. interim commit Signed-off-by: garyschulte --- .../poc/block/execution/BlockRunner.java | 37 +-- .../execution/MinimalProtocolSchedule.java | 256 ++++++++++++++++++ .../execution/SingleSpecProtocolSchedule.java | 134 +++++++++ .../besu/riscv/poc/crypto/SECP256K1Graal.java | 20 +- .../GraalAltBN128AddPrecompiledContract.java | 15 +- .../GraalAltBN128MulPrecompiledContract.java | 15 +- ...aalAltBN128PairingPrecompiledContract.java | 15 +- .../GraalECRECPrecompiledContract.java | 51 ++-- 8 files changed, 455 insertions(+), 88 deletions(-) create mode 100644 src/main/java/org/hyperledger/besu/riscv/poc/block/execution/MinimalProtocolSchedule.java create mode 100644 src/main/java/org/hyperledger/besu/riscv/poc/block/execution/SingleSpecProtocolSchedule.java diff --git a/src/main/java/org/hyperledger/besu/riscv/poc/block/execution/BlockRunner.java b/src/main/java/org/hyperledger/besu/riscv/poc/block/execution/BlockRunner.java index a07ac80..e8bfacd 100644 --- a/src/main/java/org/hyperledger/besu/riscv/poc/block/execution/BlockRunner.java +++ b/src/main/java/org/hyperledger/besu/riscv/poc/block/execution/BlockRunner.java @@ -19,7 +19,6 @@ import org.hyperledger.besu.consensus.merge.PostMergeContext; import org.hyperledger.besu.crypto.SignatureAlgorithm; import org.hyperledger.besu.crypto.SignatureAlgorithmFactory; -import org.hyperledger.besu.datatypes.Address; import org.hyperledger.besu.datatypes.Hash; import org.hyperledger.besu.ethereum.BlockProcessingResult; import org.hyperledger.besu.ethereum.ProtocolContext; @@ -30,10 +29,8 @@ import org.hyperledger.besu.ethereum.core.Block; import org.hyperledger.besu.ethereum.core.BlockHeader; import org.hyperledger.besu.ethereum.core.Difficulty; -import org.hyperledger.besu.ethereum.core.MiningConfiguration; import org.hyperledger.besu.ethereum.mainnet.HeaderValidationMode; import org.hyperledger.besu.ethereum.mainnet.MainnetBlockHeaderFunctions; -import org.hyperledger.besu.ethereum.mainnet.MainnetProtocolSchedule; import org.hyperledger.besu.ethereum.mainnet.ProtocolSchedule; import org.hyperledger.besu.ethereum.mainnet.ProtocolSpec; import org.hyperledger.besu.ethereum.rlp.RLP; @@ -47,14 +44,11 @@ import org.hyperledger.besu.ethereum.trie.pathbased.bonsai.cache.NoopBonsaiCachedMerkleTrieLoader; import org.hyperledger.besu.ethereum.trie.pathbased.bonsai.storage.BonsaiWorldStateKeyValueStorage; import org.hyperledger.besu.ethereum.worldstate.DataStorageConfiguration; -import org.hyperledger.besu.evm.gascalculator.GasCalculator; import org.hyperledger.besu.evm.internal.EvmConfiguration; -import org.hyperledger.besu.evm.precompile.PrecompileContractRegistry; import org.hyperledger.besu.metrics.noop.NoOpMetricsSystem; import org.hyperledger.besu.plugin.ServiceManager; import org.hyperledger.besu.plugin.services.BesuService; import org.hyperledger.besu.riscv.poc.crypto.SECP256K1Graal; -import org.hyperledger.besu.riscv.poc.evm.precompiles.GraalECRECPrecompiledContract; import org.hyperledger.besu.services.kvstore.InMemoryKeyValueStorage; import org.hyperledger.besu.services.kvstore.SegmentedInMemoryKeyValueStorage; @@ -94,6 +88,7 @@ private record CommandLineArgs( * to process a block. */ public static BlockRunner create( + final BlockHeader targetBlockHeader, final List prevHeaders, final Map trieNodes, final Map codes, @@ -110,15 +105,15 @@ public static BlockRunner create( EvmConfiguration.WorldUpdaterMode.STACKED, true); - // Build the mainnet protocol schedule based on the genesis config. + // Build a minimal protocol schedule with only the necessary fork spec and Graal precompiles. + // This avoids the overhead of building all fork specs from Frontier through the latest, + // and prevents loading JNA-based native libraries that will be replaced. final ProtocolSchedule protocolSchedule = - MainnetProtocolSchedule.fromConfig( - genesisConfig.getConfigOptions(), + MinimalProtocolSchedule.create( + targetBlockHeader, + genesisConfig, evmConfiguration, - MiningConfiguration.MINING_DISABLED, new BadBlockManager(), - false, - false, noOpMetricsSystem); // Construct the genesis state and world state root. @@ -242,15 +237,8 @@ public Optional getService(Class serviceType) { .withServiceManager(serviceManager) .build(); - // TODO: it would be better to implement a lean protocol schedule with its own precompile registry - // but here we just overwrite the precompileRegistry entries to use the graal-specific - // ones. Longer term, this would probably be zkvm specific configs using eCalls. but - // for now, keep-it-simple: - final ProtocolSpec spec = protocolSchedule - .getByBlockHeader(blockchain.getChainHeadHeader()); - final PrecompileContractRegistry registry = spec.getPrecompileContractRegistry(); - decoratePrecompiles(registry, spec.getGasCalculator()); - + // The MinimalProtocolSchedule already created the correct precompile registry + // with Graal-native implementations, so no additional decoration is needed. return new BlockRunner(protocolSchedule, protocolContext, blockchain); } @@ -289,10 +277,6 @@ private static BlockHeader importHeadersAndSetHead( return previous; } - static private void decoratePrecompiles(PrecompileContractRegistry registry, GasCalculator calc) { - registry.put(Address.ECREC, new GraalECRECPrecompiledContract(calc)); - } - private BlockRunner( ProtocolSchedule protocolSchedule, ProtocolContext protocolContext, @@ -508,7 +492,8 @@ public static void main(final String[] args) { System.out.println(String.format("\n✓ Setup completed in %.2f ms\n", setupTimeMs)); final BlockRunner runner = - BlockRunner.create(previousHeaders, trieNodes, codes, genesisConfigJson); + BlockRunner.create( + blockToImport.getHeader(), previousHeaders, trieNodes, codes, genesisConfigJson); runner.processBlock(blockToImport); diff --git a/src/main/java/org/hyperledger/besu/riscv/poc/block/execution/MinimalProtocolSchedule.java b/src/main/java/org/hyperledger/besu/riscv/poc/block/execution/MinimalProtocolSchedule.java new file mode 100644 index 0000000..f389edc --- /dev/null +++ b/src/main/java/org/hyperledger/besu/riscv/poc/block/execution/MinimalProtocolSchedule.java @@ -0,0 +1,256 @@ +/* + * Copyright Consensys Software Inc., 2025 + * + * 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 org.hyperledger.besu.riscv.poc.block.execution; + +import org.hyperledger.besu.config.GenesisConfig; +import org.hyperledger.besu.config.GenesisConfigOptions; +import org.hyperledger.besu.datatypes.Address; +import org.hyperledger.besu.ethereum.chain.BadBlockManager; +import org.hyperledger.besu.ethereum.core.BlockHeader; +import org.hyperledger.besu.ethereum.core.MiningConfiguration; +import org.hyperledger.besu.ethereum.mainnet.MainnetProtocolSpecFactory; +import org.hyperledger.besu.ethereum.mainnet.PrecompiledContractConfiguration; +import org.hyperledger.besu.ethereum.mainnet.ProtocolSchedule; +import org.hyperledger.besu.ethereum.mainnet.ProtocolSpec; +import org.hyperledger.besu.ethereum.mainnet.ProtocolSpecBuilder; +import org.hyperledger.besu.ethereum.mainnet.blockhash.CancunPreExecutionProcessor; +import org.hyperledger.besu.ethereum.mainnet.blockhash.FrontierPreExecutionProcessor; +import org.hyperledger.besu.ethereum.mainnet.blockhash.PraguePreExecutionProcessor; +import org.hyperledger.besu.evm.gascalculator.GasCalculator; +import org.hyperledger.besu.evm.internal.EvmConfiguration; +import org.hyperledger.besu.evm.precompile.MainnetPrecompiledContracts; +import org.hyperledger.besu.evm.precompile.PrecompileContractRegistry; +import org.hyperledger.besu.plugin.services.MetricsSystem; +import org.hyperledger.besu.riscv.poc.evm.precompiles.GraalAltBN128AddPrecompiledContract; +import org.hyperledger.besu.riscv.poc.evm.precompiles.GraalAltBN128MulPrecompiledContract; +import org.hyperledger.besu.riscv.poc.evm.precompiles.GraalAltBN128PairingPrecompiledContract; +import org.hyperledger.besu.riscv.poc.evm.precompiles.GraalECRECPrecompiledContract; + +import java.math.BigInteger; +import java.util.Optional; +import java.util.OptionalInt; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Minimal protocol schedule that creates only a single ProtocolSpec for the target block's fork. + * + *

This avoids the overhead of building protocol specs for all forks from Frontier through the + * latest, and immediately uses GraalVM-native precompiles instead of creating JNA-based ones that + * are later replaced. + */ +public class MinimalProtocolSchedule { + + private static final Logger LOG = LoggerFactory.getLogger(MinimalProtocolSchedule.class); + + /** + * Create a minimal protocol schedule for a single target block. + * + * @param targetHeader the block header being processed + * @param genesisConfig the genesis configuration + * @param evmConfiguration the EVM configuration + * @param badBlockManager the bad block manager + * @param metricsSystem the metrics system + * @return a protocol schedule with only the necessary fork spec + */ + public static ProtocolSchedule create( + final BlockHeader targetHeader, + final GenesisConfig genesisConfig, + final EvmConfiguration evmConfiguration, + final BadBlockManager badBlockManager, + final MetricsSystem metricsSystem) { + + final GenesisConfigOptions configOptions = genesisConfig.getConfigOptions(); + final Optional chainId = + configOptions.getChainId().or(() -> Optional.of(BigInteger.ONE)); + + // Determine which fork applies to this block + final long timestamp = targetHeader.getTimestamp(); + final ForkInfo forkInfo = determineFork(timestamp, configOptions); + + LOG.info( + "Creating minimal protocol schedule for fork: {} at timestamp: {}", + forkInfo.name(), + timestamp); + + // Create the protocol spec factory + final MainnetProtocolSpecFactory specFactory = + new MainnetProtocolSpecFactory( + chainId, + false, // isRevertReasonEnabled + configOptions, + evmConfiguration.overrides( + configOptions.getContractSizeLimit(), + OptionalInt.empty(), + configOptions.getEvmStackSize()), + MiningConfiguration.MINING_DISABLED, + false, // isParallelTxProcessingEnabled + false, // isBlockAccessListEnabled + metricsSystem); + + // Get the protocol spec builder for the target fork + final ProtocolSpecBuilder builder = forkInfo.getSpecBuilder(specFactory); + + // Replace the precompile registry builder with one that creates Graal implementations + builder.precompileContractRegistryBuilder( + MinimalProtocolSchedule::createGraalPrecompileRegistry); + builder.badBlocksManager(badBlockManager); + + // Ensure the appropriate pre-execution processor is set for this fork + // The fork definitions SHOULD set this, but we explicitly set it as a safety measure + // to prevent NullPointerException in AbstractBlockProcessor.processBlock + ensurePreExecutionProcessor(builder, forkInfo.name()); + + LOG.info("Building ProtocolSpec for fork: {}", forkInfo.name()); + + // Create a single-spec protocol schedule with null spec initially + // This avoids DefaultProtocolSchedule's preconditions (milestone at block 0, sorted sets, etc.) + // The spec will be set after building to resolve the circular dependency + final SingleSpecProtocolSchedule protocolSchedule = + new SingleSpecProtocolSchedule(null, chainId); + + // Build the protocol spec, passing the schedule (which will be populated below) + final ProtocolSpec spec = builder.build(protocolSchedule); + + // Verify preExecutionProcessor is set (should never be null after ensurePreExecutionProcessor) + if (spec.getPreExecutionProcessor() == null) { + throw new IllegalStateException("PreExecutionProcessor is null for fork: " + forkInfo.name()); + } + LOG.info("PreExecutionProcessor successfully set: {}", spec.getPreExecutionProcessor().getClass().getSimpleName()); + + // Now set the spec in the schedule to resolve the circular dependency + protocolSchedule.setProtocolSpec(spec); + + return protocolSchedule; + } + + /** + * Ensure the ProtocolSpecBuilder has the appropriate PreExecutionProcessor set for the fork. + * + * @param builder the protocol spec builder + * @param forkName the name of the fork (Paris, Shanghai, Cancun, Prague, Osaka) + */ + private static void ensurePreExecutionProcessor( + final ProtocolSpecBuilder builder, final String forkName) { + switch (forkName) { + case "Cancun" -> builder.preExecutionProcessor(new CancunPreExecutionProcessor()); + case "Prague", "Osaka" -> builder.preExecutionProcessor(new PraguePreExecutionProcessor()); + default -> builder.preExecutionProcessor(new FrontierPreExecutionProcessor()); + } + } + + /** + * Create a precompile registry populated with GraalVM-native implementations. + * + *

This builds the registry from scratch using only Graal-compatible implementations to avoid + * triggering static initializers in JNA-based precompile classes. + * + * @param config the precompiled contract configuration (contains gas calculator) + * @return a precompile registry with Graal implementations + */ + private static PrecompileContractRegistry createGraalPrecompileRegistry( + final PrecompiledContractConfiguration config) { + + final GasCalculator gasCalculator = config.getGasCalculator(); + + // Start with Frontier precompiles (pure Java, no native dependencies) + // This creates ECREC, SHA256, RIPEMD160, ID without triggering AltBN128 static initializers + final PrecompileContractRegistry registry = MainnetPrecompiledContracts.frontier(gasCalculator); + + // Replace ECREC with our Graal version + registry.put(Address.ECREC, new GraalECRECPrecompiledContract(gasCalculator)); + + // Add Byzantium precompiles + // TODO: Add MODEXP if needed (pure Java, needs reflection or package relocation) + // registry.put(Address.MODEXP, ...); + + // Use our Graal-native AltBN128 implementations (avoid Besu's JNA-based ones) + registry.put(Address.ALTBN128_ADD, new GraalAltBN128AddPrecompiledContract(gasCalculator)); + registry.put(Address.ALTBN128_MUL, new GraalAltBN128MulPrecompiledContract(gasCalculator)); + registry.put( + Address.ALTBN128_PAIRING, new GraalAltBN128PairingPrecompiledContract(gasCalculator)); + + // Add Istanbul precompiles + // TODO: Add BLAKE2BF if needed (pure Java, needs reflection or package relocation) + // registry.put(Address.BLAKE2B_F_COMPRESSION, ...); + + // Note: For post-Istanbul forks (Prague+), additional precompiles may be needed: + // - BLS12 precompiles require LibGnarkEIP2537Graal + // - P256Verify requires BoringSSL + // These are not included as they're not needed for Paris-Cancun era blocks + + return registry; + } + + /** + * Determine which fork applies to the given timestamp. + * + * @param timestamp the block timestamp + * @param config the genesis config options + * @return the fork info + */ + private static ForkInfo determineFork(final long timestamp, final GenesisConfigOptions config) { + // Check timestamp-based forks in reverse chronological order (newest first) + if (config.getOsakaTime().isPresent() && timestamp >= config.getOsakaTime().getAsLong()) { + return new ForkInfo( + "Osaka", + config.getOsakaTime().getAsLong(), + true, + MainnetProtocolSpecFactory::osakaDefinition); + } + if (config.getPragueTime().isPresent() && timestamp >= config.getPragueTime().getAsLong()) { + return new ForkInfo( + "Prague", + config.getPragueTime().getAsLong(), + true, + MainnetProtocolSpecFactory::pragueDefinition); + } + if (config.getCancunTime().isPresent() && timestamp >= config.getCancunTime().getAsLong()) { + return new ForkInfo( + "Cancun", + config.getCancunTime().getAsLong(), + true, + MainnetProtocolSpecFactory::cancunDefinition); + } + if (config.getShanghaiTime().isPresent() && timestamp >= config.getShanghaiTime().getAsLong()) { + return new ForkInfo( + "Shanghai", + config.getShanghaiTime().getAsLong(), + true, + MainnetProtocolSpecFactory::shanghaiDefinition); + } + + // Default to Paris (The Merge) for post-merge blocks + // This is reasonable since the user stated this only processes blocks SINCE Paris + return new ForkInfo("Paris", 0L, false, MainnetProtocolSpecFactory::parisDefinition); + } + + /** Information about a fork and how to build its protocol spec. */ + private record ForkInfo( + String name, + long activationValue, + boolean isTimestampBased, + SpecBuilderFunction getSpecBuilder) { + + ProtocolSpecBuilder getSpecBuilder(final MainnetProtocolSpecFactory factory) { + return getSpecBuilder.apply(factory); + } + } + + /** Functional interface for getting a protocol spec builder from a factory. */ + @FunctionalInterface + private interface SpecBuilderFunction { + ProtocolSpecBuilder apply(MainnetProtocolSpecFactory factory); + } +} diff --git a/src/main/java/org/hyperledger/besu/riscv/poc/block/execution/SingleSpecProtocolSchedule.java b/src/main/java/org/hyperledger/besu/riscv/poc/block/execution/SingleSpecProtocolSchedule.java new file mode 100644 index 0000000..7cb3b42 --- /dev/null +++ b/src/main/java/org/hyperledger/besu/riscv/poc/block/execution/SingleSpecProtocolSchedule.java @@ -0,0 +1,134 @@ +/* + * Copyright Consensys Software Inc., 2025 + * + * 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 org.hyperledger.besu.riscv.poc.block.execution; + +import org.hyperledger.besu.ethereum.core.BlockHeader; +import org.hyperledger.besu.ethereum.core.PermissionTransactionFilter; +import org.hyperledger.besu.ethereum.mainnet.ProtocolSchedule; +import org.hyperledger.besu.ethereum.mainnet.ProtocolSpec; +import org.hyperledger.besu.ethereum.mainnet.ScheduledProtocolSpec; +import org.hyperledger.besu.plugin.data.ProcessableBlockHeader; +import org.hyperledger.besu.plugin.services.txvalidator.TransactionValidationRule; + +import java.math.BigInteger; +import java.util.List; +import java.util.Optional; +import java.util.function.Predicate; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * A minimal protocol schedule that always returns a single ProtocolSpec. + * + *

This avoids the overhead and preconditions of DefaultProtocolSchedule which requires + * milestones at block 0 and maintains a sorted set of protocol specs for all forks. For + * single-block processing, we only need one spec that applies to the target block. + */ +public class SingleSpecProtocolSchedule implements ProtocolSchedule { + + private static final Logger LOG = LoggerFactory.getLogger(SingleSpecProtocolSchedule.class); + + private ProtocolSpec spec; + private final Optional chainId; + + public SingleSpecProtocolSchedule(final ProtocolSpec spec, final Optional chainId) { + this.spec = spec; + this.chainId = chainId; + LOG.info( + "SingleSpecProtocolSchedule constructor: spec={}, preExecutionProcessor={}", + spec, + spec == null ? "NULL_SPEC" : (spec.getPreExecutionProcessor() == null ? "NULL" : spec.getPreExecutionProcessor().getClass().getSimpleName())); + } + + /** + * Set the protocol spec. This is needed to resolve the circular dependency where + * ProtocolSpecBuilder.build() needs a ProtocolSchedule, but we need the built ProtocolSpec to + * populate the schedule. + * + * @param protocolSpec the protocol spec to set + */ + public void setProtocolSpec(final ProtocolSpec protocolSpec) { + LOG.info( + "setProtocolSpec called: preExecutionProcessor={}", + protocolSpec == null ? "NULL_SPEC" : (protocolSpec.getPreExecutionProcessor() == null ? "NULL" : protocolSpec.getPreExecutionProcessor().getClass().getSimpleName())); + this.spec = protocolSpec; + } + + @Override + public ProtocolSpec getByBlockHeader(final ProcessableBlockHeader blockHeader) { + LOG.info( + "getByBlockHeader called for block {}: returning spec with preExecutionProcessor={}", + blockHeader.getNumber(), + spec == null ? "NULL_SPEC" : (spec.getPreExecutionProcessor() == null ? "NULL" : spec.getPreExecutionProcessor().getClass().getSimpleName())); + // Always return the single spec, regardless of block number or timestamp + return spec; + } + + @Override + public Optional getNextProtocolSpec(final long currentTime) { + // No future forks in a single-spec schedule + return Optional.empty(); + } + + @Override + public Optional getLatestProtocolSpec() { + // Not needed for single-block processing + return Optional.empty(); + } + + @Override + public Optional getChainId() { + return chainId; + } + + @Override + public String listMilestones() { + // No milestones to list + return ""; + } + + @Override + public void putBlockNumberMilestone(final long blockNumber, final ProtocolSpec protocolSpec) { + // No-op - we already have our single spec + } + + @Override + public void putTimestampMilestone(final long timestamp, final ProtocolSpec protocolSpec) { + // No-op - we already have our single spec + } + + @Override + public boolean isOnMilestoneBoundary(final BlockHeader blockHeader) { + // Not relevant for single-block processing + return false; + } + + @Override + public boolean anyMatch(final Predicate predicate) { + // Not used in single-block processing + return false; + } + + @Override + public void setPermissionTransactionFilter( + final PermissionTransactionFilter permissionTransactionFilter) { + // No-op for single-block processing + } + + @Override + public void setAdditionalValidationRules( + final List additionalValidationRules) { + // No-op for single-block processing + } +} diff --git a/src/main/java/org/hyperledger/besu/riscv/poc/crypto/SECP256K1Graal.java b/src/main/java/org/hyperledger/besu/riscv/poc/crypto/SECP256K1Graal.java index 37f327c..7c7b206 100644 --- a/src/main/java/org/hyperledger/besu/riscv/poc/crypto/SECP256K1Graal.java +++ b/src/main/java/org/hyperledger/besu/riscv/poc/crypto/SECP256K1Graal.java @@ -1,17 +1,14 @@ /* - * Copyright contributors to Hyperledger Besu. + * Copyright Consensys Software Inc., 2025 * - * 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 + * 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. - * - * SPDX-License-Identifier: Apache-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 org.hyperledger.besu.riscv.poc.crypto; @@ -176,7 +173,10 @@ private boolean verifyNative( final byte[] compactSignature = signature.encodedBytes().slice(0, 64).toArrayUnsafe(); return LibSecp256k1Graal.ecdsaVerify( - LibSecp256k1Graal.getContext(), compactSignature, data.toArrayUnsafe(), pubkeyInternal) + LibSecp256k1Graal.getContext(), + compactSignature, + data.toArrayUnsafe(), + pubkeyInternal) != 0; } catch (final Exception e) { LOG.error("Native verification failed", e); diff --git a/src/main/java/org/hyperledger/besu/riscv/poc/evm/precompiles/GraalAltBN128AddPrecompiledContract.java b/src/main/java/org/hyperledger/besu/riscv/poc/evm/precompiles/GraalAltBN128AddPrecompiledContract.java index 352cfff..db23901 100644 --- a/src/main/java/org/hyperledger/besu/riscv/poc/evm/precompiles/GraalAltBN128AddPrecompiledContract.java +++ b/src/main/java/org/hyperledger/besu/riscv/poc/evm/precompiles/GraalAltBN128AddPrecompiledContract.java @@ -1,17 +1,14 @@ /* - * Copyright contributors to Hyperledger Besu. + * Copyright Consensys Software Inc., 2025 * - * 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 + * 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. - * - * SPDX-License-Identifier: Apache-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 org.hyperledger.besu.riscv.poc.evm.precompiles; diff --git a/src/main/java/org/hyperledger/besu/riscv/poc/evm/precompiles/GraalAltBN128MulPrecompiledContract.java b/src/main/java/org/hyperledger/besu/riscv/poc/evm/precompiles/GraalAltBN128MulPrecompiledContract.java index 8b691b7..16a6bbf 100644 --- a/src/main/java/org/hyperledger/besu/riscv/poc/evm/precompiles/GraalAltBN128MulPrecompiledContract.java +++ b/src/main/java/org/hyperledger/besu/riscv/poc/evm/precompiles/GraalAltBN128MulPrecompiledContract.java @@ -1,17 +1,14 @@ /* - * Copyright contributors to Hyperledger Besu. + * Copyright Consensys Software Inc., 2025 * - * 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 + * 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. - * - * SPDX-License-Identifier: Apache-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 org.hyperledger.besu.riscv.poc.evm.precompiles; diff --git a/src/main/java/org/hyperledger/besu/riscv/poc/evm/precompiles/GraalAltBN128PairingPrecompiledContract.java b/src/main/java/org/hyperledger/besu/riscv/poc/evm/precompiles/GraalAltBN128PairingPrecompiledContract.java index 05279a6..c6490d1 100644 --- a/src/main/java/org/hyperledger/besu/riscv/poc/evm/precompiles/GraalAltBN128PairingPrecompiledContract.java +++ b/src/main/java/org/hyperledger/besu/riscv/poc/evm/precompiles/GraalAltBN128PairingPrecompiledContract.java @@ -1,17 +1,14 @@ /* - * Copyright contributors to Hyperledger Besu. + * Copyright Consensys Software Inc., 2025 * - * 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 + * 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. - * - * SPDX-License-Identifier: Apache-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 org.hyperledger.besu.riscv.poc.evm.precompiles; diff --git a/src/main/java/org/hyperledger/besu/riscv/poc/evm/precompiles/GraalECRECPrecompiledContract.java b/src/main/java/org/hyperledger/besu/riscv/poc/evm/precompiles/GraalECRECPrecompiledContract.java index bd9e8a8..ff9d3e3 100644 --- a/src/main/java/org/hyperledger/besu/riscv/poc/evm/precompiles/GraalECRECPrecompiledContract.java +++ b/src/main/java/org/hyperledger/besu/riscv/poc/evm/precompiles/GraalECRECPrecompiledContract.java @@ -1,17 +1,14 @@ /* - * Copyright contributors to Hyperledger Besu. + * Copyright Consensys Software Inc., 2025 * - * 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 + * 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. - * - * SPDX-License-Identifier: Apache-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 org.hyperledger.besu.riscv.poc.evm.precompiles; @@ -62,48 +59,52 @@ public PrecompileContractResult computePrecompile( // Validate that bytes 32-62 are zero (v is in byte 63) if (!safeInput.slice(32, 31).isZero()) { + LOG.info("ECREC: bytes 32-62 are not zero, returning empty"); return PrecompileContractResult.success(Bytes.EMPTY); } try { final Bytes32 messageHash = Bytes32.wrap(safeInput, 0); final int recId = safeInput.get(63) - V_BASE; + LOG.info("ECREC: messageHash={}, recId={}", messageHash.toHexString(), recId); // Extract the 64-byte signature (r and s) final byte[] sigBytes = safeInput.slice(64, 64).toArrayUnsafe(); + LOG.info("ECREC: sigBytes length={}", sigBytes.length); + + // Get context first to see if that's where it crashes + LOG.info("ECREC: Getting context..."); + org.graalvm.word.PointerBase ctx = LibSecp256k1Graal.getContext(); + LOG.info("ECREC: Got context (isNull={})", ctx.isNull()); // Call the GraalVM-compatible native recovery function + LOG.info("ECREC: Calling ecdsaRecover..."); final byte[] recoveredPubkey = - LibSecp256k1Graal.ecdsaRecover( - LibSecp256k1Graal.getContext(), sigBytes, messageHash.toArrayUnsafe(), recId); + LibSecp256k1Graal.ecdsaRecover(ctx, sigBytes, messageHash.toArrayUnsafe(), recId); + LOG.info("ECREC: ecdsaRecover returned, pubkey={}", recoveredPubkey); if (recoveredPubkey == null) { + LOG.info("ECREC: Recovery failed, returning empty"); return PrecompileContractResult.success(Bytes.EMPTY); } - // The recovered pubkey is in internal 64-byte format - // Serialize to uncompressed format (65 bytes with 0x04 prefix) - final byte[] serialized = - LibSecp256k1Graal.ecPubkeySerialize( - LibSecp256k1Graal.getContext(), recoveredPubkey, false); - - if (serialized == null || serialized.length != 65) { - return PrecompileContractResult.success(Bytes.EMPTY); - } - - // Hash the 64-byte public key (skip the 0x04 prefix) - final Bytes32 hashed = Hash.keccak256(Bytes.wrap(serialized).slice(1)); + // The recovered pubkey is in internal 64-byte format, which is what we need + // According to the secp256k1 library, the internal representation is already the raw X,Y coordinates + // We just need to hash it directly (no serialization needed) + LOG.info("ECREC: Hashing recovered pubkey (length={})...", recoveredPubkey.length); + final Bytes32 hashed = Hash.keccak256(Bytes.wrap(recoveredPubkey)); // Return the last 20 bytes as the address (right-padded to 32 bytes) final MutableBytes32 result = MutableBytes32.create(); hashed.slice(12).copyTo(result, 12); + LOG.info("ECREC: Success, returning result={}", result.toHexString()); return PrecompileContractResult.success(result); } catch (final IllegalArgumentException e) { - LOG.debug("ECRECOVER failed with illegal argument", e); + LOG.error("ECRECOVER failed with illegal argument", e); return PrecompileContractResult.success(Bytes.EMPTY); - } catch (final Exception e) { + } catch (final Throwable e) { LOG.error("ECRECOVER failed with unexpected error", e); return PrecompileContractResult.success(Bytes.EMPTY); } From ed547258ff6a5f64209d7d82884c3b7d4b371f8f Mon Sep 17 00:00:00 2001 From: garyschulte Date: Mon, 27 Oct 2025 15:31:50 -0700 Subject: [PATCH 4/5] change GraalECRECPrecompile to use the single entry-point lib, fix some MinimalProtocolSchedule NPEs Signed-off-by: garyschulte --- build.gradle | 1 + .../execution/MinimalProtocolSchedule.java | 45 ++++++++----- .../GraalECRECPrecompiledContract.java | 65 +++++++++++-------- .../precompiles/MockPrecompiledContract.java | 60 +++++++++++++++++ 4 files changed, 129 insertions(+), 42 deletions(-) create mode 100644 src/main/java/org/hyperledger/besu/riscv/poc/evm/precompiles/MockPrecompiledContract.java diff --git a/build.gradle b/build.gradle index 835a80e..bfd8e72 100644 --- a/build.gradle +++ b/build.gradle @@ -185,6 +185,7 @@ graalvmNative { // Link against the static libraries buildArgs.add("-H:NativeLinkerOption=-lsecp256k1") + buildArgs.add("-H:NativeLinkerOption=-lsecp256k1_ecrecover") buildArgs.add("-H:NativeLinkerOption=-lgnark_jni") buildArgs.add("-H:NativeLinkerOption=-lgnark_eip_196") buildArgs.add("-H:NativeLinkerOption=-lgnark_eip_2537") diff --git a/src/main/java/org/hyperledger/besu/riscv/poc/block/execution/MinimalProtocolSchedule.java b/src/main/java/org/hyperledger/besu/riscv/poc/block/execution/MinimalProtocolSchedule.java index f389edc..0f8ae94 100644 --- a/src/main/java/org/hyperledger/besu/riscv/poc/block/execution/MinimalProtocolSchedule.java +++ b/src/main/java/org/hyperledger/besu/riscv/poc/block/execution/MinimalProtocolSchedule.java @@ -35,6 +35,7 @@ import org.hyperledger.besu.riscv.poc.evm.precompiles.GraalAltBN128MulPrecompiledContract; import org.hyperledger.besu.riscv.poc.evm.precompiles.GraalAltBN128PairingPrecompiledContract; import org.hyperledger.besu.riscv.poc.evm.precompiles.GraalECRECPrecompiledContract; +import org.hyperledger.besu.riscv.poc.evm.precompiles.MockPrecompiledContract; import java.math.BigInteger; import java.util.Optional; @@ -107,7 +108,7 @@ public static ProtocolSchedule create( MinimalProtocolSchedule::createGraalPrecompileRegistry); builder.badBlocksManager(badBlockManager); - // Ensure the appropriate pre-execution processor is set for this fork + // set the appropriate pre-execution processor // The fork definitions SHOULD set this, but we explicitly set it as a safety measure // to prevent NullPointerException in AbstractBlockProcessor.processBlock ensurePreExecutionProcessor(builder, forkInfo.name()); @@ -123,12 +124,6 @@ public static ProtocolSchedule create( // Build the protocol spec, passing the schedule (which will be populated below) final ProtocolSpec spec = builder.build(protocolSchedule); - // Verify preExecutionProcessor is set (should never be null after ensurePreExecutionProcessor) - if (spec.getPreExecutionProcessor() == null) { - throw new IllegalStateException("PreExecutionProcessor is null for fork: " + forkInfo.name()); - } - LOG.info("PreExecutionProcessor successfully set: {}", spec.getPreExecutionProcessor().getClass().getSimpleName()); - // Now set the spec in the schedule to resolve the circular dependency protocolSchedule.setProtocolSpec(spec); @@ -144,7 +139,6 @@ public static ProtocolSchedule create( private static void ensurePreExecutionProcessor( final ProtocolSpecBuilder builder, final String forkName) { switch (forkName) { - case "Cancun" -> builder.preExecutionProcessor(new CancunPreExecutionProcessor()); case "Prague", "Osaka" -> builder.preExecutionProcessor(new PraguePreExecutionProcessor()); default -> builder.preExecutionProcessor(new FrontierPreExecutionProcessor()); } @@ -181,14 +175,35 @@ private static PrecompileContractRegistry createGraalPrecompileRegistry( registry.put( Address.ALTBN128_PAIRING, new GraalAltBN128PairingPrecompiledContract(gasCalculator)); - // Add Istanbul precompiles - // TODO: Add BLAKE2BF if needed (pure Java, needs reflection or package relocation) - // registry.put(Address.BLAKE2B_F_COMPRESSION, ...); + // Add Byzantium MODEXP as mock (pure Java, but needs package access workaround) + registry.put(Address.MODEXP, new MockPrecompiledContract("MODEXP", gasCalculator)); + + // Add Istanbul BLAKE2BF as mock + registry.put( + Address.BLAKE2B_F_COMPRESSION, + new MockPrecompiledContract("BLAKE2B_F_COMPRESSION", gasCalculator)); + + // Add Cancun KZG_POINT_EVAL as mock (EIP-4844) + registry.put( + Address.KZG_POINT_EVAL, new MockPrecompiledContract("KZG_POINT_EVAL", gasCalculator)); - // Note: For post-Istanbul forks (Prague+), additional precompiles may be needed: - // - BLS12 precompiles require LibGnarkEIP2537Graal - // - P256Verify requires BoringSSL - // These are not included as they're not needed for Paris-Cancun era blocks + // Add Prague BLS12-381 precompiles as mocks (EIP-2537) + registry.put(Address.BLS12_G1ADD, new MockPrecompiledContract("BLS12_G1ADD", gasCalculator)); + registry.put( + Address.BLS12_G1MULTIEXP, + new MockPrecompiledContract("BLS12_G1MULTIEXP", gasCalculator)); + registry.put(Address.BLS12_G2ADD, new MockPrecompiledContract("BLS12_G2ADD", gasCalculator)); + registry.put( + Address.BLS12_G2MULTIEXP, + new MockPrecompiledContract("BLS12_G2MULTIEXP", gasCalculator)); + registry.put( + Address.BLS12_PAIRING, new MockPrecompiledContract("BLS12_PAIRING", gasCalculator)); + registry.put( + Address.BLS12_MAP_FP_TO_G1, + new MockPrecompiledContract("BLS12_MAP_FP_TO_G1", gasCalculator)); + registry.put( + Address.BLS12_MAP_FP2_TO_G2, + new MockPrecompiledContract("BLS12_MAP_FP2_TO_G2", gasCalculator)); return registry; } diff --git a/src/main/java/org/hyperledger/besu/riscv/poc/evm/precompiles/GraalECRECPrecompiledContract.java b/src/main/java/org/hyperledger/besu/riscv/poc/evm/precompiles/GraalECRECPrecompiledContract.java index ff9d3e3..bbd6e2e 100644 --- a/src/main/java/org/hyperledger/besu/riscv/poc/evm/precompiles/GraalECRECPrecompiledContract.java +++ b/src/main/java/org/hyperledger/besu/riscv/poc/evm/precompiles/GraalECRECPrecompiledContract.java @@ -16,7 +16,7 @@ import org.hyperledger.besu.evm.frame.MessageFrame; import org.hyperledger.besu.evm.gascalculator.GasCalculator; import org.hyperledger.besu.evm.precompile.ECRECPrecompiledContract; -import org.hyperledger.besu.nativelib.secp256k1.LibSecp256k1Graal; +import org.hyperledger.besu.nativelib.secp256k1.LibSecp256k1EcrecoverGraal; import org.apache.tuweni.bytes.Bytes; import org.apache.tuweni.bytes.Bytes32; @@ -28,9 +28,8 @@ /** * GraalVM native implementation of the ECRECOVER precompiled contract. * - *

This implementation extends the standard ECRECPrecompiledContract and overrides the compute - * method to use LibSecp256k1Graal for native execution compatible with GraalVM native image - * compilation. + *

This implementation uses LibSecp256k1EcrecoverGraal which provides the same interface as the + * JNI version but is compatible with GraalVM native image compilation. */ public class GraalECRECPrecompiledContract extends ECRECPrecompiledContract { @@ -50,55 +49,67 @@ public GraalECRECPrecompiledContract(final GasCalculator gasCalculator) { public PrecompileContractResult computePrecompile( final Bytes input, final MessageFrame messageFrame) { - LOG.info("-----------------------------------"); - LOG.info("USING GraalECRECPrecompiledContract"); - LOG.info("-----------------------------------"); + LOG.debug("==========================================="); + LOG.debug("ECREC CALLED"); + LOG.debug(" Transaction: {}", messageFrame.getOriginatorAddress()); + LOG.debug(" Recipient: {}", messageFrame.getRecipientAddress()); + LOG.debug(" Contract: {}", messageFrame.getContractAddress()); + LOG.debug("==========================================="); + final int size = input.size(); final Bytes safeInput = size >= 128 ? input : Bytes.wrap(input, MutableBytes.create(128 - size)); // Validate that bytes 32-62 are zero (v is in byte 63) if (!safeInput.slice(32, 31).isZero()) { - LOG.info("ECREC: bytes 32-62 are not zero, returning empty"); + LOG.debug("ECREC: bytes 32-62 are not zero, returning empty"); return PrecompileContractResult.success(Bytes.EMPTY); } try { final Bytes32 messageHash = Bytes32.wrap(safeInput, 0); final int recId = safeInput.get(63) - V_BASE; - LOG.info("ECREC: messageHash={}, recId={}", messageHash.toHexString(), recId); - - // Extract the 64-byte signature (r and s) final byte[] sigBytes = safeInput.slice(64, 64).toArrayUnsafe(); - LOG.info("ECREC: sigBytes length={}", sigBytes.length); - // Get context first to see if that's where it crashes - LOG.info("ECREC: Getting context..."); - org.graalvm.word.PointerBase ctx = LibSecp256k1Graal.getContext(); - LOG.info("ECREC: Got context (isNull={})", ctx.isNull()); + LOG.atDebug() + .setMessage("ECREC: messageHash={}, recId={}, sigBytes length={}") + .addArgument(messageHash::toHexString) + .addArgument(recId) + .addArgument(() -> sigBytes.length) + .log(); - // Call the GraalVM-compatible native recovery function - LOG.info("ECREC: Calling ecdsaRecover..."); + // Call LibSecp256k1EcrecoverGraal which returns a 65-byte uncompressed public key + // This matches the JNI version exactly final byte[] recoveredPubkey = - LibSecp256k1Graal.ecdsaRecover(ctx, sigBytes, messageHash.toArrayUnsafe(), recId); - LOG.info("ECREC: ecdsaRecover returned, pubkey={}", recoveredPubkey); + LibSecp256k1EcrecoverGraal.secp256k1EcrecoverWithAlloc( + messageHash.toArrayUnsafe(), sigBytes, recId); if (recoveredPubkey == null) { - LOG.info("ECREC: Recovery failed, returning empty"); + LOG.debug("ECREC: Recovery failed, returning empty"); return PrecompileContractResult.success(Bytes.EMPTY); } - // The recovered pubkey is in internal 64-byte format, which is what we need - // According to the secp256k1 library, the internal representation is already the raw X,Y coordinates - // We just need to hash it directly (no serialization needed) - LOG.info("ECREC: Hashing recovered pubkey (length={})...", recoveredPubkey.length); - final Bytes32 hashed = Hash.keccak256(Bytes.wrap(recoveredPubkey)); + LOG.atDebug() + .setMessage("ECREC: Recovered 65-byte pubkey, first byte: 0x{}") + .addArgument(() -> String.format("%02x", recoveredPubkey[0])) + .log(); + + // Strip the 0x04 prefix to get the 64-byte public key (X || Y coordinates) + // Then hash it to derive the Ethereum address + // final byte[] publicKey = new byte[64]; + // System.arraycopy(recoveredPubkey, 1, publicKey, 0, 64); + // final Bytes32 hashed = Hash.keccak256(Bytes.wrap(publicKey)); + + final Bytes32 hashed = Hash.keccak256(Bytes.wrap(recoveredPubkey).slice(1)); // Return the last 20 bytes as the address (right-padded to 32 bytes) final MutableBytes32 result = MutableBytes32.create(); hashed.slice(12).copyTo(result, 12); - LOG.info("ECREC: Success, returning result={}", result.toHexString()); + LOG.atDebug() + .setMessage("ECREC: Success, returning result={}") + .addArgument(result::toHexString) + .log(); return PrecompileContractResult.success(result); } catch (final IllegalArgumentException e) { diff --git a/src/main/java/org/hyperledger/besu/riscv/poc/evm/precompiles/MockPrecompiledContract.java b/src/main/java/org/hyperledger/besu/riscv/poc/evm/precompiles/MockPrecompiledContract.java new file mode 100644 index 0000000..218c8ed --- /dev/null +++ b/src/main/java/org/hyperledger/besu/riscv/poc/evm/precompiles/MockPrecompiledContract.java @@ -0,0 +1,60 @@ +/* + * Copyright Consensys Software Inc., 2025 + * + * 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 org.hyperledger.besu.riscv.poc.evm.precompiles; + +import org.hyperledger.besu.evm.frame.MessageFrame; +import org.hyperledger.besu.evm.gascalculator.GasCalculator; +import org.hyperledger.besu.evm.precompile.AbstractPrecompiledContract; + +import org.apache.tuweni.bytes.Bytes; + +/** + * Mock precompiled contract that logs when called and throws UnsupportedOperationException. + * + *

This is used to identify which precompiles are actually being invoked during block processing + * without implementing the full precompile logic. + */ +public class MockPrecompiledContract extends AbstractPrecompiledContract { + + private final String name; + + /** + * Instantiates a new Mock precompiled contract. + * + * @param name the name of the precompile for logging + * @param gasCalculator the gas calculator + */ + public MockPrecompiledContract(final String name, final GasCalculator gasCalculator) { + super(name, gasCalculator); + this.name = name; + } + + @Override + public long gasRequirement(final Bytes input) { + // Return a minimal gas requirement to avoid gas estimation failures + return 100L; + } + + @Override + public PrecompileContractResult computePrecompile( + final Bytes input, final MessageFrame messageFrame) { + System.out.println("==========================================="); + System.out.println("MOCK PRECOMPILE CALLED: " + name); + System.out.println("Input length: " + input.size()); + System.out.println("Input (hex): " + input.toHexString()); + System.out.println("==========================================="); + + throw new UnsupportedOperationException( + "Precompile " + name + " is not implemented (mock only)"); + } +} From c03008b81c4a9bccd6e5ec0983b7dd47d40670b5 Mon Sep 17 00:00:00 2001 From: garyschulte Date: Tue, 28 Oct 2025 12:05:25 -0700 Subject: [PATCH 5/5] add mainnet vs minimal protocol schedule implementations for comparison Signed-off-by: garyschulte --- .../poc/block/execution/BlockRunner.java | 72 +++++++++++++------ 1 file changed, 49 insertions(+), 23 deletions(-) diff --git a/src/main/java/org/hyperledger/besu/riscv/poc/block/execution/BlockRunner.java b/src/main/java/org/hyperledger/besu/riscv/poc/block/execution/BlockRunner.java index e8bfacd..2cdb8ea 100644 --- a/src/main/java/org/hyperledger/besu/riscv/poc/block/execution/BlockRunner.java +++ b/src/main/java/org/hyperledger/besu/riscv/poc/block/execution/BlockRunner.java @@ -29,8 +29,10 @@ import org.hyperledger.besu.ethereum.core.Block; import org.hyperledger.besu.ethereum.core.BlockHeader; import org.hyperledger.besu.ethereum.core.Difficulty; +import org.hyperledger.besu.ethereum.core.MiningConfiguration; import org.hyperledger.besu.ethereum.mainnet.HeaderValidationMode; import org.hyperledger.besu.ethereum.mainnet.MainnetBlockHeaderFunctions; +import org.hyperledger.besu.ethereum.mainnet.MainnetProtocolSchedule; import org.hyperledger.besu.ethereum.mainnet.ProtocolSchedule; import org.hyperledger.besu.ethereum.mainnet.ProtocolSpec; import org.hyperledger.besu.ethereum.rlp.RLP; @@ -82,11 +84,21 @@ private record CommandLineArgs( Optional blockRlpPath, Optional genesisConfigPath) {} - /** - * Factory method that builds a complete in-memory Besu execution environment. It imports previous - * headers, reconstructs the world state from a witness, and returns a {@link BlockRunner} ready - * to process a block. - */ + private static final NoOpMetricsSystem noOpMetricsSystem = new NoOpMetricsSystem(); + + // Configure the EVM with in-memory (stacked) world updater mode. + private static final EvmConfiguration evmConfiguration = + new EvmConfiguration( + EvmConfiguration.DEFAULT.jumpDestCacheWeightKB(), + EvmConfiguration.WorldUpdaterMode.STACKED, + true); + /* + + /** + * Factory method that builds a complete in-memory Besu execution environment. It imports previous + * headers, reconstructs the world state from a witness, and returns a {@link BlockRunner} ready + * to process a block. + */ public static BlockRunner create( final BlockHeader targetBlockHeader, final List prevHeaders, @@ -96,25 +108,12 @@ public static BlockRunner create( final GenesisConfig genesisConfig = GenesisConfig.fromConfig(genesisConfigJson); - final NoOpMetricsSystem noOpMetricsSystem = new NoOpMetricsSystem(); + // use a minimal protocol schedule: + final ProtocolSchedule protocolSchedule = constructMinimalConfig(genesisConfig, targetBlockHeader); - // Configure the EVM with in-memory (stacked) world updater mode. - final EvmConfiguration evmConfiguration = - new EvmConfiguration( - EvmConfiguration.DEFAULT.jumpDestCacheWeightKB(), - EvmConfiguration.WorldUpdaterMode.STACKED, - true); - - // Build a minimal protocol schedule with only the necessary fork spec and Graal precompiles. - // This avoids the overhead of building all fork specs from Frontier through the latest, - // and prevents loading JNA-based native libraries that will be replaced. - final ProtocolSchedule protocolSchedule = - MinimalProtocolSchedule.create( - targetBlockHeader, - genesisConfig, - evmConfiguration, - new BadBlockManager(), - noOpMetricsSystem); + //TODO: possibly make minimal vs mainnet protocol schedule configurable +// // or use the mainnet-derived protocol schedule +// final ProtocolSchedule protocolSchedule = constructFromGenesisConfig(genesisConfig); // Construct the genesis state and world state root. final GenesisState genesisState = @@ -342,6 +341,33 @@ public void processBlock(final Block block) { System.out.println(" Stateroot: " + result.getYield().get().getWorldState().rootHash()); } + private static ProtocolSchedule constructMinimalConfig( + GenesisConfig genesisConfig, + BlockHeader targetBlockHeader) { + return MinimalProtocolSchedule.create( + targetBlockHeader, + genesisConfig, + evmConfiguration, + new BadBlockManager(), + noOpMetricsSystem); + + } + + private static ProtocolSchedule constructFromGenesisConfig( + final GenesisConfig genesisConfig) { + // Build the mainnet protocol schedule based on the genesis config. + return + MainnetProtocolSchedule.fromConfig( + genesisConfig.getConfigOptions(), + evmConfiguration, + MiningConfiguration.MINING_DISABLED, + new BadBlockManager(), + false, + false, + noOpMetricsSystem); + + + } /** Print usage information and exit. */ private static void printUsageAndExit() { System.out.println("Usage: BlockRunner [OPTIONS]");