Skip to content

Conversation

@daniellehrner
Copy link
Contributor

@daniellehrner daniellehrner commented Dec 10, 2025

Reload deny addresses and denied events globally in all plugins that use them at once

This PR implements issue(s) #

Checklist

  • I wrote new tests for my new core changes.
  • I have successfully ran tests, style checker and build against my new changes locally.
  • I have informed the team of any breaking changes if there are any.

Note

Centralizes address/event deny lists with reloadable shared structures and updates plugins to use them and reload in sync.

  • Core infrastructure:
    • Introduce ReloadableSet<T> and ReloadableMap<K,V> for atomic, thread-safe config reloading.
    • Extend AbstractLineaSharedPrivateOptionsPlugin with shared reloadable deny lists:
      • Addresses: sharedDeniedAddresses, sharedBundleDeniedAddresses.
      • Event filters: sharedDeniedEvents, sharedDeniedBundleEvents.
    • Add initialization from CLI-configured paths and reloadSharedDenyLists() to refresh all lists.
  • Plugins updated to use shared deny lists:
    • LineaTransactionPoolValidatorFactory/Plugin: use sharedDeniedAddresses (via ReloadableSet.getReference()); remove local state/mutators; delegate reloads to shared method.
    • LineaTransactionSelectorPlugin: use sharedDeniedEvents/sharedDeniedBundleEvents (via ReloadableMap.getReference()); delegate reloads.
    • LineaBundleEndpointsPlugin: use sharedBundleDeniedAddresses and shared reload; remove local state.

Written by Cursor Bugbot for commit 7e79af1. This will update automatically on new commits. Configure here.

@cla-assistant
Copy link

cla-assistant bot commented Dec 10, 2025

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

@daniellehrner daniellehrner requested a review from fab-10 December 10, 2025 13:13
@cla-assistant
Copy link

cla-assistant bot commented Dec 10, 2025

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

} catch (Exception e) {
log.error("Failed to reload shared deny lists", e);
return CompletableFuture.failedFuture(e);
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Missing null check in reloadSharedDenyLists may cause NPE

The reloadSharedDenyLists() method accesses the static fields sharedDeniedAddresses, sharedBundleDeniedAddresses, sharedDeniedEvents, and sharedDeniedBundleEvents without checking if they are null. These fields are only initialized in initializeSharedDenyLists() which runs during start(). If reloadConfiguration() is called before start() completes, or after stop() but before restart, it will throw a NullPointerException. The previous implementation in LineaTransactionPoolValidatorPlugin had explicit initialization checks that have been removed.

Fix in Cursor Fix in Web

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it should not possible to call the reload before start and after stop (otherwise it is an issue in Besu itself) so the null check is not required

} catch (Exception e) {
log.error("Failed to reload shared deny lists", e);
return CompletableFuture.failedFuture(e);
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Static deny list fields not reset in stop method

The stop() method resets sharedRegisterTasksDone and sharedStartTasksDone flags to false, but does not reset the static fields sharedDeniedAddresses, sharedBundleDeniedAddresses, sharedDeniedEvents, and sharedDeniedBundleEvents to null. This is inconsistent with other static fields like blockchainService and metricsSystem which are set to null. On restart, initializeSharedDenyLists() will create new ReloadableSet/ReloadableMap instances, potentially causing inconsistent state if any components still hold references to the old instances.

Fix in Cursor Fix in Web

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it makes sense to reset the new fields too

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR centralizes deny list management by introducing reloadable wrappers (ReloadableSet and ReloadableMap) and moving deny list state from individual plugins to a shared parent class (AbstractLineaSharedPrivateOptionsPlugin). This enables atomic, thread-safe configuration reloads across all plugins simultaneously.

  • Introduces ReloadableSet<T> and ReloadableMap<K,V> for thread-safe, atomic swapping of deny list contents
  • Centralizes four shared deny lists in AbstractLineaSharedPrivateOptionsPlugin: addresses, bundle addresses, events, and bundle events
  • Updates three plugins (LineaTransactionSelectorPlugin, LineaTransactionPoolValidatorPlugin, LineaBundleEndpointsPlugin) to use shared deny lists instead of local state

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
ReloadableSet.java New wrapper class providing thread-safe atomic reloading of Set-based deny lists
ReloadableMap.java New wrapper class providing thread-safe atomic reloading of Map-based deny lists
AbstractLineaSharedPrivateOptionsPlugin.java Adds shared deny list initialization and centralized reloadSharedDenyLists() method
LineaTransactionSelectorPlugin.java Removes local event deny list state, delegates to shared deny lists
LineaTransactionPoolValidatorPlugin.java Removes local reload logic, delegates to shared reloadSharedDenyLists()
LineaTransactionPoolValidatorFactory.java Replaces local AtomicReference with shared ReloadableSet, removes setter method
LineaBundleEndpointsPlugin.java Removes local bundle address deny list state, uses shared deny lists

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +108 to +122
public Set<K> keySet() {
return delegate.get().keySet();
}

@NotNull
@Override
public Collection<V> values() {
return delegate.get().values();
}

@NotNull
@Override
public Set<Entry<K, V>> entrySet() {
return delegate.get().entrySet();
}
Copy link

Copilot AI Dec 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The keySet(), values(), and entrySet() methods return views over the underlying map obtained via delegate.get(). If reload() is called while code is iterating over these views, the views will continue to reference the old map (which is correct behavior for thread-safety). However, this behavior should be documented in the class-level Javadoc to clarify that these views are stable snapshots and won't see concurrent reloads.

Copilot uses AI. Check for mistakes.
}

