diff --git a/Makefile b/Makefile index 41502906ee..92f6d9e32d 100644 --- a/Makefile +++ b/Makefile @@ -14,8 +14,15 @@ clean-testnet-folders: clean-environment: docker compose -f docker/compose-tracing-v2-ci-extension.yml -f docker/compose-tracing-v2-staterecovery-extension.yml --profile l1 --profile l2 --profile debug --profile staterecovery kill -s 9 || true; docker compose -f docker/compose-tracing-v2-ci-extension.yml -f docker/compose-tracing-v2-staterecovery-extension.yml --profile l1 --profile l2 --profile debug --profile staterecovery down || true; + # Ensure RLN stacks are also torn down (rpc/sequencer + prover/karma) + docker compose -f docker/compose-spec-l2-services-rln.yml --profile l2 --profile l2-bc --profile debug --profile external-to-monorepo --profile rln kill -s 9 || true; + docker compose -f docker/compose-spec-l2-services-rln.yml --profile l2 --profile l2-bc --profile debug --profile external-to-monorepo --profile rln down || true; + # Include tracing-v2 RLN extension if used + docker compose -f docker/compose-tracing-v2-rln.yml --profile l1 --profile l2 --profile debug --profile rln kill -s 9 || true; + docker compose -f docker/compose-tracing-v2-rln.yml --profile l1 --profile l2 --profile debug --profile rln down || true; make clean-local-folders; - docker volume rm linea-local-dev linea-logs || true; # ignore failure if volumes do not exist already + # Remove volumes from both default and RLN stacks (ignore if absent) + docker volume rm linea-local-dev linea-logs docker_local-dev local-dev rln-data logs || true; # ignore failure if volumes do not exist already docker system prune -f || true; start-env: COMPOSE_PROFILES:=l1,l2 diff --git a/README.md b/README.md index b7b242aa04..58d4150b92 100644 --- a/README.md +++ b/README.md @@ -53,6 +53,14 @@ The Status Network RLN validator system can be configured using various CLI opti - **`--plugin-linea-rln-karma-service`**: Karma service endpoint in `host:port` format (default: `localhost:50052`) - **`--plugin-linea-rln-deny-list-path`**: Path to the gasless deny list file (default: `/var/lib/besu/gasless-deny-list.txt`) +### Troubleshooting +- If a 0-gas (gasless) transaction is accepted by the RPC but not included, check the sequencer logs for RLN proof cache timeouts or epoch mismatches and ensure `--plugin-linea-rln-epoch-mode=TEST` is used in local demos. +- If paid transactions fail for “min gas price” or “upfront cost” locally, ensure L2 genesis has `baseFeePerGas=0` and sequencer `min-gas-price=0` in config. +- Premium gas transactions (gas > configured threshold) bypass RLN validation by design. + +### CI +- GitHub Actions runs sequencer unit tests (Java 21) with Gradle caching. JNI-dependent RLN native tests are excluded from CI. + ## How to contribute Contributions are welcome! diff --git a/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/sequencer/txpoolvalidation/LineaTransactionPoolValidatorFactory.java b/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/sequencer/txpoolvalidation/LineaTransactionPoolValidatorFactory.java index 8b5f2815f2..0a57442baf 100644 --- a/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/sequencer/txpoolvalidation/LineaTransactionPoolValidatorFactory.java +++ b/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/sequencer/txpoolvalidation/LineaTransactionPoolValidatorFactory.java @@ -101,7 +101,11 @@ public PluginTransactionPoolValidator createTransactionValidator() { new RlnProverForwarderValidator( rlnValidatorConf, true, // enabled - sharedServiceManager.getKarmaServiceClient())); + sharedServiceManager.getKarmaServiceClient(), + transactionSimulationService, + blockchainService, + tracerConfiguration, + l1L2BridgeConfiguration)); } // Conditionally add RLN Validator (for proof verification) diff --git a/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/sequencer/txpoolvalidation/validators/RlnProverForwarderValidator.java b/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/sequencer/txpoolvalidation/validators/RlnProverForwarderValidator.java index d36e40935d..2f70bf8db2 100644 --- a/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/sequencer/txpoolvalidation/validators/RlnProverForwarderValidator.java +++ b/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/sequencer/txpoolvalidation/validators/RlnProverForwarderValidator.java @@ -18,13 +18,22 @@ import com.google.protobuf.ByteString; import io.grpc.ManagedChannel; import io.grpc.ManagedChannelBuilder; +import java.math.BigInteger; import java.io.Closeable; import java.io.IOException; import java.util.Optional; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import net.consensys.linea.config.LineaRlnValidatorConfiguration; +import net.consensys.linea.config.LineaTracerConfiguration; +import net.consensys.linea.plugins.config.LineaL1L2BridgeSharedConfiguration; import net.consensys.linea.sequencer.txpoolvalidation.shared.KarmaServiceClient; +import net.consensys.linea.zktracer.LineCountingTracer; +import net.consensys.linea.zktracer.ZkCounter; +import net.consensys.linea.zktracer.ZkTracer; +import org.hyperledger.besu.plugin.data.ProcessableBlockHeader; +import org.hyperledger.besu.plugin.services.BlockchainService; +import org.hyperledger.besu.plugin.services.TransactionSimulationService; import net.consensys.linea.sequencer.txpoolvalidation.shared.KarmaServiceClient.KarmaInfo; import net.vac.prover.Address; import net.vac.prover.RlnProverGrpc; @@ -91,6 +100,12 @@ public class RlnProverForwarderValidator implements PluginTransactionPoolValidat // Karma service for gasless validation private final KarmaServiceClient karmaServiceClient; + // Simulation dependencies for estimating gas used + private final TransactionSimulationService transactionSimulationService; + private final BlockchainService blockchainService; + private final LineaTracerConfiguration tracerConfiguration; + private final LineaL1L2BridgeSharedConfiguration l1L2BridgeConfiguration; + /** * Creates a new RLN Prover Forwarder Validator with default gRPC channel management. * @@ -98,11 +113,32 @@ public class RlnProverForwarderValidator implements PluginTransactionPoolValidat * @param enabled Whether the validator is enabled (should be false in sequencer mode) * @param karmaServiceClient Service for checking karma eligibility for gasless transactions */ + public RlnProverForwarderValidator( + LineaRlnValidatorConfiguration rlnConfig, + boolean enabled, + KarmaServiceClient karmaServiceClient, + TransactionSimulationService transactionSimulationService, + BlockchainService blockchainService, + LineaTracerConfiguration tracerConfiguration, + LineaL1L2BridgeSharedConfiguration l1L2BridgeSharedConfiguration) { + this(rlnConfig, + enabled, + karmaServiceClient, + transactionSimulationService, + blockchainService, + tracerConfiguration, + l1L2BridgeSharedConfiguration, + null); + } + + /** + * Backward-compatible constructor used by existing tests. New dependencies default to null. + */ public RlnProverForwarderValidator( LineaRlnValidatorConfiguration rlnConfig, boolean enabled, KarmaServiceClient karmaServiceClient) { - this(rlnConfig, enabled, karmaServiceClient, null); + this(rlnConfig, enabled, karmaServiceClient, null, null, null, null, null); } /** @@ -113,7 +149,7 @@ public RlnProverForwarderValidator( * @param enabled Whether the validator is enabled (should be false in sequencer mode) */ public RlnProverForwarderValidator(LineaRlnValidatorConfiguration rlnConfig, boolean enabled) { - this(rlnConfig, enabled, null, null); + this(rlnConfig, enabled, null, null, null, null, null, null); } /** @@ -132,10 +168,18 @@ public RlnProverForwarderValidator(LineaRlnValidatorConfiguration rlnConfig, boo LineaRlnValidatorConfiguration rlnConfig, boolean enabled, KarmaServiceClient karmaServiceClient, + TransactionSimulationService transactionSimulationService, + BlockchainService blockchainService, + LineaTracerConfiguration tracerConfiguration, + LineaL1L2BridgeSharedConfiguration l1L2BridgeSharedConfiguration, ManagedChannel providedChannel) { this.rlnConfig = rlnConfig; this.enabled = enabled; this.karmaServiceClient = karmaServiceClient; + this.transactionSimulationService = transactionSimulationService; + this.blockchainService = blockchainService; + this.tracerConfiguration = tracerConfiguration; + this.l1L2BridgeConfiguration = l1L2BridgeSharedConfiguration; if (enabled) { if (providedChannel != null) { @@ -317,6 +361,15 @@ public Optional validateTransaction( .setValue(ByteString.copyFrom(chainId.toByteArray())) .build())); + // Provide an estimated gas units value. As an initial implementation, + // simulate execution to estimate gas used when possible; fallback to tx gas limit. + long estimatedGasUsed = estimateGasUsed(transaction); + LOG.debug( + "Estimated gas used for tx {}: {}", + transaction.getHash().toHexString(), + estimatedGasUsed); + requestBuilder.setEstimatedGasUsed(estimatedGasUsed); + SendTransactionRequest request = requestBuilder.build(); LOG.debug( @@ -347,6 +400,49 @@ public Optional validateTransaction( } } + private LineCountingTracer createLineCountingTracer( + final ProcessableBlockHeader pendingBlockHeader, final BigInteger chainId) { + var lineCountingTracer = + tracerConfiguration != null && tracerConfiguration.isLimitless() + ? new ZkCounter(l1L2BridgeConfiguration) + : new ZkTracer(net.consensys.linea.zktracer.Fork.LONDON, l1L2BridgeConfiguration, chainId); + lineCountingTracer.traceStartConflation(1L); + lineCountingTracer.traceStartBlock(pendingBlockHeader, pendingBlockHeader.getCoinbase()); + return lineCountingTracer; + } + + private long estimateGasUsed(final Transaction transaction) { + try { + // Fast-path: simple ETH transfer with empty calldata + if (transaction.getTo().isPresent() + && transaction.getPayload().isEmpty() + && transaction.getValue().getAsBigInteger().signum() > 0) { + return 21_000L; + } + + if (transactionSimulationService == null || blockchainService == null) { + return transaction.getGasLimit(); + } + + final var pendingBlockHeader = transactionSimulationService.simulatePendingBlockHeader(); + final var chainId = blockchainService.getChainId().orElse(BigInteger.ZERO); + final var tracer = createLineCountingTracer(pendingBlockHeader, chainId); + final var maybeSimulationResults = + transactionSimulationService.simulate( + transaction, java.util.Optional.empty(), pendingBlockHeader, tracer, false, true); + + if (maybeSimulationResults.isPresent()) { + final var sim = maybeSimulationResults.get(); + if (sim.isSuccessful()) { + return sim.result().getEstimateGasUsedByTransaction(); + } + } + } catch (final Exception ignored) { + // fall through to fallback below + } + return transaction.getGasLimit(); + } + /** * Closes the gRPC channel and cleans up resources. * diff --git a/besu-plugins/linea-sequencer/sequencer/src/main/proto/rln_proof_service.proto b/besu-plugins/linea-sequencer/sequencer/src/main/proto/rln_proof_service.proto index 961652ecfa..91d731be72 100644 --- a/besu-plugins/linea-sequencer/sequencer/src/main/proto/rln_proof_service.proto +++ b/besu-plugins/linea-sequencer/sequencer/src/main/proto/rln_proof_service.proto @@ -135,6 +135,9 @@ message SendTransactionRequest { optional Address sender = 2; optional U256 chainId = 3; bytes transactionHash = 4 [(max_size) = 32]; + // Estimated gas units the transaction is expected to consume (best-effort) + // Backward-compatible addition; receivers may ignore if unsupported. + uint64 estimated_gas_used = 5; } message SendTransactionReply { diff --git a/besu-plugins/linea-sequencer/sequencer/src/test/java/net/consensys/linea/sequencer/txpoolvalidation/validators/RlnProverForwarderValidatorEstimatedGasTest.java b/besu-plugins/linea-sequencer/sequencer/src/test/java/net/consensys/linea/sequencer/txpoolvalidation/validators/RlnProverForwarderValidatorEstimatedGasTest.java new file mode 100644 index 0000000000..dfc50a2f0b --- /dev/null +++ b/besu-plugins/linea-sequencer/sequencer/src/test/java/net/consensys/linea/sequencer/txpoolvalidation/validators/RlnProverForwarderValidatorEstimatedGasTest.java @@ -0,0 +1,117 @@ +/* + * Copyright Consensys Software Inc. + * + * This file is dual-licensed under either the MIT license or Apache License 2.0. + * See the LICENSE-MIT and LICENSE-APACHE files in the repository root for details. + * + * SPDX-License-Identifier: MIT OR Apache-2.0 + */ +package net.consensys.linea.sequencer.txpoolvalidation.validators; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.apache.tuweni.bytes.Bytes; +import io.grpc.ManagedChannel; +import io.grpc.inprocess.InProcessChannelBuilder; +import io.grpc.inprocess.InProcessServerBuilder; +import io.grpc.stub.StreamObserver; +import java.util.Optional; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import net.vac.prover.RlnProverGrpc; +import net.vac.prover.SendTransactionReply; +import net.vac.prover.SendTransactionRequest; +import org.hyperledger.besu.datatypes.Address; +import org.hyperledger.besu.datatypes.Transaction; +import org.hyperledger.besu.datatypes.Wei; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class RlnProverForwarderValidatorEstimatedGasTest { + + private io.grpc.Server server; + private ManagedChannel channel; + + private volatile SendTransactionRequest capturedRequest; + + @BeforeEach + void setUp() throws Exception { + final String serverName = InProcessServerBuilder.generateName(); + + server = + InProcessServerBuilder.forName(serverName) + .directExecutor() + .addService( + new RlnProverGrpc.RlnProverImplBase() { + @Override + public void sendTransaction( + SendTransactionRequest request, StreamObserver resp) { + capturedRequest = request; + resp.onNext(SendTransactionReply.newBuilder().setResult(true).build()); + resp.onCompleted(); + } + }) + .build() + .start(); + + channel = InProcessChannelBuilder.forName(serverName).directExecutor().build(); + } + + @AfterEach + void tearDown() { + if (channel != null) { + channel.shutdownNow(); + } + if (server != null) { + server.shutdownNow(); + } + } + + @Test + void forwardsEstimatedGasUsed_21000_forSimpleEthTransfer() throws Exception { + final var validator = + new RlnProverForwarderValidator( + /* rlnConfig */ null, + /* enabled */ true, + /* karmaServiceClient */ null, + /* txSim */ null, + /* blockchain */ null, + /* tracerConfig */ null, + /* l1l2 */ null, + /* providedChannel */ channel); + + // Create a simple ETH transfer: to set, empty data, value > 0 + final org.hyperledger.besu.crypto.SECPSignature fakeSig = + org.hyperledger.besu.crypto.SECPSignature.create( + new java.math.BigInteger("1"), + new java.math.BigInteger("2"), + (byte) 0, + new java.math.BigInteger("3")); + + final org.hyperledger.besu.ethereum.core.Transaction tx = + org.hyperledger.besu.ethereum.core.Transaction.builder() + .sender(Address.fromHexString("0x2222222222222222222222222222222222222222")) + .to(Address.fromHexString("0x1111111111111111111111111111111111111111")) + .gasLimit(21_000) + .gasPrice(Wei.of(1)) + .payload(Bytes.EMPTY) + .value(Wei.of(1)) + .signature(fakeSig) + .build(); + + final CountDownLatch latch = new CountDownLatch(1); + // validateTransaction performs a blocking gRPC call; just invoke and then assert capture + final var maybeError = + validator.validateTransaction( + (org.hyperledger.besu.datatypes.Transaction) tx, /* isLocal */ true, /* hasPriority */ false); + assertThat(maybeError).isEmpty(); + latch.countDown(); + latch.await(100, TimeUnit.MILLISECONDS); + + assertThat(capturedRequest).isNotNull(); + assertThat(capturedRequest.getEstimatedGasUsed()).isEqualTo(21_000L); + } +} + + diff --git a/contracts/local-deployments-artifacts/L1RollupAddress.txt b/contracts/local-deployments-artifacts/L1RollupAddress.txt index 23f8b49d0f..556e13490a 100644 --- a/contracts/local-deployments-artifacts/L1RollupAddress.txt +++ b/contracts/local-deployments-artifacts/L1RollupAddress.txt @@ -1 +1 @@ -0xB7f8BC63BbcaD18155201308C8f3540b07f84F5e \ No newline at end of file +0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9 \ No newline at end of file diff --git a/contracts/local-deployments-artifacts/L2MessageServiceAddress.txt b/contracts/local-deployments-artifacts/L2MessageServiceAddress.txt index d50cd7addb..6f33b54674 100644 --- a/contracts/local-deployments-artifacts/L2MessageServiceAddress.txt +++ b/contracts/local-deployments-artifacts/L2MessageServiceAddress.txt @@ -1 +1 @@ -0x6A461f1BE039c0588A519Ef45C338dD2b388C703 \ No newline at end of file +0x5767aB2Ed64666bFE27e52D4675EDd60Ec7D6EDF \ No newline at end of file diff --git a/docker/compose-spec-l2-services-rln.yml b/docker/compose-spec-l2-services-rln.yml index 065566990e..e8505aa797 100644 --- a/docker/compose-spec-l2-services-rln.yml +++ b/docker/compose-spec-l2-services-rln.yml @@ -7,12 +7,13 @@ services: rln-prover: hostname: rln-prover container_name: rln-prover - image: status-rln-prover:20250914165712 + image: status-rln-prover:20250922172416 profiles: [ "l2", "l2-bc", "debug", "external-to-monorepo", "rln" ] ports: - "50051:50051" # RLN proof service - "50052:50052" # Karma service (optional, can be same port) restart: unless-stopped + user: root environment: SERVICE_IP: "0.0.0.0" SERVICE_PORT: "50051" @@ -27,8 +28,24 @@ services: volumes: - local-dev:/app/data - ./config/rln-prover/mock_users.json:/app/mock_users.json:ro + command: + - --no-config + - --ip + - 0.0.0.0 + - --port + - "50051" + - --metrics-ip + - 0.0.0.0 + - --metrics-port + - "30031" + - --db + - /app/data/prover-db + - --mock-sc + - "true" + - --mock-user + - /app/mock_users.json healthcheck: - test: ["CMD", "sh", "-c", "ps aux | grep status_rln_prover | grep -v grep"] + test: ["CMD", "sh", "-c", "ps aux | grep prover_cli | grep -v grep"] interval: 30s timeout: 10s retries: 3 @@ -42,11 +59,12 @@ services: karma-service: hostname: karma-service container_name: karma-service - image: status-rln-prover:20250914165712 + image: status-rln-prover:20250922172416 profiles: [ "l2", "l2-bc", "debug", "external-to-monorepo", "rln" ] ports: - "50053:50052" restart: unless-stopped + user: root environment: SERVICE_IP: "0.0.0.0" SERVICE_PORT: "50052" @@ -56,8 +74,24 @@ services: volumes: - local-dev:/app/data - ./config/rln-prover/mock_users.json:/app/mock_users.json:ro + command: + - --no-config + - --ip + - 0.0.0.0 + - --port + - "50052" + - --metrics-ip + - 0.0.0.0 + - --metrics-port + - "30032" + - --db + - /app/data/karma-db + - --mock-sc + - "true" + - --mock-user + - /app/mock_users.json healthcheck: - test: ["CMD", "sh", "-c", "ps aux | grep status_rln_prover | grep -v grep"] + test: ["CMD", "sh", "-c", "ps aux | grep prover_cli | grep -v grep"] interval: 30s timeout: 10s retries: 3 @@ -70,7 +104,7 @@ services: sequencer: hostname: sequencer container_name: sequencer - image: linea-besu-minimal-rln:20250914165712 + image: linea-besu-minimal-rln:20250922172416 profiles: [ "l2", "l2-bc", "debug", "external-to-monorepo" ] ports: - "8545:8545" @@ -181,7 +215,7 @@ services: l2-node-besu: hostname: l2-node-besu container_name: l2-node-besu - image: linea-besu-minimal-rln:20250914165712 + image: linea-besu-minimal-rln:20250922172416 profiles: [ "l2", "l2-bc", "debug", "external-to-monorepo" ] depends_on: sequencer: diff --git a/docker/config/l2-node-besu/log4j.xml b/docker/config/l2-node-besu/log4j.xml index 68a77a02ce..93383ffd9c 100644 --- a/docker/config/l2-node-besu/log4j.xml +++ b/docker/config/l2-node-besu/log4j.xml @@ -28,6 +28,9 @@ + + +