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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
/*
* Copyright Consensys Software Inc.
*
* This file is dual-licensed under either the MIT license or Apache License 2.0.
* See the LICENSE-MIT and LICENSE-APACHE files in the repository root for details.
*
* SPDX-License-Identifier: MIT OR Apache-2.0
*/
package linea.plugin.acc.test

import linea.plugin.acc.test.FakeChainSecurityPolicyTxValidatorPlugin.Companion.PLUGIN_NAME
import linea.plugin.acc.test.FakeChainSecurityPolicyTxValidatorPlugin.Companion.blockByHash
import linea.plugin.acc.test.FakeChainSecurityPolicyTxValidatorPlugin.Companion.blockBySender
import linea.plugin.acc.test.FakeChainSecurityPolicyTxValidatorPlugin.Companion.reset
import linea.security.ChainSecurityPolicy
import linea.txselection.LineaTransactionSelectionResult
import linea.txselection.LineaTransactionSelectionResult.chainSecurityRuleViolated
import org.apache.tuweni.bytes.Bytes
import org.hyperledger.besu.plugin.BesuPlugin
import org.hyperledger.besu.plugin.ServiceManager
import org.hyperledger.besu.plugin.data.ProcessableBlockHeader
import org.hyperledger.besu.plugin.data.TransactionProcessingResult
import org.hyperledger.besu.plugin.data.TransactionSelectionResult
import org.hyperledger.besu.plugin.services.TransactionSelectionService
import org.hyperledger.besu.plugin.services.txselection.PluginTransactionSelector
import org.hyperledger.besu.plugin.services.txselection.PluginTransactionSelectorFactory
import org.hyperledger.besu.plugin.services.txselection.SelectorsStateManager
import org.hyperledger.besu.plugin.services.txselection.TransactionEvaluationContext
import java.util.concurrent.ConcurrentHashMap

/**
* Acceptance-test fixture plugin that registers a [ChainSecurityPolicy] BesuService and a
* [PluginTransactionSelector] that can block transactions by hash or sender address.
*
* Use [blockByHash] / [blockBySender] to populate the blocklists before a test, and [reset] to
* clear them between tests.
*
* Activate by adding [PLUGIN_NAME] to the test's `requestedPlugins`.
*/
class FakeChainSecurityPolicyTxValidatorPlugin : BesuPlugin {

private lateinit var transactionSelectionService: TransactionSelectionService
private lateinit var serviceManager: ServiceManager

override fun register(serviceManager: ServiceManager) {
this.serviceManager = serviceManager
transactionSelectionService =
serviceManager
.getService(TransactionSelectionService::class.java)
.orElseThrow { RuntimeException("TransactionSelectionService not found in ServiceManager") }
}

override fun start() {
val chainSecurityPolicy = serviceManager
.getService(ChainSecurityPolicy::class.java)
.orElseThrow { RuntimeException("ChainSecurityPolicy not found in ServiceManager") }
transactionSelectionService.registerPluginTransactionSelectorFactory(
object : PluginTransactionSelectorFactory {
override fun create(
pendingBlockHeader: ProcessableBlockHeader,
selectorsStateManager: SelectorsStateManager,
): PluginTransactionSelector = BlocklistTransactionSelector(chainSecurityPolicy)
},
)
}

override fun stop() {}

private class BlocklistTransactionSelector(
val chainSecurityPolicy: ChainSecurityPolicy,
) : PluginTransactionSelector {

override fun evaluateTransactionPreProcessing(
evaluationContext: TransactionEvaluationContext,
): TransactionSelectionResult {
val tx = evaluationContext.pendingTransaction.transaction
if (chainSecurityPolicy.shallForceIncludeTransaction(evaluationContext)) {
return TransactionSelectionResult.SELECTED
}
if (blockedHashes.contains(tx.hash.bytes)) return TX_BLOCKED_BY_SECURITY_POLICY
if (blockedSenders.contains(tx.sender.bytes)) return TX_BLOCKED_BY_SECURITY_POLICY
return TransactionSelectionResult.SELECTED
}

override fun evaluateTransactionPostProcessing(
evaluationContext: TransactionEvaluationContext,
processingResult: TransactionProcessingResult,
): TransactionSelectionResult = TransactionSelectionResult.SELECTED
}

companion object {
const val PLUGIN_NAME = "FakeChainSecurityPolicyTxValidatorPlugin"

val TX_BLOCKED_BY_SECURITY_POLICY: LineaTransactionSelectionResult =
chainSecurityRuleViolated("Blocked by FakeChainSecurityPolicyPlugin")

val blockedHashes: MutableSet<Bytes> = ConcurrentHashMap.newKeySet()
val blockedSenders: MutableSet<Bytes> = ConcurrentHashMap.newKeySet()

fun blockByHash(hash: Bytes) {
blockedHashes.add(hash)
}

fun blockBySender(address: Bytes) {
blockedSenders.add(address)
}

fun blockBySender(address: String) {
blockedSenders.add(Bytes.fromHexString(address))
}

fun reset() {
blockedHashes.clear()
blockedSenders.clear()
}
}
}
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
linea.plugin.acc.test.RecordingTransactionSelectorPlugin
linea.plugin.acc.test.FakeChainSecurityPolicyTxValidatorPlugin
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ import java.util.concurrent.Executors
import java.util.concurrent.ScheduledExecutorService
import java.util.concurrent.TimeUnit
import java.util.function.Supplier
import kotlin.time.Duration

