Merge branch 'main' into 7311-add-peertask-foundation-code

pull/7628/head
Matilda-Clerke 2 months ago committed by GitHub
commit 6e349e16f8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 5
      CHANGELOG.md
  2. 3
      acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/node/ThreadBesuNodeRunner.java
  3. 51
      acceptance-tests/test-plugins/src/main/java/org/hyperledger/besu/tests/acceptance/plugins/TestPicoCLIPlugin.java
  4. 266
      acceptance-tests/test-plugins/src/test/java/org/hyperledger/besu/services/BesuPluginContextImplTest.java
  5. 6
      besu/src/main/java/org/hyperledger/besu/cli/BesuCommand.java
  6. 3
      besu/src/main/java/org/hyperledger/besu/cli/DefaultCommandValues.java
  7. 30
      besu/src/main/java/org/hyperledger/besu/cli/options/stable/PluginsConfigurationOptions.java
  8. 21
      besu/src/main/java/org/hyperledger/besu/controller/QbftBesuControllerBuilder.java
  9. 91
      besu/src/main/java/org/hyperledger/besu/services/BesuPluginContextImpl.java
  10. 5
      besu/src/test/java/org/hyperledger/besu/cli/CommandTestAbstract.java
  11. 51
      besu/src/test/java/org/hyperledger/besu/cli/PluginsOptionsTest.java
  12. 1
      besu/src/test/resources/everything_config.toml
  13. 7
      config/src/main/java/org/hyperledger/besu/config/BftConfigOptions.java
  14. 13
      config/src/main/java/org/hyperledger/besu/config/BftFork.java
  15. 7
      config/src/main/java/org/hyperledger/besu/config/GenesisConfigOptions.java
  16. 8
      config/src/main/java/org/hyperledger/besu/config/JsonBftConfigOptions.java
  17. 11
      config/src/main/java/org/hyperledger/besu/config/JsonGenesisConfigOptions.java
  18. 5
      config/src/main/java/org/hyperledger/besu/config/StubGenesisConfigOptions.java
  19. 27
      config/src/test/java/org/hyperledger/besu/config/GenesisConfigOptionsTest.java
  20. 28
      config/src/test/java/org/hyperledger/besu/config/JsonBftConfigOptionsTest.java
  21. 83
      consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/BlockTimer.java
  22. 16
      consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/MutableBftConfigOptions.java
  23. 31
      consensus/common/src/test/java/org/hyperledger/besu/consensus/common/ForksScheduleFactoryTest.java
  24. 82
      consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/BlockTimerTest.java
  25. 1
      consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/QbftForksSchedulesFactory.java
  26. 53
      consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/statemachine/QbftBlockHeightManager.java
  27. 22
      consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/statemachine/QbftRound.java
  28. 20
      consensus/qbft/src/test/java/org/hyperledger/besu/consensus/qbft/MutableQbftConfigOptionsTest.java
  29. 26
      consensus/qbft/src/test/java/org/hyperledger/besu/consensus/qbft/statemachine/QbftBlockHeightManagerTest.java
  30. 106
      consensus/qbft/src/test/java/org/hyperledger/besu/consensus/qbft/statemachine/QbftRoundTest.java
  31. 3
      consensus/qbft/src/test/java/org/hyperledger/besu/consensus/qbft/validator/QbftForksSchedulesFactoryTest.java
  32. 12
      ethereum/core/src/main/java/org/hyperledger/besu/ethereum/core/MiningParameters.java
  33. 18
      ethereum/core/src/main/java/org/hyperledger/besu/ethereum/core/plugins/PluginConfiguration.java
  34. 16
      ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/MainnetProtocolSpecs.java
  35. 9
      ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/requests/ConsolidationRequestProcessor.java
  36. 21
      ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/requests/MainnetRequestsValidator.java
  37. 61
      ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/requests/RequestContractAddresses.java
  38. 11
      ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/PragueRequestsValidatorTest.java
  39. 3
      ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/sync/tasks/PersistBlockTask.java
  40. 0
      testutil/src/main/resources/log4j2-test.xml

