diff --git a/PLUGINS.md b/PLUGINS.md index c62f1f79..645f3aa3 100644 --- a/PLUGINS.md +++ b/PLUGINS.md @@ -44,12 +44,14 @@ of a transaction. #### CLI Options -| Option Name | Default Value | Command Line Argument | -|----------------------------------|----------------------|---------------------------------------------------| -| MAX_BLOCK_CALLDATA_SIZE | 70000 | `--plugin-linea-max-block-calldata-size` | -| MODULE_LIMIT_FILE_PATH | moduleLimitFile.toml | `--plugin-linea-module-limit-file-path` | -| OVER_LINE_COUNT_LIMIT_CACHE_SIZE | 10_000 | `--plugin-linea-over-line-count-limit-cache-size` | -| MAX_GAS_PER_BLOCK | 30_000_000L | `--plugin-linea-max-block-gas` | +| Option Name | Default Value | Command Line Argument | +|-------------------------------------|----------------------|-------------------------------------------------------| +| MAX_BLOCK_CALLDATA_SIZE | 70000 | `--plugin-linea-max-block-calldata-size` | +| MODULE_LIMIT_FILE_PATH | moduleLimitFile.toml | `--plugin-linea-module-limit-file-path` | +| OVER_LINE_COUNT_LIMIT_CACHE_SIZE | 10_000 | `--plugin-linea-over-line-count-limit-cache-size` | +| MAX_GAS_PER_BLOCK | 30_000_000L | `--plugin-linea-max-block-gas` | +| TX_POOL_ENABLE_SIMULATION_CHECK_API | true | `--plugin-linea-tx-pool-simulation-check-api-enabled` | +| TX_POOL_ENABLE_SIMULATION_CHECK_P2P | false | `--plugin-linea-tx-pool-simulation-check-p2p-enabled` | ### Transaction validation - LineaTransactionValidatorPlugin diff --git a/acceptance-tests/src/test/java/linea/plugin/acc/test/LineaPluginTestBase.java b/acceptance-tests/src/test/java/linea/plugin/acc/test/LineaPluginTestBase.java index 1c1cac21..38a898f1 100644 --- a/acceptance-tests/src/test/java/linea/plugin/acc/test/LineaPluginTestBase.java +++ b/acceptance-tests/src/test/java/linea/plugin/acc/test/LineaPluginTestBase.java @@ -23,6 +23,7 @@ import java.util.HashMap; import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.stream.Collectors; @@ -149,7 +150,8 @@ private void assertTransactionsInCorrectBlocks(Web3j web3j, List hashes, protected SimpleStorage deploySimpleStorage() throws Exception { final Web3j web3j = minerNode.nodeRequests().eth(); final Credentials credentials = Credentials.create(Accounts.GENESIS_ACCOUNT_ONE_PRIVATE_KEY); - TransactionManager txManager = new RawTransactionManager(web3j, credentials, CHAIN_ID); + TransactionManager txManager = + new RawTransactionManager(web3j, credentials, CHAIN_ID, createReceiptProcessor(web3j)); final RemoteCall deploy = SimpleStorage.deploy(web3j, txManager, new DefaultGasProvider()); @@ -198,11 +200,15 @@ protected void assertTransactionNotInThePool(String hash) { .notInTransactionPool(Hash.fromHexString(hash))); } + protected List> getTxPoolContent() { + return minerNode.execute(new TxPoolTransactions().getTxPoolContents()); + } + private TransactionReceiptProcessor createReceiptProcessor(Web3j web3j) { return new PollingTransactionReceiptProcessor( web3j, - TransactionManager.DEFAULT_POLLING_FREQUENCY, - TransactionManager.DEFAULT_POLLING_ATTEMPTS_PER_TX_HASH); + Math.max(1000, LINEA_CLIQUE_OPTIONS.blockPeriodSeconds() * 1000 / 5), + LINEA_CLIQUE_OPTIONS.blockPeriodSeconds() * 3); } protected String sendTransactionWithGivenLengthPayload( diff --git a/acceptance-tests/src/test/java/linea/plugin/acc/test/TransactionTraceLimitOverflowTest.java b/acceptance-tests/src/test/java/linea/plugin/acc/test/TransactionTraceLimitOverflowTest.java index 3558e4e4..21cc92fd 100644 --- a/acceptance-tests/src/test/java/linea/plugin/acc/test/TransactionTraceLimitOverflowTest.java +++ b/acceptance-tests/src/test/java/linea/plugin/acc/test/TransactionTraceLimitOverflowTest.java @@ -43,6 +43,7 @@ public List getTestCliOptions() { .set( "--plugin-linea-module-limit-file-path=", getResourcePath("/txOverflowModuleLimits.toml")) + .set("--plugin-linea-tx-pool-simulation-check-api-enabled=", "false") .build(); } diff --git a/acceptance-tests/src/test/java/linea/plugin/acc/test/rpc/linea/EthSendRawTransactionSimulationCheckTest.java b/acceptance-tests/src/test/java/linea/plugin/acc/test/rpc/linea/EthSendRawTransactionSimulationCheckTest.java new file mode 100644 index 00000000..8534fe99 --- /dev/null +++ b/acceptance-tests/src/test/java/linea/plugin/acc/test/rpc/linea/EthSendRawTransactionSimulationCheckTest.java @@ -0,0 +1,103 @@ +/* + * Copyright Consensys Software Inc. + * + * 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 linea.plugin.acc.test.rpc.linea; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.math.BigInteger; +import java.util.ArrayList; +import java.util.List; + +import linea.plugin.acc.test.LineaPluginTestBase; +import linea.plugin.acc.test.TestCommandLineOptionsBuilder; +import linea.plugin.acc.test.tests.web3j.generated.SimpleStorage; +import org.hyperledger.besu.datatypes.Hash; +import org.hyperledger.besu.tests.acceptance.dsl.account.Account; +import org.hyperledger.besu.tests.acceptance.dsl.account.Accounts; +import org.junit.jupiter.api.Test; +import org.web3j.crypto.Credentials; +import org.web3j.crypto.RawTransaction; +import org.web3j.crypto.TransactionEncoder; +import org.web3j.protocol.Web3j; +import org.web3j.protocol.core.methods.response.EthSendTransaction; +import org.web3j.tx.gas.DefaultGasProvider; +import org.web3j.utils.Numeric; + +public class EthSendRawTransactionSimulationCheckTest extends LineaPluginTestBase { + + private static final BigInteger GAS_LIMIT = DefaultGasProvider.GAS_LIMIT; + private static final BigInteger VALUE = BigInteger.ZERO; + private static final BigInteger GAS_PRICE = BigInteger.TEN.pow(9); + + @Override + public List getTestCliOptions() { + return new TestCommandLineOptionsBuilder() + .set( + "--plugin-linea-module-limit-file-path=", + getResourcePath("/txOverflowModuleLimits.toml")) + .set("--plugin-linea-tx-pool-simulation-check-api-enabled=", "true") + .build(); + } + + @Test + public void transactionOverModuleLineCountNotAccepted() throws Exception { + final SimpleStorage simpleStorage = deploySimpleStorage(); + final Web3j web3j = minerNode.nodeRequests().eth(); + final String contractAddress = simpleStorage.getContractAddress(); + final String txData = simpleStorage.add(BigInteger.valueOf(100)).encodeFunctionCall(); + + // this tx will not be accepted since it goes above the line count limit + final RawTransaction txModuleLineCountTooBig = + RawTransaction.createTransaction( + CHAIN_ID, + BigInteger.valueOf(1), + GAS_LIMIT.divide(BigInteger.TEN), + contractAddress, + VALUE, + txData, + GAS_PRICE, + GAS_PRICE.multiply(BigInteger.TEN).add(BigInteger.ONE)); + final byte[] signedTxContractInteraction = + TransactionEncoder.signMessage( + txModuleLineCountTooBig, Credentials.create(Accounts.GENESIS_ACCOUNT_TWO_PRIVATE_KEY)); + + final EthSendTransaction signedTxContractInteractionResp = + web3j.ethSendRawTransaction(Numeric.toHexString(signedTxContractInteraction)).send(); + + assertThat(signedTxContractInteractionResp.hasError()).isTrue(); + assertThat(signedTxContractInteractionResp.getError().getMessage()) + .isEqualTo("Transaction line count for module ADD=2018 is above the limit 70"); + + assertThat(getTxPoolContent()).isEmpty(); + + // these are under the line count limit and should be accepted and selected + final Account fewLinesSender = accounts.getSecondaryBenefactor(); + final Account recipient = accounts.createAccount("recipient"); + final List expectedConfirmedTxs = new ArrayList<>(4); + + expectedConfirmedTxs.addAll( + minerNode.execute( + accountTransactions.createIncrementalTransfers(fewLinesSender, recipient, 4))); + + final var txPoolContentByHash = getTxPoolContent().stream().map(e -> e.get("hash")).toList(); + assertThat(txPoolContentByHash) + .containsExactlyInAnyOrderElementsOf( + expectedConfirmedTxs.stream().map(Hash::toHexString).toList()); + + expectedConfirmedTxs.stream() + .map(Hash::toHexString) + .forEach(hash -> minerNode.verify(eth.expectSuccessfulTransactionReceipt(hash))); + } +} diff --git a/arithmetization/src/main/java/net/consensys/linea/AbstractLineaSharedOptionsPlugin.java b/arithmetization/src/main/java/net/consensys/linea/AbstractLineaSharedOptionsPlugin.java index 5046c8c9..93e18d6d 100644 --- a/arithmetization/src/main/java/net/consensys/linea/AbstractLineaSharedOptionsPlugin.java +++ b/arithmetization/src/main/java/net/consensys/linea/AbstractLineaSharedOptionsPlugin.java @@ -23,8 +23,8 @@ import net.consensys.linea.config.LineaProfitabilityConfiguration; import net.consensys.linea.config.LineaRpcCliOptions; import net.consensys.linea.config.LineaRpcConfiguration; +import net.consensys.linea.config.LineaTracerCliOptions; import net.consensys.linea.config.LineaTracerConfiguration; -import net.consensys.linea.config.LineaTracerConfigurationCLiOptions; import net.consensys.linea.config.LineaTransactionPoolValidatorCliOptions; import net.consensys.linea.config.LineaTransactionPoolValidatorConfiguration; import net.consensys.linea.config.LineaTransactionSelectorCliOptions; @@ -44,7 +44,7 @@ public abstract class AbstractLineaSharedOptionsPlugin implements BesuPlugin { private static LineaRpcCliOptions rpcCliOptions; private static LineaProfitabilityCliOptions profitabilityCliOptions; protected static LineaTransactionSelectorConfiguration transactionSelectorConfiguration; - protected static LineaTracerConfigurationCLiOptions tracerConfigurationCliOptions; + protected static LineaTracerCliOptions tracerCliOptions; protected static LineaTransactionPoolValidatorConfiguration transactionPoolValidatorConfiguration; protected static LineaL1L2BridgeConfiguration l1L2BridgeConfiguration; protected static LineaRpcConfiguration rpcConfiguration; @@ -71,14 +71,14 @@ public synchronized void register(final BesuContext context) { l1L2BridgeCliOptions = LineaL1L2BridgeCliOptions.create(); rpcCliOptions = LineaRpcCliOptions.create(); profitabilityCliOptions = LineaProfitabilityCliOptions.create(); - tracerConfigurationCliOptions = LineaTracerConfigurationCLiOptions.create(); + tracerCliOptions = LineaTracerCliOptions.create(); cmdlineOptions.addPicoCLIOptions(CLI_OPTIONS_PREFIX, transactionSelectorCliOptions); cmdlineOptions.addPicoCLIOptions(CLI_OPTIONS_PREFIX, transactionPoolValidatorCliOptions); cmdlineOptions.addPicoCLIOptions(CLI_OPTIONS_PREFIX, l1L2BridgeCliOptions); cmdlineOptions.addPicoCLIOptions(CLI_OPTIONS_PREFIX, rpcCliOptions); cmdlineOptions.addPicoCLIOptions(CLI_OPTIONS_PREFIX, profitabilityCliOptions); - cmdlineOptions.addPicoCLIOptions(CLI_OPTIONS_PREFIX, tracerConfigurationCliOptions); + cmdlineOptions.addPicoCLIOptions(CLI_OPTIONS_PREFIX, tracerCliOptions); cliOptionsRegistered = true; } } @@ -91,7 +91,7 @@ public void beforeExternalServices() { l1L2BridgeConfiguration = l1L2BridgeCliOptions.toDomainObject(); rpcConfiguration = rpcCliOptions.toDomainObject(); profitabilityConfiguration = profitabilityCliOptions.toDomainObject(); - tracerConfiguration = tracerConfigurationCliOptions.toDomainObject(); + tracerConfiguration = tracerCliOptions.toDomainObject(); configured = true; } diff --git a/arithmetization/src/main/java/net/consensys/linea/config/LineaTracerConfigurationCLiOptions.java b/arithmetization/src/main/java/net/consensys/linea/config/LineaTracerCliOptions.java similarity index 83% rename from arithmetization/src/main/java/net/consensys/linea/config/LineaTracerConfigurationCLiOptions.java rename to arithmetization/src/main/java/net/consensys/linea/config/LineaTracerCliOptions.java index 5b6f6c04..0d75650e 100644 --- a/arithmetization/src/main/java/net/consensys/linea/config/LineaTracerConfigurationCLiOptions.java +++ b/arithmetization/src/main/java/net/consensys/linea/config/LineaTracerCliOptions.java @@ -17,7 +17,7 @@ import com.google.common.base.MoreObjects; import picocli.CommandLine; -public class LineaTracerConfigurationCLiOptions { +public class LineaTracerCliOptions { public static final String MODULE_LIMIT_FILE_PATH = "--plugin-linea-module-limit-file-path"; public static final String DEFAULT_MODULE_LIMIT_FILE_PATH = "moduleLimitFile.toml"; @@ -30,15 +30,15 @@ public class LineaTracerConfigurationCLiOptions { "Path to the toml file containing the module limits (default: ${DEFAULT-VALUE})") private String moduleLimitFilePath = DEFAULT_MODULE_LIMIT_FILE_PATH; - private LineaTracerConfigurationCLiOptions() {} + private LineaTracerCliOptions() {} /** * Create Linea cli options. * * @return the Linea cli options */ - public static LineaTracerConfigurationCLiOptions create() { - return new LineaTracerConfigurationCLiOptions(); + public static LineaTracerCliOptions create() { + return new LineaTracerCliOptions(); } /** @@ -47,9 +47,8 @@ public static LineaTracerConfigurationCLiOptions create() { * @param config the config * @return the Linea cli options */ - public static LineaTracerConfigurationCLiOptions fromConfig( - final LineaTracerConfiguration config) { - final LineaTracerConfigurationCLiOptions options = create(); + public static LineaTracerCliOptions fromConfig(final LineaTracerConfiguration config) { + final LineaTracerCliOptions options = create(); options.moduleLimitFilePath = config.moduleLimitsFilePath(); return options; } diff --git a/arithmetization/src/main/java/net/consensys/linea/config/LineaTransactionPoolValidatorCliOptions.java b/arithmetization/src/main/java/net/consensys/linea/config/LineaTransactionPoolValidatorCliOptions.java index 4cb68f78..9c0ef038 100644 --- a/arithmetization/src/main/java/net/consensys/linea/config/LineaTransactionPoolValidatorCliOptions.java +++ b/arithmetization/src/main/java/net/consensys/linea/config/LineaTransactionPoolValidatorCliOptions.java @@ -30,6 +30,14 @@ public class LineaTransactionPoolValidatorCliOptions { public static final String MAX_TX_CALLDATA_SIZE = "--plugin-linea-max-tx-calldata-size"; public static final int DEFAULT_MAX_TX_CALLDATA_SIZE = 60_000; + public static final String TX_POOL_ENABLE_SIMULATION_CHECK_API = + "--plugin-linea-tx-pool-simulation-check-api-enabled"; + public static final boolean DEFAULT_TX_POOL_ENABLE_SIMULATION_CHECK_API = true; + + public static final String TX_POOL_ENABLE_SIMULATION_CHECK_P2P = + "--plugin-linea-tx-pool-simulation-check-p2p-enabled"; + public static final boolean DEFAULT_TX_POOL_ENABLE_SIMULATION_CHECK_P2P = false; + @CommandLine.Option( names = {DENY_LIST_PATH}, hidden = true, @@ -58,6 +66,24 @@ public class LineaTransactionPoolValidatorCliOptions { + ")") private int maxTxCallDataSize = DEFAULT_MAX_TX_CALLDATA_SIZE; + @CommandLine.Option( + names = {TX_POOL_ENABLE_SIMULATION_CHECK_API}, + arity = "0..1", + hidden = true, + paramLabel = "", + description = + "Enable the simulation check for txs received via API? (default: ${DEFAULT-VALUE})") + private boolean txPoolSimulationCheckApiEnabled = DEFAULT_TX_POOL_ENABLE_SIMULATION_CHECK_API; + + @CommandLine.Option( + names = {TX_POOL_ENABLE_SIMULATION_CHECK_P2P}, + arity = "0..1", + hidden = true, + paramLabel = "", + description = + "Enable the simulation check for txs received via p2p? (default: ${DEFAULT-VALUE})") + private boolean txPoolSimulationCheckP2pEnabled = DEFAULT_TX_POOL_ENABLE_SIMULATION_CHECK_P2P; + private LineaTransactionPoolValidatorCliOptions() {} /** @@ -81,7 +107,8 @@ public static LineaTransactionPoolValidatorCliOptions fromConfig( options.denyListPath = config.denyListPath(); options.maxTxGasLimit = config.maxTxGasLimit(); options.maxTxCallDataSize = config.maxTxCalldataSize(); - + options.txPoolSimulationCheckApiEnabled = config.txPoolSimulationCheckApiEnabled(); + options.txPoolSimulationCheckP2pEnabled = config.txPoolSimulationCheckP2pEnabled(); return options; } @@ -92,7 +119,11 @@ public static LineaTransactionPoolValidatorCliOptions fromConfig( */ public LineaTransactionPoolValidatorConfiguration toDomainObject() { return new LineaTransactionPoolValidatorConfiguration( - denyListPath, maxTxGasLimit, maxTxCallDataSize); + denyListPath, + maxTxGasLimit, + maxTxCallDataSize, + txPoolSimulationCheckApiEnabled, + txPoolSimulationCheckP2pEnabled); } @Override @@ -101,6 +132,8 @@ public String toString() { .add(DENY_LIST_PATH, denyListPath) .add(MAX_TX_GAS_LIMIT_OPTION, maxTxGasLimit) .add(MAX_TX_CALLDATA_SIZE, maxTxCallDataSize) + .add(TX_POOL_ENABLE_SIMULATION_CHECK_API, txPoolSimulationCheckApiEnabled) + .add(TX_POOL_ENABLE_SIMULATION_CHECK_P2P, txPoolSimulationCheckP2pEnabled) .toString(); } } diff --git a/arithmetization/src/main/java/net/consensys/linea/config/LineaTransactionPoolValidatorConfiguration.java b/arithmetization/src/main/java/net/consensys/linea/config/LineaTransactionPoolValidatorConfiguration.java index 90d41edf..fdad5fa0 100644 --- a/arithmetization/src/main/java/net/consensys/linea/config/LineaTransactionPoolValidatorConfiguration.java +++ b/arithmetization/src/main/java/net/consensys/linea/config/LineaTransactionPoolValidatorConfiguration.java @@ -26,4 +26,8 @@ */ @Builder(toBuilder = true) public record LineaTransactionPoolValidatorConfiguration( - String denyListPath, int maxTxGasLimit, int maxTxCalldataSize) {} + String denyListPath, + int maxTxGasLimit, + int maxTxCalldataSize, + boolean txPoolSimulationCheckApiEnabled, + boolean txPoolSimulationCheckP2pEnabled) {} diff --git a/arithmetization/src/main/java/net/consensys/linea/rpc/linea/LineaEstimateGas.java b/arithmetization/src/main/java/net/consensys/linea/rpc/linea/LineaEstimateGas.java index 4e9b4707..e5c38813 100644 --- a/arithmetization/src/main/java/net/consensys/linea/rpc/linea/LineaEstimateGas.java +++ b/arithmetization/src/main/java/net/consensys/linea/rpc/linea/LineaEstimateGas.java @@ -49,6 +49,7 @@ import org.hyperledger.besu.ethereum.api.jsonrpc.internal.response.RpcErrorType; import org.hyperledger.besu.ethereum.core.Transaction; import org.hyperledger.besu.evm.tracing.EstimateGasOperationTracer; +import org.hyperledger.besu.plugin.data.BlockHeader; import org.hyperledger.besu.plugin.services.BesuConfiguration; import org.hyperledger.besu.plugin.services.BlockchainService; import org.hyperledger.besu.plugin.services.TransactionSimulationService; @@ -209,11 +210,12 @@ private Long estimateGasUsed( final Wei minGasPrice) { final var estimateGasOperationTracer = new EstimateGasOperationTracer(); - final var zkTracer = createZkTracer(); + final var chainHeadHeader = blockchainService.getChainHeadHeader(); + final var zkTracer = createZkTracer(chainHeadHeader); TracerAggregator tracerAggregator = TracerAggregator.create(estimateGasOperationTracer, zkTracer); - final var chainHeadHash = blockchainService.getChainHeadHash(); + final var chainHeadHash = chainHeadHeader.getBlockHash(); final var maybeSimulationResults = transactionSimulationService.simulate(transaction, chainHeadHash, tracerAggregator, true); @@ -417,10 +419,10 @@ private Transaction createTransactionForSimulation( return txBuilder.build(); } - private ZkTracer createZkTracer() { + private ZkTracer createZkTracer(final BlockHeader chainHeadHeader) { var zkTracer = new ZkTracer(l1L2BridgeConfiguration); zkTracer.traceStartConflation(1L); - zkTracer.traceStartBlock(blockchainService.getChainHeadHeader()); + zkTracer.traceStartBlock(chainHeadHeader); return zkTracer; } @@ -443,6 +445,11 @@ private void handleModuleOverLimit(ModuleLimitsValidationResult moduleLimitResul log.warn(txOverflowMsg); throw new PluginRpcEndpointException(new TransactionSimulationError(txOverflowMsg)); } + + final String internalErrorMsg = + String.format("Do not know what to do with result %s", moduleLimitResult.getResult()); + log.error(internalErrorMsg); + throw new PluginRpcEndpointException(RpcErrorType.PLUGIN_INTERNAL_ERROR, internalErrorMsg); } public record Response( diff --git a/arithmetization/src/main/java/net/consensys/linea/sequencer/txpoolvalidation/LineaTransactionPoolValidatorFactory.java b/arithmetization/src/main/java/net/consensys/linea/sequencer/txpoolvalidation/LineaTransactionPoolValidatorFactory.java index 6fabee4e..0873678a 100644 --- a/arithmetization/src/main/java/net/consensys/linea/sequencer/txpoolvalidation/LineaTransactionPoolValidatorFactory.java +++ b/arithmetization/src/main/java/net/consensys/linea/sequencer/txpoolvalidation/LineaTransactionPoolValidatorFactory.java @@ -16,18 +16,22 @@ package net.consensys.linea.sequencer.txpoolvalidation; import java.util.Arrays; +import java.util.Map; import java.util.Optional; import java.util.Set; +import net.consensys.linea.config.LineaL1L2BridgeConfiguration; import net.consensys.linea.config.LineaProfitabilityConfiguration; import net.consensys.linea.config.LineaTransactionPoolValidatorConfiguration; import net.consensys.linea.sequencer.txpoolvalidation.validators.AllowedAddressValidator; import net.consensys.linea.sequencer.txpoolvalidation.validators.CalldataValidator; import net.consensys.linea.sequencer.txpoolvalidation.validators.GasLimitValidator; import net.consensys.linea.sequencer.txpoolvalidation.validators.ProfitabilityValidator; +import net.consensys.linea.sequencer.txpoolvalidation.validators.SimulationValidator; import org.hyperledger.besu.datatypes.Address; import org.hyperledger.besu.plugin.services.BesuConfiguration; import org.hyperledger.besu.plugin.services.BlockchainService; +import org.hyperledger.besu.plugin.services.TransactionSimulationService; import org.hyperledger.besu.plugin.services.txvalidator.PluginTransactionPoolValidator; import org.hyperledger.besu.plugin.services.txvalidator.PluginTransactionPoolValidatorFactory; @@ -36,21 +40,30 @@ public class LineaTransactionPoolValidatorFactory implements PluginTransactionPo private final BesuConfiguration besuConfiguration; private final BlockchainService blockchainService; + private final TransactionSimulationService transactionSimulationService; private final LineaTransactionPoolValidatorConfiguration txPoolValidatorConf; private final LineaProfitabilityConfiguration profitabilityConf; private final Set
denied; + private final Map moduleLineLimitsMap; + private final LineaL1L2BridgeConfiguration l1L2BridgeConfiguration; public LineaTransactionPoolValidatorFactory( final BesuConfiguration besuConfiguration, final BlockchainService blockchainService, + final TransactionSimulationService transactionSimulationService, final LineaTransactionPoolValidatorConfiguration txPoolValidatorConf, final LineaProfitabilityConfiguration profitabilityConf, - final Set
denied) { + final Set
deniedAddresses, + final Map moduleLineLimitsMap, + final LineaL1L2BridgeConfiguration l1L2BridgeConfiguration) { this.besuConfiguration = besuConfiguration; this.blockchainService = blockchainService; + this.transactionSimulationService = transactionSimulationService; this.txPoolValidatorConf = txPoolValidatorConf; this.profitabilityConf = profitabilityConf; - this.denied = denied; + this.denied = deniedAddresses; + this.moduleLineLimitsMap = moduleLineLimitsMap; + this.l1L2BridgeConfiguration = l1L2BridgeConfiguration; } @Override @@ -60,7 +73,13 @@ public PluginTransactionPoolValidator createTransactionValidator() { new AllowedAddressValidator(denied), new GasLimitValidator(txPoolValidatorConf), new CalldataValidator(txPoolValidatorConf), - new ProfitabilityValidator(besuConfiguration, blockchainService, profitabilityConf) + new ProfitabilityValidator(besuConfiguration, blockchainService, profitabilityConf), + new SimulationValidator( + blockchainService, + transactionSimulationService, + txPoolValidatorConf, + moduleLineLimitsMap, + l1L2BridgeConfiguration) }; return (transaction, isLocal, hasPriority) -> diff --git a/arithmetization/src/main/java/net/consensys/linea/sequencer/txpoolvalidation/LineaTransactionPoolValidatorPlugin.java b/arithmetization/src/main/java/net/consensys/linea/sequencer/txpoolvalidation/LineaTransactionPoolValidatorPlugin.java index 52eb2de9..4d255cb5 100644 --- a/arithmetization/src/main/java/net/consensys/linea/sequencer/txpoolvalidation/LineaTransactionPoolValidatorPlugin.java +++ b/arithmetization/src/main/java/net/consensys/linea/sequencer/txpoolvalidation/LineaTransactionPoolValidatorPlugin.java @@ -15,6 +15,8 @@ package net.consensys.linea.sequencer.txpoolvalidation; +import static net.consensys.linea.sequencer.modulelimit.ModuleLineCountValidator.createLimitModules; + import java.io.File; import java.nio.file.Files; import java.nio.file.Path; @@ -32,6 +34,7 @@ import org.hyperledger.besu.plugin.services.BesuConfiguration; import org.hyperledger.besu.plugin.services.BlockchainService; import org.hyperledger.besu.plugin.services.TransactionPoolValidatorService; +import org.hyperledger.besu.plugin.services.TransactionSimulationService; /** * This class extends the default transaction validation rules for adding transactions to the @@ -46,6 +49,7 @@ public class LineaTransactionPoolValidatorPlugin extends AbstractLineaRequiredPl private BesuConfiguration besuConfiguration; private BlockchainService blockchainService; private TransactionPoolValidatorService transactionPoolValidatorService; + private TransactionSimulationService transactionSimulationService; @Override public Optional getName() { @@ -77,6 +81,14 @@ public void doRegister(final BesuContext context) { () -> new RuntimeException( "Failed to obtain TransactionPoolValidationService from the BesuContext.")); + + transactionSimulationService = + context + .getService(TransactionSimulationService.class) + .orElseThrow( + () -> + new RuntimeException( + "Failed to obtain TransactionSimulatorService from the BesuContext.")); } @Override @@ -85,16 +97,19 @@ public void beforeExternalServices() { try (Stream lines = Files.lines( Path.of(new File(transactionPoolValidatorConfiguration.denyListPath()).toURI()))) { - final Set
denied = + final Set
deniedAddresses = lines.map(l -> Address.fromHexString(l.trim())).collect(Collectors.toUnmodifiableSet()); transactionPoolValidatorService.registerPluginTransactionValidatorFactory( new LineaTransactionPoolValidatorFactory( besuConfiguration, blockchainService, + transactionSimulationService, transactionPoolValidatorConfiguration, profitabilityConfiguration, - denied)); + deniedAddresses, + createLimitModules(tracerConfiguration), + l1L2BridgeConfiguration)); } catch (Exception e) { throw new RuntimeException(e); diff --git a/arithmetization/src/main/java/net/consensys/linea/sequencer/txpoolvalidation/validators/SimulationValidator.java b/arithmetization/src/main/java/net/consensys/linea/sequencer/txpoolvalidation/validators/SimulationValidator.java new file mode 100644 index 00000000..0709f4d9 --- /dev/null +++ b/arithmetization/src/main/java/net/consensys/linea/sequencer/txpoolvalidation/validators/SimulationValidator.java @@ -0,0 +1,130 @@ +/* + * Copyright Consensys Software Inc. + * + * 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 net.consensys.linea.sequencer.txpoolvalidation.validators; + +import static net.consensys.linea.sequencer.modulelimit.ModuleLineCountValidator.ModuleLineCountResult.MODULE_NOT_DEFINED; +import static net.consensys.linea.sequencer.modulelimit.ModuleLineCountValidator.ModuleLineCountResult.TX_MODULE_LINE_COUNT_OVERFLOW; + +import java.util.Map; +import java.util.Optional; + +import lombok.extern.slf4j.Slf4j; +import net.consensys.linea.config.LineaL1L2BridgeConfiguration; +import net.consensys.linea.config.LineaTransactionPoolValidatorConfiguration; +import net.consensys.linea.sequencer.modulelimit.ModuleLimitsValidationResult; +import net.consensys.linea.sequencer.modulelimit.ModuleLineCountValidator; +import net.consensys.linea.zktracer.ZkTracer; +import org.hyperledger.besu.datatypes.Transaction; +import org.hyperledger.besu.plugin.data.BlockHeader; +import org.hyperledger.besu.plugin.services.BlockchainService; +import org.hyperledger.besu.plugin.services.TransactionSimulationService; +import org.hyperledger.besu.plugin.services.txvalidator.PluginTransactionPoolValidator; + +@Slf4j +public class SimulationValidator implements PluginTransactionPoolValidator { + private final BlockchainService blockchainService; + private final TransactionSimulationService transactionSimulationService; + private final LineaTransactionPoolValidatorConfiguration txPoolValidatorConf; + private final Map moduleLineLimitsMap; + private final LineaL1L2BridgeConfiguration l1L2BridgeConfiguration; + + public SimulationValidator( + final BlockchainService blockchainService, + final TransactionSimulationService transactionSimulationService, + final LineaTransactionPoolValidatorConfiguration txPoolValidatorConf, + final Map moduleLineLimitsMap, + final LineaL1L2BridgeConfiguration l1L2BridgeConfiguration) { + this.blockchainService = blockchainService; + this.transactionSimulationService = transactionSimulationService; + this.txPoolValidatorConf = txPoolValidatorConf; + this.moduleLineLimitsMap = moduleLineLimitsMap; + this.l1L2BridgeConfiguration = l1L2BridgeConfiguration; + } + + @Override + public Optional validateTransaction( + final Transaction transaction, final boolean isLocal, final boolean hasPriority) { + + final boolean isLocalAndApiEnabled = + isLocal && txPoolValidatorConf.txPoolSimulationCheckApiEnabled(); + final boolean isRemoteAndP2pEnabled = + !isLocal && txPoolValidatorConf.txPoolSimulationCheckP2pEnabled(); + if (isRemoteAndP2pEnabled || isLocalAndApiEnabled) { + + final ModuleLineCountValidator moduleLineCountValidator = + new ModuleLineCountValidator(moduleLineLimitsMap); + final var chainHeadHeader = blockchainService.getChainHeadHeader(); + + final var zkTracer = createZkTracer(chainHeadHeader); + final var maybeSimulationResults = + transactionSimulationService.simulate( + transaction, chainHeadHeader.getBlockHash(), zkTracer, true); + + ModuleLimitsValidationResult moduleLimit = + moduleLineCountValidator.validate(zkTracer.getModulesLineCount()); + + if (moduleLimit.getResult() != ModuleLineCountValidator.ModuleLineCountResult.VALID) { + return Optional.of(handleModuleOverLimit(moduleLimit)); + } + + if (maybeSimulationResults.isPresent()) { + final var simulationResult = maybeSimulationResults.get(); + if (simulationResult.isInvalid()) { + return Optional.of( + "Invalid transaction" + + simulationResult.getInvalidReason().map(ir -> ": " + ir).orElse("")); + } + if (!simulationResult.isSuccessful()) { + return Optional.of( + "Reverted transaction" + + simulationResult + .getRevertReason() + .map(rr -> ": " + rr.toHexString()) + .orElse("")); + } + } + } + + return Optional.empty(); + } + + private ZkTracer createZkTracer(final BlockHeader chainHeadHeader) { + var zkTracer = new ZkTracer(l1L2BridgeConfiguration); + zkTracer.traceStartConflation(1L); + zkTracer.traceStartBlock(chainHeadHeader); + return zkTracer; + } + + private String handleModuleOverLimit(ModuleLimitsValidationResult moduleLimitResult) { + if (moduleLimitResult.getResult() == MODULE_NOT_DEFINED) { + String moduleNotDefinedMsg = + String.format( + "Module %s does not exist in the limits file.", moduleLimitResult.getModuleName()); + log.error(moduleNotDefinedMsg); + return moduleNotDefinedMsg; + } + if (moduleLimitResult.getResult() == TX_MODULE_LINE_COUNT_OVERFLOW) { + String txOverflowMsg = + String.format( + "Transaction line count for module %s=%s is above the limit %s", + moduleLimitResult.getModuleName(), + moduleLimitResult.getModuleLineCount(), + moduleLimitResult.getModuleLineLimit()); + log.warn(txOverflowMsg); + return txOverflowMsg; + } + return "Internal Error: do not know what to do with result: " + moduleLimitResult.getResult(); + } +} diff --git a/arithmetization/src/test/java/net/consensys/linea/sequencer/txpoolvalidation/validators/ProfitabilityValidatorTest.java b/arithmetization/src/test/java/net/consensys/linea/sequencer/txpoolvalidation/validators/ProfitabilityValidatorTest.java index 34a18b6e..5a8a1143 100644 --- a/arithmetization/src/test/java/net/consensys/linea/sequencer/txpoolvalidation/validators/ProfitabilityValidatorTest.java +++ b/arithmetization/src/test/java/net/consensys/linea/sequencer/txpoolvalidation/validators/ProfitabilityValidatorTest.java @@ -16,9 +16,9 @@ package net.consensys.linea.sequencer.txpoolvalidation.validators; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.when; import java.math.BigInteger; -import java.nio.file.Path; import java.util.Optional; import lombok.RequiredArgsConstructor; @@ -30,18 +30,18 @@ import org.bouncycastle.crypto.params.ECDomainParameters; import org.hyperledger.besu.crypto.SECPSignature; import org.hyperledger.besu.datatypes.Address; -import org.hyperledger.besu.datatypes.Hash; import org.hyperledger.besu.datatypes.Wei; -import org.hyperledger.besu.plugin.data.BlockContext; -import org.hyperledger.besu.plugin.data.BlockHeader; import org.hyperledger.besu.plugin.services.BesuConfiguration; import org.hyperledger.besu.plugin.services.BlockchainService; -import org.hyperledger.besu.plugin.services.storage.DataStorageFormat; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; @Slf4j @RequiredArgsConstructor +@ExtendWith(MockitoExtension.class) public class ProfitabilityValidatorTest { public static final Address SENDER = Address.fromHexString("0x0000000000000000000000000000000000001000"); @@ -71,6 +71,9 @@ public class ProfitabilityValidatorTest { private ProfitabilityValidator profitabilityValidatorOnlyP2p; private ProfitabilityValidator profitabilityValidatorNever; + @Mock BesuConfiguration besuConfiguration; + @Mock BlockchainService blockchainService; + @BeforeEach public void initialize() { final var profitabilityConfBuilder = @@ -79,8 +82,8 @@ public void initialize() { profitabilityValidatorAlways = new ProfitabilityValidator( - new TestBesuConfiguration(), - new TestBlockchainService(), + besuConfiguration, + blockchainService, profitabilityConfBuilder .txPoolCheckP2pEnabled(true) .txPoolCheckApiEnabled(true) @@ -88,8 +91,8 @@ public void initialize() { profitabilityValidatorNever = new ProfitabilityValidator( - new TestBesuConfiguration(), - new TestBlockchainService(), + besuConfiguration, + blockchainService, profitabilityConfBuilder .txPoolCheckP2pEnabled(false) .txPoolCheckApiEnabled(false) @@ -97,8 +100,8 @@ public void initialize() { profitabilityValidatorOnlyApi = new ProfitabilityValidator( - new TestBesuConfiguration(), - new TestBlockchainService(), + besuConfiguration, + blockchainService, profitabilityConfBuilder .txPoolCheckP2pEnabled(false) .txPoolCheckApiEnabled(true) @@ -106,8 +109,8 @@ public void initialize() { profitabilityValidatorOnlyP2p = new ProfitabilityValidator( - new TestBesuConfiguration(), - new TestBlockchainService(), + besuConfiguration, + blockchainService, profitabilityConfBuilder .txPoolCheckP2pEnabled(true) .txPoolCheckApiEnabled(false) @@ -132,6 +135,8 @@ public void acceptPriorityRemoteWhenBelowMinProfitability() { @Test public void rejectRemoteWhenBelowMinProfitability() { + when(besuConfiguration.getMinGasPrice()).thenReturn(Wei.of(100_000_000)); + when(blockchainService.getNextBlockBaseFee()).thenReturn(Optional.of(Wei.of(7))); final org.hyperledger.besu.ethereum.core.Transaction transaction = org.hyperledger.besu.ethereum.core.Transaction.builder() .sender(SENDER) @@ -196,6 +201,8 @@ public void acceptRemoteWhenBelowMinProfitabilityIfCheckDisabledForP2p() { @Test public void rejectRemoteWhenBelowMinProfitabilityIfCheckEnableForP2p() { + when(besuConfiguration.getMinGasPrice()).thenReturn(Wei.of(100_000_000)); + when(blockchainService.getNextBlockBaseFee()).thenReturn(Optional.of(Wei.of(7))); final org.hyperledger.besu.ethereum.core.Transaction transaction = org.hyperledger.besu.ethereum.core.Transaction.builder() .sender(SENDER) @@ -229,6 +236,8 @@ public void acceptLocalWhenBelowMinProfitabilityIfCheckDisabledForApi() { @Test public void rejectLocalWhenBelowMinProfitabilityIfCheckEnableForApi() { + when(besuConfiguration.getMinGasPrice()).thenReturn(Wei.of(100_000_000)); + when(blockchainService.getNextBlockBaseFee()).thenReturn(Optional.of(Wei.of(7))); final org.hyperledger.besu.ethereum.core.Transaction transaction = org.hyperledger.besu.ethereum.core.Transaction.builder() .sender(SENDER) @@ -243,49 +252,4 @@ public void rejectLocalWhenBelowMinProfitabilityIfCheckEnableForApi() { .isPresent() .contains("Gas price too low"); } - - private static class TestBesuConfiguration implements BesuConfiguration { - @Override - public Path getStoragePath() { - throw new UnsupportedOperationException(); - } - - @Override - public Path getDataPath() { - throw new UnsupportedOperationException(); - } - - @Override - public DataStorageFormat getDatabaseFormat() { - throw new UnsupportedOperationException(); - } - - @Override - public Wei getMinGasPrice() { - return Wei.of(100_000_000); - } - } - - private static class TestBlockchainService implements BlockchainService { - - @Override - public Optional getBlockByNumber(final long l) { - throw new UnsupportedOperationException(); - } - - @Override - public Hash getChainHeadHash() { - throw new UnsupportedOperationException(); - } - - @Override - public BlockHeader getChainHeadHeader() { - throw new UnsupportedOperationException(); - } - - @Override - public Optional getNextBlockBaseFee() { - return Optional.of(Wei.of(7)); - } - } } diff --git a/arithmetization/src/test/java/net/consensys/linea/sequencer/txpoolvalidation/validators/SimulationValidatorTest.java b/arithmetization/src/test/java/net/consensys/linea/sequencer/txpoolvalidation/validators/SimulationValidatorTest.java new file mode 100644 index 00000000..477ca7c4 --- /dev/null +++ b/arithmetization/src/test/java/net/consensys/linea/sequencer/txpoolvalidation/validators/SimulationValidatorTest.java @@ -0,0 +1,166 @@ +/* + * Copyright Consensys Software Inc. + * + * 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 net.consensys.linea.sequencer.txpoolvalidation.validators; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.io.IOException; +import java.math.BigInteger; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import net.consensys.linea.config.LineaL1L2BridgeConfiguration; +import net.consensys.linea.config.LineaTracerConfiguration; +import net.consensys.linea.config.LineaTransactionPoolValidatorConfiguration; +import net.consensys.linea.sequencer.modulelimit.ModuleLineCountValidator; +import net.consensys.linea.sequencer.txselection.selectors.TraceLineLimitTransactionSelectorTest; +import org.apache.tuweni.bytes.Bytes; +import org.bouncycastle.asn1.sec.SECNamedCurves; +import org.bouncycastle.asn1.x9.X9ECParameters; +import org.bouncycastle.crypto.params.ECDomainParameters; +import org.hyperledger.besu.crypto.SECPSignature; +import org.hyperledger.besu.datatypes.Address; +import org.hyperledger.besu.datatypes.Wei; +import org.hyperledger.besu.ethereum.core.BlockHeader; +import org.hyperledger.besu.plugin.services.BlockchainService; +import org.hyperledger.besu.plugin.services.TransactionSimulationService; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.io.TempDir; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@Slf4j +@RequiredArgsConstructor +@ExtendWith(MockitoExtension.class) +public class SimulationValidatorTest { + private static final String MODULE_LINE_LIMITS_RESOURCE_NAME = "/sequencer/line-limits.toml"; + public static final Address SENDER = + Address.fromHexString("0x0000000000000000000000000000000000001000"); + public static final Address RECIPIENT = + Address.fromHexString("0x0000000000000000000000000000000000001001"); + private static Wei BASE_FEE = Wei.of(7); + private static Wei PROFITABLE_GAS_PRICE = Wei.of(11000000); + private static final SECPSignature FAKE_SIGNATURE; + private static final Address BRIDGE_CONTRACT = + Address.fromHexString("0x508Ca82Df566dCD1B0DE8296e70a96332cD644ec"); + private static final Bytes BRIDGE_LOG_TOPIC = + Bytes.fromHexString("e856c2b8bd4eb0027ce32eeaf595c21b0b6b4644b326e5b7bd80a1cf8db72e6c"); + + static { + final X9ECParameters params = SECNamedCurves.getByName("secp256k1"); + final ECDomainParameters curve = + new ECDomainParameters(params.getCurve(), params.getG(), params.getN(), params.getH()); + FAKE_SIGNATURE = + SECPSignature.create( + new BigInteger( + "66397251408932042429874251838229702988618145381408295790259650671563847073199"), + new BigInteger( + "24729624138373455972486746091821238755870276413282629437244319694880507882088"), + (byte) 0, + curve.getN()); + } + + private Map lineCountLimits; + + @Mock BlockchainService blockchainService; + @Mock TransactionSimulationService transactionSimulationService; + + @TempDir static Path tempDir; + static Path lineLimitsConfPath; + + @BeforeAll + public static void beforeAll() throws IOException { + lineLimitsConfPath = tempDir.resolve("line-limits.toml"); + Files.copy( + TraceLineLimitTransactionSelectorTest.class.getResourceAsStream( + MODULE_LINE_LIMITS_RESOURCE_NAME), + lineLimitsConfPath); + } + + @BeforeEach + public void initialize() { + final var tracerConf = + LineaTracerConfiguration.builder() + .moduleLimitsFilePath(lineLimitsConfPath.toString()) + .build(); + lineCountLimits = new HashMap<>(ModuleLineCountValidator.createLimitModules(tracerConf)); + final var blockHeader = mock(BlockHeader.class); + when(blockHeader.getBaseFee()).thenReturn(Optional.of(BASE_FEE)); + when(blockchainService.getChainHeadHeader()).thenReturn(blockHeader); + } + + private SimulationValidator createSimulationValidator( + final Map lineCountLimits, + final boolean enableForApi, + final boolean enableForP2p) { + return new SimulationValidator( + blockchainService, + transactionSimulationService, + LineaTransactionPoolValidatorConfiguration.builder() + .txPoolSimulationCheckApiEnabled(enableForApi) + .txPoolSimulationCheckP2pEnabled(enableForP2p) + .build(), + lineCountLimits, + LineaL1L2BridgeConfiguration.builder() + .contract(BRIDGE_CONTRACT) + .topic(BRIDGE_LOG_TOPIC) + .build()); + } + + @Test + public void successfulTransactionIsValid() { + final var simulationValidator = createSimulationValidator(lineCountLimits, true, false); + final org.hyperledger.besu.ethereum.core.Transaction transaction = + org.hyperledger.besu.ethereum.core.Transaction.builder() + .sender(SENDER) + .to(RECIPIENT) + .gasLimit(21000) + .gasPrice(PROFITABLE_GAS_PRICE) + .payload(Bytes.EMPTY) + .value(Wei.ONE) + .signature(FAKE_SIGNATURE) + .build(); + assertThat(simulationValidator.validateTransaction(transaction, true, false)).isEmpty(); + } + + @Test + public void moduleLineCountOverflowTransactionIsInvalid() { + lineCountLimits.put("ADD", 1); + final var simulationValidator = createSimulationValidator(lineCountLimits, true, false); + final org.hyperledger.besu.ethereum.core.Transaction transaction = + org.hyperledger.besu.ethereum.core.Transaction.builder() + .sender(SENDER) + .to(RECIPIENT) + .gasLimit(21000) + .gasPrice(PROFITABLE_GAS_PRICE) + .payload(Bytes.repeat((byte) 1, 1000)) + .value(Wei.ONE) + .signature(FAKE_SIGNATURE) + .build(); + assertThat(simulationValidator.validateTransaction(transaction, true, false)) + .contains("Transaction line count for module ADD=2 is above the limit 1"); + } +}