/**
* Record containing all parameters required for engine_newPayloadV4 API calls.
Expand Down Expand Up @@ -209,19 +210,27 @@ abstract class LineaPluginPoSTestBase : LineaPluginTestBase() {
* (with stale txpool state) absorbs our fcu and getPayload returns its cached
* selection, missing any txs the test sent after the background tick fired.
*/
protected fun buildNewBlockAndWait(blockBuildingTimeMs: Long) {
protected fun buildNewBlockAndWait(blockBuildingTimeMs: Long): Long {
val initialBlockNumber = getLatestBlockNumber()
val latestTimestamp = minerNode.execute(ethTransactions.block()).timestamp
buildNewBlock(
latestTimestamp.toLong() + blockTimeSeconds!! + 1L,
blockBuildingTimeMs,
) { false }
var latestBlockNumber = getLatestBlockNumber()
await()
.atMost(3 * blockTimeSeconds!!, TimeUnit.SECONDS)
.pollInterval(100, TimeUnit.MILLISECONDS)
.untilAsserted { assertThat(getLatestBlockNumber()).isGreaterThan(initialBlockNumber) }
.untilAsserted {
latestBlockNumber = getLatestBlockNumber()
assertThat(latestBlockNumber).isGreaterThan(initialBlockNumber)
}
return latestBlockNumber
}

protected fun buildNewBlockAndWait(blockBuildingTime: Duration): Long =
buildNewBlockAndWait(blockBuildingTime.inWholeMilliseconds)

/**
* Creates and sends a blob transaction. This method is designed to be stateless and should not
* rely on any class properties or instance methods. All required data should be passed as
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,13 @@ data class ForcedTransactionParam(
val forcedTransactionNumber: Long,
val transaction: String,
val deadlineBlockNumber: String,
)
) {
constructor(
forcedTransactionNumber: Long,
transaction: String,
deadlineBlockNumber: Long,
) : this(forcedTransactionNumber, transaction, deadlineBlockNumber.toString())
}

/**
* Request to send forced transactions.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
/*
* Copyright Consensys Software Inc.
*
* This file is dual-licensed under either the MIT license or Apache License 2.0.
* See the LICENSE-MIT and LICENSE-APACHE files in the repository root for details.
*
* SPDX-License-Identifier: MIT OR Apache-2.0
*/
package linea.plugin.acc.test.rpc.linea

import linea.plugin.acc.test.FakeChainSecurityPolicyTxValidatorPlugin
import linea.plugin.acc.test.RawTransactionHelper
import linea.plugin.acc.test.TestCommandLineOptionsBuilder
import linea.plugin.acc.test.rpc.ForcedTransactionParam
import linea.plugin.acc.test.rpc.GetForcedTransactionInclusionStatusRequest
import linea.plugin.acc.test.rpc.SendForcedRawTransactionRequest
import net.consensys.linea.config.LineaForcedTransactionCliOptions
import org.assertj.core.api.Assertions.assertThat
import org.awaitility.Awaitility.await
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.web3j.crypto.Hash
import kotlin.time.Duration.Companion.milliseconds

class ForcedTransactionChainSecurityPolicyTest : AbstractForcedTransactionTest() {
private val chainSecurityViolationBeforeDeadlineInclusionAllowance = 3

override fun getRequestedPlugins(): List<String> =
DEFAULT_REQUESTED_PLUGINS + FakeChainSecurityPolicyTxValidatorPlugin.PLUGIN_NAME

override fun getTestCliOptions(): List<String> =
TestCommandLineOptionsBuilder()
.set("--plugin-linea-module-limit-file-path=", getResourcePath("/moduleLimitsLimitless.toml"))
.set("--plugin-linea-limitless-enabled=", "true")
.set(
LineaForcedTransactionCliOptions.FORCED_TX_CHAIN_SECURITY_VIOLATION_BEFORE_DEADLINE_INCLUSION_ALLOWANCE + "=",
chainSecurityViolationBeforeDeadlineInclusionAllowance.toString(),
)
.build()

@BeforeEach
override fun setup() {
FakeChainSecurityPolicyTxValidatorPlugin.reset()
super.setup()
}

@Test
fun `should include forced tx when not blocked by security policy`() {
buildBlocksInBackground = false

val rawTx = createSignedTransfer(accounts.primaryBenefactor, accounts.secondaryBenefactor, 0)
val ftxNumber = nextForcedTxNumber()

val response = SendForcedRawTransactionRequest(
listOf(ForcedTransactionParam(ftxNumber, rawTx, DEADLINE)),
).execute(minerNode.nodeRequests())
assertThat(response.hasError()).isFalse()

buildNewBlockAndWait(500.milliseconds)

assertThat(ftxInclusionStatus(ftxNumber)).isEqualTo("Included")
}

@Test
fun `should not mine tx rejected by security layer before deadline toleration window`() {
val account1 = accounts.primaryBenefactor
val account2 = accounts.secondaryBenefactor
val account3 = accounts.thirdBenefactor
val recipient = accounts.createAccount("recipient")

val ftx1 = RawTransactionHelper.createSignedTransfer(CHAIN_ID, account1, recipient, 0)
val ftx2 = RawTransactionHelper.createSignedTransfer(CHAIN_ID, account2, recipient, 0)
val ftx3 = RawTransactionHelper.createSignedTransfer(CHAIN_ID, account3, recipient, 0)
val ftx4 = RawTransactionHelper.createSignedTransfer(CHAIN_ID, account1, recipient, 1)
val ftx5 = RawTransactionHelper.createSignedTransfer(CHAIN_ID, account2, recipient, 1)

val ftxs = listOf(ftx1, ftx2, ftx3, ftx4, ftx5)
val ftxHashes = ftxs.map(Hash::sha3)
val ftxNumbers = ftxs.map { nextForcedTxNumber() }

// Mark ftx3 as rejected by 3rd SecurityTransaction validator
FakeChainSecurityPolicyTxValidatorPlugin.blockBySender(account3.address)

val deadline = 10L
val sendResponse = SendForcedRawTransactionRequest(
listOf(
ForcedTransactionParam(ftxNumbers[0], ftx1, deadline),
ForcedTransactionParam(ftxNumbers[1], ftx2, deadline),
ForcedTransactionParam(ftxNumbers[2], ftx3, deadline),
ForcedTransactionParam(ftxNumbers[3], ftx4, deadline),
ForcedTransactionParam(ftxNumbers[4], ftx5, deadline),
),
).execute(minerNode.nodeRequests())

assertThat(sendResponse.hasError()).isFalse()
assertThat(sendResponse.result).hasSize(5)

buildNewBlockAndWait(blockBuildingTime = 300.milliseconds)
buildNewBlockAndWait(blockBuildingTime = 300.milliseconds)
buildNewBlockAndWait(blockBuildingTime = 300.milliseconds)
// assert that first ellegible tx get included but the rest are on hold because of the security layer rejection
assertThat(getFtxInclusionStatus(ftxNumbers))
.containsExactly("Included", "Included", null, null, null)

await()
.untilAsserted {
val blockNumber = buildNewBlockAndWait(blockBuildingTime = 300.milliseconds)
assertThat(blockNumber).isEqualTo(deadline - chainSecurityViolationBeforeDeadlineInclusionAllowance - 1)
}

minerNode.verify(eth.expectSuccessfulTransactionReceipt(ftxHashes[0]))
minerNode.verify(eth.expectSuccessfulTransactionReceipt(ftxHashes[1]))
assertThat(findTransactionReceipt(ftxHashes[2])).isNull()
assertThat(findTransactionReceipt(ftxHashes[3])).isNull()
assertThat(findTransactionReceipt(ftxHashes[4])).isNull()
assertThat(getFtxInclusionStatus(ftxNumbers))
.containsExactly("Included", "Included", null, null, null)

// mine 1 extra block to be within (deadline - chainSecurityViolationBeforeDeadlineInclusionAllowance)
buildNewBlockAndWait(blockBuildingTime = 300.milliseconds)
minerNode.verify(eth.expectSuccessfulTransactionReceipt(ftxHashes[2]))
minerNode.verify(eth.expectSuccessfulTransactionReceipt(ftxHashes[3]))
minerNode.verify(eth.expectSuccessfulTransactionReceipt(ftxHashes[4]))
assertThat(getFtxInclusionStatus(ftxNumbers))
.containsExactly("Included", "Included", "Included", "Included", "Included")
}

fun getFtxInclusionStatus(ftxNumbers: List<Long>): List<String?> {
return ftxNumbers.map { ftxNumber ->
GetForcedTransactionInclusionStatusRequest(ftxNumber)
.execute(minerNode.nodeRequests())
.let { response ->
response.result?.inclusionResult
?: response.error?.message?.let { throw RuntimeException(it) }
}
}
}

private fun ftxInclusionStatus(ftxNumber: Long): String? =
GetForcedTransactionInclusionStatusRequest(ftxNumber)
.execute(minerNode.nodeRequests())
.let { response ->
response.result?.inclusionResult
?: response.error?.message?.let { throw RuntimeException(it) }
}

companion object {
private const val DEADLINE = 1_000_000L
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -45,13 +45,15 @@ import org.junit.jupiter.api.Tag
import org.junit.jupiter.api.extension.ExtendWith
import org.slf4j.LoggerFactory
import org.web3j.protocol.core.DefaultBlockParameter
import org.web3j.protocol.core.methods.response.TransactionReceipt
import java.io.BufferedReader
import java.io.InputStreamReader
import java.lang.ProcessBuilder.Redirect
import java.math.BigInteger
import java.nio.charset.StandardCharsets.UTF_8
import java.util.concurrent.Executors
import java.util.concurrent.atomic.AtomicInteger
import kotlin.jvm.optionals.getOrNull

/** Base class for acceptance tests. */
@ExtendWith(AcceptanceTestBaseTestWatcher::class)
Expand Down Expand Up @@ -217,4 +219,14 @@ abstract class AcceptanceTestBase {

return newAccounts
}

fun findTransactionReceipt(txHash: String): TransactionReceipt? = ethTransactions
.getTransactionReceipt(txHash)
.execute(minerNode.nodeRequests())
.getOrNull()

fun getTransactionReceipt(txHash: String): TransactionReceipt {
return findTransactionReceipt(txHash)
?: throw AssertionError("Transaction $txHash not found in node ${minerNode.name}")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/*
* Copyright Consensys Software Inc.
*
* This file is dual-licensed under either the MIT license or Apache License 2.0.
* See the LICENSE-MIT and LICENSE-APACHE files in the repository root for details.
*
* SPDX-License-Identifier: MIT OR Apache-2.0
*/
package linea.security;

import static java.util.Objects.requireNonNull;

import java.util.function.Function;
import org.hyperledger.besu.plugin.services.txselection.TransactionEvaluationContext;

public class LineaChainSecurityPolicy implements ChainSecurityPolicy {
private Function<TransactionEvaluationContext, Boolean> shallForceIncludeTransactionFn;

public LineaChainSecurityPolicy() {}

public void init(Function<TransactionEvaluationContext, Boolean> shallForceIncludeTransactionFn) {
requireNonNull(
shallForceIncludeTransactionFn, "shallForceIncludeTransactionFn must not be null");
this.shallForceIncludeTransactionFn = shallForceIncludeTransactionFn;
}

@Override
public boolean shallForceIncludeTransaction(TransactionEvaluationContext txContext) {
return shallForceIncludeTransactionFn.apply(txContext);
}
}
Loading
Loading