@ -1,15 +1,20 @@
# Changelog # Changelog
## [Unreleased] ## [Unreleased]
- Add configuration of Consolidation Request Contract Address via genesis configuration [#7647](https://github.com/hyperledger/besu/pull/7647)
### Upcoming Breaking Changes ### Upcoming Breaking Changes
- k8s (KUBERNETES) Nat method is now deprecated and will be removed in a future release - k8s (KUBERNETES) Nat method is now deprecated and will be removed in a future release
### Breaking Changes ### Breaking Changes
- Besu will now fail to start if any plugins encounter errors during initialization. To allow Besu to continue running despite plugin errors, use the `--plugin-continue-on-error` option. [#7662](https://github.com/hyperledger/besu/pull/7662)
### Additions and Improvements ### Additions and Improvements
- Remove privacy test classes support [#7569](https://github.com/hyperledger/besu/pull/7569) - Remove privacy test classes support [#7569](https://github.com/hyperledger/besu/pull/7569)
- Add Blob Transaction Metrics [#7622](https://github.com/hyperledger/besu/pull/7622) - Add Blob Transaction Metrics [#7622](https://github.com/hyperledger/besu/pull/7622)
- Implemented support for emptyBlockPeriodSeconds in QBFT [#6965](https://github.com/hyperledger/besu/pull/6965)
### Bug fixes ### Bug fixes
- Fix mounted data path directory permissions for besu user [#7575](https://github.com/hyperledger/besu/pull/7575) - Fix mounted data path directory permissions for besu user [#7575](https://github.com/hyperledger/besu/pull/7575)

@ -503,8 +503,9 @@ public class ThreadBesuNodeRunner implements BesuNodeRunner {
besuPluginContext.addService(PermissioningService.class, permissioningService); besuPluginContext.addService(PermissioningService.class, permissioningService);
besuPluginContext.addService(PrivacyPluginService.class, new PrivacyPluginServiceImpl()); besuPluginContext.addService(PrivacyPluginService.class, new PrivacyPluginServiceImpl());
besuPluginContext.registerPlugins( besuPluginContext.initialize(
new PluginConfiguration.Builder().pluginsDir(pluginsPath).build()); new PluginConfiguration.Builder().pluginsDir(pluginsPath).build());
besuPluginContext.registerPlugins();
commandLine.parseArgs(extraCLIOptions.toArray(new String[0])); commandLine.parseArgs(extraCLIOptions.toArray(new String[0]));
// register built-in plugins // register built-in plugins

@ -1,5 +1,5 @@
/* /*
* Copyright ConsenSys AG. * 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 * 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 * the License. You may obtain a copy of the License at
@ -32,16 +32,25 @@ import picocli.CommandLine.Option;
public class TestPicoCLIPlugin implements BesuPlugin { public class TestPicoCLIPlugin implements BesuPlugin {
private static final Logger LOG = LoggerFactory.getLogger(TestPicoCLIPlugin.class); private static final Logger LOG = LoggerFactory.getLogger(TestPicoCLIPlugin.class);
private static final String UNSET = "UNSET";
private static final String FAIL_REGISTER = "FAILREGISTER";
private static final String FAIL_BEFORE_EXTERNAL_SERVICES = "FAILBEFOREEXTERNALSERVICES";
private static final String FAIL_START = "FAILSTART";
private static final String FAIL_AFTER_EXTERNAL_SERVICE_POST_MAIN_LOOP =
"FAILAFTEREXTERNALSERVICEPOSTMAINLOOP";
private static final String FAIL_STOP = "FAILSTOP";
private static final String PLUGIN_LIFECYCLE_PREFIX = "pluginLifecycle.";
@Option( @Option(
names = {"--Xplugin-test-option"}, names = {"--Xplugin-test-option"},
hidden = true, hidden = true,
defaultValue = "UNSET") defaultValue = UNSET)
String testOption = System.getProperty("testPicoCLIPlugin.testOption"); String testOption = System.getProperty("testPicoCLIPlugin.testOption");
@Option( @Option(
names = {"--plugin-test-stable-option"}, names = {"--plugin-test-stable-option"},
hidden = true, hidden = true,
defaultValue = "UNSET") defaultValue = UNSET)
String stableOption = ""; String stableOption = "";
private String state = "uninited"; private String state = "uninited";
@ -52,7 +61,7 @@ public class TestPicoCLIPlugin implements BesuPlugin {
LOG.info("Registering. Test Option is '{}'", testOption); LOG.info("Registering. Test Option is '{}'", testOption);
state = "registering"; state = "registering";
if ("FAILREGISTER".equals(testOption)) { if (FAIL_REGISTER.equals(testOption)) {
state = "failregister"; state = "failregister";
throw new RuntimeException("I was told to fail at registration"); throw new RuntimeException("I was told to fail at registration");
} }
@ -66,12 +75,26 @@ public class TestPicoCLIPlugin implements BesuPlugin {
state = "registered"; state = "registered";
} }
@Override
public void beforeExternalServices() {
LOG.info("Before external services. Test Option is '{}'", testOption);
state = "beforeExternalServices";
if (FAIL_BEFORE_EXTERNAL_SERVICES.equals(testOption)) {
state = "failbeforeExternalServices";
throw new RuntimeException("I was told to fail before external services");
}
writeSignal("beforeExternalServices");
state = "beforeExternalServicesFinished";
}
@Override @Override
public void start() { public void start() {
LOG.info("Starting. Test Option is '{}'", testOption); LOG.info("Starting. Test Option is '{}'", testOption);
state = "starting"; state = "starting";
if ("FAILSTART".equals(testOption)) { if (FAIL_START.equals(testOption)) {
state = "failstart"; state = "failstart";
throw new RuntimeException("I was told to fail at startup"); throw new RuntimeException("I was told to fail at startup");
} }
@ -80,12 +103,26 @@ public class TestPicoCLIPlugin implements BesuPlugin {
state = "started"; state = "started";
} }
@Override
public void afterExternalServicePostMainLoop() {
LOG.info("After external services post main loop. Test Option is '{}'", testOption);
state = "afterExternalServicePostMainLoop";
if (FAIL_AFTER_EXTERNAL_SERVICE_POST_MAIN_LOOP.equals(testOption)) {
state = "failafterExternalServicePostMainLoop";
throw new RuntimeException("I was told to fail after external services post main loop");
}
writeSignal("afterExternalServicePostMainLoop");
state = "afterExternalServicePostMainLoopFinished";
}
@Override @Override
public void stop() { public void stop() {
LOG.info("Stopping. Test Option is '{}'", testOption); LOG.info("Stopping. Test Option is '{}'", testOption);
state = "stopping"; state = "stopping";
if ("FAILSTOP".equals(testOption)) { if (FAIL_STOP.equals(testOption)) {
state = "failstop"; state = "failstop";
throw new RuntimeException("I was told to fail at stop"); throw new RuntimeException("I was told to fail at stop");
} }
@ -103,7 +140,7 @@ public class TestPicoCLIPlugin implements BesuPlugin {
@SuppressWarnings("ResultOfMethodCallIgnored") @SuppressWarnings("ResultOfMethodCallIgnored")
private void writeSignal(final String signal) { private void writeSignal(final String signal) {
try { try {
final File callbackFile = new File(callbackDir, "pluginLifecycle." + signal); final File callbackFile = new File(callbackDir, PLUGIN_LIFECYCLE_PREFIX + signal);
if (!callbackFile.getParentFile().exists()) { if (!callbackFile.getParentFile().exists()) {
callbackFile.getParentFile().mkdirs(); callbackFile.getParentFile().mkdirs();
callbackFile.getParentFile().deleteOnExit(); callbackFile.getParentFile().deleteOnExit();

@ -1,5 +1,5 @@
/* /*
* Copyright ConsenSys AG. * 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 * 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 * the License. You may obtain a copy of the License at
@ -40,8 +40,31 @@ import org.junit.jupiter.api.Test;
public class BesuPluginContextImplTest { public class BesuPluginContextImplTest {
private static final Path DEFAULT_PLUGIN_DIRECTORY = Paths.get("."); private static final Path DEFAULT_PLUGIN_DIRECTORY = Paths.get(".");
private static final String TEST_PICO_CLI_PLUGIN = "TestPicoCLIPlugin";
private static final String TEST_PICO_CLI_PLUGIN_TEST_OPTION = "testPicoCLIPlugin.testOption";
private static final String FAIL_REGISTER = "FAILREGISTER";
private static final String FAIL_START = "FAILSTART";
private static final String FAIL_STOP = "FAILSTOP";
private static final String FAIL_BEFORE_EXTERNAL_SERVICES = "FAILBEFOREEXTERNALSERVICES";
private static final String FAIL_BEFORE_MAIN_LOOP = "FAILBEFOREMAINLOOP";
private static final String FAIL_AFTER_EXTERNAL_SERVICE_POST_MAIN_LOOP =
"FAILAFTEREXTERNALSERVICEPOSTMAINLOOP";
private static final String NON_EXISTENT_PLUGIN = "NonExistentPlugin";
private static final String REGISTERED = "registered";
private static final String STARTED = "started";
private static final String STOPPED = "stopped";
private static final String FAIL_START_STATE = "failstart";
private static final String FAIL_STOP_STATE = "failstop";
private BesuPluginContextImpl contextImpl; private BesuPluginContextImpl contextImpl;
private static final PluginConfiguration DEFAULT_CONFIGURATION =
PluginConfiguration.builder()
.pluginsDir(DEFAULT_PLUGIN_DIRECTORY)
.externalPluginsEnabled(true)
.continueOnPluginError(true)
.build();
@BeforeAll @BeforeAll
public static void createFakePluginDir() throws IOException { public static void createFakePluginDir() throws IOException {
if (System.getProperty("besu.plugins.dir") == null) { if (System.getProperty("besu.plugins.dir") == null) {
@ -53,7 +76,7 @@ public class BesuPluginContextImplTest {
@AfterEach @AfterEach
public void clearTestPluginState() { public void clearTestPluginState() {
System.clearProperty("testPicoCLIPlugin.testOption"); System.clearProperty(TEST_PICO_CLI_PLUGIN_TEST_OPTION);
} }
@BeforeEach @BeforeEach
@ -64,31 +87,31 @@ public class BesuPluginContextImplTest {
@Test @Test
public void verifyEverythingGoesSmoothly() { public void verifyEverythingGoesSmoothly() {
assertThat(contextImpl.getRegisteredPlugins()).isEmpty(); assertThat(contextImpl.getRegisteredPlugins()).isEmpty();
contextImpl.registerPlugins( contextImpl.initialize(DEFAULT_CONFIGURATION);
PluginConfiguration.builder().pluginsDir(DEFAULT_PLUGIN_DIRECTORY).build()); contextImpl.registerPlugins();
assertThat(contextImpl.getRegisteredPlugins()).isNotEmpty(); assertThat(contextImpl.getRegisteredPlugins()).isNotEmpty();
final Optional<TestPicoCLIPlugin> testPluginOptional = final Optional<TestPicoCLIPlugin> testPluginOptional =
findTestPlugin(contextImpl.getRegisteredPlugins(), TestPicoCLIPlugin.class); findTestPlugin(contextImpl.getRegisteredPlugins(), TestPicoCLIPlugin.class);
assertThat(testPluginOptional).isPresent(); assertThat(testPluginOptional).isPresent();
final TestPicoCLIPlugin testPicoCLIPlugin = testPluginOptional.get(); final TestPicoCLIPlugin testPicoCLIPlugin = testPluginOptional.get();
assertThat(testPicoCLIPlugin.getState()).isEqualTo("registered"); assertThat(testPicoCLIPlugin.getState()).isEqualTo(REGISTERED);
contextImpl.beforeExternalServices(); contextImpl.beforeExternalServices();
contextImpl.startPlugins(); contextImpl.startPlugins();
assertThat(testPicoCLIPlugin.getState()).isEqualTo("started"); assertThat(testPicoCLIPlugin.getState()).isEqualTo(STARTED);
contextImpl.stopPlugins(); contextImpl.stopPlugins();
assertThat(testPicoCLIPlugin.getState()).isEqualTo("stopped"); assertThat(testPicoCLIPlugin.getState()).isEqualTo(STOPPED);
} }
@Test @Test
public void registrationErrorsHandledSmoothly() { public void registrationErrorsHandledSmoothly() {
System.setProperty("testPicoCLIPlugin.testOption", "FAILREGISTER"); System.setProperty(TEST_PICO_CLI_PLUGIN_TEST_OPTION, FAIL_REGISTER);
assertThat(contextImpl.getRegisteredPlugins()).isEmpty(); assertThat(contextImpl.getRegisteredPlugins()).isEmpty();
contextImpl.registerPlugins( contextImpl.initialize(DEFAULT_CONFIGURATION);
PluginConfiguration.builder().pluginsDir(DEFAULT_PLUGIN_DIRECTORY).build()); contextImpl.registerPlugins();
assertThat(contextImpl.getRegisteredPlugins()).isNotInstanceOfAny(TestPicoCLIPlugin.class); assertThat(contextImpl.getRegisteredPlugins()).isNotInstanceOfAny(TestPicoCLIPlugin.class);
contextImpl.beforeExternalServices(); contextImpl.beforeExternalServices();
@ -103,11 +126,11 @@ public class BesuPluginContextImplTest {
@Test @Test
public void startErrorsHandledSmoothly() { public void startErrorsHandledSmoothly() {
System.setProperty("testPicoCLIPlugin.testOption", "FAILSTART"); System.setProperty(TEST_PICO_CLI_PLUGIN_TEST_OPTION, FAIL_START);
assertThat(contextImpl.getRegisteredPlugins()).isEmpty(); assertThat(contextImpl.getRegisteredPlugins()).isEmpty();
contextImpl.registerPlugins( contextImpl.initialize(DEFAULT_CONFIGURATION);
PluginConfiguration.builder().pluginsDir(DEFAULT_PLUGIN_DIRECTORY).build()); contextImpl.registerPlugins();
assertThat(contextImpl.getRegisteredPlugins()) assertThat(contextImpl.getRegisteredPlugins())
.extracting("class") .extracting("class")
.contains(TestPicoCLIPlugin.class); .contains(TestPicoCLIPlugin.class);
@ -116,11 +139,11 @@ public class BesuPluginContextImplTest {
findTestPlugin(contextImpl.getRegisteredPlugins(), TestPicoCLIPlugin.class); findTestPlugin(contextImpl.getRegisteredPlugins(), TestPicoCLIPlugin.class);
assertThat(testPluginOptional).isPresent(); assertThat(testPluginOptional).isPresent();
final TestPicoCLIPlugin testPicoCLIPlugin = testPluginOptional.get(); final TestPicoCLIPlugin testPicoCLIPlugin = testPluginOptional.get();
assertThat(testPicoCLIPlugin.getState()).isEqualTo("registered"); assertThat(testPicoCLIPlugin.getState()).isEqualTo(REGISTERED);
contextImpl.beforeExternalServices(); contextImpl.beforeExternalServices();
contextImpl.startPlugins(); contextImpl.startPlugins();
assertThat(testPicoCLIPlugin.getState()).isEqualTo("failstart"); assertThat(testPicoCLIPlugin.getState()).isEqualTo(FAIL_START_STATE);
assertThat(contextImpl.getRegisteredPlugins()).isNotInstanceOfAny(TestPicoCLIPlugin.class); assertThat(contextImpl.getRegisteredPlugins()).isNotInstanceOfAny(TestPicoCLIPlugin.class);
contextImpl.stopPlugins(); contextImpl.stopPlugins();
@ -129,11 +152,11 @@ public class BesuPluginContextImplTest {
@Test @Test
public void stopErrorsHandledSmoothly() { public void stopErrorsHandledSmoothly() {
System.setProperty("testPicoCLIPlugin.testOption", "FAILSTOP"); System.setProperty(TEST_PICO_CLI_PLUGIN_TEST_OPTION, FAIL_STOP);
assertThat(contextImpl.getRegisteredPlugins()).isEmpty(); assertThat(contextImpl.getRegisteredPlugins()).isEmpty();
contextImpl.registerPlugins( contextImpl.initialize(DEFAULT_CONFIGURATION);
PluginConfiguration.builder().pluginsDir(DEFAULT_PLUGIN_DIRECTORY).build()); contextImpl.registerPlugins();
assertThat(contextImpl.getRegisteredPlugins()) assertThat(contextImpl.getRegisteredPlugins())
.extracting("class") .extracting("class")
.contains(TestPicoCLIPlugin.class); .contains(TestPicoCLIPlugin.class);
@ -142,22 +165,20 @@ public class BesuPluginContextImplTest {
findTestPlugin(contextImpl.getRegisteredPlugins(), TestPicoCLIPlugin.class); findTestPlugin(contextImpl.getRegisteredPlugins(), TestPicoCLIPlugin.class);
assertThat(testPluginOptional).isPresent(); assertThat(testPluginOptional).isPresent();
final TestPicoCLIPlugin testPicoCLIPlugin = testPluginOptional.get(); final TestPicoCLIPlugin testPicoCLIPlugin = testPluginOptional.get();
assertThat(testPicoCLIPlugin.getState()).isEqualTo("registered"); assertThat(testPicoCLIPlugin.getState()).isEqualTo(REGISTERED);
contextImpl.beforeExternalServices(); contextImpl.beforeExternalServices();
contextImpl.startPlugins(); contextImpl.startPlugins();
assertThat(testPicoCLIPlugin.getState()).isEqualTo("started"); assertThat(testPicoCLIPlugin.getState()).isEqualTo(STARTED);
contextImpl.stopPlugins(); contextImpl.stopPlugins();
assertThat(testPicoCLIPlugin.getState()).isEqualTo("failstop"); assertThat(testPicoCLIPlugin.getState()).isEqualTo(FAIL_STOP_STATE);
} }
@Test @Test
public void lifecycleExceptions() throws Throwable { public void lifecycleExceptions() throws Throwable {
final ThrowableAssert.ThrowingCallable registerPlugins = contextImpl.initialize(DEFAULT_CONFIGURATION);
() -> final ThrowableAssert.ThrowingCallable registerPlugins = () -> contextImpl.registerPlugins();
contextImpl.registerPlugins(
PluginConfiguration.builder().pluginsDir(DEFAULT_PLUGIN_DIRECTORY).build());
assertThatExceptionOfType(IllegalStateException.class).isThrownBy(contextImpl::startPlugins); assertThatExceptionOfType(IllegalStateException.class).isThrownBy(contextImpl::startPlugins);
assertThatExceptionOfType(IllegalStateException.class).isThrownBy(contextImpl::stopPlugins); assertThatExceptionOfType(IllegalStateException.class).isThrownBy(contextImpl::stopPlugins);
@ -179,30 +200,27 @@ public class BesuPluginContextImplTest {
@Test @Test
public void shouldRegisterAllPluginsWhenNoPluginsOption() { public void shouldRegisterAllPluginsWhenNoPluginsOption() {
final PluginConfiguration config =
PluginConfiguration.builder().pluginsDir(DEFAULT_PLUGIN_DIRECTORY).build();
assertThat(contextImpl.getRegisteredPlugins()).isEmpty(); assertThat(contextImpl.getRegisteredPlugins()).isEmpty();
contextImpl.registerPlugins(config); contextImpl.initialize(DEFAULT_CONFIGURATION);
contextImpl.registerPlugins();
final Optional<TestPicoCLIPlugin> testPluginOptional = final Optional<TestPicoCLIPlugin> testPluginOptional =
findTestPlugin(contextImpl.getRegisteredPlugins(), TestPicoCLIPlugin.class); findTestPlugin(contextImpl.getRegisteredPlugins(), TestPicoCLIPlugin.class);
assertThat(testPluginOptional).isPresent(); assertThat(testPluginOptional).isPresent();
final TestPicoCLIPlugin testPicoCLIPlugin = testPluginOptional.get(); final TestPicoCLIPlugin testPicoCLIPlugin = testPluginOptional.get();
assertThat(testPicoCLIPlugin.getState()).isEqualTo("registered"); assertThat(testPicoCLIPlugin.getState()).isEqualTo(REGISTERED);
} }
@Test @Test
public void shouldRegisterOnlySpecifiedPluginWhenPluginsOptionIsSet() { public void shouldRegisterOnlySpecifiedPluginWhenPluginsOptionIsSet() {
final PluginConfiguration config = createConfigurationForSpecificPlugin("TestPicoCLIPlugin");
assertThat(contextImpl.getRegisteredPlugins()).isEmpty(); assertThat(contextImpl.getRegisteredPlugins()).isEmpty();
contextImpl.registerPlugins(config); contextImpl.initialize(createConfigurationForSpecificPlugin(TEST_PICO_CLI_PLUGIN));
contextImpl.registerPlugins();
final Optional<TestPicoCLIPlugin> requestedPlugin = final Optional<TestPicoCLIPlugin> requestedPlugin =
findTestPlugin(contextImpl.getRegisteredPlugins(), TestPicoCLIPlugin.class); findTestPlugin(contextImpl.getRegisteredPlugins(), TestPicoCLIPlugin.class);
assertThat(requestedPlugin).isPresent(); assertThat(requestedPlugin).isPresent();
assertThat(requestedPlugin.get().getState()).isEqualTo("registered"); assertThat(requestedPlugin.get().getState()).isEqualTo(REGISTERED);
final Optional<TestPicoCLIPlugin> nonRequestedPlugin = final Optional<TestPicoCLIPlugin> nonRequestedPlugin =
findTestPlugin(contextImpl.getRegisteredPlugins(), TestBesuEventsPlugin.class); findTestPlugin(contextImpl.getRegisteredPlugins(), TestBesuEventsPlugin.class);
@ -212,9 +230,9 @@ public class BesuPluginContextImplTest {
@Test @Test
public void shouldNotRegisterUnspecifiedPluginsWhenPluginsOptionIsSet() { public void shouldNotRegisterUnspecifiedPluginsWhenPluginsOptionIsSet() {
final PluginConfiguration config = createConfigurationForSpecificPlugin("TestPicoCLIPlugin");
assertThat(contextImpl.getRegisteredPlugins()).isEmpty(); assertThat(contextImpl.getRegisteredPlugins()).isEmpty();
contextImpl.registerPlugins(config); contextImpl.initialize(createConfigurationForSpecificPlugin(TEST_PICO_CLI_PLUGIN));
contextImpl.registerPlugins();
final Optional<TestPicoCLIPlugin> nonRequestedPlugin = final Optional<TestPicoCLIPlugin> nonRequestedPlugin =
findTestPlugin(contextImpl.getRegisteredPlugins(), TestBesuEventsPlugin.class); findTestPlugin(contextImpl.getRegisteredPlugins(), TestBesuEventsPlugin.class);
@ -223,13 +241,12 @@ public class BesuPluginContextImplTest {
@Test @Test
void shouldThrowExceptionIfExplicitlySpecifiedPluginNotFound() { void shouldThrowExceptionIfExplicitlySpecifiedPluginNotFound() {
PluginConfiguration config = createConfigurationForSpecificPlugin("NonExistentPlugin"); contextImpl.initialize(createConfigurationForSpecificPlugin(NON_EXISTENT_PLUGIN));
String exceptionMessage = String exceptionMessage =
assertThrows(NoSuchElementException.class, () -> contextImpl.registerPlugins(config)) assertThrows(NoSuchElementException.class, () -> contextImpl.registerPlugins())
.getMessage(); .getMessage();
final String expectedMessage = final String expectedMessage =
"The following requested plugins were not found: NonExistentPlugin"; "The following requested plugins were not found: " + NON_EXISTENT_PLUGIN;
assertThat(exceptionMessage).isEqualTo(expectedMessage); assertThat(exceptionMessage).isEqualTo(expectedMessage);
assertThat(contextImpl.getRegisteredPlugins()).isEmpty(); assertThat(contextImpl.getRegisteredPlugins()).isEmpty();
} }
@ -241,19 +258,180 @@ public class BesuPluginContextImplTest {
.pluginsDir(DEFAULT_PLUGIN_DIRECTORY) .pluginsDir(DEFAULT_PLUGIN_DIRECTORY)
.externalPluginsEnabled(false) .externalPluginsEnabled(false)
.build(); .build();
contextImpl.registerPlugins(config); contextImpl.initialize(config);
contextImpl.registerPlugins();
assertThat(contextImpl.getRegisteredPlugins().isEmpty()).isTrue(); assertThat(contextImpl.getRegisteredPlugins().isEmpty()).isTrue();
} }
@Test @Test
void shouldRegisterPluginsIfExternalPluginsEnabled() { void shouldRegisterPluginsIfExternalPluginsEnabled() {
contextImpl.initialize(DEFAULT_CONFIGURATION);
contextImpl.registerPlugins();
assertThat(contextImpl.getRegisteredPlugins().isEmpty()).isFalse();
}
@Test
void shouldHaltOnRegisterErrorWhenFlagIsFalse() {
System.setProperty(TEST_PICO_CLI_PLUGIN_TEST_OPTION, FAIL_REGISTER);
PluginConfiguration config = PluginConfiguration config =
PluginConfiguration.builder() PluginConfiguration.builder()
.requestedPlugins(List.of(new PluginInfo(TEST_PICO_CLI_PLUGIN)))
.pluginsDir(DEFAULT_PLUGIN_DIRECTORY) .pluginsDir(DEFAULT_PLUGIN_DIRECTORY)
.externalPluginsEnabled(true) .continueOnPluginError(false)
.build(); .build();
contextImpl.registerPlugins(config);
assertThat(contextImpl.getRegisteredPlugins().isEmpty()).isFalse(); contextImpl.initialize(config);
String errorMessage =
String.format("Error registering plugin of type %s", TestPicoCLIPlugin.class.getName());
assertThatExceptionOfType(RuntimeException.class)
.isThrownBy(() -> contextImpl.registerPlugins())
.withMessageContaining(errorMessage);
}
@Test
void shouldNotHaltOnRegisterErrorWhenFlagIsTrue() {
System.setProperty(TEST_PICO_CLI_PLUGIN_TEST_OPTION, FAIL_REGISTER);
PluginConfiguration config =
PluginConfiguration.builder()
.pluginsDir(DEFAULT_PLUGIN_DIRECTORY)
.continueOnPluginError(true)
.build();
contextImpl.initialize(config);
contextImpl.registerPlugins();
assertThat(contextImpl.getRegisteredPlugins()).isNotInstanceOfAny(TestPicoCLIPlugin.class);
}
@Test
void shouldHaltOnBeforeExternalServicesErrorWhenFlagIsFalse() {
System.setProperty(TEST_PICO_CLI_PLUGIN_TEST_OPTION, FAIL_BEFORE_EXTERNAL_SERVICES);
PluginConfiguration config =
PluginConfiguration.builder()
.requestedPlugins(List.of(new PluginInfo(TEST_PICO_CLI_PLUGIN)))
.pluginsDir(DEFAULT_PLUGIN_DIRECTORY)
.continueOnPluginError(false)
.build();
contextImpl.initialize(config);
contextImpl.registerPlugins();
String errorMessage =
String.format(
"Error calling `beforeExternalServices` on plugin of type %s",
TestPicoCLIPlugin.class.getName());
assertThatExceptionOfType(RuntimeException.class)
.isThrownBy(() -> contextImpl.beforeExternalServices())
.withMessageContaining(errorMessage);
}
@Test
void shouldNotHaltOnBeforeExternalServicesErrorWhenFlagIsTrue() {
System.setProperty(TEST_PICO_CLI_PLUGIN_TEST_OPTION, FAIL_BEFORE_EXTERNAL_SERVICES);
PluginConfiguration config =
PluginConfiguration.builder()
.requestedPlugins(List.of(new PluginInfo(TEST_PICO_CLI_PLUGIN)))
.pluginsDir(DEFAULT_PLUGIN_DIRECTORY)
.continueOnPluginError(true)
.build();
contextImpl.initialize(config);
contextImpl.registerPlugins();
contextImpl.beforeExternalServices();
assertThat(contextImpl.getRegisteredPlugins()).isNotInstanceOfAny(TestPicoCLIPlugin.class);
}
@Test
void shouldHaltOnBeforeMainLoopErrorWhenFlagIsFalse() {
System.setProperty(TEST_PICO_CLI_PLUGIN_TEST_OPTION, FAIL_START);
PluginConfiguration config =
PluginConfiguration.builder()
.requestedPlugins(List.of(new PluginInfo(TEST_PICO_CLI_PLUGIN)))
.pluginsDir(DEFAULT_PLUGIN_DIRECTORY)
.continueOnPluginError(false)
.build();
contextImpl.initialize(config);
contextImpl.registerPlugins();
contextImpl.beforeExternalServices();
String errorMessage =
String.format("Error starting plugin of type %s", TestPicoCLIPlugin.class.getName());
assertThatExceptionOfType(RuntimeException.class)
.isThrownBy(() -> contextImpl.startPlugins())
.withMessageContaining(errorMessage);
}
@Test
void shouldNotHaltOnBeforeMainLoopErrorWhenFlagIsTrue() {
System.setProperty(TEST_PICO_CLI_PLUGIN_TEST_OPTION, FAIL_BEFORE_MAIN_LOOP);
PluginConfiguration config =
PluginConfiguration.builder()
.requestedPlugins(List.of(new PluginInfo(TEST_PICO_CLI_PLUGIN)))
.pluginsDir(DEFAULT_PLUGIN_DIRECTORY)
.continueOnPluginError(true)
.build();
contextImpl.initialize(config);
contextImpl.registerPlugins();
contextImpl.beforeExternalServices();
contextImpl.startPlugins();
assertThat(contextImpl.getRegisteredPlugins()).isNotInstanceOfAny(TestPicoCLIPlugin.class);
}
@Test
void shouldHaltOnAfterExternalServicePostMainLoopErrorWhenFlagIsFalse() {
System.setProperty(
TEST_PICO_CLI_PLUGIN_TEST_OPTION, FAIL_AFTER_EXTERNAL_SERVICE_POST_MAIN_LOOP);
PluginConfiguration config =
PluginConfiguration.builder()
.requestedPlugins(List.of(new PluginInfo(TEST_PICO_CLI_PLUGIN)))
.pluginsDir(DEFAULT_PLUGIN_DIRECTORY)
.continueOnPluginError(false)
.build();
contextImpl.initialize(config);
contextImpl.registerPlugins();
contextImpl.beforeExternalServices();
contextImpl.startPlugins();
String errorMessage =
String.format(
"Error calling `afterExternalServicePostMainLoop` on plugin of type %s",
TestPicoCLIPlugin.class.getName());
assertThatExceptionOfType(RuntimeException.class)
.isThrownBy(() -> contextImpl.afterExternalServicesMainLoop())
.withMessageContaining(errorMessage);
}
@Test
void shouldNotHaltOnAfterExternalServicePostMainLoopErrorWhenFlagIsTrue() {
System.setProperty(
TEST_PICO_CLI_PLUGIN_TEST_OPTION, FAIL_AFTER_EXTERNAL_SERVICE_POST_MAIN_LOOP);
PluginConfiguration config =
PluginConfiguration.builder()
.requestedPlugins(List.of(new PluginInfo(TEST_PICO_CLI_PLUGIN)))
.pluginsDir(DEFAULT_PLUGIN_DIRECTORY)
.continueOnPluginError(true)
.build();
contextImpl.initialize(config);
contextImpl.registerPlugins();
contextImpl.beforeExternalServices();
contextImpl.startPlugins();
contextImpl.afterExternalServicesMainLoop();
assertThat(contextImpl.getRegisteredPlugins()).isNotInstanceOfAny(TestPicoCLIPlugin.class);
} }
private PluginConfiguration createConfigurationForSpecificPlugin(final String pluginName) { private PluginConfiguration createConfigurationForSpecificPlugin(final String pluginName) {

@ -118,7 +118,6 @@ import org.hyperledger.besu.ethereum.core.MiningParameters;
import org.hyperledger.besu.ethereum.core.MiningParametersMetrics; import org.hyperledger.besu.ethereum.core.MiningParametersMetrics;
import org.hyperledger.besu.ethereum.core.PrivacyParameters; import org.hyperledger.besu.ethereum.core.PrivacyParameters;
import org.hyperledger.besu.ethereum.core.VersionMetadata; import org.hyperledger.besu.ethereum.core.VersionMetadata;
import org.hyperledger.besu.ethereum.core.plugins.PluginConfiguration;
import org.hyperledger.besu.ethereum.eth.sync.SyncMode; import org.hyperledger.besu.ethereum.eth.sync.SyncMode;
import org.hyperledger.besu.ethereum.eth.sync.SynchronizerConfiguration; import org.hyperledger.besu.ethereum.eth.sync.SynchronizerConfiguration;
import org.hyperledger.besu.ethereum.eth.transactions.ImmutableTransactionPoolConfiguration; import org.hyperledger.besu.ethereum.eth.transactions.ImmutableTransactionPoolConfiguration;
@ -1080,9 +1079,8 @@ public class BesuCommand implements DefaultCommandValues, Runnable {
private IExecutionStrategy createPluginRegistrationTask(final IExecutionStrategy nextStep) { private IExecutionStrategy createPluginRegistrationTask(final IExecutionStrategy nextStep) {
return parseResult -> { return parseResult -> {
PluginConfiguration configuration = besuPluginContext.initialize(PluginsConfigurationOptions.fromCommandLine(commandLine));
PluginsConfigurationOptions.fromCommandLine(parseResult.commandSpec().commandLine()); besuPluginContext.registerPlugins();
besuPluginContext.registerPlugins(configuration);
commandLine.setExecutionStrategy(nextStep); commandLine.setExecutionStrategy(nextStep);
return commandLine.execute(parseResult.originalArgs().toArray(new String[0])); return commandLine.execute(parseResult.originalArgs().toArray(new String[0]));
}; };

@ -128,6 +128,9 @@ public interface DefaultCommandValues {
/** The constant DEFAULT_PLUGINS_OPTION_NAME. */ /** The constant DEFAULT_PLUGINS_OPTION_NAME. */
String DEFAULT_PLUGINS_OPTION_NAME = "--plugins"; String DEFAULT_PLUGINS_OPTION_NAME = "--plugins";
/** The constant DEFAULT_CONTINUE_ON_PLUGIN_ERROR_OPTION_NAME. */
String DEFAULT_CONTINUE_ON_PLUGIN_ERROR_OPTION_NAME = "--plugin-continue-on-error";
/** The constant DEFAULT_PLUGINS_EXTERNAL_ENABLED_OPTION_NAME. */ /** The constant DEFAULT_PLUGINS_EXTERNAL_ENABLED_OPTION_NAME. */
String DEFAULT_PLUGINS_EXTERNAL_ENABLED_OPTION_NAME = "--Xplugins-external-enabled"; String DEFAULT_PLUGINS_EXTERNAL_ENABLED_OPTION_NAME = "--Xplugins-external-enabled";

@ -14,6 +14,7 @@
*/ */
package org.hyperledger.besu.cli.options.stable; package org.hyperledger.besu.cli.options.stable;
import static org.hyperledger.besu.cli.DefaultCommandValues.DEFAULT_CONTINUE_ON_PLUGIN_ERROR_OPTION_NAME;
import static org.hyperledger.besu.cli.DefaultCommandValues.DEFAULT_PLUGINS_EXTERNAL_ENABLED_OPTION_NAME; import static org.hyperledger.besu.cli.DefaultCommandValues.DEFAULT_PLUGINS_EXTERNAL_ENABLED_OPTION_NAME;
import static org.hyperledger.besu.cli.DefaultCommandValues.DEFAULT_PLUGINS_OPTION_NAME; import static org.hyperledger.besu.cli.DefaultCommandValues.DEFAULT_PLUGINS_OPTION_NAME;
@ -27,7 +28,7 @@ import java.util.List;
import picocli.CommandLine; import picocli.CommandLine;
/** The Plugins Options options. */ /** The Plugins options. */
public class PluginsConfigurationOptions implements CLIOptions<PluginConfiguration> { public class PluginsConfigurationOptions implements CLIOptions<PluginConfiguration> {
@CommandLine.Option( @CommandLine.Option(
@ -44,9 +45,17 @@ public class PluginsConfigurationOptions implements CLIOptions<PluginConfigurati
split = ",", split = ",",
hidden = true, hidden = true,
converter = PluginInfoConverter.class, converter = PluginInfoConverter.class,
arity = "1..*") arity = "1")
private List<PluginInfo> plugins; private List<PluginInfo> plugins;
@CommandLine.Option(
names = {DEFAULT_CONTINUE_ON_PLUGIN_ERROR_OPTION_NAME},
description =
"Allow Besu startup even if any plugins fail to initialize correctly (default: ${DEFAULT-VALUE})",
defaultValue = "false",
arity = "1")
private final Boolean continueOnPluginError = false;
/** Default Constructor. */ /** Default Constructor. */
public PluginsConfigurationOptions() {} public PluginsConfigurationOptions() {}
@ -55,6 +64,7 @@ public class PluginsConfigurationOptions implements CLIOptions<PluginConfigurati
return new PluginConfiguration.Builder() return new PluginConfiguration.Builder()
.externalPluginsEnabled(externalPluginsEnabled) .externalPluginsEnabled(externalPluginsEnabled)
.requestedPlugins(plugins) .requestedPlugins(plugins)
.continueOnPluginError(continueOnPluginError)
.build(); .build();
} }
@ -66,10 +76,15 @@ public class PluginsConfigurationOptions implements CLIOptions<PluginConfigurati
public void validate(final CommandLine commandLine) { public void validate(final CommandLine commandLine) {
String errorMessage = String errorMessage =
String.format( String.format(
"%s option can only be used when %s is true", "%s and %s option can only be used when %s is true",
DEFAULT_PLUGINS_OPTION_NAME, DEFAULT_PLUGINS_EXTERNAL_ENABLED_OPTION_NAME); DEFAULT_PLUGINS_OPTION_NAME,
DEFAULT_CONTINUE_ON_PLUGIN_ERROR_OPTION_NAME,
DEFAULT_PLUGINS_EXTERNAL_ENABLED_OPTION_NAME);
CommandLineUtils.failIfOptionDoesntMeetRequirement( CommandLineUtils.failIfOptionDoesntMeetRequirement(
commandLine, errorMessage, externalPluginsEnabled, List.of(DEFAULT_PLUGINS_OPTION_NAME)); commandLine,
errorMessage,
externalPluginsEnabled,
List.of(DEFAULT_PLUGINS_OPTION_NAME, DEFAULT_CONTINUE_ON_PLUGIN_ERROR_OPTION_NAME));
} }
@Override @Override
@ -92,9 +107,14 @@ public class PluginsConfigurationOptions implements CLIOptions<PluginConfigurati
CommandLineUtils.getOptionValueOrDefault( CommandLineUtils.getOptionValueOrDefault(
commandLine, DEFAULT_PLUGINS_EXTERNAL_ENABLED_OPTION_NAME, Boolean::parseBoolean); commandLine, DEFAULT_PLUGINS_EXTERNAL_ENABLED_OPTION_NAME, Boolean::parseBoolean);
boolean continueOnPluginError =
CommandLineUtils.getOptionValueOrDefault(
commandLine, DEFAULT_CONTINUE_ON_PLUGIN_ERROR_OPTION_NAME, Boolean::parseBoolean);
return new PluginConfiguration.Builder() return new PluginConfiguration.Builder()
.requestedPlugins(plugins) .requestedPlugins(plugins)
.externalPluginsEnabled(externalPluginsEnabled) .externalPluginsEnabled(externalPluginsEnabled)
.continueOnPluginError(continueOnPluginError)
.build(); .build();
} }
} }

@ -288,12 +288,18 @@ public class QbftBesuControllerBuilder extends BftBesuControllerBuilder {
protocolContext protocolContext
.getBlockchain() .getBlockchain()
.observeBlockAdded( .observeBlockAdded(
o -> o -> {
miningParameters.setBlockPeriodSeconds( miningParameters.setBlockPeriodSeconds(
qbftForksSchedule qbftForksSchedule
.getFork(o.getBlock().getHeader().getNumber() + 1) .getFork(o.getBlock().getHeader().getNumber() + 1)
.getValue() .getValue()
.getBlockPeriodSeconds())); .getBlockPeriodSeconds());
miningParameters.setEmptyBlockPeriodSeconds(
qbftForksSchedule
.getFork(o.getBlock().getHeader().getNumber() + 1)
.getValue()
.getEmptyBlockPeriodSeconds());
});
if (syncState.isInitialSyncPhaseDone()) { if (syncState.isInitialSyncPhaseDone()) {
miningCoordinator.enable(); miningCoordinator.enable();
@ -422,8 +428,9 @@ public class QbftBesuControllerBuilder extends BftBesuControllerBuilder {
return block -> return block ->
LOG.info( LOG.info(
String.format( String.format(
"%s #%,d / %d tx / %d pending / %,d (%01.1f%%) gas / (%s)", "%s %s #%,d / %d tx / %d pending / %,d (%01.1f%%) gas / (%s)",
block.getHeader().getCoinbase().equals(localAddress) ? "Produced" : "Imported", block.getHeader().getCoinbase().equals(localAddress) ? "Produced" : "Imported",
block.getBody().getTransactions().size() == 0 ? "empty block" : "block",
block.getHeader().getNumber(), block.getHeader().getNumber(),
block.getBody().getTransactions().size(), block.getBody().getTransactions().size(),
transactionPool.count(), transactionPool.count(),

@ -56,6 +56,8 @@ public class BesuPluginContextImpl implements BesuContext, PluginVersionsProvide
private enum Lifecycle { private enum Lifecycle {
/** Uninitialized lifecycle. */ /** Uninitialized lifecycle. */
UNINITIALIZED, UNINITIALIZED,
/** Initialized lifecycle. */
INITIALIZED,
/** Registering lifecycle. */ /** Registering lifecycle. */
REGISTERING, REGISTERING,
/** Registered lifecycle. */ /** Registered lifecycle. */
@ -83,6 +85,7 @@ public class BesuPluginContextImpl implements BesuContext, PluginVersionsProvide
private final List<BesuPlugin> registeredPlugins = new ArrayList<>(); private final List<BesuPlugin> registeredPlugins = new ArrayList<>();
private final List<String> pluginVersions = new ArrayList<>(); private final List<String> pluginVersions = new ArrayList<>();
private PluginConfiguration config;
/** Instantiates a new Besu plugin context. */ /** Instantiates a new Besu plugin context. */
public BesuPluginContextImpl() {} public BesuPluginContextImpl() {}
@ -116,19 +119,30 @@ public class BesuPluginContextImpl implements BesuContext, PluginVersionsProvide
return StreamSupport.stream(serviceLoader.spliterator(), false).toList(); return StreamSupport.stream(serviceLoader.spliterator(), false).toList();
} }
/**
* Initializes the plugin context with the provided {@link PluginConfiguration}.
*
* @param config the plugin configuration
* @throws IllegalStateException if the system is not in the UNINITIALIZED state.
*/
public void initialize(final PluginConfiguration config) {
checkState(
state == Lifecycle.UNINITIALIZED,
"Besu plugins have already been initialized. Cannot register additional plugins.");
this.config = config;
state = Lifecycle.INITIALIZED;
}
/** /**
* Registers plugins based on the provided {@link PluginConfiguration}. This method finds plugins * Registers plugins based on the provided {@link PluginConfiguration}. This method finds plugins
* according to the configuration settings, filters them if necessary and then registers the * according to the configuration settings, filters them if necessary and then registers the
* filtered or found plugins * filtered or found plugins
* *
* @param config The configuration settings used to find and filter plugins for registration. The
* configuration includes the plugin directory and any configured plugin identifiers if
* applicable.
* @throws IllegalStateException if the system is not in the UNINITIALIZED state. * @throws IllegalStateException if the system is not in the UNINITIALIZED state.
*/ */
public void registerPlugins(final PluginConfiguration config) { public void registerPlugins() {
checkState( checkState(
state == Lifecycle.UNINITIALIZED, state == Lifecycle.INITIALIZED,
"Besu plugins have already been registered. Cannot register additional plugins."); "Besu plugins have already been registered. Cannot register additional plugins.");
state = Lifecycle.REGISTERING; state = Lifecycle.REGISTERING;
@ -192,14 +206,18 @@ public class BesuPluginContextImpl implements BesuContext, PluginVersionsProvide
private boolean registerPlugin(final BesuPlugin plugin) { private boolean registerPlugin(final BesuPlugin plugin) {
try { try {
plugin.register(this); plugin.register(this);
LOG.info("Registered plugin of type {}.", plugin.getClass().getName());
pluginVersions.add(plugin.getVersion()); pluginVersions.add(plugin.getVersion());
LOG.info("Registered plugin of type {}.", plugin.getClass().getName());
} catch (final Exception e) { } catch (final Exception e) {
LOG.error( if (config.isContinueOnPluginError()) {
"Error registering plugin of type " LOG.error(
+ plugin.getClass().getName() "Error registering plugin of type {}, start and stop will not be called.",
+ ", start and stop will not be called.", plugin.getClass().getName(),
e); e);
} else {
throw new RuntimeException(
"Error registering plugin of type " + plugin.getClass().getName(), e);
}
return false; return false;
} }
return true; return true;
@ -223,15 +241,20 @@ public class BesuPluginContextImpl implements BesuContext, PluginVersionsProvide
LOG.debug( LOG.debug(
"beforeExternalServices called on plugin of type {}.", plugin.getClass().getName()); "beforeExternalServices called on plugin of type {}.", plugin.getClass().getName());
} catch (final Exception e) { } catch (final Exception e) {
LOG.error( if (config.isContinueOnPluginError()) {
"Error calling `beforeExternalServices` on plugin of type " LOG.error(
+ plugin.getClass().getName() "Error calling `beforeExternalServices` on plugin of type {}, start will not be called.",
+ ", stop will not be called.", plugin.getClass().getName(),
e); e);
pluginsIterator.remove(); pluginsIterator.remove();
} else {
throw new RuntimeException(
"Error calling `beforeExternalServices` on plugin of type "
+ plugin.getClass().getName(),
e);
}
} }
} }
LOG.debug("Plugin startup complete."); LOG.debug("Plugin startup complete.");
state = Lifecycle.BEFORE_EXTERNAL_SERVICES_FINISHED; state = Lifecycle.BEFORE_EXTERNAL_SERVICES_FINISHED;
} }
@ -253,12 +276,16 @@ public class BesuPluginContextImpl implements BesuContext, PluginVersionsProvide
plugin.start(); plugin.start();
LOG.debug("Started plugin of type {}.", plugin.getClass().getName()); LOG.debug("Started plugin of type {}.", plugin.getClass().getName());
} catch (final Exception e) { } catch (final Exception e) {
LOG.error( if (config.isContinueOnPluginError()) {
"Error starting plugin of type " LOG.error(
+ plugin.getClass().getName() "Error starting plugin of type {}, stop will not be called.",
+ ", stop will not be called.", plugin.getClass().getName(),
e); e);
pluginsIterator.remove(); pluginsIterator.remove();
} else {
throw new RuntimeException(
"Error starting plugin of type " + plugin.getClass().getName(), e);
}
} }
} }
@ -279,8 +306,20 @@ public class BesuPluginContextImpl implements BesuContext, PluginVersionsProvide
final BesuPlugin plugin = pluginsIterator.next(); final BesuPlugin plugin = pluginsIterator.next();
try { try {
plugin.afterExternalServicePostMainLoop(); plugin.afterExternalServicePostMainLoop();
} finally { } catch (final Exception e) {
pluginsIterator.remove(); if (config.isContinueOnPluginError()) {
LOG.error(
"Error calling `afterExternalServicePostMainLoop` on plugin of type "
+ plugin.getClass().getName()
+ ", stop will not be called.",
e);
pluginsIterator.remove();
} else {
throw new RuntimeException(
"Error calling `afterExternalServicePostMainLoop` on plugin of type "
+ plugin.getClass().getName(),
e);
}
} }
} }
} }

@ -138,15 +138,18 @@ public abstract class CommandTestAbstract {
private static final Logger TEST_LOGGER = LoggerFactory.getLogger(CommandTestAbstract.class); private static final Logger TEST_LOGGER = LoggerFactory.getLogger(CommandTestAbstract.class);
protected static final int POA_BLOCK_PERIOD_SECONDS = 5; protected static final int POA_BLOCK_PERIOD_SECONDS = 5;
protected static final int POA_EMPTY_BLOCK_PERIOD_SECONDS = 50;
protected static final JsonObject VALID_GENESIS_QBFT_POST_LONDON = protected static final JsonObject VALID_GENESIS_QBFT_POST_LONDON =
(new JsonObject()) (new JsonObject())
.put( .put(
"config", "config",
new JsonObject() new JsonObject()
.put("londonBlock", 0) .put("londonBlock", 0)
.put("qbft", new JsonObject().put("blockperiodseconds", POA_BLOCK_PERIOD_SECONDS))
.put( .put(
"qbft", "qbft",
new JsonObject().put("blockperiodseconds", POA_BLOCK_PERIOD_SECONDS))); new JsonObject()
.put("xemptyblockperiodseconds", POA_EMPTY_BLOCK_PERIOD_SECONDS)));
protected static final JsonObject VALID_GENESIS_IBFT2_POST_LONDON = protected static final JsonObject VALID_GENESIS_IBFT2_POST_LONDON =
(new JsonObject()) (new JsonObject())
.put( .put(

@ -33,7 +33,7 @@ public class PluginsOptionsTest extends CommandTestAbstract {
@Test @Test
public void shouldParsePluginOptionForSinglePlugin() { public void shouldParsePluginOptionForSinglePlugin() {
parseCommand("--plugins", "pluginA"); parseCommand("--plugins", "pluginA");
verify(mockBesuPluginContext).registerPlugins(pluginConfigurationArgumentCaptor.capture()); verify(mockBesuPluginContext).initialize(pluginConfigurationArgumentCaptor.capture());
assertThat(pluginConfigurationArgumentCaptor.getValue().getRequestedPlugins()) assertThat(pluginConfigurationArgumentCaptor.getValue().getRequestedPlugins())
.isEqualTo(List.of("pluginA")); .isEqualTo(List.of("pluginA"));
assertThat(commandOutput.toString(UTF_8)).isEmpty(); assertThat(commandOutput.toString(UTF_8)).isEmpty();
@ -43,7 +43,7 @@ public class PluginsOptionsTest extends CommandTestAbstract {
@Test @Test
public void shouldParsePluginOptionForMultiplePlugins() { public void shouldParsePluginOptionForMultiplePlugins() {
parseCommand("--plugins", "pluginA,pluginB"); parseCommand("--plugins", "pluginA,pluginB");
verify(mockBesuPluginContext).registerPlugins(pluginConfigurationArgumentCaptor.capture()); verify(mockBesuPluginContext).initialize(pluginConfigurationArgumentCaptor.capture());
assertThat(pluginConfigurationArgumentCaptor.getValue().getRequestedPlugins()) assertThat(pluginConfigurationArgumentCaptor.getValue().getRequestedPlugins())
.isEqualTo(List.of("pluginA", "pluginB")); .isEqualTo(List.of("pluginA", "pluginB"));
@ -54,7 +54,7 @@ public class PluginsOptionsTest extends CommandTestAbstract {
@Test @Test
public void shouldNotUsePluginOptionWhenNoPluginsSpecified() { public void shouldNotUsePluginOptionWhenNoPluginsSpecified() {
parseCommand(); parseCommand();
verify(mockBesuPluginContext).registerPlugins(pluginConfigurationArgumentCaptor.capture()); verify(mockBesuPluginContext).initialize(pluginConfigurationArgumentCaptor.capture());
assertThat(pluginConfigurationArgumentCaptor.getValue().getRequestedPlugins()) assertThat(pluginConfigurationArgumentCaptor.getValue().getRequestedPlugins())
.isEqualTo(List.of()); .isEqualTo(List.of());
assertThat(commandOutput.toString(UTF_8)).isEmpty(); assertThat(commandOutput.toString(UTF_8)).isEmpty();
@ -64,7 +64,7 @@ public class PluginsOptionsTest extends CommandTestAbstract {
@Test @Test
public void shouldNotParseAnyPluginsWhenPluginOptionIsEmpty() { public void shouldNotParseAnyPluginsWhenPluginOptionIsEmpty() {
parseCommand("--plugins", ""); parseCommand("--plugins", "");
verify(mockBesuPluginContext).registerPlugins(pluginConfigurationArgumentCaptor.capture()); verify(mockBesuPluginContext).initialize(pluginConfigurationArgumentCaptor.capture());
assertThat(pluginConfigurationArgumentCaptor.getValue().getRequestedPlugins()) assertThat(pluginConfigurationArgumentCaptor.getValue().getRequestedPlugins())
.isEqualTo(List.of()); .isEqualTo(List.of());
assertThat(commandOutput.toString(UTF_8)).isEmpty(); assertThat(commandOutput.toString(UTF_8)).isEmpty();
@ -74,7 +74,7 @@ public class PluginsOptionsTest extends CommandTestAbstract {
@Test @Test
public void shouldParsePluginsExternalEnabledOptionWhenFalse() { public void shouldParsePluginsExternalEnabledOptionWhenFalse() {
parseCommand("--Xplugins-external-enabled=false"); parseCommand("--Xplugins-external-enabled=false");
verify(mockBesuPluginContext).registerPlugins(pluginConfigurationArgumentCaptor.capture()); verify(mockBesuPluginContext).initialize(pluginConfigurationArgumentCaptor.capture());
assertThat(pluginConfigurationArgumentCaptor.getValue().isExternalPluginsEnabled()) assertThat(pluginConfigurationArgumentCaptor.getValue().isExternalPluginsEnabled())
.isEqualTo(false); .isEqualTo(false);
@ -86,7 +86,7 @@ public class PluginsOptionsTest extends CommandTestAbstract {
@Test @Test
public void shouldParsePluginsExternalEnabledOptionWhenTrue() { public void shouldParsePluginsExternalEnabledOptionWhenTrue() {
parseCommand("--Xplugins-external-enabled=true"); parseCommand("--Xplugins-external-enabled=true");
verify(mockBesuPluginContext).registerPlugins(pluginConfigurationArgumentCaptor.capture()); verify(mockBesuPluginContext).initialize(pluginConfigurationArgumentCaptor.capture());
assertThat(pluginConfigurationArgumentCaptor.getValue().isExternalPluginsEnabled()) assertThat(pluginConfigurationArgumentCaptor.getValue().isExternalPluginsEnabled())
.isEqualTo(true); .isEqualTo(true);
@ -98,7 +98,7 @@ public class PluginsOptionsTest extends CommandTestAbstract {
@Test @Test
public void shouldEnablePluginsExternalByDefault() { public void shouldEnablePluginsExternalByDefault() {
parseCommand(); parseCommand();
verify(mockBesuPluginContext).registerPlugins(pluginConfigurationArgumentCaptor.capture()); verify(mockBesuPluginContext).initialize(pluginConfigurationArgumentCaptor.capture());
assertThat(pluginConfigurationArgumentCaptor.getValue().isExternalPluginsEnabled()) assertThat(pluginConfigurationArgumentCaptor.getValue().isExternalPluginsEnabled())
.isEqualTo(true); .isEqualTo(true);
@ -109,10 +109,43 @@ public class PluginsOptionsTest extends CommandTestAbstract {
@Test @Test
public void shouldFailWhenPluginsIsDisabledAndPluginsExplicitlyRequested() { public void shouldFailWhenPluginsIsDisabledAndPluginsExplicitlyRequested() {
parseCommand("--Xplugins-external-enabled=false", "--plugins", "pluginA"); parseCommand("--Xplugins-external-enabled=false", "--plugins", "pluginA");
verify(mockBesuPluginContext).registerPlugins(pluginConfigurationArgumentCaptor.capture());
assertThat(commandOutput.toString(UTF_8)).isEmpty(); assertThat(commandOutput.toString(UTF_8)).isEmpty();
assertThat(commandErrorOutput.toString(UTF_8)) assertThat(commandErrorOutput.toString(UTF_8))
.contains("--plugins option can only be used when --Xplugins-external-enabled is true"); .contains(
"--plugins and --plugin-continue-on-error option can only be used when --Xplugins-external-enabled is true");
}
@Test
public void shouldHaveContinueOnErrorFalseByDefault() {
parseCommand();
verify(mockBesuPluginContext).initialize(pluginConfigurationArgumentCaptor.capture());
assertThat(pluginConfigurationArgumentCaptor.getValue().isContinueOnPluginError())
.isEqualTo(false);
assertThat(commandOutput.toString(UTF_8)).isEmpty();
assertThat(commandErrorOutput.toString(UTF_8)).isEmpty();
}
@Test
public void shouldUseContinueOnErrorWhenTrue() {
parseCommand("--plugin-continue-on-error=true");
verify(mockBesuPluginContext).initialize(pluginConfigurationArgumentCaptor.capture());
assertThat(pluginConfigurationArgumentCaptor.getValue().isContinueOnPluginError())
.isEqualTo(true);
assertThat(commandOutput.toString(UTF_8)).isEmpty();
assertThat(commandErrorOutput.toString(UTF_8)).isEmpty();
}
@Test
public void shouldFailWhenPluginsIsDisabledAnHaltOnErrorTrue() {
parseCommand("--Xplugins-external-enabled=false", "--plugin-continue-on-error=true");
assertThat(commandOutput.toString(UTF_8)).isEmpty();
assertThat(commandErrorOutput.toString(UTF_8))
.contains(
"--plugins and --plugin-continue-on-error option can only be used when --Xplugins-external-enabled is true");
} }
} }

@ -248,3 +248,4 @@ Xevm-jumpdest-cache-weight-kb=32000
# plugins # plugins
Xplugins-external-enabled=true Xplugins-external-enabled=true
plugins=["none"] plugins=["none"]
plugin-continue-on-error=false

@ -37,6 +37,13 @@ public interface BftConfigOptions {
*/ */
int getBlockPeriodSeconds(); int getBlockPeriodSeconds();
/**
* Gets empty block period seconds.
*
* @return the empty block period seconds
*/
int getEmptyBlockPeriodSeconds();
/** /**
* Gets block period milliseconds. For TESTING only. If set then blockperiodseconds is ignored. * Gets block period milliseconds. For TESTING only. If set then blockperiodseconds is ignored.
* *

@ -41,6 +41,9 @@ public class BftFork implements Fork {
/** The constant BLOCK_PERIOD_SECONDS_KEY. */ /** The constant BLOCK_PERIOD_SECONDS_KEY. */
public static final String BLOCK_PERIOD_SECONDS_KEY = "blockperiodseconds"; public static final String BLOCK_PERIOD_SECONDS_KEY = "blockperiodseconds";
/** The constant EMPTY_BLOCK_PERIOD_SECONDS_KEY. */
public static final String EMPTY_BLOCK_PERIOD_SECONDS_KEY = "xemptyblockperiodseconds";
/** The constant BLOCK_PERIOD_MILLISECONDS_KEY. */ /** The constant BLOCK_PERIOD_MILLISECONDS_KEY. */
public static final String BLOCK_PERIOD_MILLISECONDS_KEY = "xblockperiodmilliseconds"; public static final String BLOCK_PERIOD_MILLISECONDS_KEY = "xblockperiodmilliseconds";
@ -86,6 +89,16 @@ public class BftFork implements Fork {
return JsonUtil.getPositiveInt(forkConfigRoot, BLOCK_PERIOD_SECONDS_KEY); return JsonUtil.getPositiveInt(forkConfigRoot, BLOCK_PERIOD_SECONDS_KEY);
} }
/**
* Gets empty block period seconds.
*
* @return the empty block period seconds
*/
public OptionalInt getEmptyBlockPeriodSeconds() {
// It can be 0 to disable custom empty block periods
return JsonUtil.getInt(forkConfigRoot, EMPTY_BLOCK_PERIOD_SECONDS_KEY);
}
/** /**
* Gets block period milliseconds. Experimental for test scenarios only. * Gets block period milliseconds. Experimental for test scenarios only.
* *

@ -539,4 +539,11 @@ public interface GenesisConfigOptions {
* @return the deposit address * @return the deposit address
*/ */
Optional<Address> getDepositContractAddress(); Optional<Address> getDepositContractAddress();
/**
* The consolidation request contract address
*
* @return the consolidation request contract address
*/
Optional<Address> getConsolidationRequestContractAddress();
} }

@ -34,6 +34,8 @@ public class JsonBftConfigOptions implements BftConfigOptions {
private static final long DEFAULT_EPOCH_LENGTH = 30_000; private static final long DEFAULT_EPOCH_LENGTH = 30_000;
private static final int DEFAULT_BLOCK_PERIOD_SECONDS = 1; private static final int DEFAULT_BLOCK_PERIOD_SECONDS = 1;
// 0 keeps working as before, increase to activate it
private static final int DEFAULT_EMPTY_BLOCK_PERIOD_SECONDS = 0;
private static final int DEFAULT_BLOCK_PERIOD_MILLISECONDS = 0; // Experimental for test only private static final int DEFAULT_BLOCK_PERIOD_MILLISECONDS = 0; // Experimental for test only
private static final int DEFAULT_ROUND_EXPIRY_SECONDS = 1; private static final int DEFAULT_ROUND_EXPIRY_SECONDS = 1;
// In a healthy network this can be very small. This default limit will allow for suitable // In a healthy network this can be very small. This default limit will allow for suitable
@ -67,6 +69,12 @@ public class JsonBftConfigOptions implements BftConfigOptions {
bftConfigRoot, "blockperiodseconds", DEFAULT_BLOCK_PERIOD_SECONDS); bftConfigRoot, "blockperiodseconds", DEFAULT_BLOCK_PERIOD_SECONDS);
} }
@Override
public int getEmptyBlockPeriodSeconds() {
return JsonUtil.getInt(
bftConfigRoot, "xemptyblockperiodseconds", DEFAULT_EMPTY_BLOCK_PERIOD_SECONDS);
}
@Override @Override
public long getBlockPeriodMilliseconds() { public long getBlockPeriodMilliseconds() {
return JsonUtil.getLong( return JsonUtil.getLong(

@ -52,6 +52,8 @@ public class JsonGenesisConfigOptions implements GenesisConfigOptions {
private static final String WITHDRAWAL_REQUEST_CONTRACT_ADDRESS_KEY = private static final String WITHDRAWAL_REQUEST_CONTRACT_ADDRESS_KEY =
"withdrawalrequestcontractaddress"; "withdrawalrequestcontractaddress";
private static final String DEPOSIT_CONTRACT_ADDRESS_KEY = "depositcontractaddress"; private static final String DEPOSIT_CONTRACT_ADDRESS_KEY = "depositcontractaddress";
private static final String CONSOLIDATION_REQUEST_CONTRACT_ADDRESS_KEY =
"consolidationrequestcontractaddress";
private final ObjectNode configRoot; private final ObjectNode configRoot;
private final Map<String, String> configOverrides = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); private final Map<String, String> configOverrides = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
@ -453,6 +455,13 @@ public class JsonGenesisConfigOptions implements GenesisConfigOptions {
return inputAddress.map(Address::fromHexString); return inputAddress.map(Address::fromHexString);
} }
@Override
public Optional<Address> getConsolidationRequestContractAddress() {
Optional<String> inputAddress =
JsonUtil.getString(configRoot, CONSOLIDATION_REQUEST_CONTRACT_ADDRESS_KEY);
return inputAddress.map(Address::fromHexString);
}
@Override @Override
public Map<String, Object> asMap() { public Map<String, Object> asMap() {
final ImmutableMap.Builder<String, Object> builder = ImmutableMap.builder(); final ImmutableMap.Builder<String, Object> builder = ImmutableMap.builder();
@ -504,6 +513,8 @@ public class JsonGenesisConfigOptions implements GenesisConfigOptions {
getWithdrawalRequestContractAddress() getWithdrawalRequestContractAddress()
.ifPresent(l -> builder.put("withdrawalRequestContractAddress", l)); .ifPresent(l -> builder.put("withdrawalRequestContractAddress", l));
getDepositContractAddress().ifPresent(l -> builder.put("depositContractAddress", l)); getDepositContractAddress().ifPresent(l -> builder.put("depositContractAddress", l));
getConsolidationRequestContractAddress()
.ifPresent(l -> builder.put("consolidationRequestContractAddress", l));
if (isClique()) { if (isClique()) {
builder.put("clique", getCliqueConfigOptions().asMap()); builder.put("clique", getCliqueConfigOptions().asMap());

@ -467,6 +467,11 @@ public class StubGenesisConfigOptions implements GenesisConfigOptions, Cloneable
return Optional.empty(); return Optional.empty();
} }
@Override
public Optional<Address> getConsolidationRequestContractAddress() {
return Optional.empty();
}
/** /**
* Homestead block stub genesis config options. * Homestead block stub genesis config options.
* *

@ -382,6 +382,33 @@ class GenesisConfigOptionsTest {
.containsValue(Address.ZERO); .containsValue(Address.ZERO);
} }
@Test
void shouldGetConsolidationRequestContractAddress() {
final GenesisConfigOptions config =
fromConfigOptions(
singletonMap(
"consolidationRequestContractAddress",
"0x00000000219ab540356cbb839cbe05303d7705fa"));
assertThat(config.getConsolidationRequestContractAddress())
.hasValue(Address.fromHexString("0x00000000219ab540356cbb839cbe05303d7705fa"));
}
@Test
void shouldNotHaveConsolidationRequestContractAddressWhenEmpty() {
final GenesisConfigOptions config = fromConfigOptions(emptyMap());
assertThat(config.getConsolidationRequestContractAddress()).isEmpty();
}
@Test
void asMapIncludesConsolidationRequestContractAddress() {
final GenesisConfigOptions config =
fromConfigOptions(Map.of("consolidationRequestContractAddress", "0x0"));
assertThat(config.asMap())
.containsOnlyKeys("consolidationRequestContractAddress")
.containsValue(Address.ZERO);
}
private GenesisConfigOptions fromConfigOptions(final Map<String, Object> configOptions) { private GenesisConfigOptions fromConfigOptions(final Map<String, Object> configOptions) {
final ObjectNode rootNode = JsonUtil.createEmptyObjectNode(); final ObjectNode rootNode = JsonUtil.createEmptyObjectNode();
final ObjectNode options = JsonUtil.objectNodeFromMap(configOptions); final ObjectNode options = JsonUtil.objectNodeFromMap(configOptions);

@ -17,6 +17,7 @@ package org.hyperledger.besu.config;
import static java.util.Collections.emptyMap; import static java.util.Collections.emptyMap;
import static java.util.Collections.singletonMap; import static java.util.Collections.singletonMap;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatCode;
import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.assertj.core.api.Assertions.assertThatThrownBy;
import org.hyperledger.besu.datatypes.Address; import org.hyperledger.besu.datatypes.Address;
@ -30,6 +31,7 @@ public class JsonBftConfigOptionsTest {
private static final int EXPECTED_DEFAULT_EPOCH_LENGTH = 30_000; private static final int EXPECTED_DEFAULT_EPOCH_LENGTH = 30_000;
private static final int EXPECTED_DEFAULT_BLOCK_PERIOD = 1; private static final int EXPECTED_DEFAULT_BLOCK_PERIOD = 1;
private static final int EXPECTED_EMPTY_DEFAULT_BLOCK_PERIOD = 0;
private static final int EXPECTED_DEFAULT_REQUEST_TIMEOUT = 1; private static final int EXPECTED_DEFAULT_REQUEST_TIMEOUT = 1;
private static final int EXPECTED_DEFAULT_GOSSIPED_HISTORY_LIMIT = 1000; private static final int EXPECTED_DEFAULT_GOSSIPED_HISTORY_LIMIT = 1000;
private static final int EXPECTED_DEFAULT_MESSAGE_QUEUE_LIMIT = 1000; private static final int EXPECTED_DEFAULT_MESSAGE_QUEUE_LIMIT = 1000;
@ -61,18 +63,37 @@ public class JsonBftConfigOptionsTest {
assertThat(config.getBlockPeriodSeconds()).isEqualTo(5); assertThat(config.getBlockPeriodSeconds()).isEqualTo(5);
} }
@Test
public void shouldGetEmptyBlockPeriodFromConfig() {
final BftConfigOptions config = fromConfigOptions(singletonMap("xemptyblockperiodseconds", 60));
assertThat(config.getEmptyBlockPeriodSeconds()).isEqualTo(60);
}
@Test @Test
public void shouldFallbackToDefaultBlockPeriod() { public void shouldFallbackToDefaultBlockPeriod() {
final BftConfigOptions config = fromConfigOptions(emptyMap()); final BftConfigOptions config = fromConfigOptions(emptyMap());
assertThat(config.getBlockPeriodSeconds()).isEqualTo(EXPECTED_DEFAULT_BLOCK_PERIOD); assertThat(config.getBlockPeriodSeconds()).isEqualTo(EXPECTED_DEFAULT_BLOCK_PERIOD);
} }
@Test
public void shouldFallbackToEmptyDefaultBlockPeriod() {
final BftConfigOptions config = fromConfigOptions(emptyMap());
assertThat(config.getEmptyBlockPeriodSeconds()).isEqualTo(EXPECTED_EMPTY_DEFAULT_BLOCK_PERIOD);
}
@Test @Test
public void shouldGetDefaultBlockPeriodFromDefaultConfig() { public void shouldGetDefaultBlockPeriodFromDefaultConfig() {
assertThat(JsonBftConfigOptions.DEFAULT.getBlockPeriodSeconds()) assertThat(JsonBftConfigOptions.DEFAULT.getBlockPeriodSeconds())
.isEqualTo(EXPECTED_DEFAULT_BLOCK_PERIOD); .isEqualTo(EXPECTED_DEFAULT_BLOCK_PERIOD);
} }
@Test
public void shouldGetDefaultEmptyBlockPeriodFromDefaultConfig() {
assertThat(JsonBftConfigOptions.DEFAULT.getEmptyBlockPeriodSeconds())
.isEqualTo(EXPECTED_EMPTY_DEFAULT_BLOCK_PERIOD);
}
@Test @Test
public void shouldThrowOnNonPositiveBlockPeriod() { public void shouldThrowOnNonPositiveBlockPeriod() {
final BftConfigOptions config = fromConfigOptions(singletonMap("blockperiodseconds", -1)); final BftConfigOptions config = fromConfigOptions(singletonMap("blockperiodseconds", -1));
@ -80,6 +101,13 @@ public class JsonBftConfigOptionsTest {
.isInstanceOf(IllegalArgumentException.class); .isInstanceOf(IllegalArgumentException.class);
} }
@Test
public void shouldNotThrowOnNonPositiveEmptyBlockPeriod() {
// can be 0 to be compatible with older versions
final BftConfigOptions config = fromConfigOptions(singletonMap("xemptyblockperiodseconds", 0));
assertThatCode(() -> config.getEmptyBlockPeriodSeconds()).doesNotThrowAnyException();
}
@Test @Test
public void shouldGetRequestTimeoutFromConfig() { public void shouldGetRequestTimeoutFromConfig() {
final BftConfigOptions config = fromConfigOptions(singletonMap("requesttimeoutseconds", 5)); final BftConfigOptions config = fromConfigOptions(singletonMap("requesttimeoutseconds", 5));

@ -37,6 +37,8 @@ public class BlockTimer {
private Optional<ScheduledFuture<?>> currentTimerTask; private Optional<ScheduledFuture<?>> currentTimerTask;
private final BftEventQueue queue; private final BftEventQueue queue;
private final Clock clock; private final Clock clock;
private long blockPeriodSeconds;
private long emptyBlockPeriodSeconds;
/** /**
* Construct a BlockTimer with primed executor service ready to start timers * Construct a BlockTimer with primed executor service ready to start timers
@ -56,6 +58,8 @@ public class BlockTimer {
this.bftExecutors = bftExecutors; this.bftExecutors = bftExecutors;
this.currentTimerTask = Optional.empty(); this.currentTimerTask = Optional.empty();
this.clock = clock; this.clock = clock;
this.blockPeriodSeconds = 0;
this.emptyBlockPeriodSeconds = 0;
} }
/** Cancels the current running round timer if there is one */ /** Cancels the current running round timer if there is one */
@ -83,13 +87,11 @@ public class BlockTimer {
final ConsensusRoundIdentifier round, final BlockHeader chainHeadHeader) { final ConsensusRoundIdentifier round, final BlockHeader chainHeadHeader) {
cancelTimer(); cancelTimer();
final long now = clock.millis();
final long expiryTime; final long expiryTime;
// Experimental option for test scenarios only. Not for production use. // Experimental option for test scenarios only. Not for production use.
final long blockPeriodMilliseconds = final long blockPeriodMilliseconds =
forksSchedule.getFork(round.getSequenceNumber()).getValue().getBlockPeriodMilliseconds(); forksSchedule.getFork(round.getSequenceNumber()).getValue().getBlockPeriodMilliseconds();
if (blockPeriodMilliseconds > 0) { if (blockPeriodMilliseconds > 0) {
// Experimental mode for setting < 1 second block periods e.g. for CI/CD pipelines // Experimental mode for setting < 1 second block periods e.g. for CI/CD pipelines
// running tests against Besu // running tests against Besu
@ -99,12 +101,60 @@ public class BlockTimer {
blockPeriodMilliseconds); blockPeriodMilliseconds);
} else { } else {
// absolute time when the timer is supposed to expire // absolute time when the timer is supposed to expire
final int blockPeriodSeconds = final int currentBlockPeriodSeconds =
forksSchedule.getFork(round.getSequenceNumber()).getValue().getBlockPeriodSeconds(); forksSchedule.getFork(round.getSequenceNumber()).getValue().getBlockPeriodSeconds();
final long minimumTimeBetweenBlocksMillis = blockPeriodSeconds * 1000L; final long minimumTimeBetweenBlocksMillis = currentBlockPeriodSeconds * 1000L;
expiryTime = chainHeadHeader.getTimestamp() * 1_000 + minimumTimeBetweenBlocksMillis; expiryTime = chainHeadHeader.getTimestamp() * 1_000 + minimumTimeBetweenBlocksMillis;
} }
setBlockTimes(round);
startTimer(round, expiryTime);
}
/**
* Checks if the empty block timer is expired
*
* @param chainHeadHeader The header of the chain head
* @param currentTimeInMillis The current time
* @return a boolean value
*/
public synchronized boolean checkEmptyBlockExpired(
final BlockHeader chainHeadHeader, final long currentTimeInMillis) {
final long emptyBlockPeriodExpiryTime =
(chainHeadHeader.getTimestamp() + emptyBlockPeriodSeconds) * 1000;
if (currentTimeInMillis > emptyBlockPeriodExpiryTime) {
LOG.debug("Empty Block expired");
return true;
}
LOG.debug("Empty Block NOT expired");
return false;
}
/**
* Resets the empty block timer
*
* @param roundIdentifier The current round identifier
* @param chainHeadHeader The header of the chain head
* @param currentTimeInMillis The current time
*/
public void resetTimerForEmptyBlock(
final ConsensusRoundIdentifier roundIdentifier,
final BlockHeader chainHeadHeader,
final long currentTimeInMillis) {
final long emptyBlockPeriodExpiryTime =
(chainHeadHeader.getTimestamp() + emptyBlockPeriodSeconds) * 1000;
final long nextBlockPeriodExpiryTime = currentTimeInMillis + blockPeriodSeconds * 1000;
startTimer(roundIdentifier, Math.min(emptyBlockPeriodExpiryTime, nextBlockPeriodExpiryTime));
}
private synchronized void startTimer(
final ConsensusRoundIdentifier round, final long expiryTime) {
cancelTimer();
final long now = clock.millis();
if (expiryTime > now) { if (expiryTime > now) {
final long delay = expiryTime - now; final long delay = expiryTime - now;
@ -117,4 +167,29 @@ public class BlockTimer {
queue.add(new BlockTimerExpiry(round)); queue.add(new BlockTimerExpiry(round));
} }
} }
private synchronized void setBlockTimes(final ConsensusRoundIdentifier round) {
final BftConfigOptions currentConfigOptions =
forksSchedule.getFork(round.getSequenceNumber()).getValue();
this.blockPeriodSeconds = currentConfigOptions.getBlockPeriodSeconds();
this.emptyBlockPeriodSeconds = currentConfigOptions.getEmptyBlockPeriodSeconds();
}
/**
* Retrieves the Block Period Seconds
*
* @return the Block Period Seconds
*/
public synchronized long getBlockPeriodSeconds() {
return blockPeriodSeconds;
}
/**
* Retrieves the Empty Block Period Seconds
*
* @return the Empty Block Period Seconds
*/
public synchronized long getEmptyBlockPeriodSeconds() {
return emptyBlockPeriodSeconds;
}
} }

@ -31,6 +31,7 @@ import java.util.Optional;
public class MutableBftConfigOptions implements BftConfigOptions { public class MutableBftConfigOptions implements BftConfigOptions {
private long epochLength; private long epochLength;
private int blockPeriodSeconds; private int blockPeriodSeconds;
private int emptyBlockPeriodSeconds;
private long blockPeriodMilliseconds; private long blockPeriodMilliseconds;
private int requestTimeoutSeconds; private int requestTimeoutSeconds;
private int gossipedHistoryLimit; private int gossipedHistoryLimit;
@ -49,6 +50,7 @@ public class MutableBftConfigOptions implements BftConfigOptions {
public MutableBftConfigOptions(final BftConfigOptions bftConfigOptions) { public MutableBftConfigOptions(final BftConfigOptions bftConfigOptions) {
this.epochLength = bftConfigOptions.getEpochLength(); this.epochLength = bftConfigOptions.getEpochLength();
this.blockPeriodSeconds = bftConfigOptions.getBlockPeriodSeconds(); this.blockPeriodSeconds = bftConfigOptions.getBlockPeriodSeconds();
this.emptyBlockPeriodSeconds = bftConfigOptions.getEmptyBlockPeriodSeconds();
this.blockPeriodMilliseconds = bftConfigOptions.getBlockPeriodMilliseconds(); this.blockPeriodMilliseconds = bftConfigOptions.getBlockPeriodMilliseconds();
this.requestTimeoutSeconds = bftConfigOptions.getRequestTimeoutSeconds(); this.requestTimeoutSeconds = bftConfigOptions.getRequestTimeoutSeconds();
this.gossipedHistoryLimit = bftConfigOptions.getGossipedHistoryLimit(); this.gossipedHistoryLimit = bftConfigOptions.getGossipedHistoryLimit();
@ -70,6 +72,11 @@ public class MutableBftConfigOptions implements BftConfigOptions {
return blockPeriodSeconds; return blockPeriodSeconds;
} }
@Override
public int getEmptyBlockPeriodSeconds() {
return emptyBlockPeriodSeconds;
}
@Override @Override
public long getBlockPeriodMilliseconds() { public long getBlockPeriodMilliseconds() {
return blockPeriodMilliseconds; return blockPeriodMilliseconds;
@ -138,6 +145,15 @@ public class MutableBftConfigOptions implements BftConfigOptions {
this.blockPeriodSeconds = blockPeriodSeconds; this.blockPeriodSeconds = blockPeriodSeconds;
} }
/**
* Sets empty block period seconds.
*
* @param emptyBlockPeriodSeconds the empty block period seconds
*/
public void setEmptyBlockPeriodSeconds(final int emptyBlockPeriodSeconds) {
this.emptyBlockPeriodSeconds = emptyBlockPeriodSeconds;
}
/** /**
* Sets block period milliseconds. Experimental for test scenarios. Not for use on production * Sets block period milliseconds. Experimental for test scenarios. Not for use on production
* systems. * systems.

@ -37,7 +37,7 @@ public class ForksScheduleFactoryTest {
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
public void throwsErrorIfHasForkForGenesisBlock() { public void throwsErrorIfHasForkForGenesisBlock() {
final BftConfigOptions genesisConfigOptions = JsonBftConfigOptions.DEFAULT; final BftConfigOptions genesisConfigOptions = JsonBftConfigOptions.DEFAULT;
final BftFork fork = createFork(0, 10); final BftFork fork = createFork(0, 10, 30);
final SpecCreator<BftConfigOptions, BftFork> specCreator = Mockito.mock(SpecCreator.class); final SpecCreator<BftConfigOptions, BftFork> specCreator = Mockito.mock(SpecCreator.class);
assertThatThrownBy( assertThatThrownBy(
@ -49,9 +49,9 @@ public class ForksScheduleFactoryTest {
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
public void throwsErrorIfHasForksWithDuplicateBlock() { public void throwsErrorIfHasForksWithDuplicateBlock() {
final BftConfigOptions genesisConfigOptions = JsonBftConfigOptions.DEFAULT; final BftConfigOptions genesisConfigOptions = JsonBftConfigOptions.DEFAULT;
final BftFork fork1 = createFork(1, 10); final BftFork fork1 = createFork(1, 10, 30);
final BftFork fork2 = createFork(1, 20); final BftFork fork2 = createFork(1, 20, 60);
final BftFork fork3 = createFork(2, 30); final BftFork fork3 = createFork(2, 30, 90);
final SpecCreator<BftConfigOptions, BftFork> specCreator = Mockito.mock(SpecCreator.class); final SpecCreator<BftConfigOptions, BftFork> specCreator = Mockito.mock(SpecCreator.class);
assertThatThrownBy( assertThatThrownBy(
@ -66,12 +66,12 @@ public class ForksScheduleFactoryTest {
public void createsScheduleUsingSpecCreator() { public void createsScheduleUsingSpecCreator() {
final BftConfigOptions genesisConfigOptions = JsonBftConfigOptions.DEFAULT; final BftConfigOptions genesisConfigOptions = JsonBftConfigOptions.DEFAULT;
final ForkSpec<BftConfigOptions> genesisForkSpec = new ForkSpec<>(0, genesisConfigOptions); final ForkSpec<BftConfigOptions> genesisForkSpec = new ForkSpec<>(0, genesisConfigOptions);
final BftFork fork1 = createFork(1, 10); final BftFork fork1 = createFork(1, 10, 20);
final BftFork fork2 = createFork(2, 20); final BftFork fork2 = createFork(2, 20, 40);
final SpecCreator<BftConfigOptions, BftFork> specCreator = Mockito.mock(SpecCreator.class); final SpecCreator<BftConfigOptions, BftFork> specCreator = Mockito.mock(SpecCreator.class);
final BftConfigOptions configOptions1 = createBftConfigOptions(10); final BftConfigOptions configOptions1 = createBftConfigOptions(10, 30);
final BftConfigOptions configOptions2 = createBftConfigOptions(20); final BftConfigOptions configOptions2 = createBftConfigOptions(20, 60);
when(specCreator.create(genesisForkSpec, fork1)).thenReturn(configOptions1); when(specCreator.create(genesisForkSpec, fork1)).thenReturn(configOptions1);
when(specCreator.create(new ForkSpec<>(1, configOptions1), fork2)).thenReturn(configOptions2); when(specCreator.create(new ForkSpec<>(1, configOptions1), fork2)).thenReturn(configOptions2);
@ -82,18 +82,25 @@ public class ForksScheduleFactoryTest {
assertThat(schedule.getFork(2)).isEqualTo(new ForkSpec<>(2, configOptions2)); assertThat(schedule.getFork(2)).isEqualTo(new ForkSpec<>(2, configOptions2));
} }
private MutableBftConfigOptions createBftConfigOptions(final int blockPeriodSeconds) { private MutableBftConfigOptions createBftConfigOptions(
final int blockPeriodSeconds, final int emptyBlockPeriodSeconds) {
final MutableBftConfigOptions bftConfigOptions = final MutableBftConfigOptions bftConfigOptions =
new MutableBftConfigOptions(JsonBftConfigOptions.DEFAULT); new MutableBftConfigOptions(JsonBftConfigOptions.DEFAULT);
bftConfigOptions.setBlockPeriodSeconds(blockPeriodSeconds); bftConfigOptions.setBlockPeriodSeconds(blockPeriodSeconds);
bftConfigOptions.setEmptyBlockPeriodSeconds(emptyBlockPeriodSeconds);
return bftConfigOptions; return bftConfigOptions;
} }
private BftFork createFork(final long block, final long blockPeriodSeconds) { private BftFork createFork(
final long block, final long blockPeriodSeconds, final long emptyBlockPeriodSeconds) {
return new BftFork( return new BftFork(
JsonUtil.objectNodeFromMap( JsonUtil.objectNodeFromMap(
Map.of( Map.of(
BftFork.FORK_BLOCK_KEY, block, BftFork.FORK_BLOCK_KEY,
BftFork.BLOCK_PERIOD_SECONDS_KEY, blockPeriodSeconds))); block,
BftFork.BLOCK_PERIOD_SECONDS_KEY,
blockPeriodSeconds,
BftFork.EMPTY_BLOCK_PERIOD_SECONDS_KEY,
emptyBlockPeriodSeconds)));
} }
} }

@ -75,12 +75,18 @@ public class BlockTimerTest {
@Test @Test
public void startTimerSchedulesCorrectlyWhenExpiryIsInTheFuture() { public void startTimerSchedulesCorrectlyWhenExpiryIsInTheFuture() {
final int MINIMAL_TIME_BETWEEN_BLOCKS_SECONDS = 15; final int MINIMAL_TIME_BETWEEN_BLOCKS_SECONDS = 15;
final int MINIMAL_TIME_BETWEEN_EMPTY_BLOCKS_SECONDS = 60;
final long NOW_MILLIS = 505_000L; final long NOW_MILLIS = 505_000L;
final long BLOCK_TIME_STAMP = 500L; final long BLOCK_TIME_STAMP = 500L;
final long EXPECTED_DELAY = 10_000L; final long EXPECTED_DELAY = 10_000L;
when(mockForksSchedule.getFork(anyLong())) when(mockForksSchedule.getFork(anyLong()))
.thenReturn(new ForkSpec<>(0, createBftFork(MINIMAL_TIME_BETWEEN_BLOCKS_SECONDS))); .thenReturn(
new ForkSpec<>(
0,
createBftFork(
MINIMAL_TIME_BETWEEN_BLOCKS_SECONDS,
MINIMAL_TIME_BETWEEN_EMPTY_BLOCKS_SECONDS)));
final BlockTimer timer = new BlockTimer(mockQueue, mockForksSchedule, bftExecutors, mockClock); final BlockTimer timer = new BlockTimer(mockQueue, mockForksSchedule, bftExecutors, mockClock);
@ -104,12 +110,18 @@ public class BlockTimerTest {
@Test @Test
public void aBlockTimerExpiryEventIsAddedToTheQueueOnExpiry() throws InterruptedException { public void aBlockTimerExpiryEventIsAddedToTheQueueOnExpiry() throws InterruptedException {
final int MINIMAL_TIME_BETWEEN_BLOCKS_SECONDS = 1; final int MINIMAL_TIME_BETWEEN_BLOCKS_SECONDS = 1;
final int MINIMAL_TIME_BETWEEN_EMPTY_BLOCKS_SECONDS = 10;
final long NOW_MILLIS = 300_500L; final long NOW_MILLIS = 300_500L;
final long BLOCK_TIME_STAMP = 300; final long BLOCK_TIME_STAMP = 300;
final long EXPECTED_DELAY = 500; final long EXPECTED_DELAY = 500;
when(mockForksSchedule.getFork(anyLong())) when(mockForksSchedule.getFork(anyLong()))
.thenReturn(new ForkSpec<>(0, createBftFork(MINIMAL_TIME_BETWEEN_BLOCKS_SECONDS))); .thenReturn(
new ForkSpec<>(
0,
createBftFork(
MINIMAL_TIME_BETWEEN_BLOCKS_SECONDS,
MINIMAL_TIME_BETWEEN_EMPTY_BLOCKS_SECONDS)));
when(mockClock.millis()).thenReturn(NOW_MILLIS); when(mockClock.millis()).thenReturn(NOW_MILLIS);
final BlockHeader header = final BlockHeader header =
@ -149,11 +161,17 @@ public class BlockTimerTest {
@Test @Test
public void eventIsImmediatelyAddedToTheQueueIfAbsoluteExpiryIsEqualToNow() { public void eventIsImmediatelyAddedToTheQueueIfAbsoluteExpiryIsEqualToNow() {
final int MINIMAL_TIME_BETWEEN_BLOCKS_SECONDS = 15; final int MINIMAL_TIME_BETWEEN_BLOCKS_SECONDS = 15;
final int MINIMAL_TIME_BETWEEN_EMPTY_BLOCKS_SECONDS = 60;
final long NOW_MILLIS = 515_000L; final long NOW_MILLIS = 515_000L;
final long BLOCK_TIME_STAMP = 500; final long BLOCK_TIME_STAMP = 500;
when(mockForksSchedule.getFork(anyLong())) when(mockForksSchedule.getFork(anyLong()))
.thenReturn(new ForkSpec<>(0, createBftFork(MINIMAL_TIME_BETWEEN_BLOCKS_SECONDS))); .thenReturn(
new ForkSpec<>(
0,
createBftFork(
MINIMAL_TIME_BETWEEN_BLOCKS_SECONDS,
MINIMAL_TIME_BETWEEN_EMPTY_BLOCKS_SECONDS)));
final BlockTimer timer = new BlockTimer(mockQueue, mockForksSchedule, bftExecutors, mockClock); final BlockTimer timer = new BlockTimer(mockQueue, mockForksSchedule, bftExecutors, mockClock);
@ -179,11 +197,17 @@ public class BlockTimerTest {
@Test @Test
public void eventIsImmediatelyAddedToTheQueueIfAbsoluteExpiryIsInThePast() { public void eventIsImmediatelyAddedToTheQueueIfAbsoluteExpiryIsInThePast() {
final int MINIMAL_TIME_BETWEEN_BLOCKS_SECONDS = 15; final int MINIMAL_TIME_BETWEEN_BLOCKS_SECONDS = 15;
final int MINIMAL_TIME_BETWEEN_EMPTY_BLOCKS_SECONDS = 60;
final long NOW_MILLIS = 520_000L; final long NOW_MILLIS = 520_000L;
final long BLOCK_TIME_STAMP = 500L; final long BLOCK_TIME_STAMP = 500L;
when(mockForksSchedule.getFork(anyLong())) when(mockForksSchedule.getFork(anyLong()))
.thenReturn(new ForkSpec<>(0, createBftFork(MINIMAL_TIME_BETWEEN_BLOCKS_SECONDS))); .thenReturn(
new ForkSpec<>(
0,
createBftFork(
MINIMAL_TIME_BETWEEN_BLOCKS_SECONDS,
MINIMAL_TIME_BETWEEN_EMPTY_BLOCKS_SECONDS)));
final BlockTimer timer = new BlockTimer(mockQueue, mockForksSchedule, bftExecutors, mockClock); final BlockTimer timer = new BlockTimer(mockQueue, mockForksSchedule, bftExecutors, mockClock);
@ -209,11 +233,17 @@ public class BlockTimerTest {
@Test @Test
public void startTimerCancelsExistingTimer() { public void startTimerCancelsExistingTimer() {
final int MINIMAL_TIME_BETWEEN_BLOCKS_SECONDS = 15; final int MINIMAL_TIME_BETWEEN_BLOCKS_SECONDS = 15;
final int MINIMAL_TIME_BETWEEN_EMPTY_BLOCKS_SECONDS = 60;
final long NOW_MILLIS = 500_000L; final long NOW_MILLIS = 500_000L;
final long BLOCK_TIME_STAMP = 500L; final long BLOCK_TIME_STAMP = 500L;
when(mockForksSchedule.getFork(anyLong())) when(mockForksSchedule.getFork(anyLong()))
.thenReturn(new ForkSpec<>(0, createBftFork(MINIMAL_TIME_BETWEEN_BLOCKS_SECONDS))); .thenReturn(
new ForkSpec<>(
0,
createBftFork(
MINIMAL_TIME_BETWEEN_BLOCKS_SECONDS,
MINIMAL_TIME_BETWEEN_EMPTY_BLOCKS_SECONDS)));
final BlockTimer timer = new BlockTimer(mockQueue, mockForksSchedule, bftExecutors, mockClock); final BlockTimer timer = new BlockTimer(mockQueue, mockForksSchedule, bftExecutors, mockClock);
@ -237,11 +267,17 @@ public class BlockTimerTest {
@Test @Test
public void runningFollowsTheStateOfTheTimer() { public void runningFollowsTheStateOfTheTimer() {
final int MINIMAL_TIME_BETWEEN_BLOCKS_SECONDS = 15; final int MINIMAL_TIME_BETWEEN_BLOCKS_SECONDS = 15;
final int MINIMAL_TIME_BETWEEN_EMPTY_BLOCKS_SECONDS = 60;
final long NOW_MILLIS = 500_000L; final long NOW_MILLIS = 500_000L;
final long BLOCK_TIME_STAMP = 500L; final long BLOCK_TIME_STAMP = 500L;
when(mockForksSchedule.getFork(anyLong())) when(mockForksSchedule.getFork(anyLong()))
.thenReturn(new ForkSpec<>(0, createBftFork(MINIMAL_TIME_BETWEEN_BLOCKS_SECONDS))); .thenReturn(
new ForkSpec<>(
0,
createBftFork(
MINIMAL_TIME_BETWEEN_BLOCKS_SECONDS,
MINIMAL_TIME_BETWEEN_EMPTY_BLOCKS_SECONDS)));
final BlockTimer timer = new BlockTimer(mockQueue, mockForksSchedule, bftExecutors, mockClock); final BlockTimer timer = new BlockTimer(mockQueue, mockForksSchedule, bftExecutors, mockClock);
@ -263,10 +299,42 @@ public class BlockTimerTest {
assertThat(timer.isRunning()).isFalse(); assertThat(timer.isRunning()).isFalse();
} }
private BftConfigOptions createBftFork(final int blockPeriodSeconds) { @Test
public void checkBlockTimerEmptyAndNonEmptyPeriodSecods() {
final int MINIMAL_TIME_BETWEEN_BLOCKS_SECONDS = 15;
final int MINIMAL_TIME_BETWEEN_EMPTY_BLOCKS_SECONDS = 60;
final long BLOCK_TIME_STAMP = 500L;
final ConsensusRoundIdentifier round =
new ConsensusRoundIdentifier(0xFEDBCA9876543210L, 0x12345678);
final BlockHeader header =
new BlockHeaderTestFixture().timestamp(BLOCK_TIME_STAMP).buildHeader();
final ScheduledFuture<?> mockedFuture = mock(ScheduledFuture.class);
Mockito.<ScheduledFuture<?>>when(
bftExecutors.scheduleTask(any(Runnable.class), anyLong(), any()))
.thenReturn(mockedFuture);
when(mockForksSchedule.getFork(anyLong()))
.thenReturn(
new ForkSpec<>(
0,
createBftFork(
MINIMAL_TIME_BETWEEN_BLOCKS_SECONDS,
MINIMAL_TIME_BETWEEN_EMPTY_BLOCKS_SECONDS)));
final BlockTimer timer = new BlockTimer(mockQueue, mockForksSchedule, bftExecutors, mockClock);
timer.startTimer(round, header);
assertThat(timer.getBlockPeriodSeconds()).isEqualTo(MINIMAL_TIME_BETWEEN_BLOCKS_SECONDS);
assertThat(timer.getEmptyBlockPeriodSeconds())
.isEqualTo(MINIMAL_TIME_BETWEEN_EMPTY_BLOCKS_SECONDS);
}
private BftConfigOptions createBftFork(
final int blockPeriodSeconds, final int emptyBlockPeriodSeconds) {
final MutableBftConfigOptions bftConfigOptions = final MutableBftConfigOptions bftConfigOptions =
new MutableBftConfigOptions(JsonBftConfigOptions.DEFAULT); new MutableBftConfigOptions(JsonBftConfigOptions.DEFAULT);
bftConfigOptions.setBlockPeriodSeconds(blockPeriodSeconds); bftConfigOptions.setBlockPeriodSeconds(blockPeriodSeconds);
bftConfigOptions.setEmptyBlockPeriodSeconds(emptyBlockPeriodSeconds);
return bftConfigOptions; return bftConfigOptions;
} }
} }

@ -49,6 +49,7 @@ public class QbftForksSchedulesFactory {
new MutableQbftConfigOptions(lastSpec.getValue()); new MutableQbftConfigOptions(lastSpec.getValue());
fork.getBlockPeriodSeconds().ifPresent(bftConfigOptions::setBlockPeriodSeconds); fork.getBlockPeriodSeconds().ifPresent(bftConfigOptions::setBlockPeriodSeconds);
fork.getEmptyBlockPeriodSeconds().ifPresent(bftConfigOptions::setEmptyBlockPeriodSeconds);
fork.getBlockPeriodMilliseconds().ifPresent(bftConfigOptions::setBlockPeriodMilliseconds); fork.getBlockPeriodMilliseconds().ifPresent(bftConfigOptions::setBlockPeriodMilliseconds);
fork.getBlockRewardWei().ifPresent(bftConfigOptions::setBlockRewardWei); fork.getBlockRewardWei().ifPresent(bftConfigOptions::setBlockRewardWei);

@ -28,6 +28,7 @@ import org.hyperledger.besu.consensus.qbft.payload.MessageFactory;
import org.hyperledger.besu.consensus.qbft.validation.FutureRoundProposalMessageValidator; import org.hyperledger.besu.consensus.qbft.validation.FutureRoundProposalMessageValidator;
import org.hyperledger.besu.consensus.qbft.validation.MessageValidatorFactory; import org.hyperledger.besu.consensus.qbft.validation.MessageValidatorFactory;
import org.hyperledger.besu.datatypes.Address; import org.hyperledger.besu.datatypes.Address;
import org.hyperledger.besu.ethereum.core.Block;
import org.hyperledger.besu.ethereum.core.BlockHeader; import org.hyperledger.besu.ethereum.core.BlockHeader;
import org.hyperledger.besu.plugin.services.securitymodule.SecurityModuleException; import org.hyperledger.besu.plugin.services.securitymodule.SecurityModuleException;
@ -130,19 +131,57 @@ public class QbftBlockHeightManager implements BaseQbftBlockHeightManager {
logValidatorChanges(qbftRound); logValidatorChanges(qbftRound);
if (roundIdentifier.equals(qbftRound.getRoundIdentifier())) {
buildBlockAndMaybePropose(roundIdentifier, qbftRound);
} else {
LOG.trace(
"Block timer expired for a round ({}) other than current ({})",
roundIdentifier,
qbftRound.getRoundIdentifier());
}
}
private void buildBlockAndMaybePropose(
final ConsensusRoundIdentifier roundIdentifier, final QbftRound qbftRound) {
// mining will be checked against round 0 as the current round is initialised to 0 above // mining will be checked against round 0 as the current round is initialised to 0 above
final boolean isProposer = final boolean isProposer =
finalState.isLocalNodeProposerForRound(qbftRound.getRoundIdentifier()); finalState.isLocalNodeProposerForRound(qbftRound.getRoundIdentifier());
if (isProposer) { if (!isProposer) {
if (roundIdentifier.equals(qbftRound.getRoundIdentifier())) { // nothing to do here...
final long headerTimeStampSeconds = Math.round(clock.millis() / 1000D); LOG.trace("This node is not a proposer so it will not send a proposal: " + roundIdentifier);
qbftRound.createAndSendProposalMessage(headerTimeStampSeconds); return;
}
final long headerTimeStampSeconds = Math.round(clock.millis() / 1000D);
final Block block = qbftRound.createBlock(headerTimeStampSeconds);
final boolean blockHasTransactions = !block.getBody().getTransactions().isEmpty();
if (blockHasTransactions) {
LOG.trace(
"Block has transactions and this node is a proposer so it will send a proposal: "
+ roundIdentifier);
qbftRound.updateStateWithProposalAndTransmit(block);
} else {
// handle the block times period
final long currentTimeInMillis = finalState.getClock().millis();
boolean emptyBlockExpired =
finalState.getBlockTimer().checkEmptyBlockExpired(parentHeader, currentTimeInMillis);
if (emptyBlockExpired) {
LOG.trace(
"Block has no transactions and this node is a proposer so it will send a proposal: "
+ roundIdentifier);
qbftRound.updateStateWithProposalAndTransmit(block);
} else { } else {
LOG.trace( LOG.trace(
"Block timer expired for a round ({}) other than current ({})", "Block has no transactions but emptyBlockPeriodSeconds did not expired yet: "
roundIdentifier, + roundIdentifier);
qbftRound.getRoundIdentifier()); finalState
.getBlockTimer()
.resetTimerForEmptyBlock(roundIdentifier, parentHeader, currentTimeInMillis);
finalState.getRoundTimer().cancelTimer();
currentRound = Optional.empty();
} }
} }
} }

@ -132,17 +132,14 @@ public class QbftRound {
} }
/** /**
* Create and send proposal message. * Create a block
* *
* @param headerTimeStampSeconds the header time stamp seconds * @param headerTimeStampSeconds of the block
* @return a Block
*/ */
public void createAndSendProposalMessage(final long headerTimeStampSeconds) { public Block createBlock(final long headerTimeStampSeconds) {
LOG.debug("Creating proposed block. round={}", roundState.getRoundIdentifier()); LOG.debug("Creating proposed block. round={}", roundState.getRoundIdentifier());
final Block block = return blockCreator.createBlock(headerTimeStampSeconds, this.parentHeader).getBlock();
blockCreator.createBlock(headerTimeStampSeconds, this.parentHeader).getBlock();
LOG.trace("Creating proposed block blockHeader={}", block.getHeader());
updateStateWithProposalAndTransmit(block, emptyList(), emptyList());
} }
/** /**
@ -172,6 +169,15 @@ public class QbftRound {
bestPreparedCertificate.map(PreparedCertificate::getPrepares).orElse(emptyList())); bestPreparedCertificate.map(PreparedCertificate::getPrepares).orElse(emptyList()));
} }
/**
* Update state with proposal and transmit.
*
* @param block the block
*/
protected void updateStateWithProposalAndTransmit(final Block block) {
updateStateWithProposalAndTransmit(block, emptyList(), emptyList());
}
/** /**
* Update state with proposal and transmit. * Update state with proposal and transmit.
* *

@ -37,4 +37,24 @@ public class MutableQbftConfigOptionsTest {
assertThat(mutableQbftConfigOptions.getValidatorContractAddress()).hasValue("0xabc"); assertThat(mutableQbftConfigOptions.getValidatorContractAddress()).hasValue("0xabc");
} }
@Test
public void checkBlockPeriodSeconds() {
when(qbftConfigOptions.getBlockPeriodSeconds()).thenReturn(2);
final MutableQbftConfigOptions mutableQbftConfigOptions =
new MutableQbftConfigOptions(qbftConfigOptions);
assertThat(mutableQbftConfigOptions.getBlockPeriodSeconds()).isEqualTo(2);
}
@Test
public void checkEmptyBlockPeriodSeconds() {
when(qbftConfigOptions.getEmptyBlockPeriodSeconds()).thenReturn(60);
final MutableQbftConfigOptions mutableQbftConfigOptions =
new MutableQbftConfigOptions(qbftConfigOptions);
assertThat(mutableQbftConfigOptions.getEmptyBlockPeriodSeconds()).isEqualTo(60);
}
} }

@ -157,8 +157,10 @@ public class QbftBlockHeightManagerTest {
when(messageValidator.validateCommit(any())).thenReturn(true); when(messageValidator.validateCommit(any())).thenReturn(true);
when(messageValidator.validatePrepare(any())).thenReturn(true); when(messageValidator.validatePrepare(any())).thenReturn(true);
when(finalState.getBlockTimer()).thenReturn(blockTimer); when(finalState.getBlockTimer()).thenReturn(blockTimer);
when(finalState.getRoundTimer()).thenReturn(roundTimer);
when(finalState.getQuorum()).thenReturn(3); when(finalState.getQuorum()).thenReturn(3);
when(finalState.getValidatorMulticaster()).thenReturn(validatorMulticaster); when(finalState.getValidatorMulticaster()).thenReturn(validatorMulticaster);
when(finalState.getClock()).thenReturn(clock);
when(blockCreator.createBlock(anyLong(), any())) when(blockCreator.createBlock(anyLong(), any()))
.thenReturn( .thenReturn(
new BlockCreationResult( new BlockCreationResult(
@ -267,6 +269,7 @@ public class QbftBlockHeightManagerTest {
@Test @Test
public void onBlockTimerExpiryRoundTimerIsStartedAndProposalMessageIsTransmitted() { public void onBlockTimerExpiryRoundTimerIsStartedAndProposalMessageIsTransmitted() {
when(finalState.isLocalNodeProposerForRound(roundIdentifier)).thenReturn(true); when(finalState.isLocalNodeProposerForRound(roundIdentifier)).thenReturn(true);
when(blockTimer.checkEmptyBlockExpired(any(), eq(0l))).thenReturn(true);
final QbftBlockHeightManager manager = final QbftBlockHeightManager manager =
new QbftBlockHeightManager( new QbftBlockHeightManager(
@ -290,6 +293,7 @@ public class QbftBlockHeightManagerTest {
public void public void
onBlockTimerExpiryForNonProposerRoundTimerIsStartedAndNoProposalMessageIsTransmitted() { onBlockTimerExpiryForNonProposerRoundTimerIsStartedAndNoProposalMessageIsTransmitted() {
when(finalState.isLocalNodeProposerForRound(roundIdentifier)).thenReturn(false); when(finalState.isLocalNodeProposerForRound(roundIdentifier)).thenReturn(false);
when(blockTimer.checkEmptyBlockExpired(any(), eq(0l))).thenReturn(true);
final QbftBlockHeightManager manager = final QbftBlockHeightManager manager =
new QbftBlockHeightManager( new QbftBlockHeightManager(
@ -463,6 +467,7 @@ public class QbftBlockHeightManagerTest {
public void messagesForCurrentRoundAreBufferedAndUsedToPreloadRoundWhenItIsStarted() { public void messagesForCurrentRoundAreBufferedAndUsedToPreloadRoundWhenItIsStarted() {
when(finalState.getQuorum()).thenReturn(1); when(finalState.getQuorum()).thenReturn(1);
when(finalState.isLocalNodeProposerForRound(roundIdentifier)).thenReturn(true); when(finalState.isLocalNodeProposerForRound(roundIdentifier)).thenReturn(true);
when(blockTimer.checkEmptyBlockExpired(any(), eq(0l))).thenReturn(true);
final QbftBlockHeightManager manager = final QbftBlockHeightManager manager =
new QbftBlockHeightManager( new QbftBlockHeightManager(
@ -500,6 +505,7 @@ public class QbftBlockHeightManagerTest {
@Test @Test
public void preparedCertificateIncludedInRoundChangeMessageOnRoundTimeoutExpired() { public void preparedCertificateIncludedInRoundChangeMessageOnRoundTimeoutExpired() {
when(finalState.isLocalNodeProposerForRound(any())).thenReturn(true); when(finalState.isLocalNodeProposerForRound(any())).thenReturn(true);
when(blockTimer.checkEmptyBlockExpired(any(), eq(0l))).thenReturn(true);
final QbftBlockHeightManager manager = final QbftBlockHeightManager manager =
new QbftBlockHeightManager( new QbftBlockHeightManager(
@ -577,4 +583,24 @@ public class QbftBlockHeightManagerTest {
manager.handleProposalPayload(futureRoundProposal); manager.handleProposalPayload(futureRoundProposal);
verify(roundFactory, never()).createNewRound(any(), anyInt()); verify(roundFactory, never()).createNewRound(any(), anyInt());
} }
@Test
public void checkOnlyEmptyBlockPeriodSecondsIsInvokedForBlocksWithNoTransactions() {
when(finalState.isLocalNodeProposerForRound(roundIdentifier)).thenReturn(true);
final QbftBlockHeightManager manager =
new QbftBlockHeightManager(
headerTestFixture.buildHeader(),
finalState,
roundChangeManager,
roundFactory,
clock,
messageValidatorFactory,
messageFactory);
manager.handleBlockTimerExpiry(roundIdentifier);
verify(blockTimer, times(0)).getEmptyBlockPeriodSeconds();
verify(blockTimer, times(0)).getBlockPeriodSeconds();
}
} }

@ -29,7 +29,6 @@ import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions; import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
import org.hyperledger.besu.consensus.common.bft.BftBlockHashing;
import org.hyperledger.besu.consensus.common.bft.BftContext; import org.hyperledger.besu.consensus.common.bft.BftContext;
import org.hyperledger.besu.consensus.common.bft.BftExtraData; import org.hyperledger.besu.consensus.common.bft.BftExtraData;
import org.hyperledger.besu.consensus.common.bft.BftExtraDataCodec; import org.hyperledger.besu.consensus.common.bft.BftExtraDataCodec;
@ -48,7 +47,6 @@ import org.hyperledger.besu.crypto.SECPSignature;
import org.hyperledger.besu.crypto.SignatureAlgorithmFactory; import org.hyperledger.besu.crypto.SignatureAlgorithmFactory;
import org.hyperledger.besu.cryptoservices.NodeKey; import org.hyperledger.besu.cryptoservices.NodeKey;
import org.hyperledger.besu.cryptoservices.NodeKeyUtils; import org.hyperledger.besu.cryptoservices.NodeKeyUtils;
import org.hyperledger.besu.datatypes.Hash;
import org.hyperledger.besu.ethereum.ProtocolContext; import org.hyperledger.besu.ethereum.ProtocolContext;
import org.hyperledger.besu.ethereum.blockcreation.BlockCreationTiming; import org.hyperledger.besu.ethereum.blockcreation.BlockCreationTiming;
import org.hyperledger.besu.ethereum.blockcreation.BlockCreator.BlockCreationResult; import org.hyperledger.besu.ethereum.blockcreation.BlockCreator.BlockCreationResult;
@ -196,90 +194,6 @@ public class QbftRoundTest {
verify(transmitter, never()).multicastCommit(any(), any(), any()); verify(transmitter, never()).multicastCommit(any(), any(), any());
} }
@Test
public void sendsAProposalAndPrepareWhenSendProposalRequested() {
final RoundState roundState = new RoundState(roundIdentifier, 3, messageValidator);
final QbftRound round =
new QbftRound(
roundState,
blockCreator,
protocolContext,
protocolSchedule,
subscribers,
nodeKey,
messageFactory,
transmitter,
roundTimer,
bftExtraDataCodec,
parentHeader);
round.createAndSendProposalMessage(15);
verify(transmitter, times(1))
.multicastProposal(
roundIdentifier, proposedBlock, Collections.emptyList(), Collections.emptyList());
verify(transmitter, times(1)).multicastPrepare(roundIdentifier, proposedBlock.getHash());
verify(transmitter, never()).multicastCommit(any(), any(), any());
}
@Test
public void singleValidatorImportBlocksImmediatelyOnProposalCreation() {
final RoundState roundState = new RoundState(roundIdentifier, 1, messageValidator);
final QbftRound round =
new QbftRound(
roundState,
blockCreator,
protocolContext,
protocolSchedule,
subscribers,
nodeKey,
messageFactory,
transmitter,
roundTimer,
bftExtraDataCodec,
parentHeader);
round.createAndSendProposalMessage(15);
verify(transmitter, times(1))
.multicastProposal(
roundIdentifier, proposedBlock, Collections.emptyList(), Collections.emptyList());
verify(transmitter, times(1)).multicastPrepare(roundIdentifier, proposedBlock.getHash());
verify(transmitter, times(1)).multicastCommit(any(), any(), any());
}
@Test
public void localNodeProposesToNetworkOfTwoValidatorsImportsOnReceptionOfCommitFromPeer() {
final RoundState roundState = new RoundState(roundIdentifier, 2, messageValidator);
final QbftRound round =
new QbftRound(
roundState,
blockCreator,
protocolContext,
protocolSchedule,
subscribers,
nodeKey,
messageFactory,
transmitter,
roundTimer,
bftExtraDataCodec,
parentHeader);
final Hash commitSealHash =
new BftBlockHashing(new QbftExtraDataCodec())
.calculateDataHashForCommittedSeal(proposedBlock.getHeader(), proposedExtraData);
final SECPSignature localCommitSeal = nodeKey.sign(commitSealHash);
round.createAndSendProposalMessage(15);
verify(transmitter, never()).multicastCommit(any(), any(), any());
round.handlePrepareMessage(
messageFactory2.createPrepare(roundIdentifier, proposedBlock.getHash()));
verify(transmitter, times(1))
.multicastCommit(roundIdentifier, proposedBlock.getHash(), localCommitSeal);
round.handleCommitMessage(
messageFactory.createCommit(roundIdentifier, proposedBlock.getHash(), remoteCommitSeal));
}
@Test @Test
public void aProposalWithAnewBlockIsSentUponReceptionOfARoundChangeWithNoCertificate() { public void aProposalWithAnewBlockIsSentUponReceptionOfARoundChangeWithNoCertificate() {
final RoundState roundState = new RoundState(roundIdentifier, 2, messageValidator); final RoundState roundState = new RoundState(roundIdentifier, 2, messageValidator);
@ -393,26 +307,6 @@ public class QbftRoundTest {
assertThat(roundState.isPrepared()).isTrue(); assertThat(roundState.isPrepared()).isTrue();
} }
@Test
public void creatingNewBlockNotifiesBlockMiningObservers() {
final RoundState roundState = new RoundState(roundIdentifier, 1, messageValidator);
final QbftRound round =
new QbftRound(
roundState,
blockCreator,
protocolContext,
protocolSchedule,
subscribers,
nodeKey,
messageFactory,
transmitter,
roundTimer,
bftExtraDataCodec,
parentHeader);
round.createAndSendProposalMessage(15);
verify(minedBlockObserver).blockMined(any());
}
@Test @Test
public void blockIsOnlyImportedOnceWhenCommitsAreReceivedBeforeProposal() { public void blockIsOnlyImportedOnceWhenCommitsAreReceivedBeforeProposal() {
final ConsensusRoundIdentifier roundIdentifier = new ConsensusRoundIdentifier(1, 0); final ConsensusRoundIdentifier roundIdentifier = new ConsensusRoundIdentifier(1, 0);

@ -63,6 +63,8 @@ public class QbftForksSchedulesFactoryTest
List.of("1", "2", "3"), List.of("1", "2", "3"),
BftFork.BLOCK_PERIOD_SECONDS_KEY, BftFork.BLOCK_PERIOD_SECONDS_KEY,
10, 10,
BftFork.EMPTY_BLOCK_PERIOD_SECONDS_KEY,
60,
BftFork.BLOCK_REWARD_KEY, BftFork.BLOCK_REWARD_KEY,
"5", "5",
QbftFork.VALIDATOR_SELECTION_MODE_KEY, QbftFork.VALIDATOR_SELECTION_MODE_KEY,
@ -78,6 +80,7 @@ public class QbftForksSchedulesFactoryTest
final Map<String, Object> forkOptions = new HashMap<>(configOptions.asMap()); final Map<String, Object> forkOptions = new HashMap<>(configOptions.asMap());
forkOptions.put(BftFork.BLOCK_PERIOD_SECONDS_KEY, 10); forkOptions.put(BftFork.BLOCK_PERIOD_SECONDS_KEY, 10);
forkOptions.put(BftFork.EMPTY_BLOCK_PERIOD_SECONDS_KEY, 60);
forkOptions.put(BftFork.BLOCK_REWARD_KEY, "5"); forkOptions.put(BftFork.BLOCK_REWARD_KEY, "5");
forkOptions.put(QbftFork.VALIDATOR_SELECTION_MODE_KEY, "5"); forkOptions.put(QbftFork.VALIDATOR_SELECTION_MODE_KEY, "5");
forkOptions.put(QbftFork.VALIDATOR_CONTRACT_ADDRESS_KEY, "10"); forkOptions.put(QbftFork.VALIDATOR_CONTRACT_ADDRESS_KEY, "10");

@ -131,6 +131,11 @@ public abstract class MiningParameters {
return this; return this;
} }
public MiningParameters setEmptyBlockPeriodSeconds(final int emptyBlockPeriodSeconds) {
getMutableRuntimeValues().emptyBlockPeriodSeconds = OptionalInt.of(emptyBlockPeriodSeconds);
return this;
}
@Value.Default @Value.Default
public boolean isStratumMiningEnabled() { public boolean isStratumMiningEnabled() {
return false; return false;
@ -231,6 +236,8 @@ public abstract class MiningParameters {
OptionalInt getBlockPeriodSeconds(); OptionalInt getBlockPeriodSeconds();
OptionalInt getEmptyBlockPeriodSeconds();
Optional<Address> getCoinbase(); Optional<Address> getCoinbase();
OptionalLong getTargetGasLimit(); OptionalLong getTargetGasLimit();
@ -248,6 +255,7 @@ public abstract class MiningParameters {
private volatile OptionalLong targetGasLimit; private volatile OptionalLong targetGasLimit;
private volatile Optional<Iterable<Long>> nonceGenerator; private volatile Optional<Iterable<Long>> nonceGenerator;
private volatile OptionalInt blockPeriodSeconds; private volatile OptionalInt blockPeriodSeconds;
private volatile OptionalInt emptyBlockPeriodSeconds;
private MutableRuntimeValues(final MutableInitValues initValues) { private MutableRuntimeValues(final MutableInitValues initValues) {
miningEnabled = initValues.isMiningEnabled(); miningEnabled = initValues.isMiningEnabled();
@ -259,6 +267,7 @@ public abstract class MiningParameters {
targetGasLimit = initValues.getTargetGasLimit(); targetGasLimit = initValues.getTargetGasLimit();
nonceGenerator = initValues.nonceGenerator(); nonceGenerator = initValues.nonceGenerator();
blockPeriodSeconds = initValues.getBlockPeriodSeconds(); blockPeriodSeconds = initValues.getBlockPeriodSeconds();
emptyBlockPeriodSeconds = initValues.getEmptyBlockPeriodSeconds();
} }
@Override @Override
@ -274,7 +283,8 @@ public abstract class MiningParameters {
&& Objects.equals(minPriorityFeePerGas, that.minPriorityFeePerGas) && Objects.equals(minPriorityFeePerGas, that.minPriorityFeePerGas)
&& Objects.equals(targetGasLimit, that.targetGasLimit) && Objects.equals(targetGasLimit, that.targetGasLimit)
&& Objects.equals(nonceGenerator, that.nonceGenerator) && Objects.equals(nonceGenerator, that.nonceGenerator)
&& Objects.equals(blockPeriodSeconds, that.blockPeriodSeconds); && Objects.equals(blockPeriodSeconds, that.blockPeriodSeconds)
&& Objects.equals(emptyBlockPeriodSeconds, that.emptyBlockPeriodSeconds);
} }
@Override @Override

@ -23,14 +23,17 @@ public class PluginConfiguration {
private final List<PluginInfo> requestedPlugins; private final List<PluginInfo> requestedPlugins;
private final Path pluginsDir; private final Path pluginsDir;
private final boolean externalPluginsEnabled; private final boolean externalPluginsEnabled;
private final boolean continueOnPluginError;
public PluginConfiguration( public PluginConfiguration(
final List<PluginInfo> requestedPlugins, final List<PluginInfo> requestedPlugins,
final Path pluginsDir, final Path pluginsDir,
final boolean externalPluginsEnabled) { final boolean externalPluginsEnabled,
final boolean continueOnPluginError) {
this.requestedPlugins = requestedPlugins; this.requestedPlugins = requestedPlugins;
this.pluginsDir = pluginsDir; this.pluginsDir = pluginsDir;
this.externalPluginsEnabled = externalPluginsEnabled; this.externalPluginsEnabled = externalPluginsEnabled;
this.continueOnPluginError = continueOnPluginError;
} }
public List<String> getRequestedPlugins() { public List<String> getRequestedPlugins() {
@ -47,6 +50,10 @@ public class PluginConfiguration {
return externalPluginsEnabled; return externalPluginsEnabled;
} }
public boolean isContinueOnPluginError() {
return continueOnPluginError;
}
public static Path defaultPluginsDir() { public static Path defaultPluginsDir() {
String pluginsDirProperty = System.getProperty("besu.plugins.dir"); String pluginsDirProperty = System.getProperty("besu.plugins.dir");
return pluginsDirProperty == null return pluginsDirProperty == null
@ -62,6 +69,7 @@ public class PluginConfiguration {
private List<PluginInfo> requestedPlugins; private List<PluginInfo> requestedPlugins;
private Path pluginsDir; private Path pluginsDir;
private boolean externalPluginsEnabled = true; private boolean externalPluginsEnabled = true;
private boolean continueOnPluginError = false;
public Builder requestedPlugins(final List<PluginInfo> requestedPlugins) { public Builder requestedPlugins(final List<PluginInfo> requestedPlugins) {
this.requestedPlugins = requestedPlugins; this.requestedPlugins = requestedPlugins;
@ -78,11 +86,17 @@ public class PluginConfiguration {
return this; return this;
} }
public Builder continueOnPluginError(final boolean continueOnPluginError) {
this.continueOnPluginError = continueOnPluginError;
return this;
}
public PluginConfiguration build() { public PluginConfiguration build() {
if (pluginsDir == null) { if (pluginsDir == null) {
pluginsDir = PluginConfiguration.defaultPluginsDir(); pluginsDir = PluginConfiguration.defaultPluginsDir();
} }
return new PluginConfiguration(requestedPlugins, pluginsDir, externalPluginsEnabled); return new PluginConfiguration(
requestedPlugins, pluginsDir, externalPluginsEnabled, continueOnPluginError);
} }
} }
} }

@ -14,10 +14,8 @@
*/ */
package org.hyperledger.besu.ethereum.mainnet; package org.hyperledger.besu.ethereum.mainnet;
import static org.hyperledger.besu.ethereum.mainnet.requests.DepositRequestProcessor.DEFAULT_DEPOSIT_CONTRACT_ADDRESS;
import static org.hyperledger.besu.ethereum.mainnet.requests.MainnetRequestsValidator.pragueRequestsProcessors; import static org.hyperledger.besu.ethereum.mainnet.requests.MainnetRequestsValidator.pragueRequestsProcessors;
import static org.hyperledger.besu.ethereum.mainnet.requests.MainnetRequestsValidator.pragueRequestsValidator; import static org.hyperledger.besu.ethereum.mainnet.requests.MainnetRequestsValidator.pragueRequestsValidator;
import static org.hyperledger.besu.ethereum.mainnet.requests.WithdrawalRequestProcessor.DEFAULT_WITHDRAWAL_REQUEST_CONTRACT_ADDRESS;
import org.hyperledger.besu.config.GenesisConfigOptions; import org.hyperledger.besu.config.GenesisConfigOptions;
import org.hyperledger.besu.config.PowAlgorithm; import org.hyperledger.besu.config.PowAlgorithm;
@ -41,6 +39,7 @@ import org.hyperledger.besu.ethereum.mainnet.blockhash.PragueBlockHashProcessor;
import org.hyperledger.besu.ethereum.mainnet.feemarket.BaseFeeMarket; import org.hyperledger.besu.ethereum.mainnet.feemarket.BaseFeeMarket;
import org.hyperledger.besu.ethereum.mainnet.feemarket.FeeMarket; import org.hyperledger.besu.ethereum.mainnet.feemarket.FeeMarket;
import org.hyperledger.besu.ethereum.mainnet.parallelization.MainnetParallelBlockProcessor; import org.hyperledger.besu.ethereum.mainnet.parallelization.MainnetParallelBlockProcessor;
import org.hyperledger.besu.ethereum.mainnet.requests.RequestContractAddresses;
import org.hyperledger.besu.ethereum.privacy.PrivateTransactionProcessor; import org.hyperledger.besu.ethereum.privacy.PrivateTransactionProcessor;
import org.hyperledger.besu.ethereum.privacy.PrivateTransactionValidator; import org.hyperledger.besu.ethereum.privacy.PrivateTransactionValidator;
import org.hyperledger.besu.ethereum.privacy.storage.PrivateMetadataUpdater; import org.hyperledger.besu.ethereum.privacy.storage.PrivateMetadataUpdater;
@ -767,12 +766,8 @@ public abstract class MainnetProtocolSpecs {
final boolean isParallelTxProcessingEnabled, final boolean isParallelTxProcessingEnabled,
final MetricsSystem metricsSystem) { final MetricsSystem metricsSystem) {
final Address withdrawalRequestContractAddress = RequestContractAddresses requestContractAddresses =
genesisConfigOptions RequestContractAddresses.fromGenesis(genesisConfigOptions);
.getWithdrawalRequestContractAddress()
.orElse(DEFAULT_WITHDRAWAL_REQUEST_CONTRACT_ADDRESS);
final Address depositContractAddress =
genesisConfigOptions.getDepositContractAddress().orElse(DEFAULT_DEPOSIT_CONTRACT_ADDRESS);
return cancunDefinition( return cancunDefinition(
chainId, chainId,
@ -794,10 +789,9 @@ public abstract class MainnetProtocolSpecs {
.precompileContractRegistryBuilder(MainnetPrecompiledContractRegistries::prague) .precompileContractRegistryBuilder(MainnetPrecompiledContractRegistries::prague)
// EIP-7002 Withdrawals / EIP-6610 Deposits / EIP-7685 Requests // EIP-7002 Withdrawals / EIP-6610 Deposits / EIP-7685 Requests
.requestsValidator(pragueRequestsValidator(depositContractAddress)) .requestsValidator(pragueRequestsValidator(requestContractAddresses))
// EIP-7002 Withdrawals / EIP-6610 Deposits / EIP-7685 Requests // EIP-7002 Withdrawals / EIP-6610 Deposits / EIP-7685 Requests
.requestProcessorCoordinator( .requestProcessorCoordinator(pragueRequestsProcessors(requestContractAddresses))
pragueRequestsProcessors(withdrawalRequestContractAddress, depositContractAddress))
// change to accept EIP-7702 transactions // change to accept EIP-7702 transactions
.transactionValidatorFactoryBuilder( .transactionValidatorFactoryBuilder(

@ -22,13 +22,18 @@ import org.apache.tuweni.bytes.Bytes;
public class ConsolidationRequestProcessor public class ConsolidationRequestProcessor
extends AbstractSystemCallRequestProcessor<ConsolidationRequest> { extends AbstractSystemCallRequestProcessor<ConsolidationRequest> {
public static final Address CONSOLIDATION_REQUEST_PREDEPLOY_ADDRESS = public static final Address CONSOLIDATION_REQUEST_CONTRACT_ADDRESS =
Address.fromHexString("0x00b42dbF2194e931E80326D950320f7d9Dbeac02"); Address.fromHexString("0x00b42dbF2194e931E80326D950320f7d9Dbeac02");
private static final int ADDRESS_BYTES = 20; private static final int ADDRESS_BYTES = 20;
private static final int PUBLIC_KEY_BYTES = 48; private static final int PUBLIC_KEY_BYTES = 48;
private static final int CONSOLIDATION_REQUEST_BYTES_SIZE = private static final int CONSOLIDATION_REQUEST_BYTES_SIZE =
ADDRESS_BYTES + PUBLIC_KEY_BYTES + PUBLIC_KEY_BYTES; ADDRESS_BYTES + PUBLIC_KEY_BYTES + PUBLIC_KEY_BYTES;
private final Address consolidationRequestContractAddress;
public ConsolidationRequestProcessor(final Address consolidationRequestContractAddress) {
this.consolidationRequestContractAddress = consolidationRequestContractAddress;
}
/** /**
* Gets the call address for consolidation requests. * Gets the call address for consolidation requests.
@ -37,7 +42,7 @@ public class ConsolidationRequestProcessor
*/ */
@Override @Override
protected Address getCallAddress() { protected Address getCallAddress() {
return CONSOLIDATION_REQUEST_PREDEPLOY_ADDRESS; return consolidationRequestContractAddress;
} }
/** /**

@ -14,27 +14,34 @@
*/ */
package org.hyperledger.besu.ethereum.mainnet.requests; package org.hyperledger.besu.ethereum.mainnet.requests;
import org.hyperledger.besu.datatypes.Address;
import org.hyperledger.besu.datatypes.RequestType; import org.hyperledger.besu.datatypes.RequestType;
public class MainnetRequestsValidator { public class MainnetRequestsValidator {
public static RequestsValidatorCoordinator pragueRequestsValidator( public static RequestsValidatorCoordinator pragueRequestsValidator(
final Address depositContractAddress) { final RequestContractAddresses requestContractAddresses) {
return new RequestsValidatorCoordinator.Builder() return new RequestsValidatorCoordinator.Builder()
.addValidator(RequestType.WITHDRAWAL, new WithdrawalRequestValidator()) .addValidator(RequestType.WITHDRAWAL, new WithdrawalRequestValidator())
.addValidator(RequestType.CONSOLIDATION, new ConsolidationRequestValidator()) .addValidator(RequestType.CONSOLIDATION, new ConsolidationRequestValidator())
.addValidator(RequestType.DEPOSIT, new DepositRequestValidator(depositContractAddress)) .addValidator(
RequestType.DEPOSIT,
new DepositRequestValidator(requestContractAddresses.getDepositContractAddress()))
.build(); .build();
} }
public static RequestProcessorCoordinator pragueRequestsProcessors( public static RequestProcessorCoordinator pragueRequestsProcessors(
final Address withdrawalRequestContractAddress, final Address depositContractAddress) { final RequestContractAddresses requestContractAddresses) {
return new RequestProcessorCoordinator.Builder() return new RequestProcessorCoordinator.Builder()
.addProcessor( .addProcessor(
RequestType.WITHDRAWAL, RequestType.WITHDRAWAL,
new WithdrawalRequestProcessor(withdrawalRequestContractAddress)) new WithdrawalRequestProcessor(
.addProcessor(RequestType.CONSOLIDATION, new ConsolidationRequestProcessor()) requestContractAddresses.getWithdrawalRequestContractAddress()))
.addProcessor(RequestType.DEPOSIT, new DepositRequestProcessor(depositContractAddress)) .addProcessor(
RequestType.CONSOLIDATION,
new ConsolidationRequestProcessor(
requestContractAddresses.getConsolidationRequestContractAddress()))
.addProcessor(
RequestType.DEPOSIT,
new DepositRequestProcessor(requestContractAddresses.getDepositContractAddress()))
.build(); .build();
} }
} }

@ -0,0 +1,61 @@
/*
* 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.mainnet.requests;
import static org.hyperledger.besu.ethereum.mainnet.requests.ConsolidationRequestProcessor.CONSOLIDATION_REQUEST_CONTRACT_ADDRESS;
import static org.hyperledger.besu.ethereum.mainnet.requests.DepositRequestProcessor.DEFAULT_DEPOSIT_CONTRACT_ADDRESS;
import static org.hyperledger.besu.ethereum.mainnet.requests.WithdrawalRequestProcessor.DEFAULT_WITHDRAWAL_REQUEST_CONTRACT_ADDRESS;
import org.hyperledger.besu.config.GenesisConfigOptions;
import org.hyperledger.besu.datatypes.Address;
public class RequestContractAddresses {
private final Address withdrawalRequestContractAddress;
private final Address depositContractAddress;
private final Address consolidationRequestContractAddress;
public RequestContractAddresses(
final Address withdrawalRequestContractAddress,
final Address depositContractAddress,
final Address consolidationRequestContractAddress) {
this.withdrawalRequestContractAddress = withdrawalRequestContractAddress;
this.depositContractAddress = depositContractAddress;
this.consolidationRequestContractAddress = consolidationRequestContractAddress;
}
public static RequestContractAddresses fromGenesis(
final GenesisConfigOptions genesisConfigOptions) {
return new RequestContractAddresses(
genesisConfigOptions
.getWithdrawalRequestContractAddress()
.orElse(DEFAULT_WITHDRAWAL_REQUEST_CONTRACT_ADDRESS),
genesisConfigOptions.getDepositContractAddress().orElse(DEFAULT_DEPOSIT_CONTRACT_ADDRESS),
genesisConfigOptions
.getConsolidationRequestContractAddress()
.orElse(CONSOLIDATION_REQUEST_CONTRACT_ADDRESS));
}
public Address getWithdrawalRequestContractAddress() {
return withdrawalRequestContractAddress;
}
public Address getDepositContractAddress() {
return depositContractAddress;
}
public Address getConsolidationRequestContractAddress() {
return consolidationRequestContractAddress;
}
}

@ -17,8 +17,10 @@ package org.hyperledger.besu.ethereum.mainnet;
import static java.util.Collections.emptyList; import static java.util.Collections.emptyList;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.hyperledger.besu.ethereum.mainnet.HeaderValidationMode.NONE; import static org.hyperledger.besu.ethereum.mainnet.HeaderValidationMode.NONE;
import static org.hyperledger.besu.ethereum.mainnet.requests.ConsolidationRequestProcessor.CONSOLIDATION_REQUEST_CONTRACT_ADDRESS;
import static org.hyperledger.besu.ethereum.mainnet.requests.DepositRequestProcessor.DEFAULT_DEPOSIT_CONTRACT_ADDRESS; import static org.hyperledger.besu.ethereum.mainnet.requests.DepositRequestProcessor.DEFAULT_DEPOSIT_CONTRACT_ADDRESS;
import static org.hyperledger.besu.ethereum.mainnet.requests.MainnetRequestsValidator.pragueRequestsValidator; import static org.hyperledger.besu.ethereum.mainnet.requests.MainnetRequestsValidator.pragueRequestsValidator;
import static org.hyperledger.besu.ethereum.mainnet.requests.WithdrawalRequestProcessor.DEFAULT_WITHDRAWAL_REQUEST_CONTRACT_ADDRESS;
import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.lenient; import static org.mockito.Mockito.lenient;
@ -31,6 +33,7 @@ import org.hyperledger.besu.ethereum.core.BlockDataGenerator;
import org.hyperledger.besu.ethereum.core.BlockchainSetupUtil; import org.hyperledger.besu.ethereum.core.BlockchainSetupUtil;
import org.hyperledger.besu.ethereum.core.Request; import org.hyperledger.besu.ethereum.core.Request;
import org.hyperledger.besu.ethereum.core.WithdrawalRequest; import org.hyperledger.besu.ethereum.core.WithdrawalRequest;
import org.hyperledger.besu.ethereum.mainnet.requests.RequestContractAddresses;
import org.hyperledger.besu.ethereum.mainnet.requests.RequestsValidatorCoordinator; import org.hyperledger.besu.ethereum.mainnet.requests.RequestsValidatorCoordinator;
import org.hyperledger.besu.evm.log.LogsBloomFilter; import org.hyperledger.besu.evm.log.LogsBloomFilter;
@ -52,9 +55,13 @@ class PragueRequestsValidatorTest {
@Mock private ProtocolSchedule protocolSchedule; @Mock private ProtocolSchedule protocolSchedule;
@Mock private ProtocolSpec protocolSpec; @Mock private ProtocolSpec protocolSpec;
@Mock private WithdrawalsValidator withdrawalsValidator; @Mock private WithdrawalsValidator withdrawalsValidator;
private final RequestContractAddresses requestContractAddresses =
new RequestContractAddresses(
DEFAULT_WITHDRAWAL_REQUEST_CONTRACT_ADDRESS,
DEFAULT_DEPOSIT_CONTRACT_ADDRESS,
CONSOLIDATION_REQUEST_CONTRACT_ADDRESS);
RequestsValidatorCoordinator requestValidator = RequestsValidatorCoordinator requestValidator = pragueRequestsValidator(requestContractAddresses);
pragueRequestsValidator(DEFAULT_DEPOSIT_CONTRACT_ADDRESS);
@BeforeEach @BeforeEach
public void setUp() { public void setUp() {

@ -218,7 +218,8 @@ public class PersistBlockTask extends AbstractEthTask<Block> {
case IMPORTED: case IMPORTED:
LOG.info( LOG.info(
String.format( String.format(
"Imported #%,d / %d tx / %d om / %,d (%01.1f%%) gas / (%s) in %01.3fs. Peers: %d", "Imported %s #%,d / %d tx / %d om / %,d (%01.1f%%) gas / (%s) in %01.3fs. Peers: %d",
block.getBody().getTransactions().size() == 0 ? "empty block" : "block",
block.getHeader().getNumber(), block.getHeader().getNumber(),
block.getBody().getTransactions().size(), block.getBody().getTransactions().size(),
block.getBody().getOmmers().size(), block.getBody().getOmmers().size(),

Loading…
Cancel
Save