diff --git a/build.gradle b/build.gradle index 206a18e..bfd8e72 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 { @@ -184,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/BlockRunner.java b/src/main/java/org/hyperledger/besu/riscv/poc/block/execution/BlockRunner.java index a52fae0..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 @@ -17,6 +17,8 @@ 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.Hash; import org.hyperledger.besu.ethereum.BlockProcessingResult; import org.hyperledger.besu.ethereum.ProtocolContext; @@ -48,9 +50,11 @@ 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.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; @@ -80,12 +84,23 @@ 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, final Map trieNodes, final Map codes, @@ -93,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 the mainnet protocol schedule based on the genesis config. - final ProtocolSchedule protocolSchedule = - MainnetProtocolSchedule.fromConfig( - genesisConfig.getConfigOptions(), - evmConfiguration, - MiningConfiguration.MINING_DISABLED, - new BadBlockManager(), - false, - false, - 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 = @@ -234,6 +236,8 @@ public Optional getService(Class serviceType) { .withServiceManager(serviceManager) .build(); + // The MinimalProtocolSchedule already created the correct precompile registry + // with Graal-native implementations, so no additional decoration is needed. return new BlockRunner(protocolSchedule, protocolContext, blockchain); } @@ -337,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]"); @@ -402,7 +433,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 +446,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(); @@ -481,7 +518,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..0f8ae94 --- /dev/null +++ b/src/main/java/org/hyperledger/besu/riscv/poc/block/execution/MinimalProtocolSchedule.java @@ -0,0 +1,271 @@ +/* + * 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 org.hyperledger.besu.riscv.poc.evm.precompiles.MockPrecompiledContract; + +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); + + // 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()); + + 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); + + // 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 "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 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)); + + // 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; + } + + /** + * 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 new file mode 100644 index 0000000..7c7b206 --- /dev/null +++ b/src/main/java/org/hyperledger/besu/riscv/poc/crypto/SECP256K1Graal.java @@ -0,0 +1,219 @@ +/* + * 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.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/GraalAltBN128AddPrecompiledContract.java b/src/main/java/org/hyperledger/besu/riscv/poc/evm/precompiles/GraalAltBN128AddPrecompiledContract.java new file mode 100644 index 0000000..db23901 --- /dev/null +++ b/src/main/java/org/hyperledger/besu/riscv/poc/evm/precompiles/GraalAltBN128AddPrecompiledContract.java @@ -0,0 +1,100 @@ +/* + * 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 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..16a6bbf --- /dev/null +++ b/src/main/java/org/hyperledger/besu/riscv/poc/evm/precompiles/GraalAltBN128MulPrecompiledContract.java @@ -0,0 +1,107 @@ +/* + * 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 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..c6490d1 --- /dev/null +++ b/src/main/java/org/hyperledger/besu/riscv/poc/evm/precompiles/GraalAltBN128PairingPrecompiledContract.java @@ -0,0 +1,113 @@ +/* + * 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 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)); + } + } + } +} 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..bbd6e2e --- /dev/null +++ b/src/main/java/org/hyperledger/besu/riscv/poc/evm/precompiles/GraalECRECPrecompiledContract.java @@ -0,0 +1,123 @@ +/* + * 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.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.LibSecp256k1EcrecoverGraal; + +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 uses LibSecp256k1EcrecoverGraal which provides the same interface as the + * JNI version but is 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.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.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; + final byte[] sigBytes = safeInput.slice(64, 64).toArrayUnsafe(); + + LOG.atDebug() + .setMessage("ECREC: messageHash={}, recId={}, sigBytes length={}") + .addArgument(messageHash::toHexString) + .addArgument(recId) + .addArgument(() -> sigBytes.length) + .log(); + + // Call LibSecp256k1EcrecoverGraal which returns a 65-byte uncompressed public key + // This matches the JNI version exactly + final byte[] recoveredPubkey = + LibSecp256k1EcrecoverGraal.secp256k1EcrecoverWithAlloc( + messageHash.toArrayUnsafe(), sigBytes, recId); + + if (recoveredPubkey == null) { + LOG.debug("ECREC: Recovery failed, returning empty"); + return PrecompileContractResult.success(Bytes.EMPTY); + } + + 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.atDebug() + .setMessage("ECREC: Success, returning result={}") + .addArgument(result::toHexString) + .log(); + return PrecompileContractResult.success(result); + + } catch (final IllegalArgumentException e) { + LOG.error("ECRECOVER failed with illegal argument", e); + return PrecompileContractResult.success(Bytes.EMPTY); + } catch (final Throwable e) { + LOG.error("ECRECOVER failed with unexpected error", e); + return PrecompileContractResult.success(Bytes.EMPTY); + } + } +} 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)"); + } +}