/** Reload the set from the configured source by invoking the reloader supplier. */
public void reload() {
Copy link

Copilot AI Dec 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The reload() method is not thread-safe if called concurrently. If two threads call reload() simultaneously, both will invoke reloader.get() and call delegate.set(), potentially causing redundant expensive reload operations (e.g., file I/O). Consider either:

  1. Making reload() synchronized to prevent concurrent execution
  2. Using compareAndSet in a loop to ensure only one thread performs the reload
  3. Or documenting that callers are responsible for synchronizing reload calls
Suggested change
public void reload() {
public synchronized void reload() {

Copilot uses AI. Check for mistakes.
}

/** Reload the map from the configured source by invoking the reloader supplier. */
public void reload() {
Copy link

Copilot AI Dec 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The reload() method is not thread-safe if called concurrently. If two threads call reload() simultaneously, both will invoke reloader.get() and call delegate.set(), potentially causing redundant expensive reload operations (e.g., file I/O). Consider either:

  1. Making reload() synchronized to prevent concurrent execution
  2. Using compareAndSet in a loop to ensure only one thread performs the reload
  3. Or documenting that callers are responsible for synchronizing reload calls
Suggested change
public void reload() {
public synchronized void reload() {

Copilot uses AI. Check for mistakes.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think Cursor comment is a good one, in general it should not matter too much if the map is reloaded twice by concurrent threads, it is just a little waste, so probably it is sufficient to document that

*
* <p>This class is immutable from the perspective of Set operations - all mutation methods throw
* {@link UnsupportedOperationException}. The underlying set can only be changed via {@link
* #reload().
Copy link

Copilot AI Dec 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing closing brace } in Javadoc @link reference. The link should be {@link #reload()} instead of {@link #reload().

Suggested change
* #reload().
* #reload()}.

Copilot uses AI. Check for mistakes.
Comment on lines +48 to +155
public class ReloadableSet<T> implements Set<T> {
private final AtomicReference<Set<T>> delegate;
private final Supplier<Set<T>> reloader;

/**
* Creates a new ReloadableSet with an initial set and a reloader function.
*
* @param initial the initial set contents
* @param reloader a supplier that provides a new set when reload is called
*/
public ReloadableSet(final Set<T> initial, final Supplier<Set<T>> reloader) {
this.delegate = new AtomicReference<>(initial);
this.reloader = reloader;
}

/** Reload the set from the configured source by invoking the reloader supplier. */
public void reload() {
delegate.set(reloader.get());
}

/**
* Get the underlying AtomicReference. This is useful for validators that expect an
* AtomicReference&lt;Set&lt;T&gt;&gt;.
*
* @return the underlying AtomicReference
*/
public AtomicReference<Set<T>> getReference() {
return delegate;
}

// Delegate all Set read methods to the underlying set

@Override
public int size() {
return delegate.get().size();
}

@Override
public boolean isEmpty() {
return delegate.get().isEmpty();
}

@Override
public boolean contains(final Object o) {
return delegate.get().contains(o);
}

@NotNull
@Override
public Iterator<T> iterator() {
return delegate.get().iterator();
}

@NotNull
@Override
public Object[] toArray() {
return delegate.get().toArray();
}

@NotNull
@Override
public <T1> T1[] toArray(@NotNull final T1[] a) {
return delegate.get().toArray(a);
}

@Override
public boolean containsAll(@NotNull final Collection<?> c) {
return delegate.get().containsAll(c);
}

// Mutation methods throw UnsupportedOperationException (immutable view)

@Override
public boolean add(final T t) {
throw new UnsupportedOperationException(
"ReloadableSet is immutable; use reload() to change contents");
}

@Override
public boolean remove(final Object o) {
throw new UnsupportedOperationException(
"ReloadableSet is immutable; use reload() to change contents");
}

@Override
public boolean addAll(@NotNull final Collection<? extends T> c) {
throw new UnsupportedOperationException(
"ReloadableSet is immutable; use reload() to change contents");
}

@Override
public boolean retainAll(@NotNull final Collection<?> c) {
throw new UnsupportedOperationException(
"ReloadableSet is immutable; use reload() to change contents");
}

@Override
public boolean removeAll(@NotNull final Collection<?> c) {
throw new UnsupportedOperationException(
"ReloadableSet is immutable; use reload() to change contents");
}

@Override
public void clear() {
throw new UnsupportedOperationException(
"ReloadableSet is immutable; use reload() to change contents");
}
}
Copy link

Copilot AI Dec 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new ReloadableSet<T> class lacks test coverage. Given that this project has comprehensive test coverage for similar classes (e.g., validators in the txpoolvalidation package), tests should be added to verify:

  • Thread-safe atomic swapping behavior during reload
  • Proper delegation of read operations to the underlying set
  • UnsupportedOperationException thrown for mutation operations
  • Behavior when the reloader supplier throws exceptions

Copilot uses AI. Check for mistakes.
Comment on lines +49 to +149
public class ReloadableMap<K, V> implements Map<K, V> {
private final AtomicReference<Map<K, V>> delegate;
private final Supplier<Map<K, V>> reloader;

/**
* Creates a new ReloadableMap with an initial map and a reloader function.
*
* @param initial the initial map contents
* @param reloader a supplier that provides a new map when reload is called
*/
public ReloadableMap(final Map<K, V> initial, final Supplier<Map<K, V>> reloader) {
this.delegate = new AtomicReference<>(initial);
this.reloader = reloader;
}

/** Reload the map from the configured source by invoking the reloader supplier. */
public void reload() {
delegate.set(reloader.get());
}

/**
* Get the underlying AtomicReference. This is useful for components that expect an
* AtomicReference&lt;Map&lt;K,V&gt;&gt;.
*
* @return the underlying AtomicReference
*/
public AtomicReference<Map<K, V>> getReference() {
return delegate;
}

// Delegate all Map read methods to the underlying map

@Override
public int size() {
return delegate.get().size();
}

@Override
public boolean isEmpty() {
return delegate.get().isEmpty();
}

@Override
public boolean containsKey(final Object key) {
return delegate.get().containsKey(key);
}

@Override
public boolean containsValue(final Object value) {
return delegate.get().containsValue(value);
}

@Override
public V get(final Object key) {
return delegate.get().get(key);
}

@NotNull
@Override
public Set<K> keySet() {
return delegate.get().keySet();
}

@NotNull
@Override
public Collection<V> values() {
return delegate.get().values();
}

@NotNull
@Override
public Set<Entry<K, V>> entrySet() {
return delegate.get().entrySet();
}

// Mutation methods throw UnsupportedOperationException (immutable view)

@Override
public V put(final K key, final V value) {
throw new UnsupportedOperationException(
"ReloadableMap is immutable; use reload() to change contents");
}

@Override
public V remove(final Object key) {
throw new UnsupportedOperationException(
"ReloadableMap is immutable; use reload() to change contents");
}

@Override
public void putAll(@NotNull final Map<? extends K, ? extends V> m) {
throw new UnsupportedOperationException(
"ReloadableMap is immutable; use reload() to change contents");
}

@Override
public void clear() {
throw new UnsupportedOperationException(
"ReloadableMap is immutable; use reload() to change contents");
}
}
Copy link

Copilot AI Dec 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new ReloadableMap<K,V> class lacks test coverage. Given that this project has comprehensive test coverage for similar classes, tests should be added to verify:

  • Thread-safe atomic swapping behavior during reload
  • Proper delegation of read operations to the underlying map
  • UnsupportedOperationException thrown for mutation operations
  • Behavior when the reloader supplier throws exceptions

Copilot uses AI. Check for mistakes.
Comment on lines +332 to +343
protected CompletableFuture<Void> reloadSharedDenyLists() {
try {
sharedDeniedAddresses.reload();
sharedBundleDeniedAddresses.reload();
sharedDeniedEvents.reload();
sharedDeniedBundleEvents.reload();
log.info("Successfully reloaded all shared deny lists");
return CompletableFuture.completedFuture(null);
} catch (Exception e) {
log.error("Failed to reload shared deny lists", e);
return CompletableFuture.failedFuture(e);
}
Copy link

Copilot AI Dec 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The reloadSharedDenyLists() method reloads all four deny lists sequentially without atomicity. If one reload fails (e.g., sharedDeniedEvents.reload() throws an exception), the earlier reloads (e.g., sharedDeniedAddresses, sharedBundleDeniedAddresses) will have succeeded, leaving the system with partially updated deny lists. Consider:

  1. Wrapping all reload operations in a transaction-like pattern where you load all new values first, then atomically swap them all only if all succeed
  2. Or at minimum, document this behavior and its implications in the method's Javadoc

Copilot uses AI. Check for mistakes.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very good point, since deny lists are critical, we should avoid any possible inconsistent state

Comment on lines +97 to +99
public Iterator<T> iterator() {
return delegate.get().iterator();
}
Copy link

Copilot AI Dec 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The iterator() method returns an iterator over the underlying set obtained via delegate.get(). If reload() is called while code is iterating over the set, the iterator will continue to reference the old set (which is correct behavior for thread-safety). However, this behavior should be documented in the class-level or method-level Javadoc to clarify that iterators are stable snapshots and won't see concurrent reloads.

Copilot uses AI. Check for mistakes.
}

/** Reload the map from the configured source by invoking the reloader supplier. */
public void reload() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think Cursor comment is a good one, in general it should not matter too much if the map is reloaded twice by concurrent threads, it is just a little waste, so probably it is sufficient to document that

import java.util.Set;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Supplier;
import org.jetbrains.annotations.NotNull;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

as in Besu, could you use a more generic not null assertion?

Comment on lines +59 to +62
public ReloadableMap(final Map<K, V> initial, final Supplier<Map<K, V>> reloader) {
this.delegate = new AtomicReference<>(initial);
this.reloader = reloader;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a reason to pass a separate initial value, instead of just using the supplier for it, like in the code below?

Suggested change
public ReloadableMap(final Map<K, V> initial, final Supplier<Map<K, V>> reloader) {
this.delegate = new AtomicReference<>(initial);
this.reloader = reloader;
}
public ReloadableMap(final Supplier<Map<K, V>> reloader) {
this.delegate = new AtomicReference<>(reloader.get());
this.reloader = reloader;
}

Comment on lines +58 to +61
public ReloadableSet(final Set<T> initial, final Supplier<Set<T>> reloader) {
this.delegate = new AtomicReference<>(initial);
this.reloader = reloader;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

s.a.

Comment on lines +68 to +76
/**
* Get the underlying AtomicReference. This is useful for validators that expect an
* AtomicReference&lt;Set&lt;T&gt;&gt;.
*
* @return the underlying AtomicReference
*/
public AtomicReference<Set<T>> getReference() {
return delegate;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suggest to remove this method, since it is now possible and cleaner to always pass the Map or the Set, as it was before introducing the AtomicReference

final var validators =
new PluginTransactionPoolValidator[] {
new AllowedAddressValidator(bundleDeniedAddresses),
new AllowedAddressValidator(sharedBundleDeniedAddresses.getReference()),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

with the nice re-loadable map, parameter passaga can be simplified again, for easier code, just passing the Map here

Suggested change
new AllowedAddressValidator(sharedBundleDeniedAddresses.getReference()),
new AllowedAddressValidator(sharedBundleDeniedAddresses),

}

@Override
public CompletableFuture<Void> reloadConfiguration() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would push the shared configuration reload mechanism even forward, to the point that individual plugins themselves are not responsible for the configuration lifecycle, since it is fully managed in the shared abstract class.
I know that you raised the point that in that way, we lose the granularity, and reloading a single plugin will trigger the reload of everything, but since Sequencer plugins are coupled together, currently it is more important to keep the configuration aligned, and avoiding bugs in a single plugin that could result in partial reloads, then we can well document that reload behavior, so if there are no stronger arguments against I prefer to push the reloadConfiguration in the abstract class and remove it in the plugin classes

} catch (Exception e) {
log.error("Failed to reload shared deny lists", e);
return CompletableFuture.failedFuture(e);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it should not possible to call the reload before start and after stop (otherwise it is an issue in Besu itself) so the null check is not required

} catch (Exception e) {
log.error("Failed to reload shared deny lists", e);
return CompletableFuture.failedFuture(e);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it makes sense to reset the new fields too

Comment on lines +332 to +343
protected CompletableFuture<Void> reloadSharedDenyLists() {
try {
sharedDeniedAddresses.reload();
sharedBundleDeniedAddresses.reload();
sharedDeniedEvents.reload();
sharedDeniedBundleEvents.reload();
log.info("Successfully reloaded all shared deny lists");
return CompletableFuture.completedFuture(null);
} catch (Exception e) {
log.error("Failed to reload shared deny lists", e);
return CompletableFuture.failedFuture(e);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very good point, since deny lists are critical, we should avoid any possible inconsistent state

fluentcrafter pushed a commit that referenced this pull request Dec 15, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants