Add gRPC transaction validation support to send local incoming transaction data to the rln prover by extending transaction pool validator besu plugin

This commit is contained in:
nadeemb53
2025-06-12 01:12:01 +05:30
parent 09cbed1142
commit 7f5e2f1513
10 changed files with 1329 additions and 1 deletions

View File

@@ -61,6 +61,8 @@ public class TransactionPoolOptions implements CLIOptions<TransactionPoolConfigu
"--strict-tx-replay-protection-enabled";
private static final String TX_POOL_PRIORITY_SENDERS = "--tx-pool-priority-senders";
private static final String TX_POOL_MIN_GAS_PRICE = "--tx-pool-min-gas-price";
private static final String TX_POOL_GRPC_VALIDATION_ENDPOINT =
"--tx-pool-grpc-validation-endpoint";
private TransactionPoolValidatorService transactionPoolValidatorService;
@@ -149,6 +151,15 @@ public class TransactionPoolOptions implements CLIOptions<TransactionPoolConfigu
arity = "1")
private Wei minGasPrice = TransactionPoolConfiguration.DEFAULT_TX_POOL_MIN_GAS_PRICE;
@CommandLine.Option(
names = {TX_POOL_GRPC_VALIDATION_ENDPOINT},
paramLabel = "<host:port>",
description =
"Enable gRPC validation for local transactions by specifying validation service endpoint (e.g., localhost:9090). "
+ "If not specified, gRPC validation is disabled.",
arity = "1")
private String grpcValidationEndpoint;
@CommandLine.ArgGroup(
validate = false,
heading = "@|bold Tx Pool Layered Implementation Options|@%n")
@@ -298,6 +309,34 @@ public class TransactionPoolOptions implements CLIOptions<TransactionPoolConfigu
public void setPluginTransactionValidatorService(
final TransactionPoolValidatorService transactionPoolValidatorService) {
this.transactionPoolValidatorService = transactionPoolValidatorService;
// Register gRPC validator if endpoint is configured
if (grpcValidationEndpoint != null && !grpcValidationEndpoint.trim().isEmpty()) {
try {
// Try to dynamically load our gRPC validator factory
Class<?> factoryClass =
Class.forName(
"org.hyperledger.besu.ethereum.eth.transactions.grpc.GrpcTransactionPoolValidatorFactory");
Object factory =
factoryClass.getConstructor(String.class).newInstance(grpcValidationEndpoint);
// Register the factory with the service
transactionPoolValidatorService
.getClass()
.getMethod(
"registerPluginTransactionValidatorFactory",
Class.forName(
"org.hyperledger.besu.plugin.services.txvalidator.PluginTransactionPoolValidatorFactory"))
.invoke(transactionPoolValidatorService, factory);
// Use reflection to call the method to avoid compile-time dependency
} catch (Exception e) {
// Log warning if gRPC validator cannot be loaded (graceful degradation)
System.err.println(
"Warning: Could not load gRPC transaction validator, proceeding without gRPC validation: "
+ e.getMessage());
}
}
}
/**

View File

@@ -217,6 +217,7 @@ tx-pool-max-size=1234
tx-pool-limit-by-account-percentage=0.017
tx-pool-min-gas-price=1000
tx-pool-min-score=100
tx-pool-grpc-validation-endpoint="localhost:8080"
# Revert Reason
revert-reason-enabled=false

View File

@@ -13,7 +13,10 @@
* SPDX-License-Identifier: Apache-2.0
*/
apply plugin: 'java-library'
plugins {
id 'java-library'
id 'com.google.protobuf' version '0.9.4'
}
jar {
archiveBaseName = 'besu-eth'
@@ -62,7 +65,13 @@ dependencies {
implementation 'org.rocksdb:rocksdbjni'
implementation 'com.github.ben-manes.caffeine:caffeine'
implementation 'com.google.dagger:dagger'
implementation 'io.grpc:grpc-core'
implementation 'io.grpc:grpc-stub'
implementation 'io.grpc:grpc-protobuf'
implementation 'io.grpc:grpc-netty'
implementation 'com.google.protobuf:protobuf-java-util:3.25.1'
annotationProcessor 'com.google.dagger:dagger-compiler'
implementation 'javax.annotation:javax.annotation-api:1.3.2'
annotationProcessor "org.immutables:value"
implementation "org.immutables:value-annotations"
@@ -85,6 +94,8 @@ dependencies {
testImplementation 'org.mockito:mockito-core'
testImplementation 'org.mockito:mockito-junit-jupiter'
testImplementation 'org.openjdk.jol:jol-core'
testImplementation 'io.grpc:grpc-inprocess:1.60.0'
testImplementation 'io.grpc:grpc-stub:1.60.0'
testSupportImplementation 'org.mockito:mockito-core'
testSupportImplementation project(':testutil')
@@ -96,7 +107,41 @@ dependencies {
jmhImplementation project(':plugins:rocksdb')
}
javadoc {
enabled = false
}
artifacts {
testArtifacts testJar
testSupportArtifacts testSupportJar
}
protobuf {
protoc {
artifact = "com.google.protobuf:protoc:3.25.1"
}
plugins {
grpc {
artifact = 'io.grpc:protoc-gen-grpc-java:1.60.0'
}
}
generateProtoTasks {
all().each { task ->
task.plugins {
grpc {}
}
task.builtins {
java {}
}
}
}
}
sourceSets {
main {
java {
srcDirs 'build/generated/source/proto/main/grpc'
srcDirs 'build/generated/source/proto/main/java'
}
}
}

View File

@@ -0,0 +1,175 @@
/*
* 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.ethereum.eth.transactions.grpc;
import org.hyperledger.besu.datatypes.Transaction;
import org.hyperledger.besu.plugin.services.txvalidator.PluginTransactionPoolValidator;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import com.google.protobuf.ByteString;
import io.grpc.ManagedChannel;
import io.grpc.ManagedChannelBuilder;
import net.vac.prover.Address;
import net.vac.prover.RlnProverGrpc;
import net.vac.prover.SendTransactionReply;
import net.vac.prover.SendTransactionRequest;
import net.vac.prover.U256;
import net.vac.prover.Wei;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* gRPC-based transaction pool validator that sends local transactions to an external RLN prover
* service for validation.
*
* <p>This validator demonstrates: - Only validates local transactions (isLocal=true) - Falls back
* to default validation for peer transactions - Gracefully handles gRPC failures by falling back to
* default validation - Logs all transaction validation attempts for verification
*/
public class GrpcTransactionPoolValidator implements PluginTransactionPoolValidator {
private static final Logger LOG = LoggerFactory.getLogger(GrpcTransactionPoolValidator.class);
private final String endpoint;
private int validationCallCount = 0;
private int localTransactionCount = 0;
private int peerTransactionCount = 0;
private final ManagedChannel channel;
private final RlnProverGrpc.RlnProverBlockingStub blockingStub;
/**
* Creates a new gRPC transaction pool validator.
*
* @param endpoint the gRPC service endpoint (host:port)
*/
public GrpcTransactionPoolValidator(final String endpoint) {
this.endpoint = endpoint;
this.channel = createChannel(endpoint);
this.blockingStub = RlnProverGrpc.newBlockingStub(channel);
LOG.info("*** gRPC Transaction Pool Validator INITIALIZED for endpoint: {} ***", endpoint);
Runtime.getRuntime().addShutdownHook(new Thread(this::close));
}
/** Creates a gRPC channel. Protected to allow overriding in tests. */
protected ManagedChannel createChannel(final String endpoint) {
return ManagedChannelBuilder.forTarget(endpoint).usePlaintext().build();
}
@Override
public Optional<String> validateTransaction(
final Transaction transaction, final boolean isLocal, final boolean hasPriority) {
validationCallCount++;
LOG.info("*** TRANSACTION VALIDATION #{} ***", validationCallCount);
LOG.info("Transaction Hash: {}", transaction.getHash().toHexString());
LOG.info("Transaction Sender: {}", transaction.getSender().toHexString());
LOG.info(
"Transaction Gas Price: {}", transaction.getGasPrice().map(Object::toString).orElse("N/A"));
LOG.info("Transaction Value: {}", transaction.getValue().toHexString());
LOG.info("Is Local: {}", isLocal);
LOG.info("Has Priority: {}", hasPriority);
LOG.info("gRPC Endpoint: {}", endpoint);
// Only validate local transactions via gRPC
if (!isLocal) {
peerTransactionCount++;
LOG.debug("Skipping gRPC validation for peer transaction");
return Optional.empty(); // Accept peer transactions without gRPC validation
}
localTransactionCount++;
LOG.debug("Processing local transaction via gRPC: {}", transaction.getHash());
try {
SendTransactionRequest.Builder requestBuilder = SendTransactionRequest.newBuilder();
transaction
.getGasPrice()
.ifPresent(
gasPrice ->
requestBuilder.setGasPrice(
Wei.newBuilder()
.setValue(ByteString.copyFrom(gasPrice.getAsBigInteger().toByteArray()))
.build()));
requestBuilder.setSender(
Address.newBuilder()
.setValue(ByteString.copyFrom(transaction.getSender().toArrayUnsafe()))
.build());
transaction
.getChainId()
.ifPresent(
chainId ->
requestBuilder.setChainId(
U256.newBuilder()
.setValue(ByteString.copyFrom(chainId.toByteArray()))
.build()));
requestBuilder.setTransactionHash(ByteString.copyFrom(transaction.getHash().toArrayUnsafe()));
SendTransactionRequest request = requestBuilder.build();
LOG.debug("Sending transaction to RLN prover: {}", request);
SendTransactionReply reply = blockingStub.sendTransaction(request);
if (reply.getResult()) {
LOG.debug("RLN prover accepted transaction {}", transaction.getHash());
return Optional.empty(); // Transaction is valid
} else {
LOG.warn("RLN prover rejected transaction {}", transaction.getHash());
return Optional.of("RLN prover rejected transaction");
}
} catch (final Exception e) {
LOG.warn(
"gRPC validation failed for transaction {}, falling back to default validation: {}",
transaction.getHash(),
e.getMessage());
// Graceful fallback: accept the transaction if gRPC fails
return Optional.empty();
}
}
/** Close the gRPC channel. */
public void close() {
try {
channel.shutdown().awaitTermination(5, TimeUnit.SECONDS);
} catch (InterruptedException e) {
LOG.warn("Interrupted while shutting down gRPC channel", e);
Thread.currentThread().interrupt();
}
}
/** Get statistics for testing/verification. */
public int getValidationCallCount() {
return validationCallCount;
}
public int getLocalTransactionCount() {
return localTransactionCount;
}
public int getPeerTransactionCount() {
return peerTransactionCount;
}
public String getEndpoint() {
return endpoint;
}
}

View File

@@ -0,0 +1,58 @@
/*
* 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.ethereum.eth.transactions.grpc;
import org.hyperledger.besu.plugin.services.txvalidator.PluginTransactionPoolValidator;
import org.hyperledger.besu.plugin.services.txvalidator.PluginTransactionPoolValidatorFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/** Factory for creating gRPC transaction pool validators. */
public class GrpcTransactionPoolValidatorFactory implements PluginTransactionPoolValidatorFactory {
private static final Logger LOG =
LoggerFactory.getLogger(GrpcTransactionPoolValidatorFactory.class);
private final String endpoint;
private volatile GrpcTransactionPoolValidator validator;
/**
* Creates a new factory.
*
* @param endpoint the gRPC endpoint in format "host:port"
*/
public GrpcTransactionPoolValidatorFactory(final String endpoint) {
this.endpoint = endpoint;
}
@Override
public PluginTransactionPoolValidator createTransactionValidator() {
if (endpoint != null && !endpoint.trim().isEmpty()) {
if (validator == null) {
synchronized (this) {
if (validator == null) {
LOG.info("Creating gRPC transaction pool validator for endpoint: {}", endpoint);
validator = new GrpcTransactionPoolValidator(endpoint);
}
}
}
return validator;
} else {
LOG.debug("No gRPC endpoint configured, using default validator");
return PluginTransactionPoolValidator.VALIDATE_ALL;
}
}
}

View File

@@ -0,0 +1,120 @@
syntax = "proto3";
package prover;
import "google/protobuf/descriptor.proto";
option java_package = "net.vac.prover";
option java_outer_classname = "OuterSample";
option java_multiple_files = true;
service RlnProver {
rpc SendTransaction (SendTransactionRequest) returns (SendTransactionReply);
rpc RegisterUser (RegisterUserRequest) returns (RegisterUserReply);
// Server side streaming RPC: 1 request -> X responses (stream)
rpc GetProofs(RlnProofFilter) returns (stream RlnProofReply);
rpc GetUserTierInfo(GetUserTierInfoRequest) returns (GetUserTierInfoReply);
rpc SetTierLimits(SetTierLimitsRequest) returns (SetTierLimitsReply);
}
extend google.protobuf.FieldOptions {
optional uint32 max_size = 50000;
}
message Wei {
bytes value = 1 [(max_size) = 32];
}
message U256 {
bytes value = 1 [(max_size) = 32];
}
message Address {
bytes value = 1 [(max_size) = 20];
}
message SendTransactionRequest {
optional Wei gasPrice = 1;
optional Address sender = 2;
optional U256 chainId = 3;
bytes transactionHash = 4 [(max_size) = 32];
}
message SendTransactionReply {
bool result = 1;
}
message RlnProofFilter {
optional string address = 1;
}
message RlnProofReply {
oneof resp {
// variant for success
RlnProof proof = 1;
// variant for error
RlnProofError error = 2;
}
}
message RlnProof {
bytes sender = 1;
bytes tx_hash = 2;
bytes proof = 3;
}
message RlnProofError {
string error = 2;
}
message RegisterUserRequest {
Address user = 14;
}
enum RegistrationStatus {
Success = 0;
Failure = 1;
AlreadyRegistered = 2;
}
message RegisterUserReply {
RegistrationStatus status = 1;
}
message GetUserTierInfoRequest {
Address user = 1;
}
message GetUserTierInfoReply {
oneof resp {
UserTierInfoResult res = 1;
UserTierInfoError error = 2;
}
}
message UserTierInfoError {
string message = 1;
}
message UserTierInfoResult {
sint64 current_epoch = 1;
sint64 current_epoch_slice = 2;
uint64 tx_count = 3;
optional Tier tier = 4;
}
message Tier {
string name = 1;
uint64 quota = 2;
}
message SetTierLimitsRequest {
repeated U256 karmaAmounts = 1;
repeated Tier tiers = 2;
}
message SetTierLimitsReply {
bool status = 1;
string error = 2;
}

View File

@@ -0,0 +1,207 @@
/*
* 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.ethereum.eth.transactions.grpc;
import static org.assertj.core.api.Assertions.assertThat;
import org.hyperledger.besu.plugin.services.txvalidator.PluginTransactionPoolValidator;
import org.junit.jupiter.api.Test;
/**
* Tests for GrpcTransactionPoolValidatorFactory that focus on the factory logic: - Validator
* creation based on endpoint configuration - Singleton behavior and caching - Fallback to default
* validator - Edge cases and error handling
*/
class GrpcTransactionPoolValidatorFactoryTest {
@Test
void shouldReturnGrpcValidatorWhenValidEndpointProvided() {
// Given: factory with valid endpoint
String endpoint = "localhost:9090";
GrpcTransactionPoolValidatorFactory factory = new GrpcTransactionPoolValidatorFactory(endpoint);
// When: creating validator
PluginTransactionPoolValidator validator = factory.createTransactionValidator();
// Then: should return gRPC validator instance
assertThat(validator).isInstanceOf(GrpcTransactionPoolValidator.class);
GrpcTransactionPoolValidator grpcValidator = (GrpcTransactionPoolValidator) validator;
assertThat(grpcValidator.getEndpoint()).isEqualTo(endpoint);
// Cleanup
grpcValidator.close();
}
@Test
void shouldReturnDefaultValidatorWhenEndpointEmpty() {
// Given: factory with empty endpoint
GrpcTransactionPoolValidatorFactory factory = new GrpcTransactionPoolValidatorFactory("");
// When: creating validator
PluginTransactionPoolValidator validator = factory.createTransactionValidator();
// Then: should return default validator
assertThat(validator).isEqualTo(PluginTransactionPoolValidator.VALIDATE_ALL);
}
@Test
void shouldReturnDefaultValidatorWhenEndpointNull() {
// Given: factory with null endpoint
GrpcTransactionPoolValidatorFactory factory = new GrpcTransactionPoolValidatorFactory(null);
// When: creating validator
PluginTransactionPoolValidator validator = factory.createTransactionValidator();
// Then: should return default validator
assertThat(validator).isEqualTo(PluginTransactionPoolValidator.VALIDATE_ALL);
}
@Test
void shouldReturnDefaultValidatorWhenEndpointWhitespace() {
// Given: factory with whitespace-only endpoint
GrpcTransactionPoolValidatorFactory factory = new GrpcTransactionPoolValidatorFactory(" ");
// When: creating validator
PluginTransactionPoolValidator validator = factory.createTransactionValidator();
// Then: should return default validator (whitespace should be treated as empty)
assertThat(validator).isEqualTo(PluginTransactionPoolValidator.VALIDATE_ALL);
}
@Test
void shouldCacheValidatorInstance() {
// Given: factory with valid endpoint
String endpoint = "localhost:9091";
GrpcTransactionPoolValidatorFactory factory = new GrpcTransactionPoolValidatorFactory(endpoint);
// When: creating multiple validators
PluginTransactionPoolValidator validator1 = factory.createTransactionValidator();
PluginTransactionPoolValidator validator2 = factory.createTransactionValidator();
PluginTransactionPoolValidator validator3 = factory.createTransactionValidator();
// Then: should return same instance (singleton behavior)
assertThat(validator1).isInstanceOf(GrpcTransactionPoolValidator.class);
assertThat(validator1).isSameAs(validator2);
assertThat(validator2).isSameAs(validator3);
// Cleanup
((GrpcTransactionPoolValidator) validator1).close();
}
@Test
void shouldHandleVariousEndpointFormats() {
// Test different valid endpoint formats
String[] validEndpoints = {
"localhost:8080", "127.0.0.1:9090", "grpc-service.example.com:443", "10.0.0.1:50051"
};
for (String endpoint : validEndpoints) {
// Given: factory with endpoint in specific format
GrpcTransactionPoolValidatorFactory factory =
new GrpcTransactionPoolValidatorFactory(endpoint);
// When: creating validator
PluginTransactionPoolValidator validator = factory.createTransactionValidator();
// Then: should create gRPC validator
assertThat(validator).isInstanceOf(GrpcTransactionPoolValidator.class);
GrpcTransactionPoolValidator grpcValidator = (GrpcTransactionPoolValidator) validator;
assertThat(grpcValidator.getEndpoint()).isEqualTo(endpoint);
// Cleanup
grpcValidator.close();
}
}
@Test
void shouldNotCacheDefaultValidator() {
// Given: factory without endpoint (returns default validator)
GrpcTransactionPoolValidatorFactory factory = new GrpcTransactionPoolValidatorFactory(null);
// When: creating multiple validators
PluginTransactionPoolValidator validator1 = factory.createTransactionValidator();
PluginTransactionPoolValidator validator2 = factory.createTransactionValidator();
// Then: should return same default validator instance (but not cached by factory)
assertThat(validator1).isEqualTo(PluginTransactionPoolValidator.VALIDATE_ALL);
assertThat(validator2).isEqualTo(PluginTransactionPoolValidator.VALIDATE_ALL);
assertThat(validator1).isSameAs(validator2); // Same static instance
}
@Test
void shouldCreateDifferentValidatorsForDifferentEndpoints() {
// Given: factories with different endpoints
GrpcTransactionPoolValidatorFactory factory1 =
new GrpcTransactionPoolValidatorFactory("localhost:8080");
GrpcTransactionPoolValidatorFactory factory2 =
new GrpcTransactionPoolValidatorFactory("localhost:9090");
// When: creating validators
PluginTransactionPoolValidator validator1 = factory1.createTransactionValidator();
PluginTransactionPoolValidator validator2 = factory2.createTransactionValidator();
// Then: should create different instances
assertThat(validator1).isInstanceOf(GrpcTransactionPoolValidator.class);
assertThat(validator2).isInstanceOf(GrpcTransactionPoolValidator.class);
assertThat(validator1).isNotSameAs(validator2);
GrpcTransactionPoolValidator grpcValidator1 = (GrpcTransactionPoolValidator) validator1;
GrpcTransactionPoolValidator grpcValidator2 = (GrpcTransactionPoolValidator) validator2;
assertThat(grpcValidator1.getEndpoint()).isEqualTo("localhost:8080");
assertThat(grpcValidator2.getEndpoint()).isEqualTo("localhost:9090");
// Cleanup
grpcValidator1.close();
grpcValidator2.close();
}
@Test
void shouldHandleNormalizedEndpoints() {
// Given: factory with endpoint that could be normalized
String endpoint = " localhost:8080 "; // with whitespace
GrpcTransactionPoolValidatorFactory factory = new GrpcTransactionPoolValidatorFactory(endpoint);
// When: creating validator
PluginTransactionPoolValidator validator = factory.createTransactionValidator();
// Then: should still create gRPC validator (implementation may or may not trim)
if (validator instanceof GrpcTransactionPoolValidator) {
GrpcTransactionPoolValidator grpcValidator = (GrpcTransactionPoolValidator) validator;
assertThat(grpcValidator.getEndpoint()).isEqualTo(endpoint); // Stores as-is
grpcValidator.close();
} else {
// If implementation trims whitespace and treats as empty, default validator is returned
assertThat(validator).isEqualTo(PluginTransactionPoolValidator.VALIDATE_ALL);
}
}
@Test
void shouldProvideDescriptiveToString() {
// Given: factory with endpoint
String endpoint = "test-service:8080";
GrpcTransactionPoolValidatorFactory factory = new GrpcTransactionPoolValidatorFactory(endpoint);
// When: getting string representation
String factoryString = factory.toString();
// Then: should be descriptive (if implemented)
assertThat(factoryString).isNotNull();
// Note: Actual toString implementation depends on the class implementation
}
}

View File

@@ -0,0 +1,253 @@
/*
* 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.ethereum.eth.transactions.grpc;
import static org.assertj.core.api.Assertions.assertThat;
import org.hyperledger.besu.crypto.SignatureAlgorithm;
import org.hyperledger.besu.crypto.SignatureAlgorithmFactory;
import org.hyperledger.besu.datatypes.Address;
import org.hyperledger.besu.datatypes.Transaction;
import org.hyperledger.besu.datatypes.Wei;
import org.hyperledger.besu.ethereum.core.TransactionTestFixture;
import java.math.BigInteger;
import java.util.Optional;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
/**
* Tests for GrpcTransactionPoolValidator that focus on the actual implementation logic: -
* Transaction processing behavior (local vs peer) - Error handling and fallback mechanisms -
* Statistics tracking - Transaction data extraction and formatting
*/
class GrpcTransactionPoolValidatorTest {
private static final SignatureAlgorithm SIGNATURE_ALGORITHM =
SignatureAlgorithmFactory.getInstance();
private GrpcTransactionPoolValidator validator;
@AfterEach
void tearDown() {
if (validator != null) {
validator.close();
}
}
@Test
void shouldSkipPeerTransactionsCompletely() {
// Given: validator with invalid endpoint to ensure no real gRPC calls
validator = new GrpcTransactionPoolValidator("invalid-endpoint:9999");
Transaction peerTransaction = createTestTransaction();
// When: processing peer transaction
Optional<String> result = validator.validateTransaction(peerTransaction, false, false);
// Then: should accept without any gRPC processing
assertThat(result).isEmpty(); // Accepted
assertThat(validator.getPeerTransactionCount()).isEqualTo(1);
assertThat(validator.getLocalTransactionCount()).isEqualTo(0);
assertThat(validator.getValidationCallCount()).isEqualTo(1);
}
@Test
void shouldProcessLocalTransactionsViaGrpc() {
// Given: validator with invalid endpoint to test fallback behavior
validator = new GrpcTransactionPoolValidator("invalid-endpoint:9999");
Transaction localTransaction = createTestTransaction();
// When: processing local transaction
Optional<String> result = validator.validateTransaction(localTransaction, true, false);
// Then: should attempt gRPC call and fallback to acceptance
assertThat(result).isEmpty(); // Accepted via fallback
assertThat(validator.getLocalTransactionCount()).isEqualTo(1);
assertThat(validator.getPeerTransactionCount()).isEqualTo(0);
assertThat(validator.getValidationCallCount()).isEqualTo(1);
}
@Test
void shouldFallBackGracefullyWhenGrpcServiceUnavailable() {
// Given: validator pointing to unavailable service
validator = new GrpcTransactionPoolValidator("localhost:99999");
Transaction localTransaction = createTestTransaction();
// When: attempting to validate local transaction
Optional<String> result = validator.validateTransaction(localTransaction, true, false);
// Then: should fall back to accepting the transaction
assertThat(result).isEmpty(); // Graceful fallback accepts transaction
assertThat(validator.getLocalTransactionCount()).isEqualTo(1);
}
@Test
void shouldTrackStatisticsAccurately() {
// Given: validator for testing statistics
validator = new GrpcTransactionPoolValidator("test-endpoint:8080");
Transaction transaction = createTestTransaction();
// When: processing mix of local and peer transactions
validator.validateTransaction(transaction, true, false); // local #1
validator.validateTransaction(transaction, false, false); // peer #1
validator.validateTransaction(transaction, true, false); // local #2
validator.validateTransaction(transaction, false, false); // peer #2
validator.validateTransaction(transaction, false, false); // peer #3
validator.validateTransaction(transaction, true, false); // local #3
// Then: statistics should be accurate
assertThat(validator.getValidationCallCount()).isEqualTo(6);
assertThat(validator.getLocalTransactionCount()).isEqualTo(3);
assertThat(validator.getPeerTransactionCount()).isEqualTo(3);
}
@Test
void shouldHandleTransactionsWithVariousGasPrices() {
// Given: validator for testing different transaction types
validator = new GrpcTransactionPoolValidator("test-endpoint:8080");
// When: processing transactions with different gas prices
Transaction lowGasTransaction = createTransactionWithGasPrice(Wei.of(1_000_000_000L));
Transaction highGasTransaction = createTransactionWithGasPrice(Wei.of(100_000_000_000L));
Transaction zeroGasTransaction = createTransactionWithGasPrice(Wei.ZERO);
Optional<String> result1 = validator.validateTransaction(lowGasTransaction, true, false);
Optional<String> result2 = validator.validateTransaction(highGasTransaction, true, false);
Optional<String> result3 = validator.validateTransaction(zeroGasTransaction, true, false);
// Then: all should be processed (fallback accepts all)
assertThat(result1).isEmpty();
assertThat(result2).isEmpty();
assertThat(result3).isEmpty();
assertThat(validator.getLocalTransactionCount()).isEqualTo(3);
}
@Test
void shouldHandleTransactionsWithDifferentChainIds() {
// Given: validator for testing different chain IDs
validator = new GrpcTransactionPoolValidator("test-endpoint:8080");
// When: processing transactions with different chain IDs
Transaction mainnetTx = createTransactionWithChainId(BigInteger.valueOf(1));
Transaction testnetTx = createTransactionWithChainId(BigInteger.valueOf(3));
Transaction customTx = createTransactionWithChainId(BigInteger.valueOf(1337));
Optional<String> result1 = validator.validateTransaction(mainnetTx, true, false);
Optional<String> result2 = validator.validateTransaction(testnetTx, true, false);
Optional<String> result3 = validator.validateTransaction(customTx, true, false);
// Then: all should be processed
assertThat(result1).isEmpty();
assertThat(result2).isEmpty();
assertThat(result3).isEmpty();
assertThat(validator.getLocalTransactionCount()).isEqualTo(3);
}
@Test
void shouldMaintainEndpointConfiguration() {
// Given: validator with specific endpoint
String testEndpoint = "grpc-service.example.com:9090";
validator = new GrpcTransactionPoolValidator(testEndpoint);
// When/Then: endpoint should be stored correctly
assertThat(validator.getEndpoint()).isEqualTo(testEndpoint);
}
@Test
void shouldHandlePriorityTransactionFlag() {
// Given: validator for testing priority handling
validator = new GrpcTransactionPoolValidator("test-endpoint:8080");
Transaction transaction = createTestTransaction();
// When: processing transactions with different priority flags
Optional<String> normalResult = validator.validateTransaction(transaction, true, false);
Optional<String> priorityResult = validator.validateTransaction(transaction, true, true);
// Then: both should be processed (priority flag passed to implementation)
assertThat(normalResult).isEmpty();
assertThat(priorityResult).isEmpty();
assertThat(validator.getLocalTransactionCount()).isEqualTo(2);
}
@Test
void shouldHandleTransactionsWithComplexData() {
// Given: validator for testing complex transaction data
validator = new GrpcTransactionPoolValidator("test-endpoint:8080");
// When: processing transaction with complex data
Transaction complexTransaction =
new TransactionTestFixture()
.gasPrice(Wei.of(50_000_000_000L))
.gasLimit(21000)
.value(Wei.fromEth(1))
.chainId(Optional.of(BigInteger.valueOf(1337)))
.to(Optional.of(Address.fromHexString("0x742d35cc6634c0532925a3b8d039135682b2e78b")))
.createTransaction(SIGNATURE_ALGORITHM.generateKeyPair());
Optional<String> result = validator.validateTransaction(complexTransaction, true, false);
// Then: should process successfully
assertThat(result).isEmpty(); // Fallback accepts
assertThat(validator.getLocalTransactionCount()).isEqualTo(1);
// Verify transaction has expected properties
assertThat(complexTransaction.getGasPrice()).isPresent();
assertThat(complexTransaction.getChainId()).isPresent();
assertThat(complexTransaction.getSender()).isNotNull();
assertThat(complexTransaction.getHash()).isNotNull();
}
@Test
void shouldResetStatisticsProperly() {
// Given: validator with some processed transactions
validator = new GrpcTransactionPoolValidator("test-endpoint:8080");
Transaction transaction = createTestTransaction();
// Process some transactions
validator.validateTransaction(transaction, true, false);
validator.validateTransaction(transaction, false, false);
// When: creating new validator (simulates reset)
validator.close();
validator = new GrpcTransactionPoolValidator("test-endpoint:8080");
// Then: statistics should start fresh
assertThat(validator.getValidationCallCount()).isEqualTo(0);
assertThat(validator.getLocalTransactionCount()).isEqualTo(0);
assertThat(validator.getPeerTransactionCount()).isEqualTo(0);
}
private Transaction createTestTransaction() {
return new TransactionTestFixture()
.gasPrice(Wei.of(10_000_000_000L))
.chainId(Optional.of(BigInteger.valueOf(1)))
.createTransaction(SIGNATURE_ALGORITHM.generateKeyPair());
}
private Transaction createTransactionWithGasPrice(final Wei gasPrice) {
return new TransactionTestFixture()
.gasPrice(gasPrice)
.chainId(Optional.of(BigInteger.valueOf(1)))
.createTransaction(SIGNATURE_ALGORITHM.generateKeyPair());
}
private Transaction createTransactionWithChainId(final BigInteger chainId) {
return new TransactionTestFixture()
.gasPrice(Wei.of(10_000_000_000L))
.chainId(Optional.of(chainId))
.createTransaction(SIGNATURE_ALGORITHM.generateKeyPair());
}
}

View File

@@ -0,0 +1,286 @@
/*
* 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.ethereum.eth.transactions.grpc;
import static org.assertj.core.api.Assertions.assertThat;
import org.hyperledger.besu.crypto.SignatureAlgorithm;
import org.hyperledger.besu.crypto.SignatureAlgorithmFactory;
import org.hyperledger.besu.datatypes.Address;
import org.hyperledger.besu.datatypes.Transaction;
import org.hyperledger.besu.datatypes.Wei;
import org.hyperledger.besu.ethereum.core.TransactionTestFixture;
import org.hyperledger.besu.plugin.services.txvalidator.PluginTransactionPoolValidator;
import org.hyperledger.besu.plugin.services.txvalidator.PluginTransactionPoolValidatorFactory;
import java.math.BigInteger;
import java.util.Optional;
import org.junit.jupiter.api.Test;
/**
* Comprehensive test that validates the actual behavior of the gRPC transaction validator
* implementation without relying on external gRPC services or complex mocking.
*
* <p>Tests focus on: - Core validation logic (local vs peer transaction handling) - Error handling
* and fallback mechanisms - Transaction data processing and statistics - Integration with Besu's
* plugin system
*/
class TransactionValidatorBehaviorTest {
private static final SignatureAlgorithm SIGNATURE_ALGORITHM =
SignatureAlgorithmFactory.getInstance();
@Test
void shouldDemonstrateCompleteValidationFlow() {
// GIVEN: Create a gRPC validator factory (this is how Besu creates validators)
String endpoint = "grpc-validator-service:8080";
PluginTransactionPoolValidatorFactory factory =
new GrpcTransactionPoolValidatorFactory(endpoint);
// WHEN: Besu creates a validator using the factory
PluginTransactionPoolValidator validator = factory.createTransactionValidator();
// THEN: Should get our gRPC validator
assertThat(validator).isInstanceOf(GrpcTransactionPoolValidator.class);
GrpcTransactionPoolValidator grpcValidator = (GrpcTransactionPoolValidator) validator;
assertThat(grpcValidator.getEndpoint()).isEqualTo(endpoint);
// Cleanup
grpcValidator.close();
}
@Test
void shouldProcessTransactionsAccordingToBusinessRules() {
// GIVEN: gRPC validator with unreachable endpoint (tests fallback behavior)
GrpcTransactionPoolValidator validator =
new GrpcTransactionPoolValidator("unreachable-service:9999");
Transaction localTransaction =
createTransactionWithData(
Wei.of(50_000_000_000L), // 50 gwei gas price
Wei.fromEth(1), // 1 ETH value
BigInteger.valueOf(1337) // custom chain ID
);
Transaction peerTransaction =
createTransactionWithData(
Wei.of(10_000_000_000L), // 10 gwei gas price
Wei.fromEth(2), // 2 ETH value
BigInteger.valueOf(1) // mainnet chain ID
);
// WHEN: Processing different transaction types
Optional<String> localResult = validator.validateTransaction(localTransaction, true, false);
Optional<String> peerResult = validator.validateTransaction(peerTransaction, false, false);
Optional<String> priorityResult = validator.validateTransaction(localTransaction, true, true);
// THEN: Business rules should be applied correctly
// Local transactions attempt gRPC validation, fallback to acceptance
assertThat(localResult).isEmpty(); // Accepted via fallback
// Peer transactions bypass gRPC completely
assertThat(peerResult).isEmpty(); // Accepted without gRPC call
// Priority transactions still go through same logic
assertThat(priorityResult).isEmpty(); // Accepted via fallback
// Statistics should reflect the processing
assertThat(validator.getValidationCallCount()).isEqualTo(3);
assertThat(validator.getLocalTransactionCount()).isEqualTo(2); // local + priority
assertThat(validator.getPeerTransactionCount()).isEqualTo(1);
validator.close();
}
@Test
void shouldExtractTransactionDataCorrectly() {
// GIVEN: Validator and transaction with specific data
GrpcTransactionPoolValidator validator = new GrpcTransactionPoolValidator("test-endpoint:8080");
// Create transaction with all the data fields that should be sent to gRPC
Wei gasPrice = Wei.of(75_000_000_000L);
BigInteger chainId = BigInteger.valueOf(42);
Transaction transaction =
new TransactionTestFixture()
.gasPrice(gasPrice)
.gasLimit(100000)
.value(Wei.fromEth(5))
.chainId(Optional.of(chainId))
.to(Optional.of(Address.fromHexString("0x742d35cc6634c0532925a3b8d039135682b2e78b")))
.createTransaction(SIGNATURE_ALGORITHM.generateKeyPair());
// WHEN: Processing the transaction (will attempt gRPC call and fallback)
Optional<String> result = validator.validateTransaction(transaction, true, false);
// THEN: Should process successfully and extract key data
assertThat(result).isEmpty(); // Fallback accepts
// Verify the transaction has the expected data that would be sent to gRPC
assertThat(transaction.getHash()).isNotNull();
assertThat(transaction.getSender()).isNotNull();
assertThat(transaction.getGasPrice()).isPresent();
assertThat(transaction.getGasPrice().get()).isEqualTo(gasPrice);
assertThat(transaction.getChainId()).isPresent();
assertThat(transaction.getChainId().get()).isEqualTo(chainId);
validator.close();
}
@Test
void shouldHandleFactoryBehaviorCorrectly() {
// GIVEN: Different factory configurations
// Factory with valid endpoint
GrpcTransactionPoolValidatorFactory validFactory =
new GrpcTransactionPoolValidatorFactory("localhost:8080");
// Factory with no endpoint
GrpcTransactionPoolValidatorFactory nullFactory = new GrpcTransactionPoolValidatorFactory(null);
GrpcTransactionPoolValidatorFactory emptyFactory = new GrpcTransactionPoolValidatorFactory("");
// WHEN: Creating validators
PluginTransactionPoolValidator validValidator = validFactory.createTransactionValidator();
PluginTransactionPoolValidator nullValidator = nullFactory.createTransactionValidator();
PluginTransactionPoolValidator emptyValidator = emptyFactory.createTransactionValidator();
// THEN: Should behave according to configuration
// Valid endpoint creates gRPC validator
assertThat(validValidator).isInstanceOf(GrpcTransactionPoolValidator.class);
// Null/empty endpoints return default validator
assertThat(nullValidator).isEqualTo(PluginTransactionPoolValidator.VALIDATE_ALL);
assertThat(emptyValidator).isEqualTo(PluginTransactionPoolValidator.VALIDATE_ALL);
// Singleton behavior for same factory
PluginTransactionPoolValidator validValidator2 = validFactory.createTransactionValidator();
assertThat(validValidator).isSameAs(validValidator2);
// Cleanup
((GrpcTransactionPoolValidator) validValidator).close();
}
@Test
void shouldProvideMeaningfulStatistics() {
// GIVEN: Validator for statistics testing
GrpcTransactionPoolValidator validator = new GrpcTransactionPoolValidator("stats-test:8080");
Transaction tx = createTestTransaction();
// WHEN: Processing various transactions
validator.validateTransaction(tx, true, false); // local
validator.validateTransaction(tx, false, false); // peer
validator.validateTransaction(tx, true, true); // local priority
validator.validateTransaction(tx, false, true); // peer priority
validator.validateTransaction(tx, true, false); // local again
// THEN: Statistics should be accurate and meaningful
assertThat(validator.getValidationCallCount()).isEqualTo(5);
assertThat(validator.getLocalTransactionCount()).isEqualTo(3); // 2 local + 1 priority
assertThat(validator.getPeerTransactionCount()).isEqualTo(2);
// Statistics should be consistent
assertThat(validator.getLocalTransactionCount() + validator.getPeerTransactionCount())
.isEqualTo(validator.getValidationCallCount());
validator.close();
}
@Test
void shouldHandleEdgeCases() {
// GIVEN: Validator for edge case testing
GrpcTransactionPoolValidator validator =
new GrpcTransactionPoolValidator("edge-case-test:8080");
// WHEN: Testing edge cases
// Transaction with zero gas price
Transaction zeroGasTransaction =
new TransactionTestFixture()
.gasPrice(Wei.ZERO)
.chainId(Optional.of(BigInteger.valueOf(1)))
.createTransaction(SIGNATURE_ALGORITHM.generateKeyPair());
// Transaction with very high gas price
Transaction highGasTransaction =
new TransactionTestFixture()
.gasPrice(Wei.of(Long.MAX_VALUE))
.chainId(Optional.of(BigInteger.valueOf(1)))
.createTransaction(SIGNATURE_ALGORITHM.generateKeyPair());
// Process edge case transactions
Optional<String> zeroResult = validator.validateTransaction(zeroGasTransaction, true, false);
Optional<String> highResult = validator.validateTransaction(highGasTransaction, true, false);
// THEN: Should handle edge cases gracefully
assertThat(zeroResult).isEmpty(); // Fallback accepts
assertThat(highResult).isEmpty(); // Fallback accepts
assertThat(validator.getLocalTransactionCount()).isEqualTo(2);
validator.close();
}
@Test
void shouldIntegrateWithBesuPluginSystem() {
// GIVEN: Simulate how Besu's CLI would use the validator
// This simulates the CLI creating a factory based on configuration
String configuredEndpoint = "rln-prover.example.com:9090";
// In Besu, the TransactionPoolOptions would create this factory
PluginTransactionPoolValidatorFactory factory =
new GrpcTransactionPoolValidatorFactory(configuredEndpoint);
// WHEN: Besu's transaction pool uses the factory
PluginTransactionPoolValidator validator = factory.createTransactionValidator();
// THEN: Integration should work seamlessly
assertThat(validator).isNotNull();
assertThat(validator).isInstanceOf(GrpcTransactionPoolValidator.class);
GrpcTransactionPoolValidator grpcValidator = (GrpcTransactionPoolValidator) validator;
assertThat(grpcValidator.getEndpoint()).isEqualTo(configuredEndpoint);
// Validator should be ready to process transactions
Transaction testTx = createTestTransaction();
Optional<String> result = validator.validateTransaction(testTx, true, false);
assertThat(result).isEmpty(); // Fallback behavior
grpcValidator.close();
}
private Transaction createTestTransaction() {
return new TransactionTestFixture()
.gasPrice(Wei.of(20_000_000_000L))
.chainId(Optional.of(BigInteger.valueOf(1)))
.createTransaction(SIGNATURE_ALGORITHM.generateKeyPair());
}
private Transaction createTransactionWithData(
final Wei gasPrice, final Wei value, final BigInteger chainId) {
return new TransactionTestFixture()
.gasPrice(gasPrice)
.value(value)
.chainId(Optional.of(chainId))
.gasLimit(21000)
.createTransaction(SIGNATURE_ALGORITHM.generateKeyPair());
}
}

View File

@@ -662,6 +662,14 @@
<sha256 value="c0e547bea998888e6e25c5886a90e762272bc88b5275343dd2c05ded6ca2e360" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="com.google.code.gson" name="gson" version="2.8.9">
<artifact name="gson-2.8.9.jar">
<sha256 value="d3999291855de495c94c743761b8ab5176cfeabe281a5ab0d8e8d45326fd703e" origin="Generated by Gradle"/>
</artifact>
<artifact name="gson-2.8.9.pom">
<sha256 value="afded6e6a690fbf3ad4ae65ada397f0a90a5f630b303d1b741b9c97926fdd4de" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="com.google.code.gson" name="gson" version="2.9.0">
<artifact name="gson-2.9.0.pom">
<sha256 value="7190d0b07f278e9f4c603f44e543940f81cf1a2559f851c6f298c9bb2be2978c" origin="Generated by Gradle"/>
@@ -672,6 +680,11 @@
<sha256 value="8acb1f3b72a6f026916ac0735bad9aab0293d527edb7b365327def13a9367b7a" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="com.google.code.gson" name="gson-parent" version="2.8.9">
<artifact name="gson-parent-2.8.9.pom">
<sha256 value="b16e026e63427c1972ad0fc68703ec379b1576e411ba49c32fa9a31ab0bbcffb" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="com.google.code.gson" name="gson-parent" version="2.9.0">
<artifact name="gson-parent-2.9.0.pom">
<sha256 value="af781c9a5766ffea311a0df0536576a64decc661aa110c4de5c73ac8bf434424" origin="Generated by Gradle"/>
@@ -717,6 +730,16 @@
<sha256 value="efd58f6848c1805740d481ba919d455a2924ce7164927cbab7a83de6e26646d0" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="com.google.errorprone" name="error_prone_annotations" version="2.18.0">
<artifact name="error_prone_annotations-2.18.0.pom">
<sha256 value="920135797dcca5917b5a5c017642a58d340a4cd1bcd12f56f892a5663bd7bddc" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="com.google.errorprone" name="error_prone_annotations" version="2.20.0">
<artifact name="error_prone_annotations-2.20.0.pom">
<sha256 value="94f50653af659be1d1a0d785f97cba768571f794d2e655fc3d22e4eee6512b39" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="com.google.errorprone" name="error_prone_annotations" version="2.21.1">
<artifact name="error_prone_annotations-2.21.1.jar">
<sha256 value="d1f3c66aa91ac52549e00ae3b208ba4b9af7d72d68f230643553beb38e6118ac" origin="Generated by Gradle"/>
@@ -767,6 +790,16 @@
<sha256 value="9484087f6a15933cabe7013c834142d9726e647628192a412b4c7d50ecccaf56" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="com.google.errorprone" name="error_prone_parent" version="2.18.0">
<artifact name="error_prone_parent-2.18.0.pom">
<sha256 value="47f22e99c7bf466391def16f8377985e5d3ba6f5bbcf65853644805513e15fad" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="com.google.errorprone" name="error_prone_parent" version="2.20.0">
<artifact name="error_prone_parent-2.20.0.pom">
<sha256 value="e0b8bc77fc794d22d50aa4851896d8202a616610161b13cbff2a148ff1e102c2" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="com.google.errorprone" name="error_prone_parent" version="2.21.1">
<artifact name="error_prone_parent-2.21.1.pom">
<sha256 value="32bb0b5ff241fd6ba1feea448aebb9cedef1699be73cb6f319365387b82bf92c" origin="Generated by Gradle"/>
@@ -855,6 +888,14 @@
<sha256 value="13aaf29158343f8b9c7dd7d3f58610290b05ad29ea69c7b9504869e47fbf6319" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="com.google.gradle" name="osdetector-gradle-plugin" version="1.7.3">
<artifact name="osdetector-gradle-plugin-1.7.3.jar">
<sha256 value="6b4692f913a21b1fb603169ee78ba8f3e4ab2af9d762af9ca88b79126c1c0ad1" origin="Generated by Gradle"/>
</artifact>
<artifact name="osdetector-gradle-plugin-1.7.3.pom">
<sha256 value="8460c950127ca3598766161e38b4ff8d63b4d69fb8310a16e00b351350010c11" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="com.google.guava" name="failureaccess" version="1.0.1">
<artifact name="failureaccess-1.0.1.jar">
<sha256 value="a171ee4c734dd2da837e4b16be9df4661afab72a41adaf31eb84dfdaf936ca26" origin="Generated by Gradle"/>
@@ -992,6 +1033,11 @@
<sha256 value="63d5c0c641797deba6c051c47d5f6923f3a103ef71e4ebc3a85d01c3e878596c" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="com.google.j2objc" name="j2objc-annotations" version="2.8">
<artifact name="j2objc-annotations-2.8.pom">
<sha256 value="37f87798b18385113c918bfa9e1276fe50735ef8fa849b5800c519d54dbf11f8" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="com.google.j2objc" name="j2objc-annotations" version="3.0.0">
<artifact name="j2objc-annotations-3.0.0.jar">
<sha256 value="88241573467ddca44ffd4d74aa04c2bbfd11bf7c17e0c342c94c9de7a70a7c64" origin="Generated by Gradle"/>
@@ -1000,6 +1046,16 @@
<sha256 value="23b3d039e168ad89dd114698e6dd7be383f4a2c577b8877d82c73a6515e74a17" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="com.google.protobuf" name="com.google.protobuf.gradle.plugin" version="0.9.4">
<artifact name="com.google.protobuf.gradle.plugin-0.9.4.pom">
<sha256 value="4b03ee00dc5e3d4bbf755b26ee4d6ace2ab00c08315d3b28248910137b080edc" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="com.google.protobuf" name="protobuf-bom" version="3.25.1">
<artifact name="protobuf-bom-3.25.1.pom">
<sha256 value="43786970c1acb7c119c961f53ea856a4bc0bf6bf0ceffab0ad2a84f262cf1654" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="com.google.protobuf" name="protobuf-bom" version="3.25.5">
<artifact name="protobuf-bom-3.25.5.pom">
<sha256 value="080e2984173238b50e064c226afffbb1b0233520295c790a7fd3d6ae4593f063" origin="Generated by Gradle"/>
@@ -1010,6 +1066,14 @@
<sha256 value="be3baa9b418864b5ed7d39c3beb3141d83fa78d124554eaa92083243dcbfb27a" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="com.google.protobuf" name="protobuf-gradle-plugin" version="0.9.4">
<artifact name="protobuf-gradle-plugin-0.9.4.jar">
<sha256 value="7e554bdec3202ede0a2407f20141d8ca2e9e3ab62429ffa9b21ab0c02f435223" origin="Generated by Gradle"/>
</artifact>
<artifact name="protobuf-gradle-plugin-0.9.4.pom">
<sha256 value="adba1612b20b5cdb9350e12475e25c31db59e80f937466c257cdf3f9b758e96e" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="com.google.protobuf" name="protobuf-java" version="3.25.5">
<artifact name="protobuf-java-3.25.5.jar">
<sha256 value="8540247fad9e06baefa8fb45eb313802d019f485f14300e0f9d6b556ed88e753" origin="Generated by Gradle"/>
@@ -1026,6 +1090,14 @@
<sha256 value="f70bea74bd0b5325c2d2fc4d58d86163744019738d1ca436e067d733c5f7c125" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="com.google.protobuf" name="protobuf-java-util" version="3.25.1">
<artifact name="protobuf-java-util-3.25.1.jar">
<sha256 value="faf398ad0fe8c5a7d867f76d322e2e71bb31898fe86ec3223f787a6ed6fb4622" origin="Generated by Gradle"/>
</artifact>
<artifact name="protobuf-java-util-3.25.1.pom">
<sha256 value="c7a271d4ebdbe41e9fcc2d4584730dc644b17272f9bafd14e3e6c8fb3a233b78" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="com.google.protobuf" name="protobuf-java-util" version="3.25.5">
<artifact name="protobuf-java-util-3.25.5.jar">
<sha256 value="dacc58b2c3d2fa8d4bddc1acb881e78d6cf7c137dd78bc1d67f6aca732436a8d" origin="Generated by Gradle"/>
@@ -1034,6 +1106,11 @@
<sha256 value="a09d190eaa6a79616bc5f4b5404e94b0cab559803a98c8a090c4099962f41f92" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="com.google.protobuf" name="protobuf-parent" version="3.25.1">
<artifact name="protobuf-parent-3.25.1.pom">
<sha256 value="cd1be83739f036462b129b653759fa868bfad43fb9539c5e7d1de2ac3d85dcea" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="com.google.protobuf" name="protobuf-parent" version="3.25.5">
<artifact name="protobuf-parent-3.25.5.pom">
<sha256 value="64cc0e3ad6e85f5aec8f9dcf9341d1379e9525364ff53e23e16d8d5824673ef7" origin="Generated by Gradle"/>
@@ -1044,6 +1121,14 @@
<sha256 value="5364cdc058b2e5a212af6dfc1894299070dd434026a15827bbe313774489f403" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="com.google.protobuf" name="protoc" version="3.25.1">
<artifact name="protoc-3.25.1-osx-aarch_64.exe">
<sha256 value="b6ed65c0d20a9ab88ec6995644f747d557cb9a087eab0152fef5367c34645dc3" origin="Generated by Gradle"/>
</artifact>
<artifact name="protoc-3.25.1.pom">
<sha256 value="d7fc0531c903580a1dfff9734e41eca2f3b4380ae7e652cdc1de15a91f1fef31" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="com.google.re2j" name="re2j" version="1.7">
<artifact name="re2j-1.7.jar">
<sha256 value="4f657af51ab8bb0909bcc3eb40862d26125af8cbcf92aaaba595fed77f947bc0" origin="Generated by Gradle"/>
@@ -1530,6 +1615,11 @@
<sha256 value="264fec0a1008e3e8dab9126bcb450f10a4c6e1213e8ab9f3395059715c3f1221" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="io.grpc" name="grpc-api" version="1.60.0">
<artifact name="grpc-api-1.60.0.pom">
<sha256 value="431dd4ff71bf1bb5983ed04de53d82716fb49610f60d568e356ddd3743aa9e49" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="io.grpc" name="grpc-api" version="1.70.0">
<artifact name="grpc-api-1.70.0.jar">
<sha256 value="45faf2ac1bf2791e8fdabce53684a86b62c99b84cba26fb13a5ba3f4abf80d6c" origin="Generated by Gradle"/>
@@ -1559,6 +1649,11 @@
<sha256 value="ae761bb574d49a40f9f7723c08ad0eb423930df408577170774db5a2e55fbf14" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="io.grpc" name="grpc-core" version="1.60.0">
<artifact name="grpc-core-1.60.0.pom">
<sha256 value="52cfdb9ac707b0af5d67be780e24c0643e53ab90440d7aa4320b00e702afe88b" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="io.grpc" name="grpc-core" version="1.70.0">
<artifact name="grpc-core-1.70.0.jar">
<sha256 value="c2b5576b8b363b1b1006673c492d912500baaa1581430a7f9c05e82cc5bdfba4" origin="Generated by Gradle"/>
@@ -1583,6 +1678,11 @@
<sha256 value="b7c5483a6ab2c3bbf56ffede9f098eca1f11e119375584357ed5d1074991247e" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="io.grpc" name="grpc-inprocess" version="1.60.0">
<artifact name="grpc-inprocess-1.60.0.pom">
<sha256 value="23fef188039d606f8b16250a46dbf5a2b64e9e69ce8806af9ec44224e854d8e2" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="io.grpc" name="grpc-inprocess" version="1.70.0">
<artifact name="grpc-inprocess-1.70.0.jar">
<sha256 value="d9410b06d39383980e1489785d9b347c868839764fb69e588327471d5b73e79f" origin="Generated by Gradle"/>
@@ -1671,6 +1771,11 @@
<sha256 value="3691cd3280ec94a3f8c167bd1ee0c7ed5b94affef6e4f0810220739062686b6b" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="io.grpc" name="grpc-stub" version="1.60.0">
<artifact name="grpc-stub-1.60.0.pom">
<sha256 value="78788e2485f6c932900f6d502791c244517d9a6200dc66f0a4b3a80f950e3a44" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="io.grpc" name="grpc-stub" version="1.70.0">
<artifact name="grpc-stub-1.70.0.jar">
<sha256 value="5adaa1ec1f744b67ae14a8dbc39c9589c010fad0fd557b0a02966202e4d23a18" origin="Generated by Gradle"/>
@@ -1679,6 +1784,11 @@
<sha256 value="91a91b7d0e802674a453a8079b609542ffb52230ec6bfd376d1758eec5f63ad2" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="io.grpc" name="grpc-testing" version="1.60.0">
<artifact name="grpc-testing-1.60.0.pom">
<sha256 value="b2a3eff0c8c7d36c982f63731721694b694c5e30c82f835b090e55fb882c5836" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="io.grpc" name="grpc-testing" version="1.70.0">
<artifact name="grpc-testing-1.70.0.jar">
<sha256 value="4f910c09d81a863d7613cdb80d870477f396a6634a3cae2ce3bd21597cf29422" origin="Generated by Gradle"/>
@@ -1687,6 +1797,11 @@
<sha256 value="4a59991ee352b060b55098f4ed9c5c822fb6637f958bde02e3ce67d1ea0950f1" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="io.grpc" name="grpc-util" version="1.60.0">
<artifact name="grpc-util-1.60.0.pom">
<sha256 value="29c1a2b30827c678a3c5e93138c70561a98208ee7ff78883de0248ad31820914" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="io.grpc" name="grpc-util" version="1.70.0">
<artifact name="grpc-util-1.70.0.jar">
<sha256 value="683aff93d2cabc44ff21dc9ab7794f8ae7b4c65d18748c8474535311eabe8dc4" origin="Generated by Gradle"/>
@@ -1703,6 +1818,14 @@
<sha256 value="3b2b9f4bdb4a4993f5509de57ae29b220dfc8c7b3a01df7e019835843fef2248" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="io.grpc" name="protoc-gen-grpc-java" version="1.60.0">
<artifact name="protoc-gen-grpc-java-1.60.0-osx-aarch_64.exe">
<sha256 value="bb6c0c079998ee7080e66ea122dfb66a34a602482a7ed1760b30b7324fdf8ede" origin="Generated by Gradle"/>
</artifact>
<artifact name="protoc-gen-grpc-java-1.60.0.pom">
<sha256 value="e82d7256d7230aac0fc8276f57c96e3b98ad6fcda8a074fe7d4935ee21cb7008" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="io.netty" name="netty-all" version="4.1.118.Final">
<artifact name="netty-all-4.1.118.Final.jar">
<sha256 value="44790974f057722b9d9d19dcb7afb91504eeb46d81fa2494db4f0fe01c5b7242" origin="Generated by Gradle"/>
@@ -2739,6 +2862,14 @@
<sha256 value="0a7b9c6327968afe0d82f7cf4facbda7e157968c5808d69bcb8019863b298dc4" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="javax.annotation" name="javax.annotation-api" version="1.3.2">
<artifact name="javax.annotation-api-1.3.2.jar">
<sha256 value="e04ba5195bcd555dc95650f7cc614d151e4bcd52d29a10b8aa2197f3ab89ab9b" origin="Generated by Gradle"/>
</artifact>
<artifact name="javax.annotation-api-1.3.2.pom">
<sha256 value="46a4a251ca406e78e4853d7a2bae83282844a4992851439ee9a1f23716f06b97" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="javax.inject" name="javax.inject" version="1">
<artifact name="javax.inject-1.jar">
<sha256 value="91c77044a50c481636c32d916fd89c9118a72195390452c81065080f957de7ff" origin="Generated by Gradle"/>
@@ -2763,6 +2894,14 @@
<sha256 value="569b6977ee4603c965c1c46c3058fa6e969291b0160eb6964dd092cd89eadd94" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="kr.motd.maven" name="os-maven-plugin" version="1.7.1">
<artifact name="os-maven-plugin-1.7.1.jar">
<sha256 value="f47aeef86821e52b2b18758978bd045f03d722292e32e747082122c6228952e0" origin="Generated by Gradle"/>
</artifact>
<artifact name="os-maven-plugin-1.7.1.pom">
<sha256 value="4b758004422b9633dd318f29e784f1d180bd8a5920cd50af1930861f6d6a5476" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="me.champeau.jmh" name="jmh-gradle-plugin" version="0.7.2">
<artifact name="jmh-gradle-plugin-0.7.2.jar">
<sha256 value="d9672099ff8fc3f9bf3d4d015864e1586f07ecbd2a8a177a66184ef0b68aba65" origin="Generated by Gradle"/>
@@ -2797,6 +2936,11 @@
<sha256 value="8dc519d7a3e792112a7cd841eafbec4f00dbc633d085add20c45f0ab476ca496" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="net.java" name="jvnet-parent" version="3">
<artifact name="jvnet-parent-3.pom">
<sha256 value="30f5789efa39ddbf96095aada3fc1260c4561faf2f714686717cb2dc5049475a" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="net.java" name="jvnet-parent" version="5">
<artifact name="jvnet-parent-5.pom">
<sha256 value="1af699f8d9ddab67f9a0d202fbd7915eb0362a5a6dfd5ffc54cafa3465c9cb0a" origin="Generated by Gradle"/>