Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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!
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -91,18 +100,45 @@ 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.
*
* @param rlnConfig Configuration for RLN validation including prover service endpoint
* @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);
}

/**
Expand All @@ -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);
}

/**
Expand All @@ -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) {
Expand Down Expand Up @@ -317,6 +361,15 @@ public Optional<String> 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(
Expand Down Expand Up @@ -347,6 +400,49 @@ public Optional<String> 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.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
@@ -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<SendTransactionReply> 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);
}
}


2 changes: 1 addition & 1 deletion contracts/local-deployments-artifacts/L1RollupAddress.txt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0xB7f8BC63BbcaD18155201308C8f3540b07f84F5e
0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0x6A461f1BE039c0588A519Ef45C338dD2b388C703
0x5767aB2Ed64666bFE27e52D4675EDd60Ec7D6EDF
Loading
Loading