Skip to content

Commit 59da092

Browse files
matthew1001fab-10
andauthored
Restrict downgrade (#6307)
* Add Besu version to DB metadata. Check for downgrades and reject if version < version recorded in DB metadata. Signed-off-by: Matthew Whitehead <matthew1001@gmail.com> * Add --allow-downgrade CLI arg. If set it allows the downgrade and updates the Besu version in the metadata file to the downgraded version. Signed-off-by: Matthew Whitehead <matthew1001@gmail.com> * Update gradle verification XML Signed-off-by: Matthew Whitehead <matthew1001@gmail.com> * Add and update tests Signed-off-by: Matthew Whitehead <matthew1001@gmail.com> * Refactoring Signed-off-by: Matthew Whitehead <matthew1001@gmail.com> * Remove versioning from RocksDB, now in separate VERSION_DATADATA.json Signed-off-by: Matthew Whitehead <matthew1001@gmail.com> * Tidy up and tests for the new class Signed-off-by: Matthew Whitehead <matthew1001@gmail.com> * Move downgrade logic into VersionMetadata as BesuCommand is already very big Signed-off-by: Matthew Whitehead <matthew1001@gmail.com> * Add more tests Signed-off-by: Matthew Whitehead <matthew1001@gmail.com> * Refactor the naming of the option to version-compatibility-protection Signed-off-by: Matthew Whitehead <matthew1001@gmail.com> * Remove remaining references to allow-downgrade Signed-off-by: Matthew Whitehead <matthew1001@gmail.com> * Rename test Signed-off-by: Matthew Whitehead <matthew1001@gmail.com> * Update comments Signed-off-by: Matthew Whitehead <matthew1001@gmail.com> * Metadata verification update Signed-off-by: Matthew Whitehead <matthew1001@gmail.com> * gradle fix Signed-off-by: Matthew Whitehead <matthew1001@gmail.com> * Enable version downgrade protection by default for non-named networks Signed-off-by: Matthew Whitehead <matthew1001@gmail.com> * Fix default logic Signed-off-by: Matthew Whitehead <matthew1001@gmail.com> * Update ethereum/core/src/main/java/org/hyperledger/besu/ethereum/core/VersionMetadata.java Co-authored-by: Fabio Di Fabio <fabio.difabio@consensys.net> Signed-off-by: Matt Whitehead <matthew1001@hotmail.com> * Update ethereum/core/src/main/java/org/hyperledger/besu/ethereum/core/VersionMetadata.java Co-authored-by: Fabio Di Fabio <fabio.difabio@consensys.net> Signed-off-by: Matt Whitehead <matthew1001@hotmail.com> * mock-maker-inline no longer needed Signed-off-by: Matthew Whitehead <matthew1001@gmail.com> --------- Signed-off-by: Matthew Whitehead <matthew1001@gmail.com> Signed-off-by: Matt Whitehead <matthew.whitehead@kaleido.io> Signed-off-by: Matt Whitehead <matthew1001@hotmail.com> Co-authored-by: Fabio Di Fabio <fabio.difabio@consensys.net>
1 parent f921ddc commit 59da092

File tree

9 files changed

+455
-0
lines changed

9 files changed

+455
-0
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@
4848
- Upgrade reference tests to version 13.1 [#6574](https://github.com/hyperledger/besu/pull/6574)
4949
- Extend `BesuConfiguration` service [#6584](https://github.com/hyperledger/besu/pull/6584)
5050
- Add `ethereum_min_gas_price` and `ethereum_min_priority_fee` metrics to track runtime values of `min-gas-price` and `min-priority-fee` [#6587](https://github.com/hyperledger/besu/pull/6587)
51+
- Option to perform version incompatibility checks when starting Besu. In this first release of the feature, if `--version-compatibility-protection` is set to true it checks that the version of Besu being started is the same or higher than the previous version. [6307](https://github.com/hyperledger/besu/pull/6307)
52+
5153

5254
### Bug fixes
5355
- Fix the way an advertised host configured with `--p2p-host` is treated when communicating with the originator of a PING packet [#6225](https://github.com/hyperledger/besu/pull/6225)

besu/src/main/java/org/hyperledger/besu/cli/BesuCommand.java

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,7 @@
120120
import org.hyperledger.besu.ethereum.core.MiningParameters;
121121
import org.hyperledger.besu.ethereum.core.MiningParametersMetrics;
122122
import org.hyperledger.besu.ethereum.core.PrivacyParameters;
123+
import org.hyperledger.besu.ethereum.core.VersionMetadata;
123124
import org.hyperledger.besu.ethereum.eth.sync.SyncMode;
124125
import org.hyperledger.besu.ethereum.eth.sync.SynchronizerConfiguration;
125126
import org.hyperledger.besu.ethereum.eth.transactions.ImmutableTransactionPoolConfiguration;
@@ -562,6 +563,12 @@ private InetAddress autoDiscoverDefaultIP() {
562563
arity = "1")
563564
private final Path kzgTrustedSetupFile = null;
564565

566+
@Option(
567+
names = {"--version-compatibility-protection"},
568+
description =
569+
"Perform compatibility checks between the version of Besu being started and the version of Besu that last started with this data directory. (default: ${DEFAULT-VALUE})")
570+
private Boolean versionCompatibilityProtection = null;
571+
565572
@CommandLine.ArgGroup(validate = false, heading = "@|bold GraphQL Options|@%n")
566573
GraphQlOptions graphQlOptions = new GraphQlOptions();
567574

@@ -1078,7 +1085,13 @@ public void run() {
10781085
vertx = createVertx(createVertxOptions(metricsSystem.get()));
10791086

10801087
validateOptions();
1088+
10811089
configure();
1090+
1091+
// If we're not running against a named network, or if version compat protection has been
1092+
// explicitly enabled, perform compatibility check
1093+
VersionMetadata.versionCompatibilityChecks(versionCompatibilityProtection, dataDir());
1094+
10821095
configureNativeLibs();
10831096
besuController = buildController();
10841097

@@ -1648,6 +1661,7 @@ private void configure() throws Exception {
16481661
checkPortClash();
16491662
checkIfRequiredPortsAreAvailable();
16501663
syncMode = getDefaultSyncModeIfNotSet();
1664+
versionCompatibilityProtection = getDefaultVersionCompatibilityProtectionIfNotSet();
16511665

16521666
ethNetworkConfig = updateNetworkConfig(network);
16531667

@@ -2556,6 +2570,16 @@ String getLogLevel() {
25562570
return loggingLevelOption.getLogLevel();
25572571
}
25582572

2573+
/**
2574+
* Returns the flag indicating that version compatiblity checks will be made.
2575+
*
2576+
* @return true if compatibility checks should be made, otherwise false
2577+
*/
2578+
@VisibleForTesting
2579+
public Boolean getVersionCompatibilityProtection() {
2580+
return versionCompatibilityProtection;
2581+
}
2582+
25592583
private void instantiateSignatureAlgorithmFactory() {
25602584
if (SignatureAlgorithmFactory.isInstanceSet()) {
25612585
return;
@@ -2664,6 +2688,12 @@ private SyncMode getDefaultSyncModeIfNotSet() {
26642688
: SyncMode.FULL);
26652689
}
26662690

2691+
private Boolean getDefaultVersionCompatibilityProtectionIfNotSet() {
2692+
// Version compatibility protection is enabled by default for non-named networks
2693+
return Optional.ofNullable(versionCompatibilityProtection)
2694+
.orElse(commandLine.getParseResult().hasMatchedOption("network") ? false : true);
2695+
}
2696+
26672697
private String generateConfigurationOverview() {
26682698
final ConfigurationOverviewBuilder builder = new ConfigurationOverviewBuilder(logger);
26692699

besu/src/test/java/org/hyperledger/besu/cli/BesuCommandTest.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1341,6 +1341,13 @@ public void dnsEnabledOptionIsParsedCorrectly() {
13411341
assertThat(besuCommand.getEnodeDnsConfiguration().updateEnabled()).isFalse();
13421342
}
13431343

1344+
@Test
1345+
public void versionCompatibilityProtectionTrueOptionIsParsedCorrectly() {
1346+
final TestBesuCommand besuCommand = parseCommand("--version-compatibility-protection", "true");
1347+
1348+
assertThat(besuCommand.getVersionCompatibilityProtection()).isTrue();
1349+
}
1350+
13441351
@Test
13451352
public void dnsUpdateEnabledOptionIsParsedCorrectly() {
13461353
final TestBesuCommand besuCommand =

besu/src/test/resources/everything_config.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ node-private-key-file="./path/to/privateKey"
1616
pid-path="~/.pid"
1717
reorg-logging-threshold=0
1818
static-nodes-file="~/besudata/static-nodes.json"
19+
version-compatibility-protection=true
20+
1921
profile="NONE"
2022
# Security Module plugin to use
2123
security-module="localfile"

ethereum/core/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ dependencies {
4848
implementation 'com.fasterxml.jackson.core:jackson-databind'
4949
implementation 'com.google.guava:guava'
5050
implementation 'com.google.dagger:dagger'
51+
implementation 'org.apache.maven:maven-artifact'
5152
annotationProcessor 'com.google.dagger:dagger-compiler'
5253
implementation 'io.opentelemetry:opentelemetry-api'
5354
implementation 'io.vertx:vertx-core'
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
/*
2+
* Copyright Hyperledger Besu contributors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
5+
* the License. You may obtain a copy of the License at
6+
*
7+
* http://www.apache.org/licenses/LICENSE-2.0
8+
*
9+
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
10+
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
11+
* specific language governing permissions and limitations under the License.
12+
*
13+
* SPDX-License-Identifier: Apache-2.0
14+
*/
15+
package org.hyperledger.besu.ethereum.core;
16+
17+
import java.io.File;
18+
import java.io.FileNotFoundException;
19+
import java.io.IOException;
20+
import java.nio.file.Path;
21+
22+
import com.fasterxml.jackson.annotation.JsonCreator;
23+
import com.fasterxml.jackson.annotation.JsonProperty;
24+
import com.fasterxml.jackson.core.JsonProcessingException;
25+
import com.fasterxml.jackson.databind.ObjectMapper;
26+
import org.apache.maven.artifact.versioning.ComparableVersion;
27+
import org.slf4j.Logger;
28+
import org.slf4j.LoggerFactory;
29+
30+
public class VersionMetadata {
31+
private static final Logger LOG = LoggerFactory.getLogger(VersionMetadata.class);
32+
33+
/** Represents an unknown Besu version in the version metadata file */
34+
public static final String BESU_VERSION_UNKNOWN = "UNKNOWN";
35+
36+
private static final String METADATA_FILENAME = "VERSION_METADATA.json";
37+
private static final ObjectMapper MAPPER = new ObjectMapper();
38+
private final String besuVersion;
39+
40+
/**
41+
* Get the version of Besu that is running.
42+
*
43+
* @return the version of Besu
44+
*/
45+
public static String getRuntimeVersion() {
46+
return VersionMetadata.class.getPackage().getImplementationVersion() == null
47+
? BESU_VERSION_UNKNOWN
48+
: VersionMetadata.class.getPackage().getImplementationVersion();
49+
}
50+
51+
@JsonCreator
52+
public VersionMetadata(@JsonProperty("besuVersion") final String besuVersion) {
53+
this.besuVersion = besuVersion;
54+
}
55+
56+
public String getBesuVersion() {
57+
return besuVersion;
58+
}
59+
60+
public static VersionMetadata lookUpFrom(final Path dataDir) throws IOException {
61+
LOG.info("Lookup version metadata file in data directory: {}", dataDir);
62+
return resolveVersionMetadata(getDefaultMetadataFile(dataDir));
63+
}
64+
65+
public void writeToDirectory(final Path dataDir) throws IOException {
66+
MAPPER.writeValue(getDefaultMetadataFile(dataDir), this);
67+
}
68+
69+
private static File getDefaultMetadataFile(final Path dataDir) {
70+
return dataDir.resolve(METADATA_FILENAME).toFile();
71+
}
72+
73+
private static VersionMetadata resolveVersionMetadata(final File metadataFile)
74+
throws IOException {
75+
VersionMetadata versionMetadata;
76+
try {
77+
versionMetadata = MAPPER.readValue(metadataFile, VersionMetadata.class);
78+
LOG.info("Existing version data detected. Besu version {}", versionMetadata.besuVersion);
79+
} catch (FileNotFoundException fnfe) {
80+
versionMetadata = new VersionMetadata(BESU_VERSION_UNKNOWN);
81+
} catch (JsonProcessingException jpe) {
82+
throw new IllegalStateException(
83+
String.format("Invalid metadata file %s", metadataFile.getAbsolutePath()), jpe);
84+
}
85+
return versionMetadata;
86+
}
87+
88+
/**
89+
* This function is designed to protect a Besu instance from being unintentionally started at a
90+
* version of Besu that might be incompatible with the version that last modified the specified
91+
* data directory. Currently this check is limited to checking that the version is >= the previous
92+
* version, to avoid accidentally running a lower version of Besu and potentially corrupting data,
93+
* but the method could be extended to perform any other version-to-version compatibility checks
94+
* necessary. If the --version-compatibility-protection flag is set to true and the compatibilty
95+
* checks pass, the version metadata is updated to the current version of Besu.
96+
*/
97+
public static void versionCompatibilityChecks(
98+
final boolean enforceCompatibilityProtection, final Path dataDir) throws IOException {
99+
final VersionMetadata versionMetaData = VersionMetadata.lookUpFrom(dataDir);
100+
if (versionMetaData.getBesuVersion().equals(VersionMetadata.BESU_VERSION_UNKNOWN)) {
101+
// The version isn't known, potentially because the file doesn't exist. Write the latest
102+
// version to the metadata file.
103+
LOG.info(
104+
"No version data detected. Writing Besu version {} to metadata file",
105+
VersionMetadata.getRuntimeVersion());
106+
new VersionMetadata(VersionMetadata.getRuntimeVersion()).writeToDirectory(dataDir);
107+
} else {
108+
// Check the runtime version against the most recent version as recorded in the version
109+
// metadata file
110+
final String installedVersion = VersionMetadata.getRuntimeVersion().split("-", 2)[0];
111+
final String metadataVersion = versionMetaData.getBesuVersion().split("-", 2)[0];
112+
final int versionComparison =
113+
new ComparableVersion(installedVersion).compareTo(new ComparableVersion(metadataVersion));
114+
if (versionComparison == 0) {
115+
// Versions match - no-op
116+
} else if (versionComparison < 0) {
117+
if (!enforceCompatibilityProtection) {
118+
LOG.warn(
119+
"Besu version {} is lower than version {} that last started. Allowing startup because --version-compatibility-protection has been disabled.",
120+
installedVersion,
121+
metadataVersion);
122+
// We've allowed startup at an older version of Besu. Since the version in the metadata
123+
// file records the latest version of
124+
// Besu to write to the database we'll update the metadata version to this
125+
// downgraded-version.
126+
new VersionMetadata(VersionMetadata.getRuntimeVersion()).writeToDirectory(dataDir);
127+
} else {
128+
final String message =
129+
"Besu version "
130+
+ installedVersion
131+
+ " is lower than version "
132+
+ metadataVersion
133+
+ " that last started. Remove --version-compatibility-protection option to allow Besu to start at "
134+
+ " the lower version (warning - this may have unrecoverable effects on the database).";
135+
LOG.error(message, installedVersion, metadataVersion);
136+
throw new IllegalStateException(message);
137+
}
138+
} else {
139+
LOG.info(
140+
"Besu version {} is higher than version {} that last started. Updating version metadata.",
141+
installedVersion,
142+
metadataVersion);
143+
new VersionMetadata(VersionMetadata.getRuntimeVersion()).writeToDirectory(dataDir);
144+
}
145+
}
146+
}
147+
}

0 commit comments

Comments
 